zettcode 0.1.11__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.11 → zettcode-0.2.0}/PKG-INFO +2 -2
  2. {zettcode-0.1.11 → zettcode-0.2.0}/pyproject.toml +1 -1
  3. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/__init__.py +1 -1
  4. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/entries.py +7 -1
  5. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/presenters.py +12 -1
  6. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/rendering.py +83 -12
  7. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/tips.py +1 -1
  8. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/stream.py +5 -1
  9. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/commands.py +25 -0
  10. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/plugins/builtin_commands.py +124 -0
  11. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/plugins/builtins.py +10 -1
  12. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/app.py +60 -16
  13. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/followups.py +2 -2
  14. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/keys.py +6 -1
  15. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/messaging.py +13 -91
  16. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/sessions.py +18 -7
  17. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/shell.py +12 -4
  18. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/updating.py +57 -3
  19. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/__init__.py +5 -1
  20. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/composer.py +20 -1
  21. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/update_page.py +4 -1
  22. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/config.py +2 -2
  23. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/daemon/cli.py +76 -0
  24. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/agent/turns.py +7 -5
  25. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/core.py +8 -0
  26. zettcode-0.2.0/src/zettcode/engine/extensions/harness.py +199 -0
  27. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/mcp_notice.py +8 -2
  28. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/subagents.py +2 -1
  29. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/permission/__init__.py +18 -0
  30. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/permission/sandbox.py +10 -0
  31. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/plugins/builtin_agent.py +30 -1
  32. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/mentions.py +11 -10
  33. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/relay.py +3 -3
  34. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/sdk/client.py +7 -1
  35. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/sdk/session.py +20 -1
  36. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/server/app.py +1 -1
  37. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/theme.py +15 -7
  38. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/ayu-mirage.toml +1 -1
  39. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/catppuccin-latte.toml +1 -1
  40. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/catppuccin-mocha.toml +1 -1
  41. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/dracula.toml +1 -1
  42. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/github-dimmed.toml +1 -1
  43. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/github-light.toml +1 -1
  44. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/gruvbox-dark.toml +1 -1
  45. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/gruvbox-light.toml +1 -1
  46. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/kanagawa.toml +1 -1
  47. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/nord.toml +1 -1
  48. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/one-dark.toml +1 -1
  49. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/rose-pine-dawn.toml +1 -1
  50. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/rose-pine.toml +1 -1
  51. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/solarized-dark.toml +1 -1
  52. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/solarized-light.toml +1 -1
  53. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/themes/tokyo-night.toml +1 -1
  54. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/update.py +31 -15
  55. zettcode-0.1.11/src/zettcode/engine/extensions/harness.py +0 -89
  56. {zettcode-0.1.11 → zettcode-0.2.0}/.gitignore +0 -0
  57. {zettcode-0.1.11 → zettcode-0.2.0}/README.md +0 -0
  58. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/_compat.py +0 -0
  59. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/__init__.py +0 -0
  60. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/__init__.py +0 -0
  61. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/agent/__init__.py +0 -0
  62. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/agent/commands.py +0 -0
  63. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/agent/settings.py +0 -0
  64. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/__init__.py +0 -0
  65. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/changes.py +0 -0
  66. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/effort_gauge.py +0 -0
  67. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/projection.py +0 -0
  68. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/pulse.py +0 -0
  69. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/replay.py +0 -0
  70. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/shimmer.py +0 -0
  71. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/stops.py +0 -0
  72. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/text.py +0 -0
  73. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
  74. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/fold.py +0 -0
  75. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
  76. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
  77. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/agent/conversation/transcript/turn.py +0 -0
  78. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/brand.py +0 -0
  79. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/commandline/__init__.py +0 -0
  80. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/commandline/arguments.py +0 -0
  81. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/commandline/completion.py +0 -0
  82. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/commandline/history.py +0 -0
  83. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/commandline/runner.py +0 -0
  84. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/__init__.py +0 -0
  85. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/cat.py +0 -0
  86. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/chibi.py +0 -0
  87. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/dog.py +0 -0
  88. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/kangaroo.py +0 -0
  89. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/model.py +0 -0
  90. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/penguin.py +0 -0
  91. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/polarbear.py +0 -0
  92. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/scarfy.py +0 -0
  93. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/seagull.py +0 -0
  94. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/slime.py +0 -0
  95. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/snowbear.py +0 -0
  96. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/sprout.py +0 -0
  97. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/tiger.py +0 -0
  98. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/pets/whale.py +0 -0
  99. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/plugins/__init__.py +0 -0
  100. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/plugins/container.py +0 -0
  101. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/plugins/host.py +0 -0
  102. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/plugins/loader.py +0 -0
  103. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/plugins/mixins.py +0 -0
  104. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/plugins/plugin.py +0 -0
  105. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/plugins/state.py +0 -0
  106. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/__init__.py +0 -0
  107. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/bars.py +0 -0
  108. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/boot.py +0 -0
  109. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/clipboard.py +0 -0
  110. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/commands.py +0 -0
  111. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/config_form.py +0 -0
  112. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/demo.py +0 -0
  113. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/gc_policy.py +0 -0
  114. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/labels.py +0 -0
  115. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/notices.py +0 -0
  116. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/providers.py +0 -0
  117. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/settings.py +0 -0
  118. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/turns.py +0 -0
  119. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/approval_page.py +0 -0
  120. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
  121. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/bash_page.py +0 -0
  122. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/completer.py +0 -0
  123. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/context_charts.py +0 -0
  124. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/context_page.py +0 -0
  125. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/effort_page.py +0 -0
  126. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/follow_up.py +0 -0
  127. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/form_page.py +0 -0
  128. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/input_page.py +0 -0
  129. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/model_page.py +0 -0
  130. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/model_settings_page.py +0 -0
  131. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/panel.py +0 -0
  132. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/pet.py +0 -0
  133. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/pet_page.py +0 -0
  134. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/root.py +0 -0
  135. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/self_compact_page.py +0 -0
  136. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
  137. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
  138. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/statusline_page.py +0 -0
  139. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/steering.py +0 -0
  140. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/transcript.py +0 -0
  141. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/usage_charts.py +0 -0
  142. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/usage_page.py +0 -0
  143. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/app/ui/widgets/welcome.py +0 -0
  144. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/__init__.py +0 -0
  145. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/__main__.py +0 -0
  146. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/client.py +0 -0
  147. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/files.py +0 -0
  148. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/finder.py +0 -0
  149. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/hub.py +0 -0
  150. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/log.py +0 -0
  151. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/models.py +0 -0
  152. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/process.py +0 -0
  153. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/protocol.py +0 -0
  154. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/roster.py +0 -0
  155. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/runner.py +0 -0
  156. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/server.py +0 -0
  157. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/text.py +0 -0
  158. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/broker/web.py +0 -0
  159. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/budget.py +0 -0
  160. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/cli.py +0 -0
  161. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/collector.py +0 -0
  162. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/config_edit.py +0 -0
  163. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/daemon/__init__.py +0 -0
  164. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/daemon/__main__.py +0 -0
  165. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/daemon/process.py +0 -0
  166. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/daemon/run.py +0 -0
  167. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/daemon/state.py +0 -0
  168. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/doctor.py +0 -0
  169. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/__init__.py +0 -0
  170. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/agent/__init__.py +0 -0
  171. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/agent/base.py +0 -0
  172. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/agent/context.py +0 -0
  173. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/agent/exports.py +0 -0
  174. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/agent/interaction.py +0 -0
  175. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/agent/sessions.py +0 -0
  176. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/agent/settings.py +0 -0
  177. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/dialogue/__init__.py +0 -0
  178. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/dialogue/ask.py +0 -0
  179. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/dialogue/side.py +0 -0
  180. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/dialogue/title.py +0 -0
  181. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/export/__init__.py +0 -0
  182. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/export/trace/script.js +0 -0
  183. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/export/trace/style.css +0 -0
  184. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/export/trace/template.html +0 -0
  185. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/__init__.py +0 -0
  186. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/capabilities.py +0 -0
  187. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/compaction.py +0 -0
  188. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/context.py +0 -0
  189. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/files.py +0 -0
  190. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/harness_switch.py +0 -0
  191. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/mcp.py +0 -0
  192. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/messaging.py +0 -0
  193. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/relay.py +0 -0
  194. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/tool_output.py +0 -0
  195. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/extensions/usage.py +0 -0
  196. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/messenger.py +0 -0
  197. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/permission/approval.py +0 -0
  198. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/permission/readonly.py +0 -0
  199. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/pipeline.py +0 -0
  200. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/plugins/__init__.py +0 -0
  201. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/plugins/container.py +0 -0
  202. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/plugins/extension.py +0 -0
  203. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/plugins/loader.py +0 -0
  204. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/plugins/mixins.py +0 -0
  205. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/plugins/plugin.py +0 -0
  206. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/plugins/services.py +0 -0
  207. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/plugins/state.py +0 -0
  208. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/server_tools.py +0 -0
  209. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/storage/__init__.py +0 -0
  210. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/storage/layout.py +0 -0
  211. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/storage/ledger.py +0 -0
  212. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/storage/locks.py +0 -0
  213. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/storage/metadata.py +0 -0
  214. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/storage/records.py +0 -0
  215. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/storage/store.py +0 -0
  216. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/tools/__init__.py +0 -0
  217. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/tools/boundary.py +0 -0
  218. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/tools/files.py +0 -0
  219. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/tools/images.py +0 -0
  220. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/tools/output.py +0 -0
  221. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/tools/shell.py +0 -0
  222. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/engine/tools/workspace.py +0 -0
  223. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/error_text.py +0 -0
  224. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/filelock.py +0 -0
  225. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/oneshot.py +0 -0
  226. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/paths.py +0 -0
  227. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/prompt.py +0 -0
  228. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/providers.py +0 -0
  229. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/py.typed +0 -0
  230. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/registry.py +0 -0
  231. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/sdk/__init__.py +0 -0
  232. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/sdk/errors.py +0 -0
  233. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/sdk/events.py +0 -0
  234. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/sdk/handlers.py +0 -0
  235. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/sdk/models.py +0 -0
  236. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/sdk/translation.py +0 -0
  237. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/sdk/types.py +0 -0
  238. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/server/__init__.py +0 -0
  239. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/server/cli.py +0 -0
  240. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/server/runner.py +0 -0
  241. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/server/schemas.py +0 -0
  242. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/server/serialize.py +0 -0
  243. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/server/settings.py +0 -0
  244. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/statusline.py +0 -0
  245. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/__init__.py +0 -0
  246. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/capabilities.py +0 -0
  247. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/clipboard.py +0 -0
  248. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/console.py +0 -0
  249. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/__init__.py +0 -0
  250. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/app.py +0 -0
  251. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/events.py +0 -0
  252. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/focus.py +0 -0
  253. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/geometry.py +0 -0
  254. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/host.py +0 -0
  255. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/keymap.py +0 -0
  256. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/scheduler.py +0 -0
  257. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/screen.py +0 -0
  258. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/core/widget.py +0 -0
  259. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/gallery.py +0 -0
  260. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/glyphs.py +0 -0
  261. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/input/__init__.py +0 -0
  262. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/input/bridge.py +0 -0
  263. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/input/decoder.py +0 -0
  264. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/input/events.py +0 -0
  265. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/input/keys.py +0 -0
  266. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/input/reader.py +0 -0
  267. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/layout/__init__.py +0 -0
  268. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/layout/box.py +0 -0
  269. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/layout/overlay.py +0 -0
  270. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/layout/scroll.py +0 -0
  271. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/layout/solver.py +0 -0
  272. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/layout/spacing.py +0 -0
  273. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/render/__init__.py +0 -0
  274. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/render/ansi.py +0 -0
  275. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/render/canvas.py +0 -0
  276. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/render/code.py +0 -0
  277. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/render/color.py +0 -0
  278. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/render/renderer.py +0 -0
  279. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/render/rich_text.py +0 -0
  280. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/render/style.py +0 -0
  281. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/render/sweep.py +0 -0
  282. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/render/text.py +0 -0
  283. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/runner.py +0 -0
  284. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/terminal.py +0 -0
  285. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/testing/__init__.py +0 -0
  286. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/testing/harness.py +0 -0
  287. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/testing/snapshot.py +0 -0
  288. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/theme_file.py +0 -0
  289. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/__init__.py +0 -0
  290. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/collapsible.py +0 -0
  291. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/completion.py +0 -0
  292. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/console.py +0 -0
  293. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/dialog.py +0 -0
  294. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/diff.py +0 -0
  295. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/list.py +0 -0
  296. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/list_page.py +0 -0
  297. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/markdown.py +0 -0
  298. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/progress.py +0 -0
  299. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/rich_text.py +0 -0
  300. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/status.py +0 -0
  301. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/table.py +0 -0
  302. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/tasks.py +0 -0
  303. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/text.py +0 -0
  304. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/textarea.py +0 -0
  305. {zettcode-0.1.11 → zettcode-0.2.0}/src/zettcode/tui/widgets/toast.py +0 -0
  306. {zettcode-0.1.11 → 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.11
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.11"
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(),
@@ -22,7 +22,7 @@ TIPS: tuple[str, ...] = (
22
22
  "Type while it works: your message can steer this request, or wait and follow it.",
23
23
  "Press Shift-Enter (or Alt-Enter) to add a line to your message instead of sending it.",
24
24
  "Type @ to mention a file or a skill. A file is read when the message is sent.",
25
- "Start a message with @name to send it to another running ZettCode session.",
25
+ "Type @ to mention another running ZettCode session; the model can pass your message to it.",
26
26
  "/btw <question> is answered on the side and never joins the conversation.",
27
27
  "/bash opens a full-screen command line: your shell's history, Tab completion, and coloured output.",
28
28
  "In /bash, Tab completes git subcommands and branches, and Ctrl-C stops a running command.",
@@ -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,6 +101,31 @@ 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, *, shown: str | None = None) -> bool:
105
+ """Run ``text`` as one turn of this conversation, exactly as if the reader had typed it.
106
+
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.
114
+
115
+ The turn is awaited, so the command that asked for it holds the request slot until it has been answered,
116
+ and the row the shell shows is the command's own.
117
+
118
+ Args:
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.
122
+
123
+ Returns:
124
+ Whether a turn was started and has ended. A request already running, a blank text and a session held
125
+ by another process answer ``False``, and nothing is started. A command that means to be usable
126
+ mid-request says ``when_busy="queue"``, and this then runs when the request in flight has ended.
127
+ """
128
+
104
129
  def change_model(self, name: ModelInfo | str) -> ModelInfo:
105
130
  """Switch models and announce the change in the transcript."""
106
131
 
@@ -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
@@ -135,8 +135,8 @@ class ZettCodeApp(
135
135
  # The plugins' commands reach the shell through the context they are run with, and the words their menus
136
136
  # offer through this link; the shell is built enough by now that either may follow it.
137
137
  agent.attach_host(self)
138
- #: The line to the other sessions, when the configuration has one; the session names it offers
139
- #: for ``@`` come first, because a draft that begins with one is a message to that session.
138
+ #: The line to the other sessions, when the configuration has one; a session name it offers for ``@``
139
+ #: comes first, so it wins over a file or plugin resource of the same name.
140
140
  self.messenger: SessionLink | None = agent.messenger
141
141
  self.mentions = MentionRegistry(
142
142
  agent.plugin_mentions
@@ -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.
@@ -254,7 +255,6 @@ class ZettCodeApp(
254
255
  self._follow_ups_held = False
255
256
  self._turn_running = False
256
257
  self._inbox: list[Relayed] = []
257
- self._sends: set[asyncio.Task[None]] = set()
258
258
  self._lock_watch: asyncio.Task[None] | None = None
259
259
  self._lock_said: str | None = None
260
260
  self._messaging_live = False
@@ -660,6 +660,41 @@ class ZettCodeApp(
660
660
  if outcome:
661
661
  self._settle_wait(outcome)
662
662
 
663
+ async def prompt(self, text: str, *, shown: str | None = None) -> bool:
664
+ """Run ``text`` as one turn of this conversation, as if the reader had typed it.
665
+
666
+ The draft path is taken whole — the echo, ``@`` references, the transcript, the session, the row the
667
+ request ends on — rather than a run of its own, so a command that asks the model something produces a turn
668
+ a reader could have produced. What the command decides is the text; everything after that is the ordinary
669
+ one.
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
+
676
+ The turn is awaited, so the request slot stays the command's until the answer is in: a second draft cannot
677
+ start behind it, and the command reports what it did once it is done.
678
+ """
679
+ if not text.strip():
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
684
+ if self.client.session.locked:
685
+ self.flash("this session is open in another ZettCode process; it is read-only here", level="error")
686
+ return 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)
696
+ return True
697
+
663
698
  async def run_continue(self) -> None:
664
699
  """Pick up the request that was interrupted, where it stopped, as a turn of the conversation.
665
700
 
@@ -816,17 +851,16 @@ class ZettCodeApp(
816
851
  """
817
852
  if command is None:
818
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
819
859
  if self.client.session.locked and not command:
820
860
  # Only commands are taken, which is how a reader leaves: the composer does not let anything else be
821
861
  # typed, and this is for what arrives some other way (a draft that was there already, a recall).
822
862
  self.flash("this session is open in another ZettCode process; it is read-only here", level="error")
823
863
  return False
824
- if not command:
825
- # A draft that begins with the name of an active session is a message to it: sent whatever this
826
- # session is doing, because it is not a request to this session's model.
827
- sent = self._send_session_message(self.composer.parts())
828
- if sent is not None:
829
- return sent
830
864
  if self._busy:
831
865
  if command:
832
866
  # Which commands are taken mid-request is the command's own
@@ -860,7 +894,7 @@ class ZettCodeApp(
860
894
  self.transcript.error(f"{self.client.model.name} does not take images")
861
895
  self.app.invalidate()
862
896
  return False
863
- 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))
864
898
  return True
865
899
 
866
900
  def _queue_steering(self, value: str, key: str | None = None, parts: Sequence[PromptPart] = ()) -> None:
@@ -968,9 +1002,10 @@ class ZettCodeApp(
968
1002
  self,
969
1003
  prompt: str,
970
1004
  parts: Sequence[PromptPart] = (),
971
- hint: str | None = None,
972
1005
  *,
1006
+ instructions: str | None = None,
973
1007
  relayed: Relayed | None = None,
1008
+ advance: bool = True,
974
1009
  ) -> None:
975
1010
  """Stream one agent turn, keeping the task panel and transcript current.
976
1011
 
@@ -979,8 +1014,9 @@ class ZettCodeApp(
979
1014
  into the transcript.
980
1015
  parts: The same turn as ordered text and image parts; an empty
981
1016
  sequence falls back to the prompt alone.
982
- hint: Instructions the draft's ``@`` references contribute; the
983
- 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``.
984
1020
  relayed: The turn answers a message another session sent, which the transcript draws as
985
1021
  that, the model is told where it came from, and the session stores as that.
986
1022
  """
@@ -1015,7 +1051,12 @@ class ZettCodeApp(
1015
1051
  **self._answers(),
1016
1052
  )
1017
1053
  else:
1018
- 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
+ )
1019
1060
  async with run:
1020
1061
  async for event in run:
1021
1062
  tally.see(event)
@@ -1056,7 +1097,10 @@ class ZettCodeApp(
1056
1097
  self._note = None
1057
1098
  self._refresh_tasks()
1058
1099
  self.app.invalidate()
1059
- self._start_next_command()
1100
+ if advance:
1101
+ # A turn a command asked for leaves this to the command: it is still running, and what was queued
1102
+ # waits for it to end, not for the turn inside it.
1103
+ self._start_next_command()
1060
1104
 
1061
1105
  def _set_busy(self, busy: bool) -> None:
1062
1106
  """Track in-flight work and keep the frame loop alive while it runs.
@@ -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