zettcode 0.1.7__tar.gz → 0.1.8__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.7 → zettcode-0.1.8}/.gitignore +5 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/PKG-INFO +2 -2
- {zettcode-0.1.7 → zettcode-0.1.8}/pyproject.toml +1 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/__init__.py +1 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/agent/__init__.py +2 -1
- zettcode-0.1.8/src/zettcode/app/agent/agent/commands.py +214 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/agent/sessions.py +13 -2
- zettcode-0.1.8/src/zettcode/app/agent/agent/settings.py +327 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/agent/turns.py +88 -9
- zettcode-0.1.8/src/zettcode/app/agent/conversation/effort_gauge.py +126 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/entries.py +6 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/presenters.py +134 -57
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/projection.py +11 -0
- zettcode-0.1.8/src/zettcode/app/agent/conversation/pulse.py +116 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/rendering.py +13 -9
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/replay.py +7 -2
- zettcode-0.1.8/src/zettcode/app/agent/conversation/shimmer.py +256 -0
- zettcode-0.1.8/src/zettcode/app/agent/conversation/stops.py +57 -0
- zettcode-0.1.8/src/zettcode/app/agent/conversation/text.py +230 -0
- zettcode-0.1.8/src/zettcode/app/agent/conversation/tips.py +76 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/transcript/__init__.py +5 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/transcript/stream.py +1 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/transcript/turn.py +10 -4
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/dialogue/mentions.py +4 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/dialogue/relay.py +1 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/dialogue/title.py +10 -2
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/errors.py +95 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/capabilities.py +8 -2
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/compaction.py +44 -2
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/context.py +7 -2
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/mcp.py +42 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/messaging.py +3 -2
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/messenger.py +4 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/subagents.py +111 -13
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/usage.py +32 -13
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/permission/__init__.py +2 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/runtime.py +137 -126
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/storage/__init__.py +16 -4
- zettcode-0.1.8/src/zettcode/app/agent/storage/layout.py +161 -0
- zettcode-0.1.8/src/zettcode/app/agent/storage/ledger.py +651 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/storage/locks.py +59 -7
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/storage/metadata.py +4 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/storage/records.py +1 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/storage/store.py +112 -35
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/brand.py +15 -5
- zettcode-0.1.8/src/zettcode/app/commandline/__init__.py +47 -0
- zettcode-0.1.8/src/zettcode/app/commandline/arguments.py +169 -0
- zettcode-0.1.8/src/zettcode/app/commandline/completion.py +262 -0
- zettcode-0.1.8/src/zettcode/app/commandline/history.py +305 -0
- zettcode-0.1.8/src/zettcode/app/commandline/runner.py +365 -0
- zettcode-0.1.8/src/zettcode/app/commands.py +277 -0
- zettcode-0.1.8/src/zettcode/app/pets/__init__.py +62 -0
- zettcode-0.1.8/src/zettcode/app/pets/cat.py +52 -0
- zettcode-0.1.8/src/zettcode/app/pets/chibi.py +55 -0
- zettcode-0.1.8/src/zettcode/app/pets/dog.py +52 -0
- zettcode-0.1.8/src/zettcode/app/pets/kangaroo.py +58 -0
- zettcode-0.1.8/src/zettcode/app/pets/model.py +191 -0
- zettcode-0.1.8/src/zettcode/app/pets/penguin.py +60 -0
- zettcode-0.1.8/src/zettcode/app/pets/polarbear.py +50 -0
- zettcode-0.1.8/src/zettcode/app/pets/scarfy.py +61 -0
- zettcode-0.1.8/src/zettcode/app/pets/seagull.py +63 -0
- zettcode-0.1.8/src/zettcode/app/pets/slime.py +56 -0
- zettcode-0.1.8/src/zettcode/app/pets/snowbear.py +55 -0
- zettcode-0.1.8/src/zettcode/app/pets/sprout.py +64 -0
- zettcode-0.1.8/src/zettcode/app/pets/tiger.py +52 -0
- zettcode-0.1.8/src/zettcode/app/pets/whale.py +54 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/app.py +150 -30
- zettcode-0.1.8/src/zettcode/app/ui/bars.py +286 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/boot.py +15 -4
- zettcode-0.1.8/src/zettcode/app/ui/commands.py +282 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/config_form.py +88 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/demo.py +7 -6
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/followups.py +4 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/gc_policy.py +1 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/keys.py +76 -7
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/labels.py +86 -18
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/messaging.py +5 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/notices.py +2 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/providers.py +119 -26
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/sessions.py +5 -4
- zettcode-0.1.8/src/zettcode/app/ui/settings.py +160 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/shell.py +23 -9
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/updating.py +4 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/__init__.py +29 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/approval_page.py +4 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/ask_page.py +4 -4
- zettcode-0.1.8/src/zettcode/app/ui/widgets/bash_page.py +91 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/completer.py +41 -8
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/composer.py +52 -14
- zettcode-0.1.8/src/zettcode/app/ui/widgets/context_charts.py +283 -0
- zettcode-0.1.8/src/zettcode/app/ui/widgets/context_page.py +418 -0
- zettcode-0.1.8/src/zettcode/app/ui/widgets/effort_page.py +295 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/follow_up.py +3 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/input_page.py +3 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/model_page.py +3 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/panel.py +1 -1
- zettcode-0.1.8/src/zettcode/app/ui/widgets/pet.py +222 -0
- zettcode-0.1.8/src/zettcode/app/ui/widgets/pet_page.py +347 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/root.py +14 -7
- zettcode-0.1.8/src/zettcode/app/ui/widgets/self_compact_page.py +409 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/send_kind_page.py +4 -3
- zettcode-0.1.8/src/zettcode/app/ui/widgets/sessions_page.py +268 -0
- zettcode-0.1.8/src/zettcode/app/ui/widgets/statusline_page.py +437 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/steering.py +4 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/transcript.py +5 -5
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/update_page.py +2 -2
- zettcode-0.1.8/src/zettcode/app/ui/widgets/usage_charts.py +464 -0
- zettcode-0.1.8/src/zettcode/app/ui/widgets/usage_page.py +273 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/widgets/welcome.py +5 -2
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/cli.py +116 -25
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/config.py +222 -22
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/config_edit.py +88 -0
- zettcode-0.1.8/src/zettcode/doctor.py +167 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/paths.py +9 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/plugins/__init__.py +14 -0
- zettcode-0.1.8/src/zettcode/plugins/builtin_agent.py +383 -0
- zettcode-0.1.8/src/zettcode/plugins/builtin_commands.py +420 -0
- zettcode-0.1.8/src/zettcode/plugins/builtins.py +187 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/plugins/container.py +279 -6
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/plugins/extension.py +4 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/plugins/loader.py +68 -6
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/plugins/plugin.py +68 -3
- zettcode-0.1.8/src/zettcode/plugins/services.py +97 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/plugins/state.py +63 -4
- zettcode-0.1.8/src/zettcode/py.typed +1 -0
- zettcode-0.1.8/src/zettcode/sdk/__init__.py +128 -0
- zettcode-0.1.8/src/zettcode/sdk/_translate.py +179 -0
- zettcode-0.1.8/src/zettcode/sdk/client.py +730 -0
- zettcode-0.1.8/src/zettcode/sdk/errors.py +104 -0
- zettcode-0.1.8/src/zettcode/sdk/events.py +345 -0
- zettcode-0.1.8/src/zettcode/sdk/handlers.py +124 -0
- zettcode-0.1.8/src/zettcode/sdk/session.py +875 -0
- zettcode-0.1.8/src/zettcode/sdk/types.py +337 -0
- zettcode-0.1.8/src/zettcode/statusline.py +141 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/__init__.py +30 -2
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/events.py +1 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/theme.py +26 -6
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/widget.py +8 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/input/events.py +2 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/layout/spacing.py +2 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/render/__init__.py +4 -0
- zettcode-0.1.8/src/zettcode/tui/render/ansi.py +371 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/render/text.py +13 -1
- zettcode-0.1.8/src/zettcode/tui/theme_file.py +290 -0
- zettcode-0.1.8/src/zettcode/tui/themes/ayu-mirage.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/catppuccin-latte.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/catppuccin-mocha.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/dracula.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/github-dimmed.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/github-light.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/gruvbox-dark.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/gruvbox-light.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/kanagawa.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/nord.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/one-dark.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/rose-pine-dawn.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/rose-pine.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/solarized-dark.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/solarized-light.toml +44 -0
- zettcode-0.1.8/src/zettcode/tui/themes/tokyo-night.toml +44 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/__init__.py +3 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/collapsible.py +2 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/completion.py +6 -0
- zettcode-0.1.8/src/zettcode/tui/widgets/console.py +442 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/list.py +52 -10
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/list_page.py +11 -3
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/markdown.py +2 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/status.py +20 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/tasks.py +2 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/text.py +2 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/textarea.py +25 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/toast.py +2 -1
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/update.py +78 -0
- zettcode-0.1.7/src/zettcode/app/agent/agent/commands.py +0 -86
- zettcode-0.1.7/src/zettcode/app/agent/agent/settings.py +0 -164
- zettcode-0.1.7/src/zettcode/app/agent/conversation/rows.py +0 -437
- zettcode-0.1.7/src/zettcode/app/commands.py +0 -151
- zettcode-0.1.7/src/zettcode/app/ui/commands.py +0 -311
- zettcode-0.1.7/src/zettcode/app/ui/rows.py +0 -170
- zettcode-0.1.7/src/zettcode/app/ui/settings.py +0 -102
- zettcode-0.1.7/src/zettcode/app/ui/widgets/context_page.py +0 -114
- zettcode-0.1.7/src/zettcode/app/ui/widgets/sessions_page.py +0 -84
- zettcode-0.1.7/src/zettcode/plugins/builtins.py +0 -82
- zettcode-0.1.7/src/zettcode/tui/theme_file.py +0 -102
- {zettcode-0.1.7 → zettcode-0.1.8}/LICENSE +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/README.md +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/README.zh-CN.md +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/_compat.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/agent/base.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/agent/context.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/agent/exports.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/agent/interaction.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/agent/prompt.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/changes.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/transcript/fold.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/dialogue/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/dialogue/ask.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/dialogue/side.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/export/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/export/trace/script.js +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/export/trace/style.css +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/export/trace/template.html +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/mcp_notice.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/extensions/tool_output.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/permission/approval.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/agent/permission/readonly.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/pipeline.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/registry.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/app/ui/clipboard.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/__main__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/client.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/files.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/finder.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/hub.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/log.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/models.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/process.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/protocol.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/roster.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/runner.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/server.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/text.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/broker/web.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/collector.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/filelock.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/plugins/mixins.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/providers.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/capabilities.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/clipboard.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/console.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/app.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/focus.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/geometry.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/host.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/keymap.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/scheduler.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/core/screen.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/gallery.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/glyphs.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/input/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/input/bridge.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/input/decoder.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/input/keys.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/input/reader.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/layout/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/layout/box.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/layout/overlay.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/layout/scroll.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/layout/solver.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/render/canvas.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/render/code.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/render/color.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/render/renderer.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/render/rich_text.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/render/style.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/render/sweep.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/runner.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/terminal.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/testing/__init__.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/testing/harness.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/testing/snapshot.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/dialog.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/diff.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/progress.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/rich_text.py +0 -0
- {zettcode-0.1.7 → zettcode-0.1.8}/src/zettcode/tui/widgets/table.py +0 -0
|
@@ -20,3 +20,8 @@ node_modules/
|
|
|
20
20
|
docs/.vitepress/dist/
|
|
21
21
|
docs/.vitepress/cache/
|
|
22
22
|
docs/.vitepress/.temp/
|
|
23
|
+
|
|
24
|
+
# The palette gallery is generated by `make palettes`: every widget painted in
|
|
25
|
+
# every palette, written by docs/scripts/palette_gallery.py. Regenerate it
|
|
26
|
+
# rather than committing a 2 MB copy that goes stale the moment a colour moves.
|
|
27
|
+
docs/internal/palettes.html
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: zettcode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
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
|
|
@@ -32,7 +32,7 @@ Requires-Dist: truststore<1,>=0.10
|
|
|
32
32
|
Requires-Dist: uvicorn<1,>=0.35
|
|
33
33
|
Requires-Dist: wcwidth<1,>=0.2
|
|
34
34
|
Requires-Dist: websockets<17,>=15
|
|
35
|
-
Requires-Dist: zett-agent<0.2,>=0.1.
|
|
35
|
+
Requires-Dist: zett-agent<0.2,>=0.1.17
|
|
36
36
|
Description-Content-Type: text/markdown
|
|
37
37
|
|
|
38
38
|
<p align="center">
|
|
@@ -53,7 +53,7 @@ dependencies = [
|
|
|
53
53
|
# than it did. An exact pin would make zettcode fight every other tool that
|
|
54
54
|
# shares the dependency — including an upgrade of zett-agent itself, which pip
|
|
55
55
|
# would downgrade to satisfy it.
|
|
56
|
-
"zett-agent>=0.1.
|
|
56
|
+
"zett-agent>=0.1.17,<0.2",
|
|
57
57
|
]
|
|
58
58
|
|
|
59
59
|
[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.1.
|
|
10
|
+
__version__ = "0.1.8"
|
|
11
11
|
|
|
12
12
|
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
13
13
|
_EXPORTS = {
|
|
@@ -22,7 +22,8 @@ is the composition and the lifecycle — building the runtime, and closing it.
|
|
|
22
22
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
|
-
from
|
|
25
|
+
from zettcode.config import ZettCodeConfig
|
|
26
|
+
|
|
26
27
|
from ..runtime import ZettCodeRuntime
|
|
27
28
|
from .commands import CommandsMixin
|
|
28
29
|
from .context import ContextMixin
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""What the agent offers the shell to run and to show: its own commands, and what plugins added.
|
|
2
|
+
|
|
3
|
+
The commands report through transcript Markdown; commands that present a widget belong to
|
|
4
|
+
the shell, which owns the widgets. The plugin contributions are listed here because the
|
|
5
|
+
agent is where the shell asks for everything the runtime loaded.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from zettcode.app.commands import Command, CommandContext, CommandHost, CommandResult
|
|
11
|
+
from zettcode.app.pets import PetCatalog, bundled_pets
|
|
12
|
+
from zettcode.plugins import UiRow
|
|
13
|
+
from zettcode.plugins.services import HOST_LINK, HostLink
|
|
14
|
+
from zettcode.providers import BUILTIN_PROVIDERS, Provider, merge_providers
|
|
15
|
+
from zettcode.statusline import NAMES, unknown
|
|
16
|
+
from zettcode.tui import ThemeCatalog
|
|
17
|
+
|
|
18
|
+
from ..conversation.presenters import Presenters
|
|
19
|
+
from ..dialogue.mentions import MentionProvider
|
|
20
|
+
from .exports import ExportsMixin
|
|
21
|
+
|
|
22
|
+
#: The words ``/self-compact`` takes after it, or after ``global``.
|
|
23
|
+
_SELF_COMPACT_WORDS = frozenset({"on", "off", "default"})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CommandsMixin(ExportsMixin):
|
|
27
|
+
"""The ``/new``, ``/compact``, and ``/export`` commands, and the plugins' commands, mentions, and rows."""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def commands(self) -> tuple[Command, ...]:
|
|
31
|
+
"""Return the conversation commands the agent owns.
|
|
32
|
+
|
|
33
|
+
They report through transcript Markdown; commands that present a widget
|
|
34
|
+
(such as the model picker) belong to the shell, which owns the widgets.
|
|
35
|
+
"""
|
|
36
|
+
return (
|
|
37
|
+
Command("/new", "start a fresh session", "agent", self._command_new),
|
|
38
|
+
Command("/compact", "summarize the context now", "agent", self._command_compact),
|
|
39
|
+
Command("/export", "write this session to an HTML file: /export [path]", "agent", self._command_export),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def attach_host(self, host: CommandHost) -> None:
|
|
43
|
+
"""Tell the plugins where the shell is, so the words their commands offer can follow the session."""
|
|
44
|
+
link = self.runtime.plugins.find(HOST_LINK, HostLink)
|
|
45
|
+
if link is not None:
|
|
46
|
+
link.attach(host)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def plugin_commands(self) -> tuple[Command, ...]:
|
|
50
|
+
"""Return the commands the installed plugins registered, not counting the built-in ones.
|
|
51
|
+
|
|
52
|
+
The shell keeps them after everything built in, so a plugin cannot take over a built-in name;
|
|
53
|
+
plugin-versus-plugin duplicates are rejected when the plugins load.
|
|
54
|
+
"""
|
|
55
|
+
return self.runtime.plugins.installed_commands
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def builtin_plugin_commands(self) -> tuple[Command, ...]:
|
|
59
|
+
"""Return the commands the program's own plugins registered: ``/resume``, ``/model``, ``/usage`` and the rest."""
|
|
60
|
+
return self.runtime.plugins.builtin_commands
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def plugin_mentions(self) -> tuple[MentionProvider, ...]:
|
|
64
|
+
"""Return the ``@`` resource providers the installed plugins registered."""
|
|
65
|
+
return self.runtime.plugins.mentions
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def providers(self) -> tuple[Provider, ...]:
|
|
69
|
+
"""Return the hosts ``/config`` can offer: the built-in ones, then the plugins' own."""
|
|
70
|
+
return merge_providers(BUILTIN_PROVIDERS, self.runtime.plugins.providers)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def themes(self) -> ThemeCatalog:
|
|
74
|
+
"""Return every palette that can be asked for by name: the plugins', then the reader's own files.
|
|
75
|
+
|
|
76
|
+
Built fresh each time it is asked: it lists names and builds nothing, and the reader's directory may have
|
|
77
|
+
gained a file since the last look.
|
|
78
|
+
"""
|
|
79
|
+
return ThemeCatalog(self.runtime.plugins.themes, self.runtime.config.themes_dir)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def pets(self) -> PetCatalog:
|
|
83
|
+
"""Return every pet that can be asked for by name: the built-in plugin's, then any other plugin's.
|
|
84
|
+
|
|
85
|
+
A plugin bundle that offers none (one built by hand, without the built-in plugin) gets the program's own.
|
|
86
|
+
"""
|
|
87
|
+
sources = self.runtime.plugins.pets
|
|
88
|
+
return PetCatalog(sources or bundled_pets())
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def plugin_failures(self) -> tuple[str, ...]:
|
|
92
|
+
"""Return one message per plugin that could not be loaded or activated."""
|
|
93
|
+
return self.runtime.plugins.failures
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def presenters(self) -> Presenters:
|
|
97
|
+
"""Return how this run draws entries: the built-in presenters, and any a plugin outranks them with.
|
|
98
|
+
|
|
99
|
+
Built from the bundle rather than from a module-level default for the same reason the palettes are: the
|
|
100
|
+
program's own rows are registered through the plugin route, so a plugin that draws a kind or a family of
|
|
101
|
+
rows reaches the transcript without a second path only plugins could not take.
|
|
102
|
+
"""
|
|
103
|
+
return Presenters(self.runtime.plugins.presenters)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def plugin_rows(self) -> tuple[UiRow, ...]:
|
|
107
|
+
"""Return the header and status rows plugins asked the shell to draw."""
|
|
108
|
+
return self.runtime.plugins.rows
|
|
109
|
+
|
|
110
|
+
async def _command_new(self, context: CommandContext) -> CommandResult:
|
|
111
|
+
"""Start a fresh session."""
|
|
112
|
+
session_id = self.new_session()
|
|
113
|
+
return CommandResult(notification=f"started session {session_id[:8]}")
|
|
114
|
+
|
|
115
|
+
async def _command_export(self, context: CommandContext) -> CommandResult:
|
|
116
|
+
"""Write the session and its context to one HTML file.
|
|
117
|
+
|
|
118
|
+
The path argument is optional; without it the file lands in the
|
|
119
|
+
workspace, named after the session, and the result says where.
|
|
120
|
+
"""
|
|
121
|
+
path = await self.export_session(context.argument.strip() or None)
|
|
122
|
+
return CommandResult(message=f"Exported to `{path}`")
|
|
123
|
+
|
|
124
|
+
def self_compaction_summary(self) -> str:
|
|
125
|
+
"""Say in one line whether self-compaction is on here, where that comes from, and what the default is."""
|
|
126
|
+
state = "on" if self.self_compaction else "off"
|
|
127
|
+
default = "on" if self.self_compaction_default else "off"
|
|
128
|
+
if self.self_compaction_choice is None:
|
|
129
|
+
return f"Self-compaction is {state}: this session follows the default ({default})"
|
|
130
|
+
return f"Self-compaction is {state}: this session chose it; the default is {default}"
|
|
131
|
+
|
|
132
|
+
def self_compact_report(self) -> str:
|
|
133
|
+
"""Say what self-compaction is, whether it is on here, and where that comes from."""
|
|
134
|
+
choice = self.self_compaction_choice
|
|
135
|
+
default = "on" if self.self_compaction_default else "off"
|
|
136
|
+
state = "on" if self.self_compaction else "off"
|
|
137
|
+
source = (
|
|
138
|
+
f"this session chose it; the config default is **{default}**"
|
|
139
|
+
if choice is not None
|
|
140
|
+
else "following the config default (`[compaction] self`)"
|
|
141
|
+
)
|
|
142
|
+
return (
|
|
143
|
+
f"Self-compaction is **{state}** ({source}).\n\n"
|
|
144
|
+
"When on, the model gets a `compact_context` tool and can summarize its own earlier conversation "
|
|
145
|
+
"when it judges the detail is no longer needed. The current turn is always kept. "
|
|
146
|
+
"`/compact` and the automatic threshold work either way.\n\n"
|
|
147
|
+
"`/self-compact on|off` sets this session; `default` hands it back to the config; "
|
|
148
|
+
"`global on|off` changes the default for every session that has not chosen."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def change_self_compaction(self, argument: str) -> str:
|
|
152
|
+
"""Read or change self-compaction from the words ``/self-compact`` takes, and say what now applies.
|
|
153
|
+
|
|
154
|
+
``on``, ``off`` and ``default`` set this session's own choice; ``global on`` and ``global off``
|
|
155
|
+
write the default for every session into the config file. A failed write is never reported as saved.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ValueError: The words are not ones the command takes, or the change could not be saved.
|
|
159
|
+
"""
|
|
160
|
+
words = argument.lower().split()
|
|
161
|
+
scope = "session"
|
|
162
|
+
if words[:1] == ["global"]:
|
|
163
|
+
scope, words = "global", words[1:]
|
|
164
|
+
usage = ValueError("Usage: /self-compact [on|off|default] or /self-compact global on|off")
|
|
165
|
+
if len(words) > 1 or (scope == "global" and not words) or (words and words[0] not in _SELF_COMPACT_WORDS):
|
|
166
|
+
raise usage
|
|
167
|
+
if scope == "global":
|
|
168
|
+
if words[0] == "default":
|
|
169
|
+
raise usage
|
|
170
|
+
self.remember_self_compaction(words[0] == "on")
|
|
171
|
+
elif words:
|
|
172
|
+
self.use_self_compaction({"on": True, "off": False, "default": None}[words[0]])
|
|
173
|
+
return self.self_compact_report()
|
|
174
|
+
|
|
175
|
+
def statusline_summary(self) -> str:
|
|
176
|
+
"""Say in one line which parts of the header and the status line are hidden."""
|
|
177
|
+
hidden = [name for name in NAMES if name in self.statusline_hidden]
|
|
178
|
+
if not hidden:
|
|
179
|
+
return "Status line: every part is shown"
|
|
180
|
+
return f"Status line: {len(hidden)} of {len(NAMES)} parts hidden ({', '.join(hidden)})"
|
|
181
|
+
|
|
182
|
+
def change_statusline(self, argument: str) -> str:
|
|
183
|
+
"""Hide or show parts of the header and status line from the words ``/statusline`` takes.
|
|
184
|
+
|
|
185
|
+
``hide <name>...`` and ``show <name>...`` change the named parts, and ``show all`` brings every one
|
|
186
|
+
back. The change is written to ``[statusline]`` in the config file, and nothing is changed when any
|
|
187
|
+
name is not a part.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ValueError: The words are not ones the command takes, or the change could not be saved.
|
|
191
|
+
"""
|
|
192
|
+
words = argument.lower().split()
|
|
193
|
+
usage = ValueError(
|
|
194
|
+
f"Usage: /statusline [hide|show] <part>... or /statusline show all; parts: {', '.join(NAMES)}"
|
|
195
|
+
)
|
|
196
|
+
if len(words) < 2 or words[0] not in {"hide", "show"}:
|
|
197
|
+
raise usage
|
|
198
|
+
visible = words[0] == "show"
|
|
199
|
+
names = list(NAMES) if visible and words[1:] == ["all"] else words[1:]
|
|
200
|
+
if stray := unknown(names):
|
|
201
|
+
raise ValueError(f"No such part: {', '.join(stray)}. Parts: {', '.join(NAMES)}")
|
|
202
|
+
self.remember_statusline({name: visible for name in names})
|
|
203
|
+
return self.statusline_summary()
|
|
204
|
+
|
|
205
|
+
async def _command_compact(self, context: CommandContext) -> CommandResult:
|
|
206
|
+
"""Summarize the conversation now, whatever its size.
|
|
207
|
+
|
|
208
|
+
The row and the outcome notice arrive while this await runs: the pass
|
|
209
|
+
dispatches through the same event dispatcher a turn does, so the
|
|
210
|
+
transcript animates ``Compacting`` and ends with ``Context compaction
|
|
211
|
+
applied`` (or ``skipped`` when the summary would not be smaller).
|
|
212
|
+
"""
|
|
213
|
+
await self.compact()
|
|
214
|
+
return CommandResult()
|
|
@@ -8,15 +8,17 @@ the rest of the shell does.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
from collections.abc import Sequence
|
|
11
12
|
from dataclasses import dataclass
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
from zett_agent.messages import AssistantMessage, UserMessage
|
|
16
|
+
from zett_agent.model import ReasoningEffort
|
|
15
17
|
|
|
16
18
|
from ..conversation.replay import Replayed, replay
|
|
17
19
|
from ..conversation.transcript import Transcript
|
|
18
20
|
from ..dialogue.title import summarize_title
|
|
19
|
-
from ..storage import Session, SessionInfo
|
|
21
|
+
from ..storage import Session, SessionInfo, SessionStats
|
|
20
22
|
from .context import ContextMixin
|
|
21
23
|
|
|
22
24
|
#: The name a session shows before the agent has given it a title.
|
|
@@ -170,6 +172,13 @@ class SessionsMixin(ContextMixin):
|
|
|
170
172
|
"""List persisted session metadata by recent activity."""
|
|
171
173
|
return await self.runtime.persistence.list_sessions(limit=limit)
|
|
172
174
|
|
|
175
|
+
async def session_stats(self, sessions: Sequence[SessionInfo]) -> dict[str, SessionStats]:
|
|
176
|
+
"""Return the entry count and token totals of listed sessions, keyed by session id.
|
|
177
|
+
|
|
178
|
+
A session missing from the answer has no totals to show.
|
|
179
|
+
"""
|
|
180
|
+
return await self.runtime.persistence.session_stats([session.session_id for session in sessions])
|
|
181
|
+
|
|
173
182
|
async def rename_session(self, title: str) -> str:
|
|
174
183
|
"""Store a display title for the active session and return it cleaned.
|
|
175
184
|
|
|
@@ -218,7 +227,9 @@ class SessionsMixin(ContextMixin):
|
|
|
218
227
|
if not question or not answer:
|
|
219
228
|
return None
|
|
220
229
|
await self.runtime.start()
|
|
221
|
-
|
|
230
|
+
# A level the model takes, so naming the session cannot be what fails on an endpoint that lacks `low`.
|
|
231
|
+
effort = self.runtime.active_model.accepted_effort(ReasoningEffort.LOW)
|
|
232
|
+
title = await summarize_title(self.runtime.provider, question=question, answer=answer, effort=effort)
|
|
222
233
|
if title is None:
|
|
223
234
|
return None
|
|
224
235
|
await self.runtime.persistence.set_title(session_id, title)
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""What the reader can change about how the agent runs: model, reasoning effort, shell permission."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from zettcode.config import ModelConfig, _read_models
|
|
10
|
+
from zettcode.config_edit import (
|
|
11
|
+
ConfigEditError,
|
|
12
|
+
add_model,
|
|
13
|
+
file_entries,
|
|
14
|
+
remove_model,
|
|
15
|
+
replace_model,
|
|
16
|
+
set_pet,
|
|
17
|
+
set_self_compaction,
|
|
18
|
+
set_statusline,
|
|
19
|
+
set_theme,
|
|
20
|
+
write_text_atomically,
|
|
21
|
+
)
|
|
22
|
+
from zettcode.paths import config_path
|
|
23
|
+
from zettcode.statusline import NAMES
|
|
24
|
+
|
|
25
|
+
from ..permission import PERMISSION_LEVELS, Permission
|
|
26
|
+
from .base import AgentBase
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SettingsMixin(AgentBase):
|
|
30
|
+
"""Read and change the model, the reasoning effort, and how much of the shell may run unasked."""
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def reduced_motion(self) -> bool:
|
|
34
|
+
"""Return whether decorative animation is disabled."""
|
|
35
|
+
return self.runtime.config.reduced_motion
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def models(self) -> tuple[ModelConfig, ...]:
|
|
39
|
+
"""Return the models available for selection."""
|
|
40
|
+
return self.runtime.config.models
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def active_model(self) -> ModelConfig:
|
|
44
|
+
"""Return the model selected for the next turn."""
|
|
45
|
+
return self.runtime.active_model
|
|
46
|
+
|
|
47
|
+
def find_model(self, name: str) -> ModelConfig | None:
|
|
48
|
+
"""Return the configured model a recorded name stands for, if exactly one does."""
|
|
49
|
+
return self.runtime.find_model(name)
|
|
50
|
+
|
|
51
|
+
def use_model(self, name: str | ModelConfig) -> ModelConfig:
|
|
52
|
+
"""Select a configured model for subsequent turns."""
|
|
53
|
+
return self.runtime.use_model(name)
|
|
54
|
+
|
|
55
|
+
# -- the models in the config file --------------------------------------------------
|
|
56
|
+
@property
|
|
57
|
+
def config_file(self) -> Path:
|
|
58
|
+
"""Return the file the models are read from and written to."""
|
|
59
|
+
return config_path()
|
|
60
|
+
|
|
61
|
+
def _read_config_file(self) -> str:
|
|
62
|
+
"""Return the config file's text, or raise :class:`ConfigEditError` saying why not."""
|
|
63
|
+
path = self.config_file
|
|
64
|
+
try:
|
|
65
|
+
return path.read_text(encoding="utf-8")
|
|
66
|
+
except FileNotFoundError as error:
|
|
67
|
+
raise ConfigEditError(f"{path} does not exist") from error
|
|
68
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
69
|
+
raise ConfigEditError(f"{path} cannot be read: {error}") from error
|
|
70
|
+
|
|
71
|
+
def file_models(self) -> tuple[tuple[ModelConfig, Mapping[str, object]], ...]:
|
|
72
|
+
"""Return each model in the file with the keys the file writes for it.
|
|
73
|
+
|
|
74
|
+
The file is the source of truth for editing, not the models the session started with: a
|
|
75
|
+
reader may have changed it by hand since. The keys are what is *written* — a token left to
|
|
76
|
+
an environment variable is absent — which is what a rewrite has to start from.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ConfigEditError: When the file cannot be read, is not valid, or lays its models out in
|
|
80
|
+
a way the editor cannot safely change.
|
|
81
|
+
"""
|
|
82
|
+
text = self._read_config_file()
|
|
83
|
+
path = self.config_file
|
|
84
|
+
entries = file_entries(text, where=path)
|
|
85
|
+
try:
|
|
86
|
+
models = _read_models(entries, path)
|
|
87
|
+
except ValueError as error:
|
|
88
|
+
raise ConfigEditError(str(error)) from error
|
|
89
|
+
return tuple(zip(models, entries, strict=True))
|
|
90
|
+
|
|
91
|
+
def _commit_models(self, text: str) -> ModelConfig:
|
|
92
|
+
"""Write the edited text, then make the running session use what it now says.
|
|
93
|
+
|
|
94
|
+
The file is written first and the session is changed second, so a write that fails leaves
|
|
95
|
+
the session as it was, and what the session holds is always something the file holds too.
|
|
96
|
+
"""
|
|
97
|
+
path = self.config_file
|
|
98
|
+
try:
|
|
99
|
+
write_text_atomically(path, text)
|
|
100
|
+
except OSError as error:
|
|
101
|
+
raise ConfigEditError(f"{path} could not be written: {error}") from error
|
|
102
|
+
models = _read_models(file_entries(text, where=path), path)
|
|
103
|
+
return self.runtime.replace_models(models)
|
|
104
|
+
|
|
105
|
+
def add_configured_model(self, entry: Mapping[str, object]) -> ModelConfig:
|
|
106
|
+
"""Add one ``[[models]]`` entry to the file and to this session; return the new model.
|
|
107
|
+
|
|
108
|
+
The model in use does not change: adding is not choosing. The new one appears in ``/model``.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
entry: The keys of the new model as the file writes them.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ConfigEditError: When the entry is not a model that can be called, or the file cannot
|
|
115
|
+
be edited. Nothing is written then.
|
|
116
|
+
"""
|
|
117
|
+
text = add_model(self._read_config_file(), entry, where=self.config_file)
|
|
118
|
+
self._commit_models(text)
|
|
119
|
+
return self.runtime.config.models[-1]
|
|
120
|
+
|
|
121
|
+
def update_configured_model(self, index: int, entry: Mapping[str, object]) -> ModelConfig:
|
|
122
|
+
"""Rewrite model number ``index`` (from zero) of the file as ``entry``; return the result."""
|
|
123
|
+
text = replace_model(self._read_config_file(), index, entry, where=self.config_file)
|
|
124
|
+
self._commit_models(text)
|
|
125
|
+
return self.runtime.config.models[index]
|
|
126
|
+
|
|
127
|
+
def change_compact_percent(self, percent: float) -> tuple[ModelConfig, str | None]:
|
|
128
|
+
"""Compact the model in use at ``percent`` of its window from the next request, and write it to the config file.
|
|
129
|
+
|
|
130
|
+
The running session is changed whatever becomes of the file, since the reader asked for it now: a file
|
|
131
|
+
that cannot be edited (no file, a layout the editor refuses) leaves the change for this run only, and the
|
|
132
|
+
reason is returned beside the model so it can be said.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
percent: Share of the window, more than 0 and at most 100.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The model now in use, and why the file was not changed (``None`` when it was).
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: ``percent`` is not between 0 and 100.
|
|
142
|
+
"""
|
|
143
|
+
if not 0 < percent <= 100:
|
|
144
|
+
raise ValueError("The compression share must be more than 0 and at most 100.")
|
|
145
|
+
active = self.active_model
|
|
146
|
+
value = int(percent) if float(percent).is_integer() else percent
|
|
147
|
+
try:
|
|
148
|
+
for index, (model, entry) in enumerate(self.file_models()):
|
|
149
|
+
if model == active:
|
|
150
|
+
return self.update_configured_model(index, {**entry, "compact_percent": value}), None
|
|
151
|
+
reason = "the model in use is not in the config file"
|
|
152
|
+
except ConfigEditError as error:
|
|
153
|
+
reason = str(error)
|
|
154
|
+
edited = replace(active, compact_percent=float(percent))
|
|
155
|
+
models = tuple(edited if model is active else model for model in self.models)
|
|
156
|
+
return self.runtime.replace_models(models), reason
|
|
157
|
+
|
|
158
|
+
def remove_configured_model(self, index: int) -> ModelConfig:
|
|
159
|
+
"""Remove model number ``index`` (from zero) from the file and this session.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
The model in use afterwards: the one before unless it was the one removed.
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
ConfigEditError: When it is the only model, or the file cannot be edited.
|
|
166
|
+
"""
|
|
167
|
+
text = remove_model(self._read_config_file(), index, where=self.config_file)
|
|
168
|
+
return self._commit_models(text)
|
|
169
|
+
|
|
170
|
+
def remember_theme(self, name: str) -> Path:
|
|
171
|
+
"""Write ``[ui] theme = name`` into the config file, so the next start opens in that palette.
|
|
172
|
+
|
|
173
|
+
Only that line changes. A config file that does not exist is not created for it: a file with nothing but
|
|
174
|
+
``[ui]`` in it would stand where a reader's environment-variable setup is, and say nothing about models.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The file written.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ConfigEditError: When there is no config file, it cannot be read or edited safely, or it cannot be
|
|
181
|
+
written. Nothing is written then.
|
|
182
|
+
"""
|
|
183
|
+
text = set_theme(self._read_config_file(), name, where=self.config_file)
|
|
184
|
+
try:
|
|
185
|
+
write_text_atomically(self.config_file, text)
|
|
186
|
+
except OSError as error:
|
|
187
|
+
raise ConfigEditError(f"{self.config_file} could not be written: {error}") from error
|
|
188
|
+
return self.config_file
|
|
189
|
+
|
|
190
|
+
def remember_pet(self, name: str) -> Path:
|
|
191
|
+
"""Write ``[ui] pet = name`` into the config file, so the next start opens with that pet.
|
|
192
|
+
|
|
193
|
+
Only that line changes, and a config file that does not exist is not created for it. The pet in force
|
|
194
|
+
for this run is the shell's to switch; what this keeps up to date is the setting the shell reads back.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
The file written.
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
ConfigEditError: When there is no config file, it cannot be read or edited safely, or it cannot be
|
|
201
|
+
written. Nothing is written then.
|
|
202
|
+
"""
|
|
203
|
+
text = set_pet(self._read_config_file(), name, where=self.config_file)
|
|
204
|
+
try:
|
|
205
|
+
write_text_atomically(self.config_file, text)
|
|
206
|
+
except OSError as error:
|
|
207
|
+
raise ConfigEditError(f"{self.config_file} could not be written: {error}") from error
|
|
208
|
+
self.runtime.config = replace(self.runtime.config, pet=name)
|
|
209
|
+
return self.config_file
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def statusline_hidden(self) -> frozenset[str]:
|
|
213
|
+
"""Return the names of the header and status-line parts the reader switched off."""
|
|
214
|
+
return frozenset(self.runtime.config.statusline_hidden)
|
|
215
|
+
|
|
216
|
+
def remember_statusline(self, shown: Mapping[str, bool]) -> Path:
|
|
217
|
+
"""Write which parts of the header and status line are shown into ``[statusline]``.
|
|
218
|
+
|
|
219
|
+
Only the parts that differ from what is in force are written. The file is written first and the
|
|
220
|
+
running configuration changed second, so a write that fails leaves both as they were; the rows
|
|
221
|
+
follow at the next paint. A config file that does not exist is not created for it, as for the theme.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
shown: Part name to whether it is shown. A part left out keeps what it has.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
The file written.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
ConfigEditError: When there is no config file, it cannot be read or edited safely, or it cannot
|
|
231
|
+
be written.
|
|
232
|
+
"""
|
|
233
|
+
hidden = set(self.statusline_hidden)
|
|
234
|
+
changes = {name: visible for name, visible in shown.items() if (name not in hidden) != visible}
|
|
235
|
+
if not changes:
|
|
236
|
+
return self.config_file
|
|
237
|
+
text = set_statusline(self._read_config_file(), changes, where=self.config_file)
|
|
238
|
+
try:
|
|
239
|
+
write_text_atomically(self.config_file, text)
|
|
240
|
+
except OSError as error:
|
|
241
|
+
raise ConfigEditError(f"{self.config_file} could not be written: {error}") from error
|
|
242
|
+
hidden = {name for name in hidden if name not in changes} | {name for name, v in changes.items() if not v}
|
|
243
|
+
ordered = tuple(name for name in NAMES if name in hidden)
|
|
244
|
+
self.runtime.config = replace(self.runtime.config, statusline_hidden=ordered)
|
|
245
|
+
return self.config_file
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def self_compaction(self) -> bool:
|
|
249
|
+
"""Return whether the model may request compaction in this session right now."""
|
|
250
|
+
return self.runtime.self_compacting(self.session_id)
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def self_compaction_choice(self) -> bool | None:
|
|
254
|
+
"""Return this session's own choice, or ``None`` when it follows the default."""
|
|
255
|
+
return self.runtime.self_compaction_choice(self.session_id)
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def self_compaction_default(self) -> bool:
|
|
259
|
+
"""Return what a session that has made no choice does: ``[compaction] self`` in the config."""
|
|
260
|
+
return self.runtime.config.self_compaction
|
|
261
|
+
|
|
262
|
+
def use_self_compaction(self, choice: bool | None) -> None:
|
|
263
|
+
"""Set this session's own choice, or give it back with ``None`` so it follows the default.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
SessionLocked: Another process has the session open; the choice is part of the session.
|
|
267
|
+
ConfigEditError: The choice could not be saved. Nothing is reported as saved then.
|
|
268
|
+
"""
|
|
269
|
+
self.require_session()
|
|
270
|
+
try:
|
|
271
|
+
self.runtime.choose_self_compaction(self.session_id, choice)
|
|
272
|
+
except OSError as error:
|
|
273
|
+
raise ConfigEditError(f"Self-compaction setting was not saved: {error}") from error
|
|
274
|
+
|
|
275
|
+
def remember_self_compaction(self, enabled: bool) -> Path:
|
|
276
|
+
"""Write ``[compaction] self = enabled`` into the config file: the default for every session.
|
|
277
|
+
|
|
278
|
+
The file is written first and the running configuration changed second, so a write that fails
|
|
279
|
+
leaves both as they were. A session with a choice of its own keeps it; one without follows this at
|
|
280
|
+
once. A config file that does not exist is not created for it, as for the theme.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
The file written.
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
ConfigEditError: When there is no config file, it cannot be read or edited safely, or it cannot
|
|
287
|
+
be written.
|
|
288
|
+
"""
|
|
289
|
+
text = set_self_compaction(self._read_config_file(), enabled, where=self.config_file)
|
|
290
|
+
try:
|
|
291
|
+
write_text_atomically(self.config_file, text)
|
|
292
|
+
except OSError as error:
|
|
293
|
+
raise ConfigEditError(f"{self.config_file} could not be written: {error}") from error
|
|
294
|
+
self.runtime.config = replace(self.runtime.config, self_compaction=enabled)
|
|
295
|
+
return self.config_file
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def effort(self) -> str:
|
|
299
|
+
"""Return the reasoning effort the next request will ask for."""
|
|
300
|
+
return self.runtime.effort.value
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def efforts(self) -> tuple[str, ...]:
|
|
304
|
+
"""Return the reasoning levels the active model takes, cheapest first."""
|
|
305
|
+
return tuple(level.value for level in self.runtime.active_model.efforts)
|
|
306
|
+
|
|
307
|
+
def use_effort(self, name: str) -> str:
|
|
308
|
+
"""Select a reasoning level for later requests and return its value."""
|
|
309
|
+
return self.runtime.use_effort(name).value
|
|
310
|
+
|
|
311
|
+
def approve_all_shell_commands(self) -> None:
|
|
312
|
+
"""Approve every shell command for the rest of this run, without asking."""
|
|
313
|
+
self.runtime.approve_all_shell_commands()
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def permission(self) -> Permission:
|
|
317
|
+
"""Return how much of the shell may run without asking."""
|
|
318
|
+
return self.runtime.permission
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def permissions(self) -> tuple[str, ...]:
|
|
322
|
+
"""Return the permission levels, strictest first."""
|
|
323
|
+
return tuple(level.value for level in PERMISSION_LEVELS)
|
|
324
|
+
|
|
325
|
+
def use_permission(self, name: str | Permission) -> Permission:
|
|
326
|
+
"""Set how much of the shell may run without asking, for the rest of this run."""
|
|
327
|
+
return self.runtime.use_permission(name)
|