zettcode 0.1.12__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zettcode-0.1.12 → zettcode-0.2.0}/PKG-INFO +2 -2
- {zettcode-0.1.12 → zettcode-0.2.0}/pyproject.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/__init__.py +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/entries.py +7 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/presenters.py +12 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/rendering.py +83 -12
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/stream.py +5 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/commands.py +11 -6
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/plugins/builtin_commands.py +124 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/plugins/builtins.py +10 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/app.py +36 -9
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/followups.py +2 -2
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/keys.py +6 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/messaging.py +4 -2
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/sessions.py +18 -7
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/shell.py +9 -5
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/updating.py +57 -3
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/__init__.py +5 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/composer.py +20 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/update_page.py +4 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/daemon/cli.py +76 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/agent/turns.py +7 -5
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/core.py +8 -0
- zettcode-0.2.0/src/zettcode/engine/extensions/harness.py +199 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/mcp_notice.py +8 -2
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/subagents.py +2 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/permission/__init__.py +18 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/permission/sandbox.py +10 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/plugins/builtin_agent.py +30 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/sdk/client.py +6 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/sdk/session.py +20 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/theme.py +15 -7
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/ayu-mirage.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/catppuccin-latte.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/catppuccin-mocha.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/dracula.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/github-dimmed.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/github-light.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/gruvbox-dark.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/gruvbox-light.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/kanagawa.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/nord.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/one-dark.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/rose-pine-dawn.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/rose-pine.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/solarized-dark.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/solarized-light.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/themes/tokyo-night.toml +1 -1
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/update.py +31 -15
- zettcode-0.1.12/src/zettcode/engine/extensions/harness.py +0 -89
- {zettcode-0.1.12 → zettcode-0.2.0}/.gitignore +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/README.md +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/_compat.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/agent/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/agent/commands.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/agent/settings.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/changes.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/effort_gauge.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/projection.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/pulse.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/replay.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/shimmer.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/stops.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/text.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/tips.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/fold.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/turn.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/brand.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/commandline/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/commandline/arguments.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/commandline/completion.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/commandline/history.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/commandline/runner.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/cat.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/chibi.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/dog.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/kangaroo.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/model.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/penguin.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/polarbear.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/scarfy.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/seagull.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/slime.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/snowbear.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/sprout.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/tiger.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/pets/whale.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/plugins/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/plugins/container.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/plugins/host.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/plugins/loader.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/plugins/mixins.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/plugins/plugin.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/plugins/state.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/bars.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/boot.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/clipboard.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/commands.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/config_form.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/demo.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/gc_policy.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/labels.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/notices.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/providers.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/settings.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/turns.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/approval_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/bash_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/completer.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/context_charts.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/context_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/effort_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/follow_up.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/form_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/input_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/model_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/model_settings_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/panel.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/pet.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/pet_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/root.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/self_compact_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/statusline_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/steering.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/transcript.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/usage_charts.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/usage_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/welcome.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/__main__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/client.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/files.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/finder.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/hub.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/log.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/models.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/process.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/protocol.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/roster.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/runner.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/server.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/text.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/broker/web.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/budget.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/cli.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/collector.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/config.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/config_edit.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/daemon/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/daemon/__main__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/daemon/process.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/daemon/run.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/daemon/state.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/doctor.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/agent/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/agent/base.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/agent/context.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/agent/exports.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/agent/interaction.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/agent/sessions.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/agent/settings.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/dialogue/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/dialogue/ask.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/dialogue/side.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/dialogue/title.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/export/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/export/trace/script.js +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/export/trace/style.css +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/export/trace/template.html +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/capabilities.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/compaction.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/context.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/files.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/harness_switch.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/mcp.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/messaging.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/relay.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/tool_output.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/extensions/usage.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/messenger.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/permission/approval.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/permission/readonly.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/pipeline.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/plugins/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/plugins/container.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/plugins/extension.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/plugins/loader.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/plugins/mixins.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/plugins/plugin.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/plugins/services.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/plugins/state.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/server_tools.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/storage/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/storage/layout.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/storage/ledger.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/storage/locks.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/storage/metadata.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/storage/records.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/storage/store.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/tools/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/tools/boundary.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/tools/files.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/tools/images.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/tools/output.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/tools/shell.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/engine/tools/workspace.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/error_text.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/filelock.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/mentions.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/oneshot.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/paths.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/prompt.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/providers.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/py.typed +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/registry.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/relay.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/sdk/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/sdk/errors.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/sdk/events.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/sdk/handlers.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/sdk/models.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/sdk/translation.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/sdk/types.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/server/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/server/app.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/server/cli.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/server/runner.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/server/schemas.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/server/serialize.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/server/settings.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/statusline.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/capabilities.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/clipboard.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/console.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/app.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/events.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/focus.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/geometry.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/host.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/keymap.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/scheduler.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/screen.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/core/widget.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/gallery.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/glyphs.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/input/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/input/bridge.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/input/decoder.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/input/events.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/input/keys.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/input/reader.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/layout/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/layout/box.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/layout/overlay.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/layout/scroll.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/layout/solver.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/layout/spacing.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/render/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/render/ansi.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/render/canvas.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/render/code.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/render/color.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/render/renderer.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/render/rich_text.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/render/style.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/render/sweep.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/render/text.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/runner.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/terminal.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/testing/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/testing/harness.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/testing/snapshot.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/theme_file.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/__init__.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/collapsible.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/completion.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/console.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/dialog.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/diff.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/list.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/list_page.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/markdown.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/progress.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/rich_text.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/status.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/table.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/tasks.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/text.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/textarea.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/tui/widgets/toast.py +0 -0
- {zettcode-0.1.12 → zettcode-0.2.0}/src/zettcode/wording.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: zettcode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A focused terminal coding agent powered by zett-agent
|
|
5
5
|
Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
|
|
6
6
|
Project-URL: Issues, https://github.com/Chang-LeHung/zettcode/issues
|
|
@@ -35,7 +35,7 @@ Requires-Dist: truststore<1,>=0.10
|
|
|
35
35
|
Requires-Dist: uvicorn<1,>=0.35
|
|
36
36
|
Requires-Dist: wcwidth<1,>=0.2
|
|
37
37
|
Requires-Dist: websockets<17,>=15
|
|
38
|
-
Requires-Dist: zett-agent<0.3,>=0.2.
|
|
38
|
+
Requires-Dist: zett-agent<0.3,>=0.2.1
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
|
|
41
41
|
# ZettCode
|
|
@@ -58,7 +58,7 @@ dependencies = [
|
|
|
58
58
|
# than it did. An exact pin would make zettcode fight every other tool that
|
|
59
59
|
# shares the dependency — including an upgrade of zett-agent itself, which pip
|
|
60
60
|
# would downgrade to satisfy it.
|
|
61
|
-
"zett-agent>=0.2.
|
|
61
|
+
"zett-agent>=0.2.1,<0.3",
|
|
62
62
|
]
|
|
63
63
|
|
|
64
64
|
[project.scripts]
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
#: The one place the version is written. ``pyproject.toml`` reads it from here
|
|
9
9
|
#: and the release workflow checks the tag against it, so a bump is one edit.
|
|
10
|
-
__version__ = "0.
|
|
10
|
+
__version__ = "0.2.0"
|
|
11
11
|
|
|
12
12
|
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
13
13
|
_EXPORTS = {
|
|
@@ -243,10 +243,16 @@ class ToolEntry(TimedEntry):
|
|
|
243
243
|
class ReadEntry(ToolEntry):
|
|
244
244
|
"""A file that was read: its body is source code, drawn as the language it is.
|
|
245
245
|
|
|
246
|
-
``language`` names the scanner for the output, or is ``None`` for a file that is not code.
|
|
246
|
+
``language`` names the scanner for the output, or is ``None`` for a file that is not code. ``lines`` is how many
|
|
247
|
+
lines came back, once a completed read has one; it is a field of the row rather than part of its title, so the
|
|
248
|
+
title being restated or cut at the width cannot lose it. ``picture`` marks a path that looks like a picture — the suffix the read tool
|
|
249
|
+
sends as an image — since a row is opened from the arguments, before the result exists; a file whose content is an
|
|
250
|
+
image under another name is counted as text until the result itself says otherwise.
|
|
247
251
|
"""
|
|
248
252
|
|
|
249
253
|
language: str | None = None
|
|
254
|
+
lines: int | None = None
|
|
255
|
+
picture: bool = False
|
|
250
256
|
|
|
251
257
|
|
|
252
258
|
@dataclass(slots=True, kw_only=True)
|
|
@@ -680,10 +680,21 @@ class SessionMessagePresenter(ToolPresenter[MessageEntry]):
|
|
|
680
680
|
|
|
681
681
|
|
|
682
682
|
class ReadPresenter(ToolPresenter[ReadEntry]):
|
|
683
|
-
"""A file that was read: the body is highlighted as the language the file is in."""
|
|
683
|
+
"""A file that was read: the body is highlighted as the language the file is in, and the row says how much."""
|
|
684
684
|
|
|
685
685
|
entry_type = ReadEntry
|
|
686
686
|
|
|
687
|
+
def after_title(self, entry: ReadEntry, theme: Theme) -> list[Span]:
|
|
688
|
+
"""Say how many lines came back, beside the title rather than inside it.
|
|
689
|
+
|
|
690
|
+
A field of the row survives what a title does not: `restate_tool` rewrites the title from the arguments, and
|
|
691
|
+
the count is drawn from the entry, the way an edit's ``+N -N`` is. A row too narrow for the title and the
|
|
692
|
+
count together cuts the count, as it cuts any tail — an edit's counts go the same way.
|
|
693
|
+
"""
|
|
694
|
+
if entry.lines is None:
|
|
695
|
+
return []
|
|
696
|
+
return [Span(f" \u00b7 {entry.lines} line" + ("" if entry.lines == 1 else "s"), Style(foreground=theme.subtle))]
|
|
697
|
+
|
|
687
698
|
def body_lines(self, entry: ReadEntry, width: int, theme: Theme, muted: Style) -> list[TextLine]:
|
|
688
699
|
"""Show the output as source code when the file is."""
|
|
689
700
|
return self.output_lines(entry, width, theme, muted, language=entry.language)
|
|
@@ -33,7 +33,7 @@ from pathlib import Path
|
|
|
33
33
|
from zettcode.tui import ELLIPSIS, SEPARATOR
|
|
34
34
|
from zettcode.tui.render import GENERIC, language_for
|
|
35
35
|
|
|
36
|
-
from .entries import DeleteEntry, EditEntry, MessageEntry, ReadEntry, ToolEntry, WriteEntry
|
|
36
|
+
from .entries import DeleteEntry, EditEntry, EntryStatus, MessageEntry, ReadEntry, ToolEntry, WriteEntry
|
|
37
37
|
from .text import DECLINED_NOTE, limit_output
|
|
38
38
|
|
|
39
39
|
#: Output kinds the chain shapes; a link claims one or more of them.
|
|
@@ -41,6 +41,22 @@ TOOL = "tool"
|
|
|
41
41
|
THINKING = "thinking"
|
|
42
42
|
ANSWER = "answer"
|
|
43
43
|
|
|
44
|
+
#: Suffixes that look like a picture, copied from the read tool's accepted set: a row is opened from the arguments,
|
|
45
|
+
#: before the result exists, so the suffix is all it has. The durable answer is a boolean on the finished event, taken
|
|
46
|
+
#: from the result's own content — see :func:`is_picture_path`.
|
|
47
|
+
PICTURE_SUFFIXES = frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp"})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_picture_path(path: str) -> bool:
|
|
51
|
+
"""Return whether ``path`` looks like a picture by its suffix.
|
|
52
|
+
|
|
53
|
+
A guess, and named like one: the read tool decides by the file's signature, so a PNG copied to ``.bin`` is counted
|
|
54
|
+
as text and a text file named ``notes.png`` is not counted. Both are a line about a row nobody is reading closely;
|
|
55
|
+
the exact answer needs the boolean the result will carry (see :data:`PICTURE_SUFFIXES`).
|
|
56
|
+
"""
|
|
57
|
+
return Path(path).suffix.lower() in PICTURE_SUFFIXES
|
|
58
|
+
|
|
59
|
+
|
|
44
60
|
#: The hint a bounded read leaves behind; it is advice for the model, not the
|
|
45
61
|
#: reader, and the row already shows the text it applies to.
|
|
46
62
|
CONTINUATION = re.compile(r"\n?\.\.\. \[(?:line truncated|more lines)[^\]]*\]\s*\Z")
|
|
@@ -128,6 +144,16 @@ class RenderHandler(ABC):
|
|
|
128
144
|
"""Return the text under a tool row; only a tool link changes this."""
|
|
129
145
|
return output
|
|
130
146
|
|
|
147
|
+
def finished(self, entry: ToolEntry, text: str, status: EntryStatus) -> None:
|
|
148
|
+
"""Adjust a row now that its result is in — a count, a size — and change nothing by default.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
entry: The row, whose body is already set and bounded.
|
|
152
|
+
text: The body as it came back, after :meth:`body` shaped it and before it was bounded.
|
|
153
|
+
status: Whether the call completed, failed or was skipped: a row may only count what worked.
|
|
154
|
+
"""
|
|
155
|
+
return None
|
|
156
|
+
|
|
131
157
|
def text(self, text: str, *, opening: bool) -> str:
|
|
132
158
|
"""Return one streamed fragment, unchanged unless a link shapes it.
|
|
133
159
|
|
|
@@ -183,6 +209,12 @@ class Renderers:
|
|
|
183
209
|
handler = self._claim(TOOL, name)
|
|
184
210
|
return output if handler is None else handler.body(name, output)
|
|
185
211
|
|
|
212
|
+
def finished(self, entry: ToolEntry, text: str, status: EntryStatus) -> None:
|
|
213
|
+
"""Let the claiming link adjust a row once its result is in."""
|
|
214
|
+
handler = self._claim(TOOL, entry.tool)
|
|
215
|
+
if handler is not None:
|
|
216
|
+
handler.finished(entry, text, status)
|
|
217
|
+
|
|
186
218
|
def text(self, kind: str, text: str, *, opening: bool = False) -> str:
|
|
187
219
|
"""Return one streamed fragment of ``kind``, shaped by the claiming link."""
|
|
188
220
|
handler = self._claim(kind)
|
|
@@ -369,16 +401,34 @@ class ReadTool(ToolHandler):
|
|
|
369
401
|
def open_tool(
|
|
370
402
|
self, name: str, arguments: Mapping[str, object], *, id: int, call_id: str, started_at: float
|
|
371
403
|
) -> ToolEntry:
|
|
372
|
-
"""A read row, which knows the language its body is in."""
|
|
404
|
+
"""A read row, which knows the language its body is in, and whether the file is a picture."""
|
|
373
405
|
row = self.describe(name, arguments)
|
|
406
|
+
path = argument(arguments, "path")
|
|
374
407
|
return ReadEntry(
|
|
375
|
-
id=id,
|
|
408
|
+
id=id,
|
|
409
|
+
call_id=call_id,
|
|
410
|
+
tool=name,
|
|
411
|
+
title=row.title,
|
|
412
|
+
language=row.language,
|
|
413
|
+
picture=is_picture_path(path),
|
|
414
|
+
started_at=started_at,
|
|
376
415
|
)
|
|
377
416
|
|
|
378
417
|
def body(self, name: str, output: str) -> str:
|
|
379
418
|
"""Drop the continuation hint; the row bounds the text it applies to."""
|
|
380
419
|
return CONTINUATION.sub("", output).rstrip()
|
|
381
420
|
|
|
421
|
+
def finished(self, entry: ToolEntry, text: str, status: EntryStatus) -> None:
|
|
422
|
+
"""Count what the read returned, on the row, if it returned text and the call worked.
|
|
423
|
+
|
|
424
|
+
The count is of what came back — a range asked for is not the same number — and of the whole of it, not of
|
|
425
|
+
the preview the row keeps, so a read past the output limit is counted in full. A picture carries no lines,
|
|
426
|
+
and a failed call's body is the error message rather than the file, so neither is counted.
|
|
427
|
+
"""
|
|
428
|
+
if status is not EntryStatus.COMPLETED or not isinstance(entry, ReadEntry) or entry.picture:
|
|
429
|
+
return
|
|
430
|
+
entry.lines = len(text.splitlines())
|
|
431
|
+
|
|
382
432
|
|
|
383
433
|
class WriteTool(ToolHandler):
|
|
384
434
|
"""``write_file``: the file being written."""
|
|
@@ -631,6 +681,33 @@ class MessageTool(ToolHandler):
|
|
|
631
681
|
)
|
|
632
682
|
|
|
633
683
|
|
|
684
|
+
class NamespacedTool(ToolHandler):
|
|
685
|
+
"""A tool whose name carries where it came from: ``server__tool``, so the row names the server.
|
|
686
|
+
|
|
687
|
+
The server is part of what the call was: the same ``search`` in two servers is two different tools, with
|
|
688
|
+
different reach and different answers, and a row that drops the server makes them look like one.
|
|
689
|
+
|
|
690
|
+
The name is all a renderer knows, so the row says what the name says — ``github/create_issue`` — and not that
|
|
691
|
+
the tool speaks MCP: a plugin registers a namespaced name of its own with the same shape, and the runtime does
|
|
692
|
+
not mark its remote tools here. A name without both parts is shown as it arrived.
|
|
693
|
+
"""
|
|
694
|
+
|
|
695
|
+
def handles(self, kind: str, name: str = "") -> bool:
|
|
696
|
+
"""Claim a namespaced name that no earlier link claimed."""
|
|
697
|
+
return kind == TOOL and "__" in name.strip("_") and not name.startswith("__")
|
|
698
|
+
|
|
699
|
+
def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
|
|
700
|
+
"""Show ``server/tool``, so the row names where the capability came from.
|
|
701
|
+
|
|
702
|
+
The runtime names a remote tool ``<server>__<tool>``; a name that prefixed the family with ``mcp`` —
|
|
703
|
+
``mcp__github__create_issue`` — is read the same way, and one without both parts keeps its own name.
|
|
704
|
+
"""
|
|
705
|
+
remote = name[5:] if name.startswith("mcp__") else name
|
|
706
|
+
server, _, tool = remote.partition("__")
|
|
707
|
+
label = f"{server}/{tool}" if server and tool else name
|
|
708
|
+
return ToolRow(f"{label} {summarise(arguments)}".rstrip())
|
|
709
|
+
|
|
710
|
+
|
|
634
711
|
class FallbackTool(ToolHandler):
|
|
635
712
|
"""Anything no other link claims: its name and flat arguments, never JSON."""
|
|
636
713
|
|
|
@@ -639,15 +716,8 @@ class FallbackTool(ToolHandler):
|
|
|
639
716
|
return kind == TOOL
|
|
640
717
|
|
|
641
718
|
def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
|
|
642
|
-
"""Show ``name key=value`` so an unknown tool still reads as a sentence.
|
|
643
|
-
|
|
644
|
-
An MCP tool is namespaced ``<server>__<tool>``; the server is worth
|
|
645
|
-
keeping because it says where the capability comes from, but it reads
|
|
646
|
-
better after the tool than in front of it.
|
|
647
|
-
"""
|
|
648
|
-
server, _, tool = name.partition("__")
|
|
649
|
-
label = f"{tool} (via {server})" if tool else name
|
|
650
|
-
return ToolRow(f"{label} {summarise(arguments)}".rstrip())
|
|
719
|
+
"""Show ``name key=value`` so an unknown tool still reads as a sentence."""
|
|
720
|
+
return ToolRow(f"{name} {summarise(arguments)}".rstrip())
|
|
651
721
|
|
|
652
722
|
|
|
653
723
|
class ReasoningHandler(RenderHandler):
|
|
@@ -705,6 +775,7 @@ DEFAULT_RENDERERS = Renderers(
|
|
|
705
775
|
SkillTool(),
|
|
706
776
|
TaskTool(),
|
|
707
777
|
MessageTool(),
|
|
778
|
+
NamespacedTool(),
|
|
708
779
|
FallbackTool(),
|
|
709
780
|
ReasoningHandler(),
|
|
710
781
|
AnswerHandler(),
|
|
@@ -164,7 +164,11 @@ class StreamMixin(TurnMixin):
|
|
|
164
164
|
return
|
|
165
165
|
# An unknown status is refused before the row is touched at all.
|
|
166
166
|
outcome = EntryStatus(status)
|
|
167
|
-
|
|
167
|
+
body = self.renderers.body(entry.tool, output)
|
|
168
|
+
entry.text = limit_output(body)
|
|
169
|
+
# The result, unbounded: a count is of what came back, not of the preview the row keeps, and what it says about
|
|
170
|
+
# the body depends on whether the call worked.
|
|
171
|
+
self.renderers.finished(entry, body, outcome)
|
|
168
172
|
entry.finish(outcome, self.clock())
|
|
169
173
|
self._changed(entry)
|
|
170
174
|
# The loop calls the model again once the batch has produced its
|
|
@@ -101,19 +101,24 @@ class CommandHost(Protocol):
|
|
|
101
101
|
async def run_side_question(self, question: str) -> None:
|
|
102
102
|
"""Answer a question that is kept out of the conversation."""
|
|
103
103
|
|
|
104
|
-
async def prompt(self, text: str) -> bool:
|
|
104
|
+
async def prompt(self, text: str, *, shown: str | None = None) -> bool:
|
|
105
105
|
"""Run ``text`` as one turn of this conversation, exactly as if the reader had typed it.
|
|
106
106
|
|
|
107
|
-
The text reaches the model the way a draft does:
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
The text reaches the model the way a draft does: stored in the session, streamed as it is answered, and
|
|
108
|
+
settled by the same row. ``@`` references are resolved.
|
|
109
|
+
|
|
110
|
+
A command whose instructions are longer than what it wants to show passes the two apart: ``text`` is what
|
|
111
|
+
the model reads, ``shown`` is what the transcript draws in its place. A reader who typed ``/code-review``
|
|
112
|
+
sees that, not the brief it stands for — while the model reads the brief with that turn, then and in every
|
|
113
|
+
later turn, because the brief is part of the message the session keeps. Only its display is short.
|
|
110
114
|
|
|
111
115
|
The turn is awaited, so the command that asked for it holds the request slot until it has been answered,
|
|
112
116
|
and the row the shell shows is the command's own.
|
|
113
117
|
|
|
114
118
|
Args:
|
|
115
|
-
text: What to ask.
|
|
116
|
-
|
|
119
|
+
text: What to ask. With no ``shown``, this is also what the reader sees.
|
|
120
|
+
shown: What the transcript shows, when it differs from ``text``. The message the session keeps is
|
|
121
|
+
the same either way: ``text`` is in it, and ``shown`` is what a reader is shown of it.
|
|
117
122
|
|
|
118
123
|
Returns:
|
|
119
124
|
Whether a turn was started and has ended. A request already running, a blank text and a session held
|
|
@@ -18,6 +18,10 @@ walked (``/theme``, ``/pet``) and the panels that edit a form of their own (``/c
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import asyncio
|
|
21
|
+
import os
|
|
22
|
+
import shlex
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
21
25
|
from pathlib import Path
|
|
22
26
|
|
|
23
27
|
from zettcode.app.commands import (
|
|
@@ -447,6 +451,126 @@ class BashCommand(TerminalPlugin):
|
|
|
447
451
|
return CommandResult(widget=page)
|
|
448
452
|
|
|
449
453
|
|
|
454
|
+
class CodeReview(TerminalPlugin):
|
|
455
|
+
"""``/code-review``: ask for a review of the uncommitted changes, in this conversation."""
|
|
456
|
+
|
|
457
|
+
name = "code-review-command"
|
|
458
|
+
description = "code review"
|
|
459
|
+
|
|
460
|
+
#: What the model is asked, in pieces: a preset for the turn, not a report about it. The reader gets the review
|
|
461
|
+
#: as an answer they could have asked for by typing all of this themselves.
|
|
462
|
+
INTRO = "Review the uncommitted changes in this workspace."
|
|
463
|
+
COLLECT = (
|
|
464
|
+
"Collect them first: `git status --short` for what changed, `git diff HEAD` for the patch, and the contents "
|
|
465
|
+
"of any untracked file that belongs to the change."
|
|
466
|
+
)
|
|
467
|
+
#: The reviewer is a session of its own, at the shell level of the reader who asked — never above it, since
|
|
468
|
+
#: nobody here approved anything. `{spawn}` is the whole line to run, so the model copies it rather than
|
|
469
|
+
#: reassembling an alias.
|
|
470
|
+
DAEMON = (
|
|
471
|
+
"Have it read by a reviewer of its own, so this conversation keeps its context. `{command} list` shows the "
|
|
472
|
+
"daemons running on this machine. If one is already there for this workspace — `code-review` is the name "
|
|
473
|
+
"this command gives one — reuse it rather than starting another: it has read the earlier rounds and "
|
|
474
|
+
"already knows this change, which makes it the better reviewer. Send it what you need with "
|
|
475
|
+
'`send_message(to="<session id>", message=...)` — the request as you would put it, and that the change '
|
|
476
|
+
"is the uncommitted diff this workspace holds, which it can collect itself. Only when there is none, run\n"
|
|
477
|
+
"\n"
|
|
478
|
+
" {spawn}\n"
|
|
479
|
+
"\n"
|
|
480
|
+
"and take the session id it prints; it is recorded under the name `code-review`, so the next review in "
|
|
481
|
+
"this workspace finds it.\n"
|
|
482
|
+
"\n"
|
|
483
|
+
"Say in that message, every round, that it must answer: send its findings back to you with `send_message`, "
|
|
484
|
+
"and if it has nothing to change, say that in a word. A reviewer that goes quiet leaves the change "
|
|
485
|
+
"waiting, and a review that found nothing cannot be told apart from one that never happened.\n"
|
|
486
|
+
"\n"
|
|
487
|
+
"Keep the reviewer running while you work on what it found: make the changes here, then send it the new "
|
|
488
|
+
"state of the change for another look, and go round again until it has nothing left to say. A reviewer "
|
|
489
|
+
"stopped after the first round cannot tell you whether the fix worked. If a round brings no answer at all, "
|
|
490
|
+
"send the request once more; if that also goes unanswered, read what it did say: its conversation is "
|
|
491
|
+
"`~/.zettcode/sessions/*/<session id>/data.jsonl`, and the last assistant message in it is its review — "
|
|
492
|
+
"report that here, then stop it with `{command} kill <session id>` so that it is not left running. If a "
|
|
493
|
+
"daemon cannot be started here, say why and do the review yourself."
|
|
494
|
+
)
|
|
495
|
+
#: A reviewer is told plainly what it is: the instruction is the safeguard, and the reader is told the same in
|
|
496
|
+
#: the guide, since a session at `full` could change things if it chose to.
|
|
497
|
+
NOT_YOURS = (
|
|
498
|
+
"Tell the reviewer plainly that it reads and does not change: no edits, no writes, no commits, no command "
|
|
499
|
+
"that modifies the workspace. The changes are yours to make, and you keep making them, round after round, "
|
|
500
|
+
"until nothing it found is left."
|
|
501
|
+
)
|
|
502
|
+
#: Under `deny` there is no shell for a daemon to collect a diff with, and the session that asked has none
|
|
503
|
+
#: either: the review is a reading one.
|
|
504
|
+
ALONE = (
|
|
505
|
+
"This session runs no shell commands, so neither the diff nor a daemon to read it can be gathered: review "
|
|
506
|
+
"the files themselves with the reading tools, and say which ones you read."
|
|
507
|
+
)
|
|
508
|
+
SHAPE = (
|
|
509
|
+
"In the review, name concrete problems with `file:line` — correctness, error handling, tests, naming — say "
|
|
510
|
+
"what you would change and why, and do not restate the diff.\n"
|
|
511
|
+
"\n"
|
|
512
|
+
"What to pay attention to: {focus}"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
#: The shell levels a reviewer may be started at. `ask` has no meaning in a process nobody sits at, so it is
|
|
516
|
+
#: started `safe`: sandboxed, which is still more than a reader at `ask` has granted.
|
|
517
|
+
LEVELS = {"full": "full", "safe": "safe", "ask": "safe"}
|
|
518
|
+
|
|
519
|
+
def activate(self, container: TerminalContainer) -> None:
|
|
520
|
+
"""Register the command; it queues, since a review is a request like any other."""
|
|
521
|
+
container.register_command(
|
|
522
|
+
"code-review",
|
|
523
|
+
"review the uncommitted changes: /code-review [what to look for]",
|
|
524
|
+
self.review,
|
|
525
|
+
when_busy="queue",
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
async def review(self, context: CommandContext) -> CommandResult:
|
|
529
|
+
"""Run the review as one turn, with what the reader typed as its focus.
|
|
530
|
+
|
|
531
|
+
The command adds no work of its own: it writes the brief, hands it to `host.prompt`, and the ordinary turn
|
|
532
|
+
does the rest — the echo in the transcript, the session, the streaming, the line it ends on. Nothing is
|
|
533
|
+
gathered here, so the model decides what the review needs and how to get it.
|
|
534
|
+
|
|
535
|
+
The transcript shows the command the reader typed, not the brief it stands for: the brief is what the model
|
|
536
|
+
reads, and `shown` is what the conversation keeps.
|
|
537
|
+
"""
|
|
538
|
+
host = context.require_host()
|
|
539
|
+
argument = context.argument.strip()
|
|
540
|
+
focus = argument or "anything that would make this change wrong or hard to keep"
|
|
541
|
+
shown = f"/code-review {argument}" if argument else "/code-review"
|
|
542
|
+
await host.prompt(self.brief(level=host.client.permission, focus=focus), shown=shown)
|
|
543
|
+
return CommandResult()
|
|
544
|
+
|
|
545
|
+
def brief(self, *, level: str, focus: str) -> str:
|
|
546
|
+
"""Return what the model reads, for a reader whose shell level is ``level``.
|
|
547
|
+
|
|
548
|
+
The reviewer is started at the reader's own level (`ask` becoming `safe`, which is what a level that asks
|
|
549
|
+
means in a process nobody can answer in), and not at all under `deny`.
|
|
550
|
+
"""
|
|
551
|
+
command = self.command()
|
|
552
|
+
parts = [self.INTRO, self.COLLECT]
|
|
553
|
+
if level in self.LEVELS:
|
|
554
|
+
spawn = f"{command} spawn --name code-review --permission {self.LEVELS[level]}"
|
|
555
|
+
parts.append(self.DAEMON.format(command=command, spawn=spawn))
|
|
556
|
+
parts.append(self.NOT_YOURS)
|
|
557
|
+
else:
|
|
558
|
+
parts.append(self.ALONE)
|
|
559
|
+
parts.append(self.SHAPE.format(focus=focus))
|
|
560
|
+
return "\n\n".join(parts)
|
|
561
|
+
|
|
562
|
+
@staticmethod
|
|
563
|
+
def command() -> str:
|
|
564
|
+
"""Return the command that runs this installation's daemon, quoted for the shell that will run it.
|
|
565
|
+
|
|
566
|
+
`python -m zettcode.daemon` and the interpreter that is running this terminal: a checkout whose console
|
|
567
|
+
script is not on the PATH, or a virtualenv nothing else can see, still works. The quoting follows the
|
|
568
|
+
platform's shell, because a Windows path with a space cannot be passed the way POSIX quotes it.
|
|
569
|
+
"""
|
|
570
|
+
argv = [sys.executable, "-m", "zettcode.daemon"]
|
|
571
|
+
return subprocess.list2cmdline(argv) if os.name == "nt" else shlex.join(argv)
|
|
572
|
+
|
|
573
|
+
|
|
450
574
|
class ShellControl(TerminalPlugin):
|
|
451
575
|
"""``/clear``, ``/quit`` and ``/exit``."""
|
|
452
576
|
|
|
@@ -27,7 +27,15 @@ from zettcode.app.ui.widgets import UsagePage
|
|
|
27
27
|
from zettcode.statusline import TURN_PARTS, Row, context_share, usage_parts
|
|
28
28
|
from zettcode.tui import STATUS, bundled_themes
|
|
29
29
|
|
|
30
|
-
from .builtin_commands import
|
|
30
|
+
from .builtin_commands import (
|
|
31
|
+
BashCommand,
|
|
32
|
+
CodeReview,
|
|
33
|
+
HelpCommand,
|
|
34
|
+
ModelCommands,
|
|
35
|
+
SessionCommands,
|
|
36
|
+
ShellControl,
|
|
37
|
+
SideQuestion,
|
|
38
|
+
)
|
|
31
39
|
from .container import TerminalContainer
|
|
32
40
|
from .plugin import TerminalPlugin
|
|
33
41
|
from .state import ShellContext, TurnBuilder
|
|
@@ -171,6 +179,7 @@ BUILTIN_PLUGINS: tuple[TerminalPlugin, ...] = (
|
|
|
171
179
|
SessionCommands(),
|
|
172
180
|
ModelCommands(),
|
|
173
181
|
SideQuestion(),
|
|
182
|
+
CodeReview(),
|
|
174
183
|
BashCommand(),
|
|
175
184
|
ShellControl(),
|
|
176
185
|
TurnLine(),
|
|
@@ -49,7 +49,7 @@ if TYPE_CHECKING: # pragma: no cover - annotations only
|
|
|
49
49
|
pass
|
|
50
50
|
from zettcode.error_text import describe_error
|
|
51
51
|
from zettcode.mentions import MentionRegistry, SessionMentions
|
|
52
|
-
from zettcode.prompt import PromptPart, carries_image,
|
|
52
|
+
from zettcode.prompt import PromptPart, carries_image, mention_parts
|
|
53
53
|
from zettcode.registry import Registry
|
|
54
54
|
from zettcode.relay import Relayed
|
|
55
55
|
from zettcode.sdk.events import QuestionAsked
|
|
@@ -238,6 +238,7 @@ class ZettCodeApp(
|
|
|
238
238
|
#: that never started one — because the update check is off, or because it
|
|
239
239
|
#: is leaving before the background work began — has nothing else to drop.
|
|
240
240
|
self._update_task: asyncio.Task[None] | None = None
|
|
241
|
+
self._updating = False
|
|
241
242
|
self._theme_before = None
|
|
242
243
|
self._previewing = None
|
|
243
244
|
#: The runtime warm-up this shell started, released on the way out.
|
|
@@ -659,7 +660,7 @@ class ZettCodeApp(
|
|
|
659
660
|
if outcome:
|
|
660
661
|
self._settle_wait(outcome)
|
|
661
662
|
|
|
662
|
-
async def prompt(self, text: str) -> bool:
|
|
663
|
+
async def prompt(self, text: str, *, shown: str | None = None) -> bool:
|
|
663
664
|
"""Run ``text`` as one turn of this conversation, as if the reader had typed it.
|
|
664
665
|
|
|
665
666
|
The draft path is taken whole — the echo, ``@`` references, the transcript, the session, the row the
|
|
@@ -667,16 +668,31 @@ class ZettCodeApp(
|
|
|
667
668
|
a reader could have produced. What the command decides is the text; everything after that is the ordinary
|
|
668
669
|
one.
|
|
669
670
|
|
|
671
|
+
With ``shown``, the reader sees one thing and the model reads another: the shown text is what the turn
|
|
672
|
+
draws in the transcript, and the text is the instructions the model reads after it. Both are in the message
|
|
673
|
+
the session keeps — the instructions are not dropped, they are only kept out of what is displayed — so a
|
|
674
|
+
later turn, and a restored session, still have them in front of the model.
|
|
675
|
+
|
|
670
676
|
The turn is awaited, so the request slot stays the command's until the answer is in: a second draft cannot
|
|
671
677
|
start behind it, and the command reports what it did once it is done.
|
|
672
678
|
"""
|
|
673
679
|
if not text.strip():
|
|
674
680
|
return False
|
|
681
|
+
if self._updating:
|
|
682
|
+
self.flash("updating ZettCode \u2014 this window takes no input until it is done", level="info")
|
|
683
|
+
return False
|
|
675
684
|
if self.client.session.locked:
|
|
676
685
|
self.flash("this session is open in another ZettCode process; it is read-only here", level="error")
|
|
677
686
|
return False
|
|
678
|
-
|
|
679
|
-
|
|
687
|
+
display = (shown if shown is not None else text).strip() or text.strip()
|
|
688
|
+
if display == text.strip():
|
|
689
|
+
# No `@` pass here: the SDK resolves the references itself (it is asked for them below), and doing
|
|
690
|
+
# both is how the same instructions once reached the model twice.
|
|
691
|
+
await self._run_prompt(text, (), advance=False)
|
|
692
|
+
else:
|
|
693
|
+
# The brief is not the reader's message: it rides after the shown text, which is what the transcript
|
|
694
|
+
# draws. The SDK keeps both in the message and shows the reader the first.
|
|
695
|
+
await self._run_prompt(display, (), instructions=text, advance=False)
|
|
680
696
|
return True
|
|
681
697
|
|
|
682
698
|
async def run_continue(self) -> None:
|
|
@@ -835,6 +851,11 @@ class ZettCodeApp(
|
|
|
835
851
|
"""
|
|
836
852
|
if command is None:
|
|
837
853
|
command = self._is_command(value)
|
|
854
|
+
if self._updating:
|
|
855
|
+
# The installation is being replaced under this process: nothing typed now can be run against it, and a
|
|
856
|
+
# half-installed package is not a thing to hand a prompt to.
|
|
857
|
+
self.flash("updating ZettCode \u2014 this window takes no input until it is done", level="info")
|
|
858
|
+
return False
|
|
838
859
|
if self.client.session.locked and not command:
|
|
839
860
|
# Only commands are taken, which is how a reader leaves: the composer does not let anything else be
|
|
840
861
|
# typed, and this is for what arrives some other way (a draft that was there already, a recall).
|
|
@@ -873,7 +894,7 @@ class ZettCodeApp(
|
|
|
873
894
|
self.transcript.error(f"{self.client.model.name} does not take images")
|
|
874
895
|
self.app.invalidate()
|
|
875
896
|
return False
|
|
876
|
-
self._task = asyncio.create_task(self._run_prompt(value, parts,
|
|
897
|
+
self._task = asyncio.create_task(self._run_prompt(value, parts, instructions=None))
|
|
877
898
|
return True
|
|
878
899
|
|
|
879
900
|
def _queue_steering(self, value: str, key: str | None = None, parts: Sequence[PromptPart] = ()) -> None:
|
|
@@ -981,8 +1002,8 @@ class ZettCodeApp(
|
|
|
981
1002
|
self,
|
|
982
1003
|
prompt: str,
|
|
983
1004
|
parts: Sequence[PromptPart] = (),
|
|
984
|
-
hint: str | None = None,
|
|
985
1005
|
*,
|
|
1006
|
+
instructions: str | None = None,
|
|
986
1007
|
relayed: Relayed | None = None,
|
|
987
1008
|
advance: bool = True,
|
|
988
1009
|
) -> None:
|
|
@@ -993,8 +1014,9 @@ class ZettCodeApp(
|
|
|
993
1014
|
into the transcript.
|
|
994
1015
|
parts: The same turn as ordered text and image parts; an empty
|
|
995
1016
|
sequence falls back to the prompt alone.
|
|
996
|
-
|
|
997
|
-
|
|
1017
|
+
instructions: What the model reads after the prompt but the reader does not: the SDK resolves the
|
|
1018
|
+
draft's ``@`` references itself, so this carries only what a command adds (see
|
|
1019
|
+
:meth:`CommandHost.prompt`). The stored message keeps ``prompt``.
|
|
998
1020
|
relayed: The turn answers a message another session sent, which the transcript draws as
|
|
999
1021
|
that, the model is told where it came from, and the session stores as that.
|
|
1000
1022
|
"""
|
|
@@ -1029,7 +1051,12 @@ class ZettCodeApp(
|
|
|
1029
1051
|
**self._answers(),
|
|
1030
1052
|
)
|
|
1031
1053
|
else:
|
|
1032
|
-
run = self.client.session.prompt(
|
|
1054
|
+
run = self.client.session.prompt(
|
|
1055
|
+
prompt_pieces(parts or (prompt,)),
|
|
1056
|
+
mentions=True,
|
|
1057
|
+
instructions=instructions,
|
|
1058
|
+
**self._answers(),
|
|
1059
|
+
)
|
|
1033
1060
|
async with run:
|
|
1034
1061
|
async for event in run:
|
|
1035
1062
|
tally.see(event)
|
|
@@ -18,7 +18,7 @@ import asyncio
|
|
|
18
18
|
from dataclasses import dataclass
|
|
19
19
|
from typing import TYPE_CHECKING
|
|
20
20
|
|
|
21
|
-
from zettcode.prompt import PromptPart, carries_image,
|
|
21
|
+
from zettcode.prompt import PromptPart, carries_image, mention_parts
|
|
22
22
|
from zettcode.tui import Screen
|
|
23
23
|
from zettcode.tui.render import truncate
|
|
24
24
|
|
|
@@ -423,4 +423,4 @@ class FollowUpsMixin(ShellState):
|
|
|
423
423
|
# be able to start a second turn beside it.
|
|
424
424
|
self._set_busy(True)
|
|
425
425
|
parts = mention_parts(item.parts, self.mentions)
|
|
426
|
-
self._task = asyncio.create_task(self._run_prompt(item.text, parts
|
|
426
|
+
self._task = asyncio.create_task(self._run_prompt(item.text, parts))
|
|
@@ -59,7 +59,12 @@ class KeysMixin(ShellState):
|
|
|
59
59
|
"ctrl_d",
|
|
60
60
|
"quit",
|
|
61
61
|
priority="capture",
|
|
62
|
-
when=lambda:
|
|
62
|
+
when=lambda: (
|
|
63
|
+
self.app.screens.top.name != PAGE_SCREEN
|
|
64
|
+
and not self._busy
|
|
65
|
+
and not self.composer.text
|
|
66
|
+
and not self._updating
|
|
67
|
+
),
|
|
63
68
|
)
|
|
64
69
|
# The follow-up list. The modifier key (Option on a Mac) is the list's own: with
|
|
65
70
|
# nothing highlighted it walks into the list from the composer's side, and with a
|
|
@@ -134,6 +134,8 @@ class MessagingMixin(ShellState):
|
|
|
134
134
|
"""
|
|
135
135
|
if not self._messaging_live:
|
|
136
136
|
return Receipt(False, "this session is closing")
|
|
137
|
+
if self._updating:
|
|
138
|
+
return Receipt(False, "this window is installing an update; try again when it is done")
|
|
137
139
|
if self.client.session.locked:
|
|
138
140
|
return Receipt(False, "this session is open in another ZettCode process, so it is read-only here")
|
|
139
141
|
if not self._busy:
|
|
@@ -156,11 +158,11 @@ class MessagingMixin(ShellState):
|
|
|
156
158
|
# Busy before the task has run, exactly as for a follow-up: between here and its first step a
|
|
157
159
|
# second message, or a keystroke, must not be able to start a turn beside this one.
|
|
158
160
|
self._set_busy(True)
|
|
159
|
-
self._task = asyncio.create_task(self._run_prompt(relayed.text, (relayed.text,),
|
|
161
|
+
self._task = asyncio.create_task(self._run_prompt(relayed.text, (relayed.text,), relayed=relayed))
|
|
160
162
|
|
|
161
163
|
def _start_next_relayed(self) -> bool:
|
|
162
164
|
"""Start the oldest waiting message, if there is one and nothing is running."""
|
|
163
|
-
if self._busy or not self._inbox:
|
|
165
|
+
if self._busy or self._updating or not self._inbox:
|
|
164
166
|
return False
|
|
165
167
|
self._start_relayed(self._inbox.pop(0))
|
|
166
168
|
return True
|