zettcode 0.1.12__tar.gz → 0.2.0__tar.gz

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