hx-cli 0.2.8__tar.gz → 0.2.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hx_cli-0.2.8 → hx_cli-0.2.10}/CHANGELOG.md +36 -1
- {hx_cli-0.2.8 → hx_cli-0.2.10}/PKG-INFO +16 -2
- {hx_cli-0.2.8 → hx_cli-0.2.10}/README.md +15 -1
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/__init__.py +1 -1
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/agents/subagent.py +6 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/cli.py +18 -5
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/context.py +66 -11
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/loop.py +4 -1
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/paths.py +77 -1
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/commands.py +17 -8
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/format.py +0 -15
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/runtime.py +12 -14
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/views/pickers.py +23 -3
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/test_cli.py +10 -1
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/test_commands.py +101 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/test_extend.py +129 -1
- {hx_cli-0.2.8 → hx_cli-0.2.10}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/.gitignore +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/examples/hooks/check.sh +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/install.sh +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/pyproject.toml +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/auth/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/auth/oauth/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/auth/oauth/browser.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/auth/oauth/callback.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/auth/oauth/codex.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/auth/oauth/devin.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/auth/oauth/pkce.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/auth/resolve.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/auth/store.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/config.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/checkpoints.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/compaction.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/events.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/images.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/messages.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/session.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/title.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/core/usage.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/docs.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/git.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/hooks/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/hooks/engine.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/hooks/spec.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/keys.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/mcp/client.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/mcp/manager.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/mcp/oauth.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/net.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/permissions/engine.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/permissions/sandbox.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/base.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/codex.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/codex_catalogue.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/devin.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/devin_catalogue.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/devin_wire.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/models.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/openrouter.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/protowire.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/registry.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/providers/responses_codec.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/ansi.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/buffer.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/component.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/demo.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/editor.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/keydecode.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/loop.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/markdown.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/modifiers.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/primitives.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/sanitize.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/screen.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/syntax.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/terminal.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/undo.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/width.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/term/word_nav.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/anchors.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/base.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/bash.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/edit.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/output.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/read.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/registry.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/symbols.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/task.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/websearch.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tools/write.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/trace.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/app.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/glyphs.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/limits.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/paint.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/renderers.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/views/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/views/blocks.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/views/dialog.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/views/login.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/views/permission.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/views/prompt.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/views/status.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/src/hx/tui/views/transcript.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/conftest.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/__init__.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/conftest.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/fixtures/echo_server.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/report.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/stub.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/terminal.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/test_chat.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/test_input.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/test_print.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/e2e/test_tools.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/test_live.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/test_live_codex.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/test_live_devin.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/tests/test_live_tavily.py +0 -0
- {hx_cli-0.2.8 → hx_cli-0.2.10}/uv.lock +0 -0
|
@@ -28,6 +28,39 @@ the project follows [semantic versioning](https://semver.org/).
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
## [0.2.10] - 2026-09-29
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- `AGENTS.md` at the repository root now applies when HX is started in a
|
|
36
|
+
subdirectory, followed by each `AGENTS.md` down to that directory.
|
|
37
|
+
- Subagents started with `Task` now follow `~/.hx/AGENTS.md` and the project's
|
|
38
|
+
`AGENTS.md`, as the main session does.
|
|
39
|
+
- A saved `/effort` level shows on the status bar from the first screen of every
|
|
40
|
+
new session, not only after `/effort` is run again.
|
|
41
|
+
- `ctrl+l` switches the model and saves it for later sessions, as `/model` does;
|
|
42
|
+
it used to relabel the status bar only.
|
|
43
|
+
- `/model`, `ctrl+l` and `/effort` open with the cursor on the choice in force.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## [0.2.9] - 2026-09-27
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- `~/.hx/AGENTS.md` holds your own instructions and is loaded into every
|
|
52
|
+
session in every project, ahead of the project's `AGENTS.md`. The first
|
|
53
|
+
session creates it empty, ready to fill in. `hx prompt` and `/prompt` now
|
|
54
|
+
name each `AGENTS.md` in force, and `/prompt` says when one has changed on
|
|
55
|
+
disk since the session started.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- An `AGENTS.md` or system-prompt override that is not valid UTF-8 is skipped
|
|
60
|
+
like an unreadable one, instead of stopping HX from starting.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
31
64
|
## [0.2.8] - 2026-09-27
|
|
32
65
|
|
|
33
66
|
### Changed
|
|
@@ -725,7 +758,9 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
|
|
|
725
758
|
- `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
|
|
726
759
|
self-update, and `install.sh` bootstrapping uv with a pinned Python.
|
|
727
760
|
|
|
728
|
-
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.
|
|
761
|
+
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.10...HEAD
|
|
762
|
+
[0.2.10]: https://github.com/aletisunil/hx/compare/v0.2.9...v0.2.10
|
|
763
|
+
[0.2.9]: https://github.com/aletisunil/hx/compare/v0.2.8...v0.2.9
|
|
729
764
|
[0.2.8]: https://github.com/aletisunil/hx/compare/v0.2.7...v0.2.8
|
|
730
765
|
[0.2.7]: https://github.com/aletisunil/hx/compare/v0.2.6...v0.2.7
|
|
731
766
|
[0.2.6]: https://github.com/aletisunil/hx/compare/v0.2.5...v0.2.6
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hx-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.10
|
|
4
4
|
Summary: HX - an agent harness for the terminal
|
|
5
5
|
Project-URL: Homepage, https://hx.sunilaleti.dev
|
|
6
6
|
Project-URL: Documentation, https://hx.sunilaleti.dev
|
|
@@ -753,6 +753,7 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
753
753
|
| `~/.hx/keybindings.json` | your key overrides |
|
|
754
754
|
| `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
|
|
755
755
|
| `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
|
|
756
|
+
| `~/.hx/AGENTS.md` | your instructions, loaded into every session in every project; created empty |
|
|
756
757
|
| `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
|
|
757
758
|
| `~/.hx/sessions/<id>/trace.html` | where `/trace` writes, unless you name a path |
|
|
758
759
|
| `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
|
|
@@ -761,12 +762,25 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
761
762
|
| `./.hx/mcp.json` | project MCP servers |
|
|
762
763
|
| `./.hx/system-prompt.md`, `./.hx/system-prompt-append.md` | project prompt overrides |
|
|
763
764
|
| `./.hx/skills/`, `./.hx/agents/` | project skills and agents |
|
|
764
|
-
|
|
|
765
|
+
| `AGENTS.md` | project instructions: the repository root's, then each one down to where HX started |
|
|
765
766
|
|
|
766
767
|
`AGENTS.md` is the place for things a newcomer would get wrong: how to run the
|
|
767
768
|
tests, conventions, what not to touch. `/init` writes a first draft. It is
|
|
768
769
|
loaded once per session and frozen, so it costs one prefix, not one per turn.
|
|
769
770
|
|
|
771
|
+
`~/.hx/AGENTS.md` is the same thing for you rather than the project: how you
|
|
772
|
+
like commits written, tools you always want used, in every repository. It is
|
|
773
|
+
loaded first and the project's `AGENTS.md` after it, so where the two disagree
|
|
774
|
+
the project has the last word. The first session creates it empty, ready to
|
|
775
|
+
fill in; empty, it adds nothing. `hx prompt` and `/prompt` list each one in force.
|
|
776
|
+
|
|
777
|
+
Started below the repository root - in `services/billing`, say - HX still loads
|
|
778
|
+
the root's `AGENTS.md`, then every `AGENTS.md` on the way down to that directory,
|
|
779
|
+
the nearest last, so a package can refine the repository's rules. A submodule
|
|
780
|
+
counts as part of the repository around it. Nothing above the repository root is
|
|
781
|
+
read; outside a repository only the current directory is.
|
|
782
|
+
Subagents run under the same files as the session that started them.
|
|
783
|
+
|
|
770
784
|
---
|
|
771
785
|
|
|
772
786
|
## Safety
|
|
@@ -713,6 +713,7 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
713
713
|
| `~/.hx/keybindings.json` | your key overrides |
|
|
714
714
|
| `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
|
|
715
715
|
| `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
|
|
716
|
+
| `~/.hx/AGENTS.md` | your instructions, loaded into every session in every project; created empty |
|
|
716
717
|
| `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
|
|
717
718
|
| `~/.hx/sessions/<id>/trace.html` | where `/trace` writes, unless you name a path |
|
|
718
719
|
| `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
|
|
@@ -721,12 +722,25 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
721
722
|
| `./.hx/mcp.json` | project MCP servers |
|
|
722
723
|
| `./.hx/system-prompt.md`, `./.hx/system-prompt-append.md` | project prompt overrides |
|
|
723
724
|
| `./.hx/skills/`, `./.hx/agents/` | project skills and agents |
|
|
724
|
-
|
|
|
725
|
+
| `AGENTS.md` | project instructions: the repository root's, then each one down to where HX started |
|
|
725
726
|
|
|
726
727
|
`AGENTS.md` is the place for things a newcomer would get wrong: how to run the
|
|
727
728
|
tests, conventions, what not to touch. `/init` writes a first draft. It is
|
|
728
729
|
loaded once per session and frozen, so it costs one prefix, not one per turn.
|
|
729
730
|
|
|
731
|
+
`~/.hx/AGENTS.md` is the same thing for you rather than the project: how you
|
|
732
|
+
like commits written, tools you always want used, in every repository. It is
|
|
733
|
+
loaded first and the project's `AGENTS.md` after it, so where the two disagree
|
|
734
|
+
the project has the last word. The first session creates it empty, ready to
|
|
735
|
+
fill in; empty, it adds nothing. `hx prompt` and `/prompt` list each one in force.
|
|
736
|
+
|
|
737
|
+
Started below the repository root - in `services/billing`, say - HX still loads
|
|
738
|
+
the root's `AGENTS.md`, then every `AGENTS.md` on the way down to that directory,
|
|
739
|
+
the nearest last, so a package can refine the repository's rules. A submodule
|
|
740
|
+
counts as part of the repository around it. Nothing above the repository root is
|
|
741
|
+
read; outside a repository only the current directory is.
|
|
742
|
+
Subagents run under the same files as the session that started them.
|
|
743
|
+
|
|
730
744
|
---
|
|
731
745
|
|
|
732
746
|
## Safety
|
|
@@ -59,6 +59,7 @@ class SubagentRunner:
|
|
|
59
59
|
parent_session_id: str | None = None,
|
|
60
60
|
parent_usage: object | None = None,
|
|
61
61
|
hooks: HookEngine | None = None,
|
|
62
|
+
project_context: str | None = None,
|
|
62
63
|
) -> None:
|
|
63
64
|
self.definitions = definitions
|
|
64
65
|
self.provider = provider
|
|
@@ -72,6 +73,10 @@ class SubagentRunner:
|
|
|
72
73
|
self.hooks = hooks
|
|
73
74
|
"""Shared with the parent: a hook that guards Bash must guard a subagent's
|
|
74
75
|
Bash too, or the guard is one delegation away from being bypassed."""
|
|
76
|
+
self.project_context = project_context
|
|
77
|
+
"""The parent's project preamble, AGENTS.md included. A subagent edits the
|
|
78
|
+
same project under the same rules; without it "never add unit tests" is
|
|
79
|
+
one delegation away from being ignored."""
|
|
75
80
|
self._active: dict[str, str] = {}
|
|
76
81
|
|
|
77
82
|
async def run(self, agent_type: str, prompt: str, description: str) -> SubagentResult:
|
|
@@ -160,6 +165,7 @@ class SubagentRunner:
|
|
|
160
165
|
bus=self.bus,
|
|
161
166
|
settings=self.settings,
|
|
162
167
|
model_info=self.models.get_or_default(model) if self.models else None,
|
|
168
|
+
project_context=self.project_context,
|
|
163
169
|
hooks=self.hooks,
|
|
164
170
|
)
|
|
165
171
|
loop.origin = f"{definition.name} subagent"
|
|
@@ -346,7 +346,12 @@ def build_runtime(parsed: ParsedArgs, *, resume: str | None = None) -> Runtime:
|
|
|
346
346
|
from hx.config import load_settings
|
|
347
347
|
from hx.core.checkpoints import CheckpointStore
|
|
348
348
|
from hx.core.compaction import Compactor
|
|
349
|
-
from hx.core.context import
|
|
349
|
+
from hx.core.context import (
|
|
350
|
+
ContextBuilder,
|
|
351
|
+
build_project_context,
|
|
352
|
+
load_instructions,
|
|
353
|
+
load_system_prompt,
|
|
354
|
+
)
|
|
350
355
|
from hx.core.events import EventBus
|
|
351
356
|
from hx.core.lateinject import Injection, InjectionRegistry
|
|
352
357
|
from hx.core.loop import AgentLoop
|
|
@@ -430,6 +435,8 @@ def build_runtime(parsed: ParsedArgs, *, resume: str | None = None) -> Runtime:
|
|
|
430
435
|
settings.cwd,
|
|
431
436
|
keep_recent_turns=settings.context.keep_recent_turns,
|
|
432
437
|
)
|
|
438
|
+
instructions = load_instructions(settings.cwd)
|
|
439
|
+
project_context = build_project_context(settings.cwd, instructions)
|
|
433
440
|
|
|
434
441
|
checkpoints = CheckpointStore(session, session_checkpoints_dir(session.meta.session_id))
|
|
435
442
|
tools = build_default_registry(shell, jobs, tracker, todos, bus_holder, auth, checkpoints)
|
|
@@ -458,6 +465,7 @@ def build_runtime(parsed: ParsedArgs, *, resume: str | None = None) -> Runtime:
|
|
|
458
465
|
parent_session_id=session.meta.session_id,
|
|
459
466
|
parent_usage=session.usage,
|
|
460
467
|
hooks=hooks,
|
|
468
|
+
project_context=project_context,
|
|
461
469
|
)
|
|
462
470
|
tools.register(TaskTool(subagents))
|
|
463
471
|
|
|
@@ -478,7 +486,8 @@ def build_runtime(parsed: ParsedArgs, *, resume: str | None = None) -> Runtime:
|
|
|
478
486
|
settings=settings,
|
|
479
487
|
model_info=model_info,
|
|
480
488
|
skills_index=build_index(list(skills.values())) or None,
|
|
481
|
-
project_context=
|
|
489
|
+
project_context=project_context,
|
|
490
|
+
instructions=instructions,
|
|
482
491
|
hooks=hooks,
|
|
483
492
|
)
|
|
484
493
|
|
|
@@ -842,15 +851,17 @@ def run_print_command(parsed: ParsedArgs) -> int:
|
|
|
842
851
|
def run_prompt_command(parsed: ParsedArgs) -> int:
|
|
843
852
|
"""``hx prompt`` - print the system prompt this directory resolves to.
|
|
844
853
|
|
|
845
|
-
The prompt goes to stdout so it can be piped or diffed; where it came from
|
|
846
|
-
goes to stderr so it never
|
|
854
|
+
The prompt goes to stdout so it can be piped or diffed; where it came from,
|
|
855
|
+
and which AGENTS.md files ride alongside it, goes to stderr so it never
|
|
856
|
+
contaminates that output.
|
|
847
857
|
"""
|
|
848
858
|
from hx.config import load_settings
|
|
849
|
-
from hx.core.context import resolve_system_prompt
|
|
859
|
+
from hx.core.context import load_instructions, resolve_system_prompt
|
|
850
860
|
|
|
851
861
|
try:
|
|
852
862
|
settings = load_settings(parsed.cwd, parsed.overrides)
|
|
853
863
|
resolved = resolve_system_prompt(settings.cwd, settings.prompt)
|
|
864
|
+
instructions = load_instructions(settings.cwd)
|
|
854
865
|
except Exception as exc:
|
|
855
866
|
return _report(exc)
|
|
856
867
|
|
|
@@ -858,6 +869,8 @@ def run_prompt_command(parsed: ParsedArgs) -> int:
|
|
|
858
869
|
print(f"[source] {resolved.source}", file=sys.stderr)
|
|
859
870
|
for append in resolved.appends:
|
|
860
871
|
print(f"[append] {append}", file=sys.stderr)
|
|
872
|
+
for loaded in instructions:
|
|
873
|
+
print(f"[instructions] {loaded.path}", file=sys.stderr)
|
|
861
874
|
return 0
|
|
862
875
|
|
|
863
876
|
|
|
@@ -10,7 +10,8 @@ Layout, in order::
|
|
|
10
10
|
[1] system prompt static for the session
|
|
11
11
|
[2] tool schemas deterministic sort: builtins, then mcp__* alphabetical
|
|
12
12
|
[3] skills index name + description only (progressive disclosure)
|
|
13
|
-
[4] project context
|
|
13
|
+
[4] project context cwd, git branch, top-level listing, ~/.hx/AGENTS.md,
|
|
14
|
+
each AGENTS.md from the repository root down to cwd
|
|
14
15
|
--- breakpoint A (static) ---
|
|
15
16
|
[5] conversation history
|
|
16
17
|
--- breakpoint B (rolling, before the last few turns) ---
|
|
@@ -238,11 +239,63 @@ def _message_text(message: Message) -> str:
|
|
|
238
239
|
return "\n".join(parts)
|
|
239
240
|
|
|
240
241
|
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
@dataclass(slots=True)
|
|
243
|
+
class Instructions:
|
|
244
|
+
"""One AGENTS.md in force: what it is, where it lives, what it says."""
|
|
245
|
+
|
|
246
|
+
heading: str
|
|
247
|
+
path: Path
|
|
248
|
+
text: str
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def load_instructions(cwd: Path) -> list[Instructions]:
|
|
252
|
+
"""Every AGENTS.md in force, least specific first.
|
|
253
|
+
|
|
254
|
+
``~/.hx/AGENTS.md`` follows the user into every project; then each
|
|
255
|
+
``AGENTS.md`` from the repository root down to ``cwd``. Where two disagree
|
|
256
|
+
the more specific one comes later and has the last word. A blank or
|
|
257
|
+
unreadable file is skipped, and a path reached twice - running from inside
|
|
258
|
+
``$HX_HOME``, say - is loaded once.
|
|
259
|
+
"""
|
|
260
|
+
from hx.paths import project_instructions_files, project_root, tilde, user_instructions_file
|
|
261
|
+
|
|
262
|
+
user = user_instructions_file()
|
|
263
|
+
here = cwd.resolve()
|
|
264
|
+
root = project_root(here)
|
|
265
|
+
base = root or here
|
|
266
|
+
candidates = [(f"User instructions ({tilde(user)})", user)]
|
|
267
|
+
for path in project_instructions_files(here, root):
|
|
268
|
+
where = path.relative_to(base).as_posix()
|
|
269
|
+
candidates.append((f"Project instructions ({where})", path))
|
|
270
|
+
|
|
271
|
+
found: list[Instructions] = []
|
|
272
|
+
seen: set[Path] = set()
|
|
273
|
+
for heading, path in candidates:
|
|
274
|
+
body = _read_prompt_file(path)
|
|
275
|
+
if not body or path.resolve() in seen:
|
|
276
|
+
continue
|
|
277
|
+
seen.add(path.resolve())
|
|
278
|
+
found.append(Instructions(heading, path, body))
|
|
279
|
+
return found
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
INSTRUCTIONS_PREAMBLE = (
|
|
283
|
+
"The sections below are AGENTS.md files: standing instructions from the user "
|
|
284
|
+
"and this project, already loaded in full - there is no need to read them "
|
|
285
|
+
"again. Follow them over your own defaults and habits. Where two disagree, "
|
|
286
|
+
"the later, more specific one wins."
|
|
287
|
+
)
|
|
288
|
+
"""Said once, ahead of the files. Without it a rule like "no unit tests" reads
|
|
289
|
+
as background next to a request to make something "well covered", and the
|
|
290
|
+
model re-reads the file it was already given to find out whether it counts."""
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def build_project_context(cwd: Path, instructions: list[Instructions] | None = None) -> str:
|
|
294
|
+
"""Static per-session project preamble: cwd, git branch, listing, AGENTS.md contents.
|
|
243
295
|
|
|
244
296
|
Computed once at startup and then frozen - refreshing it mid-session would
|
|
245
|
-
invalidate the prefix.
|
|
297
|
+
invalidate the prefix. Pass ``instructions`` to fold in the AGENTS.md files
|
|
298
|
+
already loaded, so the caller can remember exactly which ones are in force.
|
|
246
299
|
"""
|
|
247
300
|
lines = [f"Working directory: {cwd}"]
|
|
248
301
|
|
|
@@ -256,9 +309,11 @@ def build_project_context(cwd: Path) -> str:
|
|
|
256
309
|
if entries:
|
|
257
310
|
lines.append("Top level: " + ", ".join(entries[:60]))
|
|
258
311
|
|
|
259
|
-
|
|
260
|
-
if
|
|
261
|
-
lines.append(f"\n
|
|
312
|
+
loaded = load_instructions(cwd) if instructions is None else instructions
|
|
313
|
+
if loaded:
|
|
314
|
+
lines.append(f"\n{INSTRUCTIONS_PREAMBLE}")
|
|
315
|
+
for entry in loaded:
|
|
316
|
+
lines.append(f"\n# {entry.heading}\n\n{entry.text}")
|
|
262
317
|
|
|
263
318
|
return "\n".join(lines)
|
|
264
319
|
|
|
@@ -367,12 +422,12 @@ def resolve_system_prompt(cwd: Path, prompt: PromptSettings | None = None) -> Re
|
|
|
367
422
|
def _read_prompt_file(path: Path) -> str:
|
|
368
423
|
"""Contents of a prompt override file, or ``""`` when it is absent or empty.
|
|
369
424
|
|
|
370
|
-
An unreadable file is treated as absent: a
|
|
371
|
-
optional override must not stop the session from starting.
|
|
425
|
+
An unreadable file - no permission, or not UTF-8 - is treated as absent: a
|
|
426
|
+
problem with an optional override must not stop the session from starting.
|
|
372
427
|
"""
|
|
373
428
|
try:
|
|
374
|
-
return path.read_text().strip() if path.is_file() else ""
|
|
375
|
-
except OSError:
|
|
429
|
+
return path.read_text(encoding="utf-8").strip() if path.is_file() else ""
|
|
430
|
+
except (OSError, UnicodeDecodeError):
|
|
376
431
|
return ""
|
|
377
432
|
|
|
378
433
|
|
|
@@ -18,7 +18,7 @@ import time
|
|
|
18
18
|
from dataclasses import dataclass, replace
|
|
19
19
|
from typing import TYPE_CHECKING, Any, ClassVar
|
|
20
20
|
|
|
21
|
-
from hx.core.context import AssembledContext
|
|
21
|
+
from hx.core.context import AssembledContext, Instructions
|
|
22
22
|
from hx.core.events import (
|
|
23
23
|
CompactionFinished,
|
|
24
24
|
CompactionStarted,
|
|
@@ -103,6 +103,7 @@ class AgentLoop:
|
|
|
103
103
|
active_skills: ActiveSkills | None = None,
|
|
104
104
|
skills_index: str | None = None,
|
|
105
105
|
project_context: str | None = None,
|
|
106
|
+
instructions: list[Instructions] | None = None,
|
|
106
107
|
hooks: HookEngine | None = None,
|
|
107
108
|
) -> None:
|
|
108
109
|
self.provider = provider
|
|
@@ -118,6 +119,8 @@ class AgentLoop:
|
|
|
118
119
|
self.active_skills = active_skills
|
|
119
120
|
self.skills_index = skills_index
|
|
120
121
|
self.project_context = project_context
|
|
122
|
+
self.instructions = instructions or []
|
|
123
|
+
"""The AGENTS.md files folded into ``project_context``, least specific first."""
|
|
121
124
|
self.hooks = hooks
|
|
122
125
|
self._cancelled = False
|
|
123
126
|
self._turn_index = 0
|
|
@@ -30,6 +30,20 @@ def user_home() -> Path:
|
|
|
30
30
|
return Path(override).expanduser() if override else Path.home() / ".hx"
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def tilde(path: str | Path) -> str:
|
|
34
|
+
"""``path`` with the home directory written as ``~``.
|
|
35
|
+
|
|
36
|
+
Compared as text, not resolved: a session records the path it resolved
|
|
37
|
+
when it was made, and the directory may be gone by the time it is drawn.
|
|
38
|
+
"""
|
|
39
|
+
text, home = str(path), str(Path.home())
|
|
40
|
+
if text == home:
|
|
41
|
+
return "~"
|
|
42
|
+
if text.startswith(home.rstrip("/") + "/"):
|
|
43
|
+
return "~" + text[len(home.rstrip("/")) :]
|
|
44
|
+
return text
|
|
45
|
+
|
|
46
|
+
|
|
33
47
|
def user_settings_file() -> Path:
|
|
34
48
|
return user_home() / "settings.json"
|
|
35
49
|
|
|
@@ -198,7 +212,69 @@ def project_system_prompt_append_file(cwd: Path | None = None) -> Path:
|
|
|
198
212
|
return project_dir(cwd) / "system-prompt-append.md"
|
|
199
213
|
|
|
200
214
|
|
|
215
|
+
def user_instructions_file() -> Path:
|
|
216
|
+
"""Your ``AGENTS.md``: instructions that follow you into every project."""
|
|
217
|
+
return user_home() / "AGENTS.md"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def project_root(cwd: Path | None = None) -> Path | None:
|
|
221
|
+
"""The repository ``cwd`` sits in: the nearest directory holding ``.git``.
|
|
222
|
+
|
|
223
|
+
``.git`` is a directory in a clone and a file in a worktree or submodule.
|
|
224
|
+
A submodule is part of the repository around it, so the walk carries on
|
|
225
|
+
past one to the superproject. ``None`` outside a repository.
|
|
226
|
+
"""
|
|
227
|
+
start = (cwd or Path.cwd()).resolve()
|
|
228
|
+
submodule: Path | None = None
|
|
229
|
+
for directory in (start, *start.parents):
|
|
230
|
+
marker = directory / ".git"
|
|
231
|
+
if marker.is_dir() or (marker.is_file() and not _is_submodule(marker)):
|
|
232
|
+
return directory
|
|
233
|
+
if marker.is_file() and submodule is None:
|
|
234
|
+
submodule = directory
|
|
235
|
+
return submodule
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _is_submodule(marker: Path) -> bool:
|
|
239
|
+
"""Whether a ``.git`` file points into a superproject's ``.git/modules``."""
|
|
240
|
+
try:
|
|
241
|
+
text = marker.read_text(encoding="utf-8", errors="replace")
|
|
242
|
+
except OSError:
|
|
243
|
+
return False
|
|
244
|
+
return text.startswith("gitdir:") and "/modules/" in text.replace("\\", "/")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def project_instructions_files(cwd: Path, root: Path | None) -> list[Path]:
|
|
248
|
+
"""Every ``AGENTS.md`` that governs ``cwd``, least specific first.
|
|
249
|
+
|
|
250
|
+
From the repository root down to ``cwd``, one candidate per directory, so
|
|
251
|
+
starting HX in ``src/app`` still finds the root's file - and ``src/app``'s
|
|
252
|
+
own, which comes last and so has the last word. Outside a repository only
|
|
253
|
+
``cwd`` itself is looked at: walking up from an arbitrary directory would
|
|
254
|
+
pick up whatever happens to sit in its parents.
|
|
255
|
+
|
|
256
|
+
``root`` is :func:`project_root` of ``cwd``, passed in because the caller
|
|
257
|
+
needs it too and finding it walks the file system.
|
|
258
|
+
"""
|
|
259
|
+
here = cwd.resolve()
|
|
260
|
+
if root is None:
|
|
261
|
+
return [here / "AGENTS.md"]
|
|
262
|
+
chain = [here, *here.parents]
|
|
263
|
+
return [directory / "AGENTS.md" for directory in reversed(chain[: chain.index(root) + 1])]
|
|
264
|
+
|
|
265
|
+
|
|
201
266
|
def ensure_user_dirs() -> None:
|
|
202
|
-
"""Create the user-level
|
|
267
|
+
"""Create the user-level skeleton if it does not exist.
|
|
268
|
+
|
|
269
|
+
That includes an empty ``AGENTS.md``, so there is a file to find and fill
|
|
270
|
+
in; empty, it adds nothing to the prompt. An existing one - including a
|
|
271
|
+
symlink, dangling or not - is never touched, and failing to create it never
|
|
272
|
+
stops a session: the file is optional.
|
|
273
|
+
"""
|
|
203
274
|
for path in (user_home(), sessions_dir(), user_skills_dir(), user_agents_dir(), logs_dir()):
|
|
204
275
|
path.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
try:
|
|
277
|
+
with user_instructions_file().open("x"):
|
|
278
|
+
pass
|
|
279
|
+
except OSError:
|
|
280
|
+
pass
|
|
@@ -107,7 +107,10 @@ async def cmd_model(ctx: CommandContext, args: str) -> None:
|
|
|
107
107
|
from hx.providers.models import ModelRegistry
|
|
108
108
|
from hx.tui.views.pickers import ModelPicker
|
|
109
109
|
|
|
110
|
-
registry: ModelRegistry = ctx.app.models
|
|
110
|
+
registry: ModelRegistry | None = ctx.app.models
|
|
111
|
+
if registry is None:
|
|
112
|
+
ctx.app.notice("No model registry is loaded.", "warning")
|
|
113
|
+
return
|
|
111
114
|
models = _reachable(ctx, registry.all())
|
|
112
115
|
if not models:
|
|
113
116
|
# A recorded failure replaces the remedy rather than trailing it:
|
|
@@ -507,7 +510,7 @@ async def cmd_title(ctx: CommandContext, args: str) -> None:
|
|
|
507
510
|
|
|
508
511
|
async def cmd_prompt(ctx: CommandContext, args: str) -> None:
|
|
509
512
|
"""``/prompt`` - the system prompt this session is actually running with."""
|
|
510
|
-
from hx.core.context import resolve_system_prompt
|
|
513
|
+
from hx.core.context import load_instructions, resolve_system_prompt
|
|
511
514
|
|
|
512
515
|
in_use = ctx.app.loop.context.system_prompt
|
|
513
516
|
lines = [in_use.rstrip()]
|
|
@@ -516,10 +519,15 @@ async def cmd_prompt(ctx: CommandContext, args: str) -> None:
|
|
|
516
519
|
lines.append(f"\nSource: {resolved.source}")
|
|
517
520
|
for append in resolved.appends:
|
|
518
521
|
lines.append(f"Appended: {append}")
|
|
522
|
+
in_force = ctx.app.loop.instructions
|
|
523
|
+
for loaded in in_force:
|
|
524
|
+
lines.append(f"Instructions: {loaded.path}")
|
|
525
|
+
# The prompt and AGENTS.md are read once at startup to keep the cache prefix
|
|
526
|
+
# stable, so an edit since then is real but not yet in force.
|
|
519
527
|
if resolved.text.strip() != in_use.strip():
|
|
520
|
-
# The prompt is read once at startup to keep the cache prefix stable, so
|
|
521
|
-
# an override edited since then is real but not yet in force.
|
|
522
528
|
lines.append("An override has changed on disk since startup; it applies next run.")
|
|
529
|
+
if load_instructions(ctx.settings.cwd) != in_force:
|
|
530
|
+
lines.append("An AGENTS.md has changed on disk since startup; it applies next run.")
|
|
523
531
|
ctx.app.notice("\n".join(lines))
|
|
524
532
|
|
|
525
533
|
|
|
@@ -741,8 +749,7 @@ async def cmd_permissions(ctx: CommandContext, args: str) -> None:
|
|
|
741
749
|
lines.append("")
|
|
742
750
|
lines.append('No rules configured. Add them under "permissions" in .hx/settings.json.')
|
|
743
751
|
|
|
744
|
-
from hx.paths import project_local_settings_file
|
|
745
|
-
from hx.tui.format import tilde
|
|
752
|
+
from hx.paths import project_local_settings_file, tilde
|
|
746
753
|
|
|
747
754
|
# Where, and why there: a grant is machine-local, so it is kept per project
|
|
748
755
|
# under the user's home instead of being dropped into the checkout.
|
|
@@ -776,7 +783,9 @@ async def cmd_mode(ctx: CommandContext, args: str) -> None:
|
|
|
776
783
|
|
|
777
784
|
|
|
778
785
|
INIT_PROMPT = """\
|
|
779
|
-
Write an AGENTS.md
|
|
786
|
+
Write an AGENTS.md in {directory}, the directory HX was started in. Any AGENTS.md
|
|
787
|
+
between the repository root and there is loaded before it, so leave out what
|
|
788
|
+
those already say.
|
|
780
789
|
|
|
781
790
|
Read enough of the codebase to be accurate. Cover: what the project is, how to
|
|
782
791
|
build, test and lint it, the layout of the source tree, and any conventions a
|
|
@@ -788,7 +797,7 @@ If AGENTS.md already exists, improve it rather than replacing it wholesale."""
|
|
|
788
797
|
|
|
789
798
|
async def cmd_init(ctx: CommandContext, args: str) -> None:
|
|
790
799
|
"""``/init`` - generate an AGENTS.md describing this project."""
|
|
791
|
-
await ctx.app.submit_to_model(INIT_PROMPT)
|
|
800
|
+
await ctx.app.submit_to_model(INIT_PROMPT.format(directory=ctx.settings.cwd))
|
|
792
801
|
|
|
793
802
|
|
|
794
803
|
async def cmd_configure(ctx: CommandContext, args: str) -> None:
|
|
@@ -11,7 +11,6 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
13
|
from collections.abc import Sequence
|
|
14
|
-
from pathlib import Path
|
|
15
14
|
|
|
16
15
|
from hx.term.sanitize import plain_text
|
|
17
16
|
from hx.term.width import cell_width, truncate_to_width
|
|
@@ -76,17 +75,3 @@ def one_line(text: str, width: int) -> str:
|
|
|
76
75
|
if cell_width(flat) <= width:
|
|
77
76
|
return flat
|
|
78
77
|
return truncate_to_width(flat, max(0, width - cell_width(ELLIPSIS))) + ELLIPSIS
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def tilde(path: str | Path) -> str:
|
|
82
|
-
"""``path`` with the home directory written as ``~``.
|
|
83
|
-
|
|
84
|
-
Compared as text, not resolved: a session records the path it resolved
|
|
85
|
-
when it was made, and the directory may be gone by the time it is drawn.
|
|
86
|
-
"""
|
|
87
|
-
text, home = str(path), str(Path.home())
|
|
88
|
-
if text == home:
|
|
89
|
-
return "~"
|
|
90
|
-
if text.startswith(home.rstrip("/") + "/"):
|
|
91
|
-
return "~" + text[len(home.rstrip("/")) :]
|
|
92
|
-
return text
|
|
@@ -26,10 +26,10 @@ from hx.core.messages import (
|
|
|
26
26
|
)
|
|
27
27
|
from hx.core.usage import format_tokens
|
|
28
28
|
from hx.git import BranchWatcher
|
|
29
|
+
from hx.paths import tilde
|
|
29
30
|
from hx.term.loop import TuiRunner
|
|
30
31
|
from hx.term.modifiers import ShiftEnter
|
|
31
32
|
from hx.term.terminal import Terminal
|
|
32
|
-
from hx.tui.format import tilde
|
|
33
33
|
from hx.tui.renderers import ToolCall
|
|
34
34
|
from hx.tui.views.blocks import (
|
|
35
35
|
AssistantMessage,
|
|
@@ -785,17 +785,9 @@ class HXSession:
|
|
|
785
785
|
await self._run_command(f"/{chosen}")
|
|
786
786
|
|
|
787
787
|
async def _open_models(self) -> None:
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if registry is None:
|
|
792
|
-
self._notice("no model registry is loaded", "warning")
|
|
793
|
-
return
|
|
794
|
-
current = getattr(getattr(self.loop, "model_info", None), "id", "") or ""
|
|
795
|
-
chosen = await self.ask(ModelPicker(list(registry.all()), current))
|
|
796
|
-
if chosen:
|
|
797
|
-
self.view.dock.status.set_model(chosen)
|
|
798
|
-
self._notice(f"model set to {chosen}")
|
|
788
|
+
# The same path as `/model`: a picker that only relabelled the status
|
|
789
|
+
# bar left the session on the old model and the choice unsaved.
|
|
790
|
+
await self._run_command("/model")
|
|
799
791
|
|
|
800
792
|
def _commands(self) -> list[Any]:
|
|
801
793
|
return list(self.commands.all()) if self.commands is not None else []
|
|
@@ -963,7 +955,8 @@ class HXSession:
|
|
|
963
955
|
asyncio.create_task(self._open_commands()) # noqa: RUF006
|
|
964
956
|
return
|
|
965
957
|
if name == "ctrl+l":
|
|
966
|
-
|
|
958
|
+
# Tracked like a typed `/model`, so an interrupt cancels it too.
|
|
959
|
+
self._side = asyncio.create_task(self._open_models())
|
|
967
960
|
return
|
|
968
961
|
# Only here, on the way to the prompt: nothing else binds shift+enter,
|
|
969
962
|
# so an approval or a picker has to keep the enter it was sent. Not
|
|
@@ -1334,8 +1327,13 @@ class HXSession:
|
|
|
1334
1327
|
)
|
|
1335
1328
|
model = getattr(self.loop, "model_info", None)
|
|
1336
1329
|
if model is not None:
|
|
1337
|
-
|
|
1330
|
+
model_id = getattr(model, "id", "") or ""
|
|
1331
|
+
status.set_model(model_id, subscription=bool(getattr(model, "is_subscription", False)))
|
|
1338
1332
|
status.set_context(0, getattr(model, "context_window", 0) or 0)
|
|
1333
|
+
# The saved effort is in force from the first turn; a bar that only
|
|
1334
|
+
# shows it once /effort is run again reads as the choice not sticking.
|
|
1335
|
+
if self.models is not None:
|
|
1336
|
+
status.set_effort(self.models.displayed_effort(model_id))
|
|
1339
1337
|
|
|
1340
1338
|
|
|
1341
1339
|
async def run_session(
|