zettcode 0.1.5__tar.gz → 0.1.7__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.5 → zettcode-0.1.7}/PKG-INFO +5 -1
- {zettcode-0.1.5 → zettcode-0.1.7}/pyproject.toml +7 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/__init__.py +1 -1
- zettcode-0.1.7/src/zettcode/app/agent/agent/base.py +54 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/commands.py +6 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/context.py +2 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/prompt.py +26 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/sessions.py +6 -0
- zettcode-0.1.7/src/zettcode/app/agent/agent/settings.py +164 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/turns.py +14 -4
- zettcode-0.1.7/src/zettcode/app/agent/conversation/changes.py +64 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/entries.py +156 -6
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/presenters.py +383 -42
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/projection.py +16 -2
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/rendering.py +225 -10
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/replay.py +7 -1
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/rows.py +19 -5
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/fold.py +2 -2
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/stream.py +3 -13
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/turn.py +110 -8
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/mentions.py +114 -0
- zettcode-0.1.7/src/zettcode/app/agent/dialogue/relay.py +153 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/side.py +1 -1
- zettcode-0.1.7/src/zettcode/app/agent/errors.py +125 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/export/__init__.py +9 -2
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/mcp.py +72 -1
- zettcode-0.1.7/src/zettcode/app/agent/extensions/messaging.py +130 -0
- zettcode-0.1.7/src/zettcode/app/agent/extensions/messenger.py +183 -0
- zettcode-0.1.7/src/zettcode/app/agent/extensions/subagents.py +218 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/runtime.py +94 -8
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/storage/__init__.py +8 -0
- zettcode-0.1.7/src/zettcode/app/agent/storage/locks.py +130 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/commands.py +10 -3
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/app.py +149 -15
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/boot.py +5 -2
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/commands.py +49 -8
- zettcode-0.1.7/src/zettcode/app/ui/config_form.py +203 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/followups.py +61 -10
- zettcode-0.1.7/src/zettcode/app/ui/gc_policy.py +147 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/keys.py +14 -1
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/labels.py +40 -6
- zettcode-0.1.7/src/zettcode/app/ui/messaging.py +244 -0
- zettcode-0.1.7/src/zettcode/app/ui/providers.py +474 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/rows.py +1 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/sessions.py +89 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/shell.py +38 -1
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/updating.py +63 -11
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/__init__.py +5 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/composer.py +98 -2
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/follow_up.py +2 -2
- zettcode-0.1.7/src/zettcode/app/ui/widgets/input_page.py +175 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/transcript.py +3 -2
- zettcode-0.1.7/src/zettcode/broker/__init__.py +22 -0
- zettcode-0.1.7/src/zettcode/broker/__main__.py +10 -0
- zettcode-0.1.7/src/zettcode/broker/client.py +533 -0
- zettcode-0.1.7/src/zettcode/broker/files.py +131 -0
- zettcode-0.1.7/src/zettcode/broker/finder.py +190 -0
- zettcode-0.1.7/src/zettcode/broker/hub.py +275 -0
- zettcode-0.1.7/src/zettcode/broker/log.py +11 -0
- zettcode-0.1.7/src/zettcode/broker/models.py +77 -0
- zettcode-0.1.7/src/zettcode/broker/process.py +117 -0
- zettcode-0.1.7/src/zettcode/broker/protocol.py +282 -0
- zettcode-0.1.7/src/zettcode/broker/roster.py +176 -0
- zettcode-0.1.7/src/zettcode/broker/runner.py +131 -0
- zettcode-0.1.7/src/zettcode/broker/server.py +184 -0
- zettcode-0.1.7/src/zettcode/broker/text.py +81 -0
- zettcode-0.1.7/src/zettcode/broker/web.py +192 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/cli.py +55 -1
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/collector.py +10 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/config.py +47 -6
- zettcode-0.1.7/src/zettcode/config_edit.py +268 -0
- zettcode-0.1.7/src/zettcode/filelock.py +125 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/builtins.py +4 -4
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/container.py +44 -2
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/loader.py +16 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/plugin.py +4 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/state.py +3 -0
- zettcode-0.1.7/src/zettcode/providers.py +212 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/theme.py +49 -17
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/scroll.py +52 -7
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/__init__.py +2 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/text.py +43 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/runner.py +24 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/theme_file.py +5 -2
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/textarea.py +11 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/update.py +113 -4
- zettcode-0.1.5/src/zettcode/app/agent/agent/base.py +0 -29
- zettcode-0.1.5/src/zettcode/app/agent/agent/settings.py +0 -68
- zettcode-0.1.5/src/zettcode/app/agent/errors.py +0 -39
- zettcode-0.1.5/src/zettcode/app/agent/extensions/subagents.py +0 -140
- {zettcode-0.1.5 → zettcode-0.1.7}/.gitignore +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/LICENSE +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/README.md +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/README.zh-CN.md +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/_compat.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/exports.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/interaction.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/ask.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/title.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/export/trace/script.js +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/export/trace/style.css +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/export/trace/template.html +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/capabilities.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/compaction.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/context.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/mcp_notice.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/tool_output.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/usage.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/permission/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/permission/approval.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/permission/readonly.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/storage/metadata.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/storage/records.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/storage/store.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/brand.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/pipeline.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/registry.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/clipboard.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/demo.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/notices.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/settings.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/approval_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/completer.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/context_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/model_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/panel.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/root.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/steering.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/update_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/welcome.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/paths.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/extension.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/mixins.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/capabilities.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/clipboard.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/console.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/app.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/events.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/focus.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/geometry.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/host.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/keymap.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/scheduler.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/screen.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/widget.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/gallery.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/glyphs.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/bridge.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/decoder.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/events.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/keys.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/reader.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/box.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/overlay.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/solver.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/spacing.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/canvas.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/code.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/color.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/renderer.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/rich_text.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/style.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/sweep.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/terminal.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/testing/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/testing/harness.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/testing/snapshot.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/collapsible.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/completion.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/dialog.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/diff.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/list.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/list_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/markdown.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/progress.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/rich_text.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/status.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/table.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/tasks.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/text.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/toast.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: zettcode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
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
|
|
@@ -21,13 +21,17 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
21
21
|
Classifier: Typing :: Typed
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
Requires-Dist: exceptiongroup<2,>=1.2; python_version < '3.11'
|
|
24
|
+
Requires-Dist: httpx<1,>=0.27
|
|
24
25
|
Requires-Dist: pydantic<3,>=2
|
|
25
26
|
Requires-Dist: pyperclip<2,>=1.9
|
|
26
27
|
Requires-Dist: setproctitle<2,>=1.3.7; os_name == 'posix'
|
|
28
|
+
Requires-Dist: starlette<2,>=0.48
|
|
27
29
|
Requires-Dist: tiktoken<1,>=0.12
|
|
28
30
|
Requires-Dist: tomli<3,>=2; python_version < '3.11'
|
|
29
31
|
Requires-Dist: truststore<1,>=0.10
|
|
32
|
+
Requires-Dist: uvicorn<1,>=0.35
|
|
30
33
|
Requires-Dist: wcwidth<1,>=0.2
|
|
34
|
+
Requires-Dist: websockets<17,>=15
|
|
31
35
|
Requires-Dist: zett-agent<0.2,>=0.1.15
|
|
32
36
|
Description-Content-Type: text/markdown
|
|
33
37
|
|
|
@@ -29,6 +29,13 @@ dependencies = [
|
|
|
29
29
|
"pydantic>=2,<3",
|
|
30
30
|
"pyperclip>=1.9,<2",
|
|
31
31
|
"setproctitle>=1.3.7,<2; os_name == 'posix'",
|
|
32
|
+
# The session broker is an ordinary web server: a Starlette application served by uvicorn. A session reaches it
|
|
33
|
+
# with httpx (to send) and websockets (to be reachable). The floors are the oldest versions the broker's tests
|
|
34
|
+
# were run against; all of them arrive through the runtime today and are declared so that is not luck.
|
|
35
|
+
"starlette>=0.48,<2",
|
|
36
|
+
"uvicorn>=0.35,<1",
|
|
37
|
+
"httpx>=0.27,<1",
|
|
38
|
+
"websockets>=15,<17",
|
|
32
39
|
# The counter behind ``/context`` measures a request with it, and imports it
|
|
33
40
|
# itself: the floor matches the runtime's, so neither resolver has to fight.
|
|
34
41
|
"tiktoken>=0.12,<1",
|
|
@@ -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.7"
|
|
11
11
|
|
|
12
12
|
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
13
13
|
_EXPORTS = {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""What every part of the agent shares: the runtime it speaks to, and who and where it is.
|
|
2
|
+
|
|
3
|
+
The mixins in this package are split by what they are responsible for, and each one
|
|
4
|
+
inherits this class only to be type-checked against what the others rely on. The two
|
|
5
|
+
properties here are read by nearly all of them, which is why they live below all of them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..runtime import ZettCodeRuntime
|
|
13
|
+
from ..storage import SessionLocked
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentBase:
|
|
17
|
+
"""The runtime, the workspace it works in, and the session the next turn belongs to."""
|
|
18
|
+
|
|
19
|
+
#: The runtime this agent speaks to; the one thing the application agent holds.
|
|
20
|
+
runtime: ZettCodeRuntime
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def workspace(self) -> Path:
|
|
24
|
+
"""Return the workspace displayed in the header."""
|
|
25
|
+
return self.runtime.config.workspace
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def session_id(self) -> str:
|
|
29
|
+
"""Return the session used for the next turn."""
|
|
30
|
+
return self.runtime.session_id
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def session_locked(self) -> bool:
|
|
34
|
+
"""Return whether another process has the active session open, so that this one may only read it.
|
|
35
|
+
|
|
36
|
+
Every session is locked by the process that has it open (see :mod:`..storage.locks`). A session this
|
|
37
|
+
process started is always its own; one it resumed may be somebody else's, and then it is shown and
|
|
38
|
+
nothing is written to it: no turn, no title, no compaction.
|
|
39
|
+
"""
|
|
40
|
+
return self.runtime.session_locked
|
|
41
|
+
|
|
42
|
+
def retake_session(self) -> bool:
|
|
43
|
+
"""Try again to lock the active session; return whether this process holds it now."""
|
|
44
|
+
return self.runtime.retake_session()
|
|
45
|
+
|
|
46
|
+
def require_session(self) -> None:
|
|
47
|
+
"""Refuse to go on when the active session is another process's.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
SessionLocked: The lock is not held. A ``ValueError``, which the shell shows as a line in the
|
|
51
|
+
conversation.
|
|
52
|
+
"""
|
|
53
|
+
if self.session_locked:
|
|
54
|
+
raise SessionLocked()
|
|
@@ -8,6 +8,7 @@ agent is where the shell asks for everything the runtime loaded.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from ....plugins import UiRow
|
|
11
|
+
from ....providers import BUILTIN_PROVIDERS, Provider, merge_providers
|
|
11
12
|
from ...commands import Command, CommandContext, CommandResult
|
|
12
13
|
from ..dialogue.mentions import MentionProvider
|
|
13
14
|
from .exports import ExportsMixin
|
|
@@ -44,6 +45,11 @@ class CommandsMixin(ExportsMixin):
|
|
|
44
45
|
"""Return the ``@`` resource providers the installed plugins registered."""
|
|
45
46
|
return self.runtime.plugins.mentions
|
|
46
47
|
|
|
48
|
+
@property
|
|
49
|
+
def providers(self) -> tuple[Provider, ...]:
|
|
50
|
+
"""Return the hosts ``/config`` can offer: the built-in ones, then the plugins' own."""
|
|
51
|
+
return merge_providers(BUILTIN_PROVIDERS, self.runtime.plugins.providers)
|
|
52
|
+
|
|
47
53
|
@property
|
|
48
54
|
def plugin_failures(self) -> tuple[str, ...]:
|
|
49
55
|
"""Return one message per plugin that could not be loaded or activated."""
|
|
@@ -80,9 +80,11 @@ class ContextMixin(AgentBase):
|
|
|
80
80
|
worth replacing.
|
|
81
81
|
|
|
82
82
|
Raises:
|
|
83
|
+
SessionLocked: Another process has this session open, and a checkpoint is a write.
|
|
83
84
|
ValueError: When the newest checkpoint already covers the branch —
|
|
84
85
|
a second pass would summarize nothing but the checkpoint.
|
|
85
86
|
"""
|
|
87
|
+
self.require_session()
|
|
86
88
|
if self._compacted_at_the_head():
|
|
87
89
|
raise ValueError("Already compacted; send a message before compacting again")
|
|
88
90
|
await self.runtime.start()
|
|
@@ -23,6 +23,9 @@ class MentionPart:
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
token: str
|
|
26
|
+
#: The words the model reads in the token's place, when it is not to see the token itself. ``None``
|
|
27
|
+
#: leaves the token as typed; the typed text is what the transcript and the store keep either way.
|
|
28
|
+
says: str | None = None
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
#: One piece of a user turn, in the order it was written: a run of text, or the
|
|
@@ -39,6 +42,29 @@ def plain_text_part(part: PromptPart) -> str:
|
|
|
39
42
|
return ""
|
|
40
43
|
|
|
41
44
|
|
|
45
|
+
def model_text_part(part: PromptPart) -> str:
|
|
46
|
+
"""Return the text one part is to the model: a reference that was resolved reads as what it says."""
|
|
47
|
+
if isinstance(part, MentionPart) and part.says is not None:
|
|
48
|
+
return part.says
|
|
49
|
+
return plain_text_part(part)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def mention_parts(parts: Sequence[PromptPart], mentions: MentionRegistry) -> tuple[PromptPart, ...]:
|
|
53
|
+
"""Return a turn with each reference that has words of its own carrying them.
|
|
54
|
+
|
|
55
|
+
``@work`` typed for a session that is running is not a word the model knows, so it is put in the
|
|
56
|
+
model's reading as ``session "work"`` and :func:`mention_hint` explains it. Anything else — a skill, a
|
|
57
|
+
file, a name nobody has — keeps the token as it was typed.
|
|
58
|
+
"""
|
|
59
|
+
if not mentions.providers:
|
|
60
|
+
return tuple(parts)
|
|
61
|
+
resolved: list[PromptPart] = []
|
|
62
|
+
for part in parts:
|
|
63
|
+
says = mentions.rename_token(part.token) if isinstance(part, MentionPart) else None
|
|
64
|
+
resolved.append(MentionPart(part.token, says) if says is not None and isinstance(part, MentionPart) else part)
|
|
65
|
+
return tuple(resolved)
|
|
66
|
+
|
|
67
|
+
|
|
42
68
|
def plain_text(parts: Sequence[PromptPart]) -> str:
|
|
43
69
|
"""Return a turn as the reader typed it, with its ``@`` tokens back in place."""
|
|
44
70
|
return "".join(plain_text_part(part) for part in parts)
|
|
@@ -180,7 +180,11 @@ class SessionsMixin(ContextMixin):
|
|
|
180
180
|
Args:
|
|
181
181
|
title: Name to store; surrounding space is trimmed, and a blank or
|
|
182
182
|
over-long name is rejected by the store.
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
SessionLocked: Another process has this session open.
|
|
183
186
|
"""
|
|
187
|
+
self.require_session()
|
|
184
188
|
await self.runtime.persistence.set_title(self.session_id, title)
|
|
185
189
|
return title.strip()
|
|
186
190
|
|
|
@@ -199,6 +203,8 @@ class SessionsMixin(ContextMixin):
|
|
|
199
203
|
"""
|
|
200
204
|
if self.runtime.persistence.session_title(session_id) is not None:
|
|
201
205
|
return None
|
|
206
|
+
if session_id == self.session_id and self.session_locked:
|
|
207
|
+
return None # a nicety, and not one that is worth writing into a session that is not ours
|
|
202
208
|
session = self.runtime.persistence.read(session_id)
|
|
203
209
|
branch = session.branch()
|
|
204
210
|
question = next(
|
|
@@ -0,0 +1,164 @@
|
|
|
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 pathlib import Path
|
|
7
|
+
|
|
8
|
+
from zett_agent.model import ReasoningEffort
|
|
9
|
+
|
|
10
|
+
from ....config import ModelConfig, _read_models
|
|
11
|
+
from ....config_edit import (
|
|
12
|
+
ConfigEditError,
|
|
13
|
+
add_model,
|
|
14
|
+
file_entries,
|
|
15
|
+
remove_model,
|
|
16
|
+
replace_model,
|
|
17
|
+
write_text_atomically,
|
|
18
|
+
)
|
|
19
|
+
from ....paths import config_path
|
|
20
|
+
from ..permission import PERMISSION_LEVELS, Permission
|
|
21
|
+
from .base import AgentBase
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SettingsMixin(AgentBase):
|
|
25
|
+
"""Read and change the model, the reasoning effort, and how much of the shell may run unasked."""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def reduced_motion(self) -> bool:
|
|
29
|
+
"""Return whether decorative animation is disabled."""
|
|
30
|
+
return self.runtime.config.reduced_motion
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def models(self) -> tuple[ModelConfig, ...]:
|
|
34
|
+
"""Return the models available for selection."""
|
|
35
|
+
return self.runtime.config.models
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def active_model(self) -> ModelConfig:
|
|
39
|
+
"""Return the model selected for the next turn."""
|
|
40
|
+
return self.runtime.active_model
|
|
41
|
+
|
|
42
|
+
def find_model(self, name: str) -> ModelConfig | None:
|
|
43
|
+
"""Return the configured model a recorded name stands for, if exactly one does."""
|
|
44
|
+
return self.runtime.find_model(name)
|
|
45
|
+
|
|
46
|
+
def use_model(self, name: str | ModelConfig) -> ModelConfig:
|
|
47
|
+
"""Select a configured model for subsequent turns."""
|
|
48
|
+
return self.runtime.use_model(name)
|
|
49
|
+
|
|
50
|
+
# -- the models in the config file --------------------------------------------------
|
|
51
|
+
@property
|
|
52
|
+
def config_file(self) -> Path:
|
|
53
|
+
"""Return the file the models are read from and written to."""
|
|
54
|
+
return config_path()
|
|
55
|
+
|
|
56
|
+
def _read_config_file(self) -> str:
|
|
57
|
+
"""Return the config file's text, or raise :class:`ConfigEditError` saying why not."""
|
|
58
|
+
path = self.config_file
|
|
59
|
+
try:
|
|
60
|
+
return path.read_text(encoding="utf-8")
|
|
61
|
+
except FileNotFoundError as error:
|
|
62
|
+
raise ConfigEditError(f"{path} does not exist") from error
|
|
63
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
64
|
+
raise ConfigEditError(f"{path} cannot be read: {error}") from error
|
|
65
|
+
|
|
66
|
+
def file_models(self) -> tuple[tuple[ModelConfig, Mapping[str, object]], ...]:
|
|
67
|
+
"""Return each model in the file with the keys the file writes for it.
|
|
68
|
+
|
|
69
|
+
The file is the source of truth for editing, not the models the session started with: a
|
|
70
|
+
reader may have changed it by hand since. The keys are what is *written* — a token left to
|
|
71
|
+
an environment variable is absent — which is what a rewrite has to start from.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ConfigEditError: When the file cannot be read, is not valid, or lays its models out in
|
|
75
|
+
a way the editor cannot safely change.
|
|
76
|
+
"""
|
|
77
|
+
text = self._read_config_file()
|
|
78
|
+
path = self.config_file
|
|
79
|
+
entries = file_entries(text, where=path)
|
|
80
|
+
try:
|
|
81
|
+
models = _read_models(entries, path)
|
|
82
|
+
except ValueError as error:
|
|
83
|
+
raise ConfigEditError(str(error)) from error
|
|
84
|
+
return tuple(zip(models, entries, strict=True))
|
|
85
|
+
|
|
86
|
+
def _commit_models(self, text: str) -> ModelConfig:
|
|
87
|
+
"""Write the edited text, then make the running session use what it now says.
|
|
88
|
+
|
|
89
|
+
The file is written first and the session is changed second, so a write that fails leaves
|
|
90
|
+
the session as it was, and what the session holds is always something the file holds too.
|
|
91
|
+
"""
|
|
92
|
+
path = self.config_file
|
|
93
|
+
try:
|
|
94
|
+
write_text_atomically(path, text)
|
|
95
|
+
except OSError as error:
|
|
96
|
+
raise ConfigEditError(f"{path} could not be written: {error}") from error
|
|
97
|
+
models = _read_models(file_entries(text, where=path), path)
|
|
98
|
+
return self.runtime.replace_models(models)
|
|
99
|
+
|
|
100
|
+
def add_configured_model(self, entry: Mapping[str, object]) -> ModelConfig:
|
|
101
|
+
"""Add one ``[[models]]`` entry to the file and to this session; return the new model.
|
|
102
|
+
|
|
103
|
+
The model in use does not change: adding is not choosing. The new one appears in ``/model``.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
entry: The keys of the new model as the file writes them.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
ConfigEditError: When the entry is not a model that can be called, or the file cannot
|
|
110
|
+
be edited. Nothing is written then.
|
|
111
|
+
"""
|
|
112
|
+
text = add_model(self._read_config_file(), entry, where=self.config_file)
|
|
113
|
+
self._commit_models(text)
|
|
114
|
+
return self.runtime.config.models[-1]
|
|
115
|
+
|
|
116
|
+
def update_configured_model(self, index: int, entry: Mapping[str, object]) -> ModelConfig:
|
|
117
|
+
"""Rewrite model number ``index`` (from zero) of the file as ``entry``; return the result."""
|
|
118
|
+
text = replace_model(self._read_config_file(), index, entry, where=self.config_file)
|
|
119
|
+
self._commit_models(text)
|
|
120
|
+
return self.runtime.config.models[index]
|
|
121
|
+
|
|
122
|
+
def remove_configured_model(self, index: int) -> ModelConfig:
|
|
123
|
+
"""Remove model number ``index`` (from zero) from the file and this session.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The model in use afterwards: the one before unless it was the one removed.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ConfigEditError: When it is the only model, or the file cannot be edited.
|
|
130
|
+
"""
|
|
131
|
+
text = remove_model(self._read_config_file(), index, where=self.config_file)
|
|
132
|
+
return self._commit_models(text)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def effort(self) -> str:
|
|
136
|
+
"""Return the reasoning effort the next request will ask for."""
|
|
137
|
+
return self.runtime.effort.value
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def efforts(self) -> tuple[str, ...]:
|
|
141
|
+
"""Return the reasoning levels the runtime accepts, cheapest first."""
|
|
142
|
+
return tuple(level.value for level in ReasoningEffort)
|
|
143
|
+
|
|
144
|
+
def use_effort(self, name: str) -> str:
|
|
145
|
+
"""Select a reasoning level for later requests and return its value."""
|
|
146
|
+
return self.runtime.use_effort(name).value
|
|
147
|
+
|
|
148
|
+
def approve_all_shell_commands(self) -> None:
|
|
149
|
+
"""Approve every shell command for the rest of this run, without asking."""
|
|
150
|
+
self.runtime.approve_all_shell_commands()
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def permission(self) -> Permission:
|
|
154
|
+
"""Return how much of the shell may run without asking."""
|
|
155
|
+
return self.runtime.permission
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def permissions(self) -> tuple[str, ...]:
|
|
159
|
+
"""Return the permission levels, strictest first."""
|
|
160
|
+
return tuple(level.value for level in PERMISSION_LEVELS)
|
|
161
|
+
|
|
162
|
+
def use_permission(self, name: str | Permission) -> Permission:
|
|
163
|
+
"""Set how much of the shell may run without asking, for the rest of this run."""
|
|
164
|
+
return self.runtime.use_permission(name)
|
|
@@ -12,9 +12,10 @@ from zett_agent.extensions.external import ExternalEvent
|
|
|
12
12
|
from zett_agent.extensions.steering import STEERING_MESSAGE_EVENT_NAME
|
|
13
13
|
from zett_agent.messages import ImageBytesSource, ImageContent, TextContent, UserMessage
|
|
14
14
|
|
|
15
|
+
from ..dialogue.relay import Relayed
|
|
15
16
|
from ..dialogue.side import SIDE, question
|
|
16
17
|
from .base import AgentBase
|
|
17
|
-
from .prompt import MentionPart, PromptPart, carries_image,
|
|
18
|
+
from .prompt import MentionPart, PromptPart, carries_image, model_text_part, plain_text
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class TurnsMixin(AgentBase):
|
|
@@ -30,6 +31,7 @@ class TurnsMixin(AgentBase):
|
|
|
30
31
|
*,
|
|
31
32
|
hint: str | None = None,
|
|
32
33
|
side: bool = False,
|
|
34
|
+
relayed: Relayed | None = None,
|
|
33
35
|
) -> AsyncIterator[AgentEvent]:
|
|
34
36
|
"""Run one turn with the selected session, model, and reasoning effort.
|
|
35
37
|
|
|
@@ -43,14 +45,21 @@ class TurnsMixin(AgentBase):
|
|
|
43
45
|
what was typed and a restored session shows it.
|
|
44
46
|
side: Ask without letting the exchange join the conversation; see
|
|
45
47
|
:mod:`zettcode.app.agent.side`.
|
|
48
|
+
relayed: The turn is a message another session sent. It is what the model reads
|
|
49
|
+
(with where it came from), and ``parts`` and ``hint`` are not used.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
SessionLocked: Another process has the session open. Checked before anything else, and a side
|
|
53
|
+
question is no exception: the store records it even though it never replays it.
|
|
46
54
|
"""
|
|
55
|
+
self.require_session()
|
|
47
56
|
await self.runtime.start()
|
|
48
57
|
client = self.runtime.started
|
|
49
58
|
used = self.runtime.active_model.shown_name
|
|
50
59
|
session_id = self.session_id
|
|
51
60
|
try:
|
|
52
61
|
async for event in client.stream(
|
|
53
|
-
self.request(parts, hint=hint, side=side),
|
|
62
|
+
self.request(parts, hint=hint, side=side) if relayed is None else relayed.message(),
|
|
54
63
|
config=AgentRunConfig(session_id=session_id),
|
|
55
64
|
model=self.runtime.provider,
|
|
56
65
|
reasoning_effort=self.runtime.effort,
|
|
@@ -105,11 +114,12 @@ class TurnsMixin(AgentBase):
|
|
|
105
114
|
text = plain_text(parts)
|
|
106
115
|
if hint is None:
|
|
107
116
|
return text
|
|
108
|
-
|
|
117
|
+
said = "".join(model_text_part(part) for part in parts)
|
|
118
|
+
return UserMessage(content=f"{said}\n\n{hint}", attributes={"prompt": text})
|
|
109
119
|
content: list[TextContent | ImageContent] = []
|
|
110
120
|
for part in parts:
|
|
111
121
|
if isinstance(part, (str, MentionPart)):
|
|
112
|
-
text =
|
|
122
|
+
text = model_text_part(part)
|
|
113
123
|
if text:
|
|
114
124
|
content.append(TextContent(text))
|
|
115
125
|
continue
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""What a request did to the files, added up: the data behind the summary at the end of a turn."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from pathlib import PurePath
|
|
7
|
+
|
|
8
|
+
from .entries import DeleteEntry, Entry, EntryStatus, FileChangeEntry, FileSummary
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def shown_path(path: str, workspace: PurePath | None) -> str:
|
|
12
|
+
"""Return a file as the reader knows it: relative to the workspace when it is inside it.
|
|
13
|
+
|
|
14
|
+
The model names the same file as ``app.py`` in one call and as ``/work/app.py`` in the next, and
|
|
15
|
+
the summary must not count them twice. Only the text is looked at, never the disk.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
path: The path as the call named it.
|
|
19
|
+
workspace: The directory the session works in, or ``None`` to leave every path as it is.
|
|
20
|
+
"""
|
|
21
|
+
if workspace is None or not path:
|
|
22
|
+
return path
|
|
23
|
+
candidate = PurePath(path)
|
|
24
|
+
# Rooted, not only absolute: on Windows ``/work/a.py`` has a root and no drive, which is not
|
|
25
|
+
# "absolute" to pathlib, and it still has to be compared with the workspace it is inside.
|
|
26
|
+
if candidate.is_absolute() or candidate.root:
|
|
27
|
+
try:
|
|
28
|
+
return candidate.relative_to(workspace).as_posix()
|
|
29
|
+
except ValueError:
|
|
30
|
+
return path
|
|
31
|
+
return candidate.as_posix()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def changed_files(entries: Iterable[Entry], workspace: PurePath | None = None) -> tuple[FileSummary, ...]:
|
|
35
|
+
"""Return the files a run of entries changed, each once, in the order they were first touched.
|
|
36
|
+
|
|
37
|
+
Only a call that *succeeded* counts: a failed edit changed nothing, and a skipped one never ran.
|
|
38
|
+
A file changed several times is one row with the totals. A file deleted and then written again is
|
|
39
|
+
a file that exists, counted from the write, and one written and then deleted is deleted.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
entries: The entries of one request, in order; anything that is not a file change is ignored.
|
|
43
|
+
workspace: The directory paths are shown relative to.
|
|
44
|
+
"""
|
|
45
|
+
order: list[str] = []
|
|
46
|
+
totals: dict[str, FileSummary] = {}
|
|
47
|
+
for entry in entries:
|
|
48
|
+
if not isinstance(entry, (FileChangeEntry, DeleteEntry)) or entry.status is not EntryStatus.COMPLETED:
|
|
49
|
+
continue
|
|
50
|
+
if not entry.path:
|
|
51
|
+
continue
|
|
52
|
+
path = shown_path(entry.path, workspace)
|
|
53
|
+
if path not in totals:
|
|
54
|
+
order.append(path)
|
|
55
|
+
totals[path] = FileSummary(path)
|
|
56
|
+
current = totals[path]
|
|
57
|
+
if isinstance(entry, DeleteEntry):
|
|
58
|
+
totals[path] = FileSummary(path, deleted=True)
|
|
59
|
+
continue
|
|
60
|
+
base = FileSummary(path) if current.deleted else current
|
|
61
|
+
totals[path] = FileSummary(
|
|
62
|
+
path, base.added + entry.added, base.removed + getattr(entry, "removed", 0), deleted=False
|
|
63
|
+
)
|
|
64
|
+
return tuple(totals[path] for path in order)
|