zettcode 0.1.5__tar.gz → 0.1.6__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.6}/PKG-INFO +1 -1
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/__init__.py +1 -1
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/commands.py +6 -0
- zettcode-0.1.6/src/zettcode/app/agent/agent/settings.py +164 -0
- zettcode-0.1.6/src/zettcode/app/agent/conversation/changes.py +64 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/entries.py +106 -6
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/presenters.py +223 -34
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/rendering.py +190 -10
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/rows.py +6 -5
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/fold.py +2 -2
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/stream.py +3 -13
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/turn.py +42 -6
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/mcp.py +50 -1
- zettcode-0.1.6/src/zettcode/app/agent/extensions/subagents.py +218 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/runtime.py +45 -3
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/commands.py +10 -3
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/app.py +69 -6
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/commands.py +45 -8
- zettcode-0.1.6/src/zettcode/app/ui/config_form.py +203 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/followups.py +54 -6
- zettcode-0.1.6/src/zettcode/app/ui/gc_policy.py +147 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/keys.py +14 -1
- zettcode-0.1.6/src/zettcode/app/ui/providers.py +470 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/shell.py +12 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/updating.py +63 -11
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/__init__.py +5 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/follow_up.py +2 -2
- zettcode-0.1.6/src/zettcode/app/ui/widgets/input_page.py +175 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/cli.py +55 -1
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/collector.py +10 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/config.py +20 -4
- zettcode-0.1.6/src/zettcode/config_edit.py +268 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/container.py +44 -2
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/loader.py +16 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/plugin.py +4 -0
- zettcode-0.1.6/src/zettcode/providers.py +205 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/theme.py +17 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/scroll.py +47 -6
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/runner.py +24 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/theme_file.py +2 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/textarea.py +11 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/update.py +113 -4
- zettcode-0.1.5/src/zettcode/app/agent/agent/settings.py +0 -68
- zettcode-0.1.5/src/zettcode/app/agent/extensions/subagents.py +0 -140
- {zettcode-0.1.5 → zettcode-0.1.6}/.gitignore +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/LICENSE +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/README.md +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/README.zh-CN.md +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/pyproject.toml +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/_compat.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/base.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/context.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/exports.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/interaction.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/prompt.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/sessions.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/turns.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/projection.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/replay.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/dialogue/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/dialogue/ask.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/dialogue/mentions.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/dialogue/side.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/dialogue/title.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/errors.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/export/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/export/trace/script.js +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/export/trace/style.css +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/export/trace/template.html +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/capabilities.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/compaction.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/context.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/mcp_notice.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/tool_output.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/usage.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/permission/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/permission/approval.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/permission/readonly.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/storage/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/storage/metadata.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/storage/records.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/storage/store.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/brand.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/pipeline.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/registry.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/boot.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/clipboard.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/demo.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/labels.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/notices.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/rows.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/sessions.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/settings.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/approval_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/completer.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/composer.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/context_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/model_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/panel.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/root.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/steering.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/transcript.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/update_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/welcome.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/paths.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/builtins.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/extension.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/mixins.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/state.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/capabilities.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/clipboard.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/console.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/app.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/events.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/focus.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/geometry.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/host.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/keymap.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/scheduler.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/screen.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/widget.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/gallery.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/glyphs.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/bridge.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/decoder.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/events.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/keys.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/reader.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/box.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/overlay.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/solver.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/spacing.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/canvas.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/code.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/color.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/renderer.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/rich_text.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/style.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/sweep.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/text.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/terminal.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/testing/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/testing/harness.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/testing/snapshot.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/__init__.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/collapsible.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/completion.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/dialog.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/diff.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/list.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/list_page.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/markdown.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/progress.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/rich_text.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/status.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/table.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/tasks.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/text.py +0 -0
- {zettcode-0.1.5 → zettcode-0.1.6}/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.6
|
|
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
|
|
@@ -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.6"
|
|
11
11
|
|
|
12
12
|
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
13
13
|
_EXPORTS = {
|
|
@@ -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."""
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -37,6 +37,7 @@ class EntryKind(StrEnum):
|
|
|
37
37
|
the line it ended on.
|
|
38
38
|
THINKING: Reasoning, or a compaction's summary, with its own clock.
|
|
39
39
|
TOOL: One tool call and its bounded result.
|
|
40
|
+
CHANGES: The files a request changed, said once when it ends.
|
|
40
41
|
"""
|
|
41
42
|
|
|
42
43
|
WELCOME = "welcome"
|
|
@@ -48,6 +49,7 @@ class EntryKind(StrEnum):
|
|
|
48
49
|
WAIT = "wait"
|
|
49
50
|
THINKING = "thinking"
|
|
50
51
|
TOOL = "tool"
|
|
52
|
+
CHANGES = "changes"
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
class EntryStatus(StrEnum):
|
|
@@ -200,19 +202,117 @@ class ThinkingEntry(TimedEntry):
|
|
|
200
202
|
|
|
201
203
|
@dataclass(slots=True, kw_only=True)
|
|
202
204
|
class ToolEntry(TimedEntry):
|
|
203
|
-
"""
|
|
205
|
+
"""One tool call and its bounded result: what every tool has, and nothing only some do.
|
|
204
206
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
A tool whose row shows something of its own has an entry of its own that adds it
|
|
208
|
+
(:class:`ReadEntry`, :class:`WriteEntry`, :class:`EditEntry`); they are all ``tool`` rows,
|
|
209
|
+
folded and timed alike, and the presenter registry finds the one that draws each by its class.
|
|
210
|
+
Which class a call gets is decided by the render handler that describes it
|
|
211
|
+
(:class:`~.rendering.ToolRow`), so nothing here, and nothing in the transcript, names a tool.
|
|
208
212
|
"""
|
|
209
213
|
|
|
210
214
|
kind: EntryKind = field(default=EntryKind.TOOL, init=False)
|
|
211
215
|
call_id: str
|
|
212
216
|
tool: str
|
|
213
217
|
title: str
|
|
218
|
+
text: str = ""
|
|
219
|
+
expanded: bool = False
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def foldable(self) -> bool:
|
|
223
|
+
"""Return whether there is a body to expand and collapse."""
|
|
224
|
+
return not self.running and bool(self.text)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(slots=True, kw_only=True)
|
|
228
|
+
class ReadEntry(ToolEntry):
|
|
229
|
+
"""A file that was read: its body is source code, drawn as the language it is.
|
|
230
|
+
|
|
231
|
+
``language`` names the scanner for the output, or is ``None`` for a file that is not code.
|
|
232
|
+
"""
|
|
233
|
+
|
|
214
234
|
language: str | None = None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass(slots=True, kw_only=True)
|
|
238
|
+
class FileChangeEntry(ToolEntry):
|
|
239
|
+
"""A tool that changed a file, and the change it made, as rows.
|
|
240
|
+
|
|
241
|
+
``path`` is the file, as the call named it. ``added`` is the number of lines the change adds, drawn
|
|
242
|
+
beside the title in the colour an addition has. ``diff`` is the change itself (see :data:`~.rendering.DiffRow`), which the row shows in place
|
|
243
|
+
of the tool's one-line answer once the call has succeeded: the answer says "Wrote 120 bytes", the
|
|
244
|
+
diff says what was written.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
path: str = ""
|
|
215
248
|
added: int = 0
|
|
249
|
+
diff: tuple[tuple[str, str], ...] = ()
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def shows_diff(self) -> bool:
|
|
253
|
+
"""Return whether the row's body is its diff: there is one, and the call succeeded.
|
|
254
|
+
|
|
255
|
+
A call that failed did not change the file, so a diff of what it meant to do would
|
|
256
|
+
describe something that did not happen; its error is the body then.
|
|
257
|
+
"""
|
|
258
|
+
return bool(self.diff) and self.status is EntryStatus.COMPLETED
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def foldable(self) -> bool:
|
|
262
|
+
"""Return whether there is a body to expand: the tool's answer, or the diff."""
|
|
263
|
+
return not self.running and (bool(self.text) or self.shows_diff)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass(slots=True, kw_only=True)
|
|
267
|
+
class WriteEntry(FileChangeEntry):
|
|
268
|
+
"""A file that was written whole: every line of it is an addition.
|
|
269
|
+
|
|
270
|
+
The tool overwrites, and what it overwrote is not in the call, so there is nothing to take away
|
|
271
|
+
from: the diff is the new content as additions, and there is no ``removed`` to count.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass(slots=True, kw_only=True)
|
|
276
|
+
class EditEntry(FileChangeEntry):
|
|
277
|
+
"""A file that was edited in place: the lines the edits add and remove, and the change.
|
|
278
|
+
|
|
279
|
+
``removed`` is counted from the call's arguments when the row opens, as ``added`` is.
|
|
280
|
+
"""
|
|
281
|
+
|
|
216
282
|
removed: int = 0
|
|
217
|
-
|
|
218
|
-
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass(slots=True, kw_only=True)
|
|
286
|
+
class DeleteEntry(ToolEntry):
|
|
287
|
+
"""A file that was deleted; ``path`` is the file, as the call named it."""
|
|
288
|
+
|
|
289
|
+
path: str = ""
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass(frozen=True, slots=True)
|
|
293
|
+
class FileSummary:
|
|
294
|
+
"""What one request did to one file, in total.
|
|
295
|
+
|
|
296
|
+
Attributes:
|
|
297
|
+
path: The file, relative to the workspace when it is inside it.
|
|
298
|
+
added: Lines added across every edit and write of it.
|
|
299
|
+
removed: Lines removed across every edit of it.
|
|
300
|
+
deleted: The file's last change was its deletion.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
path: str
|
|
304
|
+
added: int = 0
|
|
305
|
+
removed: int = 0
|
|
306
|
+
deleted: bool = False
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass(slots=True, kw_only=True)
|
|
310
|
+
class ChangesEntry(Entry):
|
|
311
|
+
"""The files a request changed, listed once when it ends.
|
|
312
|
+
|
|
313
|
+
Never made for a request that changed nothing: a row that says "no changes" would be said after
|
|
314
|
+
every question.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
kind: EntryKind = field(default=EntryKind.CHANGES, init=False)
|
|
318
|
+
files: tuple[FileSummary, ...] = ()
|