hx-cli 0.2.3__tar.gz → 0.2.5__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 (230) hide show
  1. hx_cli-0.2.5/.gitignore +19 -0
  2. {hx_cli-0.2.3 → hx_cli-0.2.5}/CHANGELOG.md +114 -1
  3. hx_cli-0.2.3/README.md → hx_cli-0.2.5/PKG-INFO +162 -0
  4. hx_cli-0.2.3/PKG-INFO → hx_cli-0.2.5/README.md +122 -37
  5. {hx_cli-0.2.3 → hx_cli-0.2.5}/pyproject.toml +4 -1
  6. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/__init__.py +1 -1
  7. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/callback.py +13 -1
  8. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/store.py +16 -2
  9. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/cli.py +120 -3
  10. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/compaction.py +17 -2
  11. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/context.py +25 -4
  12. hx_cli-0.2.5/src/hx/core/images.py +394 -0
  13. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/loop.py +98 -26
  14. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/messages.py +80 -4
  15. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/session.py +69 -5
  16. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/keys.py +3 -0
  17. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/mcp/client.py +56 -9
  18. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/mcp/manager.py +31 -1
  19. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/permissions/engine.py +128 -11
  20. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/permissions/sandbox.py +46 -6
  21. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/codex_catalogue.py +12 -0
  22. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/devin.py +14 -1
  23. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/devin_catalogue.py +4 -0
  24. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/devin_wire.py +21 -0
  25. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/models.py +36 -0
  26. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/openrouter.py +51 -3
  27. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/responses_codec.py +29 -6
  28. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/base.py +18 -9
  29. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/bash.py +68 -19
  30. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/read.py +36 -1
  31. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/websearch.py +8 -0
  32. hx_cli-0.2.5/src/hx/trace.py +919 -0
  33. hx_cli-0.2.5/src/hx/tui/clipboard.py +308 -0
  34. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/commands.py +104 -1
  35. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/glyphs.py +3 -0
  36. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/runtime.py +98 -20
  37. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/blocks.py +27 -9
  38. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/pickers.py +5 -2
  39. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/prompt.py +108 -8
  40. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/test_codex_oauth.py +21 -0
  41. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/test_store.py +58 -0
  42. hx_cli-0.2.5/tests/core/test_images.py +281 -0
  43. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_loop.py +206 -1
  44. hx_cli-0.2.5/tests/core/test_loop_images.py +181 -0
  45. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_session.py +70 -0
  46. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_steering.py +3 -3
  47. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/mcp/fixtures/echo_server.py +47 -1
  48. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/mcp/test_manager.py +54 -0
  49. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/permissions/test_engine.py +111 -0
  50. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/permissions/test_sandbox.py +63 -0
  51. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_catalogue.py +55 -0
  52. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_codex_catalogue.py +7 -0
  53. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_devin.py +36 -0
  54. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_devin_catalogue.py +23 -0
  55. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_openrouter.py +135 -0
  56. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_responses_codec.py +34 -0
  57. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_cli.py +49 -0
  58. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_readme.py +1 -0
  59. hx_cli-0.2.5/tests/test_trace.py +538 -0
  60. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_background.py +78 -0
  61. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_bash.py +48 -0
  62. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_files.py +33 -0
  63. hx_cli-0.2.5/tests/tui/test_images.py +319 -0
  64. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_session.py +22 -11
  65. hx_cli-0.2.5/tests/tui/test_trace_command.py +257 -0
  66. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_prompt.py +9 -9
  67. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_runtime.py +1 -1
  68. {hx_cli-0.2.3 → hx_cli-0.2.5}/uv.lock +87 -0
  69. hx_cli-0.2.3/.gitignore +0 -16
  70. hx_cli-0.2.3/src/hx/tui/clipboard.py +0 -125
  71. {hx_cli-0.2.3 → hx_cli-0.2.5}/.github/workflows/ci.yml +0 -0
  72. {hx_cli-0.2.3 → hx_cli-0.2.5}/examples/hooks/check.sh +0 -0
  73. {hx_cli-0.2.3 → hx_cli-0.2.5}/install.sh +0 -0
  74. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/agents/__init__.py +0 -0
  75. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/agents/definitions.py +0 -0
  76. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/agents/subagent.py +0 -0
  77. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/__init__.py +0 -0
  78. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/__init__.py +0 -0
  79. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/browser.py +0 -0
  80. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/codex.py +0 -0
  81. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/devin.py +0 -0
  82. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/pkce.py +0 -0
  83. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/resolve.py +0 -0
  84. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/config.py +0 -0
  85. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/__init__.py +0 -0
  86. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/checkpoints.py +0 -0
  87. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/events.py +0 -0
  88. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/lateinject.py +0 -0
  89. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/title.py +0 -0
  90. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/usage.py +0 -0
  91. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/docs.py +0 -0
  92. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/frontmatter.py +0 -0
  93. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/git.py +0 -0
  94. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/hooks/__init__.py +0 -0
  95. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/hooks/engine.py +0 -0
  96. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/hooks/spec.py +0 -0
  97. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/mcp/__init__.py +0 -0
  98. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/net.py +0 -0
  99. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/paths.py +0 -0
  100. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/permissions/__init__.py +0 -0
  101. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/permissions/parser.py +0 -0
  102. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/__init__.py +0 -0
  103. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/base.py +0 -0
  104. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/codex.py +0 -0
  105. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/fake.py +0 -0
  106. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/protowire.py +0 -0
  107. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/registry.py +0 -0
  108. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/skills/__init__.py +0 -0
  109. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/skills/loader.py +0 -0
  110. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/skills/runtime.py +0 -0
  111. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/__init__.py +0 -0
  112. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/ansi.py +0 -0
  113. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/buffer.py +0 -0
  114. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/component.py +0 -0
  115. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/demo.py +0 -0
  116. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/editor.py +0 -0
  117. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/keydecode.py +0 -0
  118. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/loop.py +0 -0
  119. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/markdown.py +0 -0
  120. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/primitives.py +0 -0
  121. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/sanitize.py +0 -0
  122. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/screen.py +0 -0
  123. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/syntax.py +0 -0
  124. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/terminal.py +0 -0
  125. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/undo.py +0 -0
  126. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/width.py +0 -0
  127. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/word_nav.py +0 -0
  128. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/__init__.py +0 -0
  129. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/anchors.py +0 -0
  130. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/edit.py +0 -0
  131. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/glob.py +0 -0
  132. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/grep.py +0 -0
  133. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/output.py +0 -0
  134. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/registry.py +0 -0
  135. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/symbols.py +0 -0
  136. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/task.py +0 -0
  137. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/todo.py +0 -0
  138. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/write.py +0 -0
  139. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/__init__.py +0 -0
  140. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/app.py +0 -0
  141. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/format.py +0 -0
  142. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/fuzzy.py +0 -0
  143. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/killring.py +0 -0
  144. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/limits.py +0 -0
  145. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/paint.py +0 -0
  146. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/renderers.py +0 -0
  147. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/roles.py +0 -0
  148. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/theme.py +0 -0
  149. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/theme_json.py +0 -0
  150. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/themes/ansi.json +0 -0
  151. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/themes/dark.json +0 -0
  152. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/themes/light.json +0 -0
  153. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/__init__.py +0 -0
  154. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/dialog.py +0 -0
  155. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/login.py +0 -0
  156. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/permission.py +0 -0
  157. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/status.py +0 -0
  158. {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/transcript.py +0 -0
  159. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/__init__.py +0 -0
  160. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/agents/__init__.py +0 -0
  161. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/agents/test_subagent.py +0 -0
  162. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/__init__.py +0 -0
  163. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/test_devin_oauth.py +0 -0
  164. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/test_pkce.py +0 -0
  165. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/test_resolve.py +0 -0
  166. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/conftest.py +0 -0
  167. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/__init__.py +0 -0
  168. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_checkpoints.py +0 -0
  169. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_compaction.py +0 -0
  170. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_context.py +0 -0
  171. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_lateinject.py +0 -0
  172. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_rewind.py +0 -0
  173. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_settings_layers.py +0 -0
  174. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_settings_migration.py +0 -0
  175. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_title.py +0 -0
  176. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_usage.py +0 -0
  177. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/hooks/__init__.py +0 -0
  178. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/hooks/test_engine.py +0 -0
  179. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/hooks/test_example_check.py +0 -0
  180. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/mcp/__init__.py +0 -0
  181. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/permissions/__init__.py +0 -0
  182. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/permissions/test_parser.py +0 -0
  183. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/__init__.py +0 -0
  184. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_codex.py +0 -0
  185. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_protowire.py +0 -0
  186. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_registry.py +0 -0
  187. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/skills/__init__.py +0 -0
  188. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/skills/test_loader.py +0 -0
  189. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/__init__.py +0 -0
  190. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/conftest.py +0 -0
  191. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_ansi.py +0 -0
  192. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_keydecode.py +0 -0
  193. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_loop.py +0 -0
  194. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_markdown.py +0 -0
  195. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_primitives.py +0 -0
  196. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_sanitize.py +0 -0
  197. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_screen.py +0 -0
  198. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_terminal.py +0 -0
  199. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_width.py +0 -0
  200. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_docs.py +0 -0
  201. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_git.py +0 -0
  202. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_install.py +0 -0
  203. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_integration.py +0 -0
  204. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_live.py +0 -0
  205. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_live_codex.py +0 -0
  206. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_live_devin.py +0 -0
  207. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_live_tavily.py +0 -0
  208. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_net.py +0 -0
  209. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_paths.py +0 -0
  210. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/__init__.py +0 -0
  211. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_hashline.py +0 -0
  212. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_output.py +0 -0
  213. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_symbols.py +0 -0
  214. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_todo.py +0 -0
  215. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_websearch.py +0 -0
  216. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/__init__.py +0 -0
  217. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/support.py +0 -0
  218. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_autocomplete.py +0 -0
  219. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_clipboard.py +0 -0
  220. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_effort.py +0 -0
  221. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_keys.py +0 -0
  222. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_login.py +0 -0
  223. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_renderers.py +0 -0
  224. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_theme_json.py +0 -0
  225. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/__init__.py +0 -0
  226. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_blocks.py +0 -0
  227. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_permission.py +0 -0
  228. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_pickers.py +0 -0
  229. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_status.py +0 -0
  230. {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_transcript.py +0 -0
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ .DS_Store
14
+ .hx/sessions/
15
+ # Not .hx/settings.json: that file is the project's *shared* configuration and
16
+ # is meant to be checked in. What HX writes on your behalf - permission grants
17
+ # and the rest - lives under $HX_HOME/projects/<slug>, outside the checkout
18
+ # entirely, so there is nothing here left to hide.
19
+ snapshot_report.html
@@ -28,6 +28,117 @@ the project follows [semantic versioning](https://semver.org/).
28
28
 
29
29
  ---
30
30
 
31
+ ## [0.2.5] - 2026-09-23
32
+
33
+ ### Added
34
+
35
+ - Images as input. `ctrl+v` attaches the image on the clipboard, dragging an
36
+ image file into the terminal attaches it, and `hx -p "..." --image PATH`
37
+ attaches one to a headless prompt. Each shows in the prompt as
38
+ `[Image #N]`.
39
+ - `Read` returns image files as images, so the model can look at a screenshot
40
+ or mockup it is pointed at with `@path`.
41
+ - Images returned by MCP tools reach the model instead of being dropped.
42
+ - Every route carries images: OpenRouter, the Codex subscription and the Devin
43
+ subscription. Large images are scaled and re-encoded to fit each route's
44
+ limits.
45
+ - A model that does not accept images is warned about when one is attached,
46
+ and is told the image was left out rather than sent a request that fails.
47
+ The `/model` picker marks the models that can see images.
48
+
49
+ ### Fixed
50
+
51
+ - Background jobs are waited for, not only signalled, when a session closes.
52
+ An unwaited subprocess transport was collected after the event loop had
53
+ gone, which raised `Event loop is closed` as an unraisable warning on CI
54
+ runs that otherwise passed.
55
+
56
+ ---
57
+
58
+ ## [0.2.4] - 2026-09-19
59
+
60
+ ### Added
61
+
62
+ - `/trace [path]`: write the whole session to one self-contained HTML page and
63
+ open it. The system prompt, the project context, the skills index, every tool
64
+ schema, every prompt and reply, every thinking block, every tool call beside
65
+ the result it produced, the per-call token ledger and the files HX changed.
66
+ Late-injected reminders and compacted turns are included behind a toggle.
67
+ A session still running traces as it stands, including the turn that just
68
+ landed. The page follows your system's dark or light setting and a button
69
+ overrides it; nothing is fetched and nothing is uploaded.
70
+ - `hx trace [SESSION_ID] [PATH]`: the same page for any transcript on the
71
+ machine, without starting a session. Defaults to the last session in this
72
+ directory and prints only the path, so `open "$(hx trace)"` works. Either
73
+ argument may be given alone - a lone one that looks like a path is one.
74
+ - The system prompt and the tool schemas are now written to the transcript on a
75
+ session's first provider call, so a trace of a finished session can say what
76
+ the model was told and not only what it said.
77
+
78
+ ### Security
79
+
80
+ - Permission rules are matched against the path a specifier resolves to, not
81
+ the text it was written as. `deny: Read(~/.ssh/**)` was got past by asking
82
+ for `../../.ssh/id_rsa`, or through a symlink planted in the project. The
83
+ same resolution stops an allow rule leaking: `allow: Edit(src/**)` used to
84
+ cover `src/../../etc/passwd`.
85
+ - `WebFetch` is matched against every URL it was given rather than the first,
86
+ so `deny: WebFetch(https://host/**)` is no longer a question of list order.
87
+ The approval prompt spells the URLs out too; it said `urls=[2 items]`, which
88
+ answered the only question that mattered with a number.
89
+ - The generated Seatbelt profile no longer grants write access to all of
90
+ `/private/var/folders` - every user's and every process's temp and cache.
91
+ `$TMPDIR` was resolved precisely to avoid that, and the profile handed it
92
+ back on the next line. This user's own container stays writable.
93
+ - `~/.hx/auth.json` is created with mode 0600 rather than narrowed to it after
94
+ the tokens are written, closing a window at a predictable path where the
95
+ credential was readable by any local account.
96
+ - The OAuth callback page escapes what the provider sent. An `error` parameter
97
+ carrying markup ran as HTML on a loopback origin that is otherwise the
98
+ user's own.
99
+
100
+ ### Fixed
101
+
102
+ - Slash commands read the session that is in force rather than the one HX
103
+ opened with. After `/resume`, `/trace` wrote a page of zeros into the empty
104
+ startup session's directory and `/cost` reported nobody's usage: the command
105
+ context had captured the session object at startup, and `/resume` rebinds it.
106
+ - A session whose title was the trace template's own data placeholder had the
107
+ whole payload spliced into the page's `<title>`.
108
+ - A trace labelled an encrypted reasoning block "thinking (signed)", which read
109
+ as though the page had dropped the rest of it. It now says the text is the
110
+ provider's summary and the reasoning came back encrypted, and a block that
111
+ arrived with no summary at all says that rather than rendering an empty box.
112
+ - MCP servers survive a large response. One JSON-RPC message is one line and
113
+ asyncio's stream limit is 64 KiB, so a tool returning a file did not truncate
114
+ - it raised out of the read loop and killed the connection for the rest of
115
+ the session. The limit is now 16 MiB, and overshooting it says so.
116
+ - A tool call is no longer dropped when a stream ends without a `finish_reason`.
117
+ The arguments were buffered until that field arrived, so a complete call was
118
+ discarded and the turn reported `end_turn`: the model appeared to stop for no
119
+ reason, and the tokens were already paid for.
120
+ - The Linux sandbox starts again for anyone with credential *files*. Denied
121
+ paths were all shadowed with `--tmpfs`, which mounts a directory and aborts
122
+ bwrap when aimed at a file - and six of them are files, `~/.hx/auth.json`
123
+ among them. Files are shadowed with `/dev/null` instead. bwrap also starts in
124
+ the policy's directory rather than whatever `$PWD` happened to hold.
125
+ - `PersistentShell.cwd` follows the shell. It claimed to track it and was in
126
+ fact the launch directory forever, which meant a background job always
127
+ started where the session opened rather than where `cd` had left it.
128
+ - A background job no longer leaks its log file descriptor for the life of the
129
+ session.
130
+ - A record appended while the transcript was being written is no longer
131
+ dropped. The queue is taken in one step, and a failed write puts its records
132
+ back rather than losing them.
133
+ - An automatic compaction with nothing to summarise is no longer announced and
134
+ rerun every turn for the rest of the session. `/compact` still answers either
135
+ way.
136
+ - `/trace` takes `--no-open`, and never launches a *terminal* browser: with no
137
+ display, `webbrowser` falls through to lynx or w3m, which opens in the
138
+ terminal HX is drawing on and takes the session display with it.
139
+
140
+ ---
141
+
31
142
  ## [0.2.3] - 2026-09-18
32
143
 
33
144
  ### Added
@@ -481,7 +592,9 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
481
592
  - `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
482
593
  self-update, and `install.sh` bootstrapping uv with a pinned Python.
483
594
 
484
- [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.3...HEAD
595
+ [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.5...HEAD
596
+ [0.2.5]: https://github.com/aletisunil/hx/compare/v0.2.4...v0.2.5
597
+ [0.2.4]: https://github.com/aletisunil/hx/compare/v0.2.3...v0.2.4
485
598
  [0.2.3]: https://github.com/aletisunil/hx/compare/v0.2.2...v0.2.3
486
599
  [0.2.2]: https://github.com/aletisunil/hx/compare/v0.2.1...v0.2.2
487
600
  [0.2.1]: https://github.com/aletisunil/hx/compare/v0.2.0...v0.2.1
@@ -1,3 +1,43 @@
1
+ Metadata-Version: 2.5
2
+ Name: hx-cli
3
+ Version: 0.2.5
4
+ Summary: HX - an agent harness for the terminal
5
+ Project-URL: Homepage, https://hx.sunilaleti.dev
6
+ Project-URL: Documentation, https://hx.sunilaleti.dev
7
+ Project-URL: Source, https://github.com/aletisunil/hx
8
+ Project-URL: Issues, https://github.com/aletisunil/hx/issues
9
+ Author: Sunil Aleti
10
+ License: MIT
11
+ Keywords: agent,cli,coding-agent,llm,openrouter,tui
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: markdown-it-py>=3.0
21
+ Requires-Dist: pillow>=10.4
22
+ Requires-Dist: pydantic>=2.7
23
+ Requires-Dist: pygments>=2.17
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: truststore>=0.10
26
+ Requires-Dist: wcwidth>=0.2.13
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy>=1.11; extra == 'dev'
29
+ Requires-Dist: pyte>=0.8; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
32
+ Requires-Dist: pytest>=8.2; extra == 'dev'
33
+ Requires-Dist: ruff>=0.6; extra == 'dev'
34
+ Requires-Dist: types-pygments; extra == 'dev'
35
+ Requires-Dist: types-pyyaml; extra == 'dev'
36
+ Provides-Extra: symbols
37
+ Requires-Dist: tree-sitter-language-pack>=0.7; extra == 'symbols'
38
+ Requires-Dist: tree-sitter>=0.23; extra == 'symbols'
39
+ Description-Content-Type: text/markdown
40
+
1
41
  # HX
2
42
 
3
43
  An agent harness for the terminal. Python core, its own terminal renderer,
@@ -257,9 +297,13 @@ available through OpenRouter.
257
297
  ```sh
258
298
  hx # interactive TUI in the current directory
259
299
  hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
300
+ hx -p "why?" --image s.png # attach an image to a headless prompt; repeatable
260
301
  hx resume # resume the last session here
261
302
  hx resume <session-id> # resume a specific one
262
303
  hx prompt # print the system prompt this directory would use
304
+ hx trace # write the last session here to an HTML page
305
+ hx trace out.html # the same, to a path you choose
306
+ hx trace <id> out.html # a specific session, to a path you choose
263
307
  hx --model openai/gpt-5 # override the model for one run
264
308
  hx --mode plan # start read-only
265
309
  hx --cwd ../other-project # run against a different directory
@@ -333,6 +377,7 @@ be rebound (see [Keybindings](#keybindings)).
333
377
  | `pgdn` | scroll the transcript down (fullscreen; otherwise your terminal scrolls) |
334
378
  | `ctrl+home` | jump to the start: the viewport in fullscreen, the first message otherwise |
335
379
  | `ctrl+end` | jump back to the newest output |
380
+ | `ctrl+v` | attach the image on the clipboard (see [Images](#images)) |
336
381
  | `@path` | complete a file path |
337
382
  | `!command` | run a shell command directly, no model turn |
338
383
 
@@ -364,6 +409,44 @@ text](#selecting-and-copying-text) for the terminal's own selection.
364
409
  HX does not emit OSC 133 prompt markers yet, so shell-integration features that
365
410
  jump between prompts will not see HX's messages.
366
411
 
412
+ #### Images
413
+
414
+ Give the model a picture three ways, and each lands in the prompt as a token
415
+ such as `[Image #1]`:
416
+
417
+ - **`ctrl+v`** attaches whatever image is on the clipboard: a screenshot copied
418
+ with `cmd+ctrl+shift+4`, an image copied in a browser, or an image file
419
+ copied in Finder or a file manager. Terminals whose own paste sends nothing
420
+ when the clipboard holds a picture are caught too - an empty paste is read as
421
+ a request for the image.
422
+ - **Drag an image file** into the terminal. The terminal pastes its path, and a
423
+ paste that is nothing but paths to image files attaches them instead of
424
+ inserting the text. Quoted, backslash-escaped and `file://` paths all work.
425
+ - **Mention it.** `look at @docs/mockup.png` sends the path as text, and the
426
+ model opens it with `Read`, which returns images as images.
427
+
428
+ The token is the attachment: move it and the text refers to the picture in a
429
+ different place, delete it (one `backspace` takes the whole token) and the
430
+ image is not sent. Sent messages list their images underneath, with size and
431
+ weight. Numbers run for the whole session, so a message recalled from history
432
+ or restored by a rewind still carries its pictures.
433
+
434
+ PNG, JPEG, GIF, WebP, BMP and TIFF are accepted. An image within every route's
435
+ limits goes byte for byte; a larger one is scaled to 2000 px on its long side
436
+ and re-encoded to stay under 5 MB, which is what a retina screenshot usually
437
+ needs. Images an MCP tool returns - a browser screenshot, say - are handled the
438
+ same way rather than dropped.
439
+
440
+ Whether a model can see images comes from the catalogue: OpenRouter's input
441
+ modalities, the Codex catalogue, and Devin's model config. The `/model` picker
442
+ marks the ones that can with `images`. A model that cannot
443
+ is warned about when you attach, and is sent a line saying an image was left
444
+ out, rather than a request that fails. The transcript keeps the image either
445
+ way, so `/model` to one that can see it and ask again.
446
+
447
+ Over SSH the clipboard you copied into is on your own machine, so `ctrl+v`
448
+ says so; copy the file over and drag or paste its path instead.
449
+
367
450
  #### Keybindings
368
451
 
369
452
  Any key can be rebound in `~/.hx/keybindings.json`, keyed by the action ids in
@@ -407,6 +490,7 @@ than being silently resolved.
407
490
  | `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
408
491
  | `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
409
492
  | `/copy` | copy the last reply to the clipboard |
493
+ | `/trace [path]` | write the whole session to a self-contained HTML page |
410
494
  | `/fullscreen [on\|off]` | take the whole window, with the prompt pinned to the bottom |
411
495
  | `/mouse [on\|off]` | mouse reporting, and with it your terminal's own text selection |
412
496
  | `/init` | generate an `AGENTS.md` for the project |
@@ -652,6 +736,7 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
652
736
  | `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
653
737
  | `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
654
738
  | `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
739
+ | `~/.hx/sessions/<id>/trace.html` | where `/trace` writes, unless you name a path |
655
740
  | `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
656
741
  | `~/.hx/projects/<project>/settings.local.json` | this machine's settings for one project - where "always allow" lands |
657
742
  | `./.hx/settings.json` | project settings, checked in if you like — read by HX, never written |
@@ -735,6 +820,79 @@ rest to the session directory, and hands the model that path to grep.
735
820
 
736
821
  ---
737
822
 
823
+ ## Tracing a session
824
+
825
+ `/trace` writes everything that happened to one self-contained HTML file and
826
+ opens it in a browser:
827
+
828
+ ```
829
+ /trace # ~/.hx/sessions/<id>/trace.html
830
+ /trace ~/Desktop/bug.html # somewhere you choose
831
+ ```
832
+
833
+ Everything means everything. The system prompt, the project context, the skills
834
+ index, every tool schema as the provider received it, every prompt, every
835
+ reply, every thinking block, every tool call next to the result it produced,
836
+ the per-call token ledger, and the files HX changed. Late-injected reminders
837
+ and compacted turns are in there too, behind a toggle that says how many there
838
+ are — a trace that hides the reminders and the superseded turns is not a trace
839
+ of the session that ran.
840
+
841
+ Nothing is summarised and nothing is prettified. Assistant text is shown as it
842
+ arrived, backticks and all, because a trace exists to answer "what actually
843
+ went through" and a rendered version of that is a different document. Long tool
844
+ results are folded rather than truncated, with the character count on the fold.
845
+
846
+ **Thinking, where the provider allows it.** How much of a model's reasoning HX
847
+ ever sees is the provider's decision, not HX's. Anthropic's models return the
848
+ thinking itself. The Responses API - the OpenAI and Codex routes - returns a
849
+ one-line summary in plaintext and the reasoning encrypted, to be replayed on
850
+ the next call and read by nobody, HX included. A trace labels which of the two
851
+ it is holding, because a one-line thinking block is otherwise indistinguishable
852
+ from a trace that dropped the rest.
853
+
854
+ A running session traces as it stands right now, including the turn that just
855
+ landed. The page says so, because a trace of a live session is a snapshot.
856
+
857
+ **Reading it.** Long tool results are folded and the reference panels - the
858
+ prompt, the schemas, the ledger - start shut, so the transcript is what you
859
+ land on. "Expand all" opens the whole page, panels included, and turns into
860
+ "Collapse all". A session with no turns yet opens its panels on its own, since
861
+ there is nothing else to read.
862
+
863
+ **Dark or light.** The page follows your system by default and a button in the
864
+ corner overrides it, remembered per browser. No stylesheet is fetched to do it.
865
+
866
+ **Nothing is uploaded and nothing is fetched.** The payload, the stylesheet and
867
+ the script all live in the file, so it opens offline, renders the same a year
868
+ later, and a trace of a private repository stays as private as the directory
869
+ you wrote it into. The embedded payload is also the machine-readable trace: it
870
+ is JSON, in a `<script id="trace-data">` element, and `/trace` is the only way
871
+ HX will ever put your transcript anywhere but `~/.hx`.
872
+
873
+ From outside a session, on any transcript on the machine:
874
+
875
+ ```sh
876
+ hx trace # the last session in this directory
877
+ hx trace 20260918-214743-b5b8f9 # one by id
878
+ hx trace ~/somewhere.html # the last session, to a path you choose
879
+ hx trace <id> ~/somewhere.html # both, to a path or a directory
880
+ open "$(hx trace)" # the path is all that goes to stdout
881
+ ```
882
+
883
+ `hx trace` reads the transcript and runs nothing, so it works on a session that
884
+ ended months ago and on one another terminal has open right now.
885
+
886
+ **What a trace can be missing.** The prompt and the tool schemas are written to
887
+ the transcript on the session's first provider call — the first moment the tool
888
+ registry has settled, because MCP servers connect after the loop is built. A
889
+ session traced before that call, or one recorded by a version of HX older than
890
+ this feature, has the transcript but not the prompt, and the page leaves those
891
+ panels out rather than guessing. `/trace` inside a live session fills them in
892
+ from the loop either way.
893
+
894
+ ---
895
+
738
896
  ## Reading and editing code
739
897
 
740
898
  **Line anchors.** `Read` labels every line with a short content hash:
@@ -760,6 +918,10 @@ and is unchanged; anchors are a second shape, not a replacement. Turn the whole
760
918
  thing off with `"tools": {"hashline": false}` and reads go back to plain
761
919
  `cat -n`.
762
920
 
921
+ **Images.** `Read` on a PNG, JPEG, GIF, WebP, BMP or TIFF - or on a file with
922
+ no extension whose bytes are one - returns the image itself for the model to
923
+ look at, normalised the way a pasted one is (see [Images](#images)).
924
+
763
925
  **Symbols** answers structural questions with a parse tree rather than a
764
926
  regex — three modes, `outline` for one file's shape, `definition` for where a
765
927
  name is defined, `references` for where it is used:
@@ -1,40 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: hx-cli
3
- Version: 0.2.3
4
- Summary: HX - an agent harness for the terminal
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: httpx>=0.27
18
- Requires-Dist: markdown-it-py>=3.0
19
- Requires-Dist: pydantic>=2.7
20
- Requires-Dist: pygments>=2.17
21
- Requires-Dist: pyyaml>=6.0
22
- Requires-Dist: truststore>=0.10
23
- Requires-Dist: wcwidth>=0.2.13
24
- Provides-Extra: dev
25
- Requires-Dist: mypy>=1.11; extra == 'dev'
26
- Requires-Dist: pyte>=0.8; extra == 'dev'
27
- Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
28
- Requires-Dist: pytest-cov>=5.0; extra == 'dev'
29
- Requires-Dist: pytest>=8.2; extra == 'dev'
30
- Requires-Dist: ruff>=0.6; extra == 'dev'
31
- Requires-Dist: types-pygments; extra == 'dev'
32
- Requires-Dist: types-pyyaml; extra == 'dev'
33
- Provides-Extra: symbols
34
- Requires-Dist: tree-sitter-language-pack>=0.7; extra == 'symbols'
35
- Requires-Dist: tree-sitter>=0.23; extra == 'symbols'
36
- Description-Content-Type: text/markdown
37
-
38
1
  # HX
39
2
 
40
3
  An agent harness for the terminal. Python core, its own terminal renderer,
@@ -294,9 +257,13 @@ available through OpenRouter.
294
257
  ```sh
295
258
  hx # interactive TUI in the current directory
296
259
  hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
260
+ hx -p "why?" --image s.png # attach an image to a headless prompt; repeatable
297
261
  hx resume # resume the last session here
298
262
  hx resume <session-id> # resume a specific one
299
263
  hx prompt # print the system prompt this directory would use
264
+ hx trace # write the last session here to an HTML page
265
+ hx trace out.html # the same, to a path you choose
266
+ hx trace <id> out.html # a specific session, to a path you choose
300
267
  hx --model openai/gpt-5 # override the model for one run
301
268
  hx --mode plan # start read-only
302
269
  hx --cwd ../other-project # run against a different directory
@@ -370,6 +337,7 @@ be rebound (see [Keybindings](#keybindings)).
370
337
  | `pgdn` | scroll the transcript down (fullscreen; otherwise your terminal scrolls) |
371
338
  | `ctrl+home` | jump to the start: the viewport in fullscreen, the first message otherwise |
372
339
  | `ctrl+end` | jump back to the newest output |
340
+ | `ctrl+v` | attach the image on the clipboard (see [Images](#images)) |
373
341
  | `@path` | complete a file path |
374
342
  | `!command` | run a shell command directly, no model turn |
375
343
 
@@ -401,6 +369,44 @@ text](#selecting-and-copying-text) for the terminal's own selection.
401
369
  HX does not emit OSC 133 prompt markers yet, so shell-integration features that
402
370
  jump between prompts will not see HX's messages.
403
371
 
372
+ #### Images
373
+
374
+ Give the model a picture three ways, and each lands in the prompt as a token
375
+ such as `[Image #1]`:
376
+
377
+ - **`ctrl+v`** attaches whatever image is on the clipboard: a screenshot copied
378
+ with `cmd+ctrl+shift+4`, an image copied in a browser, or an image file
379
+ copied in Finder or a file manager. Terminals whose own paste sends nothing
380
+ when the clipboard holds a picture are caught too - an empty paste is read as
381
+ a request for the image.
382
+ - **Drag an image file** into the terminal. The terminal pastes its path, and a
383
+ paste that is nothing but paths to image files attaches them instead of
384
+ inserting the text. Quoted, backslash-escaped and `file://` paths all work.
385
+ - **Mention it.** `look at @docs/mockup.png` sends the path as text, and the
386
+ model opens it with `Read`, which returns images as images.
387
+
388
+ The token is the attachment: move it and the text refers to the picture in a
389
+ different place, delete it (one `backspace` takes the whole token) and the
390
+ image is not sent. Sent messages list their images underneath, with size and
391
+ weight. Numbers run for the whole session, so a message recalled from history
392
+ or restored by a rewind still carries its pictures.
393
+
394
+ PNG, JPEG, GIF, WebP, BMP and TIFF are accepted. An image within every route's
395
+ limits goes byte for byte; a larger one is scaled to 2000 px on its long side
396
+ and re-encoded to stay under 5 MB, which is what a retina screenshot usually
397
+ needs. Images an MCP tool returns - a browser screenshot, say - are handled the
398
+ same way rather than dropped.
399
+
400
+ Whether a model can see images comes from the catalogue: OpenRouter's input
401
+ modalities, the Codex catalogue, and Devin's model config. The `/model` picker
402
+ marks the ones that can with `images`. A model that cannot
403
+ is warned about when you attach, and is sent a line saying an image was left
404
+ out, rather than a request that fails. The transcript keeps the image either
405
+ way, so `/model` to one that can see it and ask again.
406
+
407
+ Over SSH the clipboard you copied into is on your own machine, so `ctrl+v`
408
+ says so; copy the file over and drag or paste its path instead.
409
+
404
410
  #### Keybindings
405
411
 
406
412
  Any key can be rebound in `~/.hx/keybindings.json`, keyed by the action ids in
@@ -444,6 +450,7 @@ than being silently resolved.
444
450
  | `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
445
451
  | `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
446
452
  | `/copy` | copy the last reply to the clipboard |
453
+ | `/trace [path]` | write the whole session to a self-contained HTML page |
447
454
  | `/fullscreen [on\|off]` | take the whole window, with the prompt pinned to the bottom |
448
455
  | `/mouse [on\|off]` | mouse reporting, and with it your terminal's own text selection |
449
456
  | `/init` | generate an `AGENTS.md` for the project |
@@ -689,6 +696,7 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
689
696
  | `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
690
697
  | `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
691
698
  | `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
699
+ | `~/.hx/sessions/<id>/trace.html` | where `/trace` writes, unless you name a path |
692
700
  | `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
693
701
  | `~/.hx/projects/<project>/settings.local.json` | this machine's settings for one project - where "always allow" lands |
694
702
  | `./.hx/settings.json` | project settings, checked in if you like — read by HX, never written |
@@ -772,6 +780,79 @@ rest to the session directory, and hands the model that path to grep.
772
780
 
773
781
  ---
774
782
 
783
+ ## Tracing a session
784
+
785
+ `/trace` writes everything that happened to one self-contained HTML file and
786
+ opens it in a browser:
787
+
788
+ ```
789
+ /trace # ~/.hx/sessions/<id>/trace.html
790
+ /trace ~/Desktop/bug.html # somewhere you choose
791
+ ```
792
+
793
+ Everything means everything. The system prompt, the project context, the skills
794
+ index, every tool schema as the provider received it, every prompt, every
795
+ reply, every thinking block, every tool call next to the result it produced,
796
+ the per-call token ledger, and the files HX changed. Late-injected reminders
797
+ and compacted turns are in there too, behind a toggle that says how many there
798
+ are — a trace that hides the reminders and the superseded turns is not a trace
799
+ of the session that ran.
800
+
801
+ Nothing is summarised and nothing is prettified. Assistant text is shown as it
802
+ arrived, backticks and all, because a trace exists to answer "what actually
803
+ went through" and a rendered version of that is a different document. Long tool
804
+ results are folded rather than truncated, with the character count on the fold.
805
+
806
+ **Thinking, where the provider allows it.** How much of a model's reasoning HX
807
+ ever sees is the provider's decision, not HX's. Anthropic's models return the
808
+ thinking itself. The Responses API - the OpenAI and Codex routes - returns a
809
+ one-line summary in plaintext and the reasoning encrypted, to be replayed on
810
+ the next call and read by nobody, HX included. A trace labels which of the two
811
+ it is holding, because a one-line thinking block is otherwise indistinguishable
812
+ from a trace that dropped the rest.
813
+
814
+ A running session traces as it stands right now, including the turn that just
815
+ landed. The page says so, because a trace of a live session is a snapshot.
816
+
817
+ **Reading it.** Long tool results are folded and the reference panels - the
818
+ prompt, the schemas, the ledger - start shut, so the transcript is what you
819
+ land on. "Expand all" opens the whole page, panels included, and turns into
820
+ "Collapse all". A session with no turns yet opens its panels on its own, since
821
+ there is nothing else to read.
822
+
823
+ **Dark or light.** The page follows your system by default and a button in the
824
+ corner overrides it, remembered per browser. No stylesheet is fetched to do it.
825
+
826
+ **Nothing is uploaded and nothing is fetched.** The payload, the stylesheet and
827
+ the script all live in the file, so it opens offline, renders the same a year
828
+ later, and a trace of a private repository stays as private as the directory
829
+ you wrote it into. The embedded payload is also the machine-readable trace: it
830
+ is JSON, in a `<script id="trace-data">` element, and `/trace` is the only way
831
+ HX will ever put your transcript anywhere but `~/.hx`.
832
+
833
+ From outside a session, on any transcript on the machine:
834
+
835
+ ```sh
836
+ hx trace # the last session in this directory
837
+ hx trace 20260918-214743-b5b8f9 # one by id
838
+ hx trace ~/somewhere.html # the last session, to a path you choose
839
+ hx trace <id> ~/somewhere.html # both, to a path or a directory
840
+ open "$(hx trace)" # the path is all that goes to stdout
841
+ ```
842
+
843
+ `hx trace` reads the transcript and runs nothing, so it works on a session that
844
+ ended months ago and on one another terminal has open right now.
845
+
846
+ **What a trace can be missing.** The prompt and the tool schemas are written to
847
+ the transcript on the session's first provider call — the first moment the tool
848
+ registry has settled, because MCP servers connect after the loop is built. A
849
+ session traced before that call, or one recorded by a version of HX older than
850
+ this feature, has the transcript but not the prompt, and the page leaves those
851
+ panels out rather than guessing. `/trace` inside a live session fills them in
852
+ from the loop either way.
853
+
854
+ ---
855
+
775
856
  ## Reading and editing code
776
857
 
777
858
  **Line anchors.** `Read` labels every line with a short content hash:
@@ -797,6 +878,10 @@ and is unchanged; anchors are a second shape, not a replacement. Turn the whole
797
878
  thing off with `"tools": {"hashline": false}` and reads go back to plain
798
879
  `cat -n`.
799
880
 
881
+ **Images.** `Read` on a PNG, JPEG, GIF, WebP, BMP or TIFF - or on a file with
882
+ no extension whose bytes are one - returns the image itself for the model to
883
+ look at, normalised the way a pasted one is (see [Images](#images)).
884
+
800
885
  **Symbols** answers structural questions with a parse tree rather than a
801
886
  regex — three modes, `outline` for one file's shape, `definition` for where a
802
887
  name is defined, `references` for where it is used:
@@ -33,6 +33,7 @@ dependencies = [
33
33
  "pygments>=2.17",
34
34
  # CommonMark parsing. The renderer is ours; the parser is not worth writing.
35
35
  "markdown-it-py>=3.0",
36
+ "pillow>=10.4",
36
37
  ]
37
38
 
38
39
  [project.optional-dependencies]
@@ -60,7 +61,9 @@ dev = [
60
61
  hx = "hx.cli:main"
61
62
 
62
63
  [project.urls]
63
- Homepage = "https://github.com/aletisunil/hx"
64
+ Homepage = "https://hx.sunilaleti.dev"
65
+ Documentation = "https://hx.sunilaleti.dev"
66
+ Source = "https://github.com/aletisunil/hx"
64
67
  Issues = "https://github.com/aletisunil/hx/issues"
65
68
 
66
69
  # The version lives in the package, not here - three hand-maintained copies
@@ -1,5 +1,5 @@
1
1
  """HX - an agent harness for the terminal."""
2
2
 
3
- __version__ = "0.2.3"
3
+ __version__ = "0.2.5"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -12,6 +12,7 @@ where the browser reaches the host by another address.
12
12
  from __future__ import annotations
13
13
 
14
14
  import asyncio
15
+ import html
15
16
  import os
16
17
  import threading
17
18
  from dataclasses import dataclass
@@ -49,7 +50,18 @@ p{{margin:0;color:#9aa4b2}}
49
50
 
50
51
 
51
52
  def _page(title: str, body: str, *, ok: bool) -> bytes:
52
- return _PAGE.format(title=title, body=body, colour="#7ee787" if ok else "#ff7b72").encode()
53
+ """Render the one page this server serves.
54
+
55
+ Both fields are escaped: ``body`` carries the provider's ``error``
56
+ parameter, which arrives from the network and is under nobody's control
57
+ here. Interpolated raw, ``?error=<img src=x onerror=...>`` executed on an
58
+ origin that is otherwise this machine's own.
59
+ """
60
+ return _PAGE.format(
61
+ title=html.escape(title),
62
+ body=html.escape(body),
63
+ colour="#7ee787" if ok else "#ff7b72",
64
+ ).encode()
53
65
 
54
66
 
55
67
  class _Handler(BaseHTTPRequestHandler):
@@ -21,6 +21,7 @@ from __future__ import annotations
21
21
 
22
22
  import asyncio
23
23
  import json
24
+ import os
24
25
  import time
25
26
  from collections.abc import Awaitable, Callable
26
27
  from dataclasses import dataclass, field, replace
@@ -172,9 +173,22 @@ class AuthStore:
172
173
  path.parent.mkdir(parents=True, exist_ok=True)
173
174
  payload = {pid: cred.to_json() for pid, cred in sorted(data.items())}
174
175
 
176
+ # Created 0600, not chmod'd to it afterwards. The file is opened at
177
+ # whatever the umask allows and only narrowed once the tokens are
178
+ # already in it, which leaves a window - at a path anyone can predict -
179
+ # where the credential is world-readable. Passing the mode to `open`
180
+ # closes it: there is no instant at which the descriptor exists and the
181
+ # permissions are wrong.
175
182
  tmp = path.with_suffix(".json.tmp")
176
- tmp.write_text(json.dumps(payload, indent=2))
177
- # Restrict before the rename so the token is never briefly world-readable.
183
+ descriptor = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
184
+ try:
185
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
186
+ handle.write(json.dumps(payload, indent=2))
187
+ except BaseException:
188
+ tmp.unlink(missing_ok=True)
189
+ raise
190
+ # O_CREAT leaves an existing file's mode alone, so a store written by
191
+ # an older HX is narrowed here rather than keeping its old permissions.
178
192
  tmp.chmod(0o600)
179
193
  tmp.replace(path)
180
194