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.
Files changed (177) hide show
  1. {zettcode-0.1.5 → zettcode-0.1.6}/PKG-INFO +1 -1
  2. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/__init__.py +1 -1
  3. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/commands.py +6 -0
  4. zettcode-0.1.6/src/zettcode/app/agent/agent/settings.py +164 -0
  5. zettcode-0.1.6/src/zettcode/app/agent/conversation/changes.py +64 -0
  6. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/entries.py +106 -6
  7. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/presenters.py +223 -34
  8. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/rendering.py +190 -10
  9. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/rows.py +6 -5
  10. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/fold.py +2 -2
  11. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/stream.py +3 -13
  12. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/turn.py +42 -6
  13. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/mcp.py +50 -1
  14. zettcode-0.1.6/src/zettcode/app/agent/extensions/subagents.py +218 -0
  15. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/runtime.py +45 -3
  16. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/commands.py +10 -3
  17. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/app.py +69 -6
  18. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/commands.py +45 -8
  19. zettcode-0.1.6/src/zettcode/app/ui/config_form.py +203 -0
  20. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/followups.py +54 -6
  21. zettcode-0.1.6/src/zettcode/app/ui/gc_policy.py +147 -0
  22. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/keys.py +14 -1
  23. zettcode-0.1.6/src/zettcode/app/ui/providers.py +470 -0
  24. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/shell.py +12 -0
  25. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/updating.py +63 -11
  26. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/__init__.py +5 -0
  27. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/follow_up.py +2 -2
  28. zettcode-0.1.6/src/zettcode/app/ui/widgets/input_page.py +175 -0
  29. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/cli.py +55 -1
  30. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/collector.py +10 -0
  31. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/config.py +20 -4
  32. zettcode-0.1.6/src/zettcode/config_edit.py +268 -0
  33. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/container.py +44 -2
  34. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/loader.py +16 -0
  35. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/plugin.py +4 -0
  36. zettcode-0.1.6/src/zettcode/providers.py +205 -0
  37. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/theme.py +17 -0
  38. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/scroll.py +47 -6
  39. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/runner.py +24 -0
  40. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/theme_file.py +2 -0
  41. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/textarea.py +11 -0
  42. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/update.py +113 -4
  43. zettcode-0.1.5/src/zettcode/app/agent/agent/settings.py +0 -68
  44. zettcode-0.1.5/src/zettcode/app/agent/extensions/subagents.py +0 -140
  45. {zettcode-0.1.5 → zettcode-0.1.6}/.gitignore +0 -0
  46. {zettcode-0.1.5 → zettcode-0.1.6}/LICENSE +0 -0
  47. {zettcode-0.1.5 → zettcode-0.1.6}/README.md +0 -0
  48. {zettcode-0.1.5 → zettcode-0.1.6}/README.zh-CN.md +0 -0
  49. {zettcode-0.1.5 → zettcode-0.1.6}/pyproject.toml +0 -0
  50. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/_compat.py +0 -0
  51. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/__init__.py +0 -0
  52. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/__init__.py +0 -0
  53. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/__init__.py +0 -0
  54. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/base.py +0 -0
  55. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/context.py +0 -0
  56. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/exports.py +0 -0
  57. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/interaction.py +0 -0
  58. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/prompt.py +0 -0
  59. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/sessions.py +0 -0
  60. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/agent/turns.py +0 -0
  61. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/__init__.py +0 -0
  62. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/projection.py +0 -0
  63. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/replay.py +0 -0
  64. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
  65. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
  66. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
  67. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/dialogue/__init__.py +0 -0
  68. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/dialogue/ask.py +0 -0
  69. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/dialogue/mentions.py +0 -0
  70. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/dialogue/side.py +0 -0
  71. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/dialogue/title.py +0 -0
  72. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/errors.py +0 -0
  73. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/export/__init__.py +0 -0
  74. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/export/trace/script.js +0 -0
  75. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/export/trace/style.css +0 -0
  76. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/export/trace/template.html +0 -0
  77. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/__init__.py +0 -0
  78. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/capabilities.py +0 -0
  79. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/compaction.py +0 -0
  80. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/context.py +0 -0
  81. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/mcp_notice.py +0 -0
  82. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/tool_output.py +0 -0
  83. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/extensions/usage.py +0 -0
  84. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/permission/__init__.py +0 -0
  85. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/permission/approval.py +0 -0
  86. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/permission/readonly.py +0 -0
  87. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/storage/__init__.py +0 -0
  88. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/storage/metadata.py +0 -0
  89. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/storage/records.py +0 -0
  90. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/agent/storage/store.py +0 -0
  91. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/brand.py +0 -0
  92. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/pipeline.py +0 -0
  93. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/registry.py +0 -0
  94. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/__init__.py +0 -0
  95. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/boot.py +0 -0
  96. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/clipboard.py +0 -0
  97. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/demo.py +0 -0
  98. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/labels.py +0 -0
  99. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/notices.py +0 -0
  100. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/rows.py +0 -0
  101. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/sessions.py +0 -0
  102. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/settings.py +0 -0
  103. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/approval_page.py +0 -0
  104. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
  105. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/completer.py +0 -0
  106. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/composer.py +0 -0
  107. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/context_page.py +0 -0
  108. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/model_page.py +0 -0
  109. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/panel.py +0 -0
  110. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/root.py +0 -0
  111. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
  112. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
  113. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/steering.py +0 -0
  114. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/transcript.py +0 -0
  115. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/update_page.py +0 -0
  116. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/app/ui/widgets/welcome.py +0 -0
  117. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/paths.py +0 -0
  118. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/__init__.py +0 -0
  119. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/builtins.py +0 -0
  120. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/extension.py +0 -0
  121. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/mixins.py +0 -0
  122. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/plugins/state.py +0 -0
  123. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/__init__.py +0 -0
  124. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/capabilities.py +0 -0
  125. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/clipboard.py +0 -0
  126. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/console.py +0 -0
  127. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/__init__.py +0 -0
  128. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/app.py +0 -0
  129. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/events.py +0 -0
  130. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/focus.py +0 -0
  131. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/geometry.py +0 -0
  132. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/host.py +0 -0
  133. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/keymap.py +0 -0
  134. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/scheduler.py +0 -0
  135. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/screen.py +0 -0
  136. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/core/widget.py +0 -0
  137. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/gallery.py +0 -0
  138. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/glyphs.py +0 -0
  139. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/__init__.py +0 -0
  140. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/bridge.py +0 -0
  141. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/decoder.py +0 -0
  142. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/events.py +0 -0
  143. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/keys.py +0 -0
  144. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/input/reader.py +0 -0
  145. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/__init__.py +0 -0
  146. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/box.py +0 -0
  147. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/overlay.py +0 -0
  148. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/solver.py +0 -0
  149. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/layout/spacing.py +0 -0
  150. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/__init__.py +0 -0
  151. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/canvas.py +0 -0
  152. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/code.py +0 -0
  153. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/color.py +0 -0
  154. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/renderer.py +0 -0
  155. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/rich_text.py +0 -0
  156. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/style.py +0 -0
  157. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/sweep.py +0 -0
  158. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/render/text.py +0 -0
  159. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/terminal.py +0 -0
  160. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/testing/__init__.py +0 -0
  161. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/testing/harness.py +0 -0
  162. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/testing/snapshot.py +0 -0
  163. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/__init__.py +0 -0
  164. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/collapsible.py +0 -0
  165. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/completion.py +0 -0
  166. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/dialog.py +0 -0
  167. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/diff.py +0 -0
  168. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/list.py +0 -0
  169. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/list_page.py +0 -0
  170. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/markdown.py +0 -0
  171. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/progress.py +0 -0
  172. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/rich_text.py +0 -0
  173. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/status.py +0 -0
  174. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/table.py +0 -0
  175. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/tasks.py +0 -0
  176. {zettcode-0.1.5 → zettcode-0.1.6}/src/zettcode/tui/widgets/text.py +0 -0
  177. {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.5
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.5"
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
- """Expandable tool invocation and bounded result.
205
+ """One tool call and its bounded result: what every tool has, and nothing only some do.
204
206
 
205
- ``added`` and ``removed`` are the lines an edit changes, counted from the call's
206
- arguments when the row opens; they ride beside the title because the presenter
207
- paints them in colours of their own.
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
- text: str = ""
218
- expanded: bool = False
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, ...] = ()