hx-cli 0.2.0__tar.gz → 0.2.1__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.0 → hx_cli-0.2.1}/CHANGELOG.md +20 -1
- {hx_cli-0.2.0 → hx_cli-0.2.1}/PKG-INFO +1 -1
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/__init__.py +1 -1
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/markdown.py +85 -6
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/primitives.py +3 -2
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/paint.py +14 -3
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/roles.py +1 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/runtime.py +7 -2
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/theme_json.py +6 -1
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/themes/ansi.json +1 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/themes/dark.json +3 -1
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/themes/light.json +3 -1
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/views/blocks.py +23 -6
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/test_markdown.py +62 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/views/test_blocks.py +17 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/.gitignore +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/README.md +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/install.sh +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/pyproject.toml +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/agents/subagent.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/auth/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/auth/oauth/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/auth/oauth/callback.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/auth/oauth/codex.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/auth/oauth/pkce.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/auth/resolve.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/auth/store.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/cli.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/config.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/checkpoints.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/compaction.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/context.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/events.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/loop.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/messages.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/session.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/title.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/core/usage.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/docs.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/git.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/keys.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/mcp/client.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/mcp/manager.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/net.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/paths.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/permissions/engine.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/permissions/sandbox.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/providers/base.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/providers/codex.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/providers/codex_catalogue.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/providers/fake.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/providers/models.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/providers/openrouter.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/providers/registry.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/providers/responses_codec.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/ansi.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/buffer.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/component.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/demo.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/editor.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/keydecode.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/loop.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/sanitize.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/screen.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/syntax.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/terminal.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/undo.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/width.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/term/word_nav.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/base.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/bash.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/edit.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/output.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/read.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/registry.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/task.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/websearch.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tools/write.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/app.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/commands.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/format.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/glyphs.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/limits.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/renderers.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/views/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/views/dialog.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/views/login.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/views/permission.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/views/pickers.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/views/prompt.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/views/status.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/src/hx/tui/views/transcript.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/agents/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/agents/test_subagent.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/auth/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/auth/test_codex_oauth.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/auth/test_pkce.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/auth/test_resolve.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/auth/test_store.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/conftest.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_checkpoints.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_compaction.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_context.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_lateinject.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_loop.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_rewind.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_session.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_settings_layers.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_settings_migration.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_steering.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_title.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/core/test_usage.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/mcp/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/mcp/fixtures/echo_server.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/mcp/test_manager.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/permissions/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/permissions/test_engine.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/permissions/test_parser.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/permissions/test_sandbox.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/providers/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/providers/test_catalogue.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/providers/test_codex.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/providers/test_codex_catalogue.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/providers/test_openrouter.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/providers/test_registry.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/providers/test_responses_codec.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/skills/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/skills/test_loader.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/conftest.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/test_ansi.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/test_keydecode.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/test_loop.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/test_primitives.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/test_sanitize.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/test_screen.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/test_terminal.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/term/test_width.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_cli.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_docs.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_git.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_install.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_integration.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_live.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_live_codex.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_live_tavily.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_net.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_paths.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/test_readme.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tools/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tools/test_background.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tools/test_bash.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tools/test_files.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tools/test_output.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tools/test_todo.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tools/test_websearch.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/support.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/test_autocomplete.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/test_clipboard.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/test_effort.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/test_keys.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/test_login.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/test_renderers.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/test_session.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/test_theme_json.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/views/__init__.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/views/test_permission.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/views/test_pickers.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/views/test_prompt.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/views/test_runtime.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/views/test_status.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/tests/tui/views/test_transcript.py +0 -0
- {hx_cli-0.2.0 → hx_cli-0.2.1}/uv.lock +0 -0
|
@@ -28,6 +28,24 @@ the project follows [semantic versioning](https://semver.org/).
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
## [0.2.1] - 2026-09-14
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Code blocks in a reply are drawn as a block - the code on a band of its own
|
|
36
|
+
colour, syntax highlighted, with the language named at the top - instead of
|
|
37
|
+
printing the ``` fences back out as text. Themes set the band with the new
|
|
38
|
+
`mdCodeBlockBg` role, and a theme that leaves colour to the terminal, as
|
|
39
|
+
`ansi` does, gets a rule above and below instead.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- The model's reasoning is shown again. It was collapsed to a bare `Thinking…`
|
|
44
|
+
with no key wired to open it, so the text never reached the screen. `ctrl+o`
|
|
45
|
+
now folds and unfolds it, the same key that expands a tool call.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
31
49
|
## [0.2.0] - 2026-09-14
|
|
32
50
|
|
|
33
51
|
### Changed
|
|
@@ -390,7 +408,8 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
|
|
|
390
408
|
- `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
|
|
391
409
|
self-update, and `install.sh` bootstrapping uv with a pinned Python.
|
|
392
410
|
|
|
393
|
-
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.
|
|
411
|
+
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.1...HEAD
|
|
412
|
+
[0.2.1]: https://github.com/aletisunil/hx/compare/v0.2.0...v0.2.1
|
|
394
413
|
[0.2.0]: https://github.com/aletisunil/hx/compare/v0.1.9...v0.2.0
|
|
395
414
|
[0.1.9]: https://github.com/aletisunil/hx/compare/v0.1.8...v0.1.9
|
|
396
415
|
[0.1.8]: https://github.com/aletisunil/hx/compare/v0.1.7...v0.1.8
|
|
@@ -13,6 +13,11 @@ Two conventions worth naming, both taken from pi:
|
|
|
13
13
|
hashes are syntax, and leaving them in makes prose look like a source file.
|
|
14
14
|
H3 and below keep theirs, because by then the level is worth stating and
|
|
15
15
|
bold-on-bold no longer distinguishes them.
|
|
16
|
+
* **Code blocks lose their fences.** ``` is syntax too, and printing it back
|
|
17
|
+
out tells the reader only that the renderer gave up. The block is a band of
|
|
18
|
+
its own colour instead, with the language named at the top of it - and a rule
|
|
19
|
+
above and below for a theme that has no colour to tint with, which is what
|
|
20
|
+
the ``ansi`` one deliberately is.
|
|
16
21
|
* **Tables are the one place box-drawing is allowed.** A table without rules is
|
|
17
22
|
not a table. Everywhere else, a rule above and below - never a rectangle.
|
|
18
23
|
"""
|
|
@@ -27,6 +32,7 @@ LIST_INDENT = 4
|
|
|
27
32
|
"""Columns per nesting level, wide enough that a marker and its text both fit."""
|
|
28
33
|
|
|
29
34
|
CODE_INDENT = " "
|
|
35
|
+
CODE_RULE = "─"
|
|
30
36
|
QUOTE_RAIL = "│ "
|
|
31
37
|
HR_MAX = 80
|
|
32
38
|
"""A rule spanning a very wide terminal reads as a divider in a book, not a
|
|
@@ -43,6 +49,17 @@ class Painter:
|
|
|
43
49
|
def paint(self, role: str, text: str, **kwargs: bool) -> str:
|
|
44
50
|
return text
|
|
45
51
|
|
|
52
|
+
def fill(self, role: str, text: str) -> str | None:
|
|
53
|
+
"""``text`` on a background, or ``None`` if this role has none.
|
|
54
|
+
|
|
55
|
+
A theme is allowed to have no opinion about colour - the ``ansi`` one
|
|
56
|
+
deliberately has none, deferring to the terminal's own palette - and a
|
|
57
|
+
band tinted in a colour that resolves to nothing is not a band. So the
|
|
58
|
+
renderer asks rather than assumes, and rules the block instead when the
|
|
59
|
+
answer is no.
|
|
60
|
+
"""
|
|
61
|
+
return None
|
|
62
|
+
|
|
46
63
|
def code(self, source: str, language: str | None) -> list[str]:
|
|
47
64
|
return source.split("\n")
|
|
48
65
|
|
|
@@ -126,15 +143,77 @@ class _Renderer:
|
|
|
126
143
|
|
|
127
144
|
def _fence(self, node: object, indent: int) -> None:
|
|
128
145
|
language = (node.info or "").strip().split(" ")[0] or None # type: ignore[attr-defined]
|
|
129
|
-
|
|
146
|
+
available = max(1, self.width - indent)
|
|
147
|
+
room = max(1, available - 2 * len(CODE_INDENT))
|
|
148
|
+
body = [
|
|
149
|
+
truncate_to_width(line, room)
|
|
150
|
+
for line in self.painter.code(node.content.rstrip("\n"), language) # type: ignore[attr-defined]
|
|
151
|
+
]
|
|
152
|
+
|
|
130
153
|
self.blank()
|
|
131
|
-
self.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
self.
|
|
135
|
-
self.emit(self.painter.paint("md_code_block_border", "```"), indent)
|
|
154
|
+
if self.painter.fill("md_code_block_bg", "") is None:
|
|
155
|
+
self._ruled_block(body, language, available, indent)
|
|
156
|
+
else:
|
|
157
|
+
self._tinted_block(body, language, available, indent)
|
|
136
158
|
self.blank()
|
|
137
159
|
|
|
160
|
+
def _tinted_block(
|
|
161
|
+
self, body: list[str], language: str | None, available: int, indent: int
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Code on a band of its own colour, the language named at the top.
|
|
164
|
+
|
|
165
|
+
The band spans the column rather than the longest line, for the same
|
|
166
|
+
reason a tinted message does: a block whose right edge follows the code
|
|
167
|
+
is a ragged column, not a block. The language label and a blank row
|
|
168
|
+
below the code are the padding - inside the tint, so the band is a
|
|
169
|
+
rectangle rather than a highlighted run of text.
|
|
170
|
+
"""
|
|
171
|
+
span = available
|
|
172
|
+
label = truncate_to_width(language or "", max(0, span - 2 * len(CODE_INDENT)))
|
|
173
|
+
head = self.painter.paint("md_code_block", label) if label else ""
|
|
174
|
+
rows = [head, *body, ""]
|
|
175
|
+
for row in rows:
|
|
176
|
+
padding = " " * max(0, span - len(CODE_INDENT) - cell_width(row))
|
|
177
|
+
filled = self.painter.fill("md_code_block_bg", CODE_INDENT + row + padding)
|
|
178
|
+
self.emit(filled if filled is not None else CODE_INDENT + row, indent)
|
|
179
|
+
|
|
180
|
+
def _ruled_block(
|
|
181
|
+
self, body: list[str], language: str | None, available: int, indent: int
|
|
182
|
+
) -> None:
|
|
183
|
+
"""The same block for a theme with no colour of its own to tint with.
|
|
184
|
+
|
|
185
|
+
Capped like a thematic break, but never shorter than the code it
|
|
186
|
+
encloses: a rule the content overhangs reads as a broken block.
|
|
187
|
+
"""
|
|
188
|
+
widest = max((cell_width(line) for line in body), default=0) + len(CODE_INDENT)
|
|
189
|
+
span = min(available, max(HR_MAX, widest))
|
|
190
|
+
self.emit(self._code_rule(span, language), indent)
|
|
191
|
+
for line in body:
|
|
192
|
+
self.emit(CODE_INDENT + line, indent)
|
|
193
|
+
self.emit(self.painter.paint("md_code_block_border", CODE_RULE * span), indent)
|
|
194
|
+
|
|
195
|
+
def _code_rule(self, span: int, language: str | None) -> str:
|
|
196
|
+
"""The rule opening a code block, carrying the language if there is one.
|
|
197
|
+
|
|
198
|
+
The fence markers themselves are syntax, the same way a heading's
|
|
199
|
+
hashes are: the reader is looking at source either way, and ``` in the
|
|
200
|
+
middle of rendered prose only tells them the renderer gave up. A rule
|
|
201
|
+
says the same thing in the vocabulary the rest of the UI uses, and the
|
|
202
|
+
language sits in it rather than on a line of its own.
|
|
203
|
+
"""
|
|
204
|
+
paint = self.painter.paint
|
|
205
|
+
label = truncate_to_width(language or "", max(0, span - 4))
|
|
206
|
+
if not label:
|
|
207
|
+
return paint("md_code_block_border", CODE_RULE * span)
|
|
208
|
+
trail = CODE_RULE * (span - 4 - cell_width(label))
|
|
209
|
+
return (
|
|
210
|
+
paint("md_code_block_border", CODE_RULE * 2)
|
|
211
|
+
+ " "
|
|
212
|
+
+ paint("md_code_block", label)
|
|
213
|
+
+ " "
|
|
214
|
+
+ paint("md_code_block_border", trail)
|
|
215
|
+
)
|
|
216
|
+
|
|
138
217
|
_code_block = _fence
|
|
139
218
|
|
|
140
219
|
def _hr(self, _node: object, indent: int) -> None:
|
|
@@ -139,8 +139,9 @@ class Box(Widget):
|
|
|
139
139
|
|
|
140
140
|
The tint is the only thing marking the block, and it reaches the full width
|
|
141
141
|
of the terminal so the block reads as a band rather than a ragged column.
|
|
142
|
-
Backgrounds mean one of
|
|
143
|
-
|
|
142
|
+
Backgrounds mean one of three things in this UI - whose turn this is, how a
|
|
143
|
+
tool call ended, or that a run of text is code - and are never used for
|
|
144
|
+
emphasis.
|
|
144
145
|
"""
|
|
145
146
|
|
|
146
147
|
__slots__ = ("_padding_x", "_padding_y", "_tint", "children")
|
|
@@ -66,9 +66,9 @@ def bg(role: str, text: str) -> str:
|
|
|
66
66
|
def tint(role: str) -> Callable[[str], str]:
|
|
67
67
|
"""A background filler, for :class:`~hx.term.primitives.Box`.
|
|
68
68
|
|
|
69
|
-
Backgrounds mean exactly
|
|
70
|
-
|
|
71
|
-
for emphasis.
|
|
69
|
+
Backgrounds mean exactly three things in this UI - whose turn this is, how
|
|
70
|
+
a tool call ended, and that a run of text is code - so there are few of
|
|
71
|
+
these and they are never used for emphasis.
|
|
72
72
|
"""
|
|
73
73
|
|
|
74
74
|
def paint(line: str) -> str:
|
|
@@ -103,6 +103,17 @@ class ThemePainter(Painter):
|
|
|
103
103
|
def paint(self, role: str, text: str, **kwargs: bool) -> str:
|
|
104
104
|
return fg(role, text, **kwargs)
|
|
105
105
|
|
|
106
|
+
def fill(self, role: str, text: str) -> str | None:
|
|
107
|
+
"""The role as a background, unless the theme leaves it to the terminal.
|
|
108
|
+
|
|
109
|
+
``ansi_default`` and an empty value both mean "whatever the terminal is
|
|
110
|
+
already using", which as a background is nothing at all.
|
|
111
|
+
"""
|
|
112
|
+
value = color(role)
|
|
113
|
+
if not value or value == "ansi_default":
|
|
114
|
+
return None
|
|
115
|
+
return bg(role, text)
|
|
116
|
+
|
|
106
117
|
def code(self, source: str, language: str | None) -> list[str]:
|
|
107
118
|
from hx.term.syntax import highlight
|
|
108
119
|
|
|
@@ -951,13 +951,18 @@ class HXSession:
|
|
|
951
951
|
self._notice(f"Copied to the clipboard ({used}).", "success")
|
|
952
952
|
|
|
953
953
|
def expand_cursored(self) -> None:
|
|
954
|
-
"""Expand the cursored block, or every
|
|
954
|
+
"""Expand the cursored block, or every foldable one when none is.
|
|
955
|
+
|
|
956
|
+
Reasoning folds with the tool calls rather than on a key of its own:
|
|
957
|
+
both are the detail under an answer, and one key for "show me the
|
|
958
|
+
detail" is the whole point of having a key for it.
|
|
959
|
+
"""
|
|
955
960
|
cursor = self.view.transcript.cursor
|
|
956
961
|
if cursor is not None and hasattr(cursor, "toggle"):
|
|
957
962
|
cursor.toggle()
|
|
958
963
|
else:
|
|
959
964
|
for block in self.view.transcript.blocks:
|
|
960
|
-
if isinstance(block, ToolBlock):
|
|
965
|
+
if isinstance(block, ToolBlock | ThinkingMessage):
|
|
961
966
|
block.toggle()
|
|
962
967
|
self.runner.request_immediate_render()
|
|
963
968
|
|
|
@@ -40,7 +40,12 @@ _EXPORT_FALLBACKS = {"background": "pageBg", "surface": "cardBg"}
|
|
|
40
40
|
|
|
41
41
|
#: Roles that borrow another role when absent. Only for roles pi has no concept
|
|
42
42
|
#: of; a role pi does define must be present or the file is rejected.
|
|
43
|
-
_DERIVED = {
|
|
43
|
+
_DERIVED = {
|
|
44
|
+
"panel": "surface",
|
|
45
|
+
"diff_hunk": "md_link",
|
|
46
|
+
"user_text": "text",
|
|
47
|
+
"md_code_block_bg": "surface",
|
|
48
|
+
}
|
|
44
49
|
|
|
45
50
|
_FIELDS = frozenset(f.name for f in fields(Palette)) - {"name", "dark"}
|
|
46
51
|
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"toolPendingBg": "#282832",
|
|
21
21
|
"toolSuccessBg": "#283228",
|
|
22
22
|
"toolErrorBg": "#3c2828",
|
|
23
|
-
"customMsgBg": "#2d2838"
|
|
23
|
+
"customMsgBg": "#2d2838",
|
|
24
|
+
"codeBg": "#22222a"
|
|
24
25
|
},
|
|
25
26
|
"colors": {
|
|
26
27
|
"background": "pageBg",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"mdLink": "#81a2be",
|
|
60
61
|
"mdLinkUrl": "dimGray",
|
|
61
62
|
"mdCode": "accent",
|
|
63
|
+
"mdCodeBlockBg": "codeBg",
|
|
62
64
|
"mdCodeBlock": "green",
|
|
63
65
|
"mdCodeBlockBorder": "gray",
|
|
64
66
|
"mdQuote": "gray",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"toolPendingBg": "#e8e8f0",
|
|
20
20
|
"toolSuccessBg": "#e8f0e8",
|
|
21
21
|
"toolErrorBg": "#f0e8e8",
|
|
22
|
-
"customMsgBg": "#ede7f6"
|
|
22
|
+
"customMsgBg": "#ede7f6",
|
|
23
|
+
"codeBg": "#eceef0"
|
|
23
24
|
},
|
|
24
25
|
"colors": {
|
|
25
26
|
"background": "pageBg",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"mdLink": "blue",
|
|
59
60
|
"mdLinkUrl": "dimGray",
|
|
60
61
|
"mdCode": "teal",
|
|
62
|
+
"mdCodeBlockBg": "codeBg",
|
|
61
63
|
"mdCodeBlock": "green",
|
|
62
64
|
"mdCodeBlockBorder": "mediumGray",
|
|
63
65
|
"mdQuote": "mediumGray",
|
|
@@ -17,7 +17,7 @@ from typing import Any
|
|
|
17
17
|
|
|
18
18
|
from hx.term.component import Widget
|
|
19
19
|
from hx.term.markdown import render_markdown
|
|
20
|
-
from hx.term.primitives import Box, HangingText, Lines
|
|
20
|
+
from hx.term.primitives import Box, HangingText, Lines
|
|
21
21
|
from hx.term.sanitize import plain_text
|
|
22
22
|
from hx.tui.glyphs import NOTICE, SPINNER, TOOL_DONE, TOOL_FAILED
|
|
23
23
|
from hx.tui.limits import PREVIEW_LINES
|
|
@@ -89,11 +89,18 @@ class AssistantMessage(Widget):
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
class ThinkingMessage(Widget):
|
|
92
|
-
"""The model's reasoning, in its own muted colour.
|
|
92
|
+
"""The model's reasoning, in its own muted colour.
|
|
93
|
+
|
|
94
|
+
Shown rather than hidden. Reasoning is the part of a turn that explains the
|
|
95
|
+
part you can see, and a reader who does not want it has a key for that -
|
|
96
|
+
``ctrl+o``, the same one that expands a tool call. A one-line ``Thinking…``
|
|
97
|
+
with no way to open it, which is what this was, is the reasoning simply
|
|
98
|
+
not arriving.
|
|
99
|
+
"""
|
|
93
100
|
|
|
94
101
|
__slots__ = ("_collapsed", "_text")
|
|
95
102
|
|
|
96
|
-
def __init__(self, text: str = "", collapsed: bool =
|
|
103
|
+
def __init__(self, text: str = "", collapsed: bool = False) -> None:
|
|
97
104
|
super().__init__()
|
|
98
105
|
self._text = plain_text(text)
|
|
99
106
|
self._collapsed = collapsed
|
|
@@ -107,13 +114,23 @@ class ThinkingMessage(Widget):
|
|
|
107
114
|
self._collapsed = collapsed
|
|
108
115
|
self.invalidate()
|
|
109
116
|
|
|
117
|
+
def toggle(self) -> None:
|
|
118
|
+
self.set_collapsed(not self._collapsed)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def text(self) -> str:
|
|
122
|
+
return self._text
|
|
123
|
+
|
|
110
124
|
def draw(self, width: int) -> list[str]:
|
|
111
125
|
if not self._text.strip():
|
|
112
126
|
return []
|
|
113
|
-
if self._collapsed:
|
|
114
|
-
return Text(fg("thinking", "Thinking…", italic=True)).render(width)
|
|
115
127
|
lines = [fg("thinking", line, italic=True) for line in self._text.strip().split("\n")]
|
|
116
|
-
|
|
128
|
+
if not self._collapsed:
|
|
129
|
+
return Lines(lines).render(width)
|
|
130
|
+
# Folded by the reader, with the key they just pressed - so the count
|
|
131
|
+
# is what is worth saying here, not the key again.
|
|
132
|
+
note = fg("dim", f" ({len(lines)} lines)") if len(lines) > 1 else ""
|
|
133
|
+
return Lines([fg("thinking", "Thinking…", italic=True) + note]).render(width)
|
|
117
134
|
|
|
118
135
|
|
|
119
136
|
class ToolBlock(Widget):
|
|
@@ -31,6 +31,14 @@ class Recording(Painter):
|
|
|
31
31
|
return source.split("\n")
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class Tinting(Recording):
|
|
35
|
+
"""A painter that has a background to offer, as a real theme does."""
|
|
36
|
+
|
|
37
|
+
def fill(self, role: str, text: str) -> str | None:
|
|
38
|
+
self.roles.append(f"fill:{role}")
|
|
39
|
+
return f"<{role}>{text}</{role}>"
|
|
40
|
+
|
|
41
|
+
|
|
34
42
|
def plain(text: str, width: int = 60) -> list[str]:
|
|
35
43
|
return [strip_ansi(line) for line in render_markdown(text, width)]
|
|
36
44
|
|
|
@@ -146,6 +154,60 @@ def test_a_fenced_block_is_highlighted_and_indented() -> None:
|
|
|
146
154
|
assert body and strip_ansi(body[0]).startswith(" ")
|
|
147
155
|
|
|
148
156
|
|
|
157
|
+
def test_a_fenced_block_is_ruled_rather_than_fenced() -> None:
|
|
158
|
+
"""``` is syntax, like a heading's hashes: printing it back out tells the
|
|
159
|
+
reader only that the renderer gave up."""
|
|
160
|
+
lines = plain("```json\n{}\n```")
|
|
161
|
+
assert not any("```" in line for line in lines)
|
|
162
|
+
assert lines[0].startswith("──") and lines[-1] == "─" * cell_width(lines[0])
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_the_language_sits_in_the_opening_rule() -> None:
|
|
166
|
+
painter = Recording()
|
|
167
|
+
out = render_markdown("```json\n{}\n```", 40, painter)
|
|
168
|
+
assert "md_code_block" in painter.roles, "the role finally does something"
|
|
169
|
+
assert "json" in strip_ansi(out[0])
|
|
170
|
+
assert "json" not in strip_ansi(out[-1]), "only the opening rule carries it"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_a_block_without_a_language_gets_a_plain_rule() -> None:
|
|
174
|
+
lines = plain("```\nx\n```")
|
|
175
|
+
assert lines[0] == lines[-1] == "─" * cell_width(lines[0])
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_a_theme_with_a_colour_tints_the_block_instead_of_ruling_it() -> None:
|
|
179
|
+
painter = Tinting()
|
|
180
|
+
out = render_markdown("```json\n{}\n```", 40, painter)
|
|
181
|
+
assert "fill:md_code_block_bg" in painter.roles
|
|
182
|
+
assert not any("─" in line for line in out), "tinted, so no rules"
|
|
183
|
+
assert "json" in out[0], "the language is named at the top of the band"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_the_band_spans_the_column_rather_than_the_longest_line() -> None:
|
|
187
|
+
"""A block whose right edge follows the code is a ragged column."""
|
|
188
|
+
painter = Tinting()
|
|
189
|
+
out = render_markdown("```\nx\n```", 40, painter)
|
|
190
|
+
inner = [line.split(">", 1)[1].rsplit("</", 1)[0] for line in out]
|
|
191
|
+
assert {cell_width(line) for line in inner} == {40}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_a_theme_with_no_colour_of_its_own_falls_back_to_rules() -> None:
|
|
195
|
+
"""The ansi theme defers to the terminal's palette, so it has no surface
|
|
196
|
+
colour to tint with - and a band tinted in nothing is not a band."""
|
|
197
|
+
painter = Recording()
|
|
198
|
+
out = render_markdown("```json\n{}\n```", 40, painter)
|
|
199
|
+
assert any("─" in line for line in out)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_the_rule_is_never_shorter_than_the_code_it_encloses() -> None:
|
|
203
|
+
"""A rule the content overhangs reads as a broken block."""
|
|
204
|
+
code = "x = " + "y" * 90
|
|
205
|
+
lines = plain(f"```python\n{code}\n```", width=120)
|
|
206
|
+
span = cell_width(lines[0])
|
|
207
|
+
assert all(cell_width(line) <= span for line in lines)
|
|
208
|
+
assert span >= cell_width(lines[1])
|
|
209
|
+
|
|
210
|
+
|
|
149
211
|
def test_a_table_is_the_one_place_box_drawing_is_allowed() -> None:
|
|
150
212
|
lines = plain("| a | b |\n| - | - |\n| 1 | 2 |")
|
|
151
213
|
assert any("┼" in line for line in lines)
|
|
@@ -80,6 +80,23 @@ def test_thinking_collapses_to_one_line() -> None:
|
|
|
80
80
|
assert "reasoning" in " ".join(plain(block.render(40)))
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def test_thinking_is_shown_rather_than_hidden() -> None:
|
|
84
|
+
"""The port collapsed it by default and wired no way to open it, so the
|
|
85
|
+
reasoning was simply not arriving."""
|
|
86
|
+
block = ThinkingMessage("first thought\nsecond thought")
|
|
87
|
+
shown = " ".join(plain(block.render(40)))
|
|
88
|
+
assert "first thought" in shown and "second thought" in shown
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_thinking_folds_with_the_same_key_as_a_tool_call() -> None:
|
|
92
|
+
block = ThinkingMessage("first thought\nsecond thought")
|
|
93
|
+
block.toggle()
|
|
94
|
+
folded = plain(block.render(40))
|
|
95
|
+
assert folded == [" Thinking… (2 lines)"], "folded, and says how much is folded"
|
|
96
|
+
block.toggle()
|
|
97
|
+
assert "second thought" in " ".join(plain(block.render(40)))
|
|
98
|
+
|
|
99
|
+
|
|
83
100
|
# -- tool calls -------------------------------------------------------------
|
|
84
101
|
|
|
85
102
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|