zettcode 0.1.9__tar.gz → 0.1.10__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 (281) hide show
  1. {zettcode-0.1.9 → zettcode-0.1.10}/PKG-INFO +2 -2
  2. {zettcode-0.1.9 → zettcode-0.1.10}/pyproject.toml +1 -1
  3. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/__init__.py +1 -1
  4. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/settings.py +36 -3
  5. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/presenters.py +14 -3
  6. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/projection.py +58 -10
  7. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/rendering.py +59 -3
  8. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/replay.py +32 -2
  9. zettcode-0.1.10/src/zettcode/app/agent/conversation/server_tools.py +157 -0
  10. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/text.py +3 -0
  11. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/tips.py +4 -1
  12. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/stream.py +18 -0
  13. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/ask.py +17 -0
  14. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/side.py +11 -2
  15. zettcode-0.1.10/src/zettcode/app/agent/extensions/capabilities.py +68 -0
  16. zettcode-0.1.10/src/zettcode/app/agent/extensions/files.py +101 -0
  17. zettcode-0.1.10/src/zettcode/app/agent/extensions/harness.py +89 -0
  18. zettcode-0.1.10/src/zettcode/app/agent/extensions/harness_switch.py +18 -0
  19. zettcode-0.1.10/src/zettcode/app/agent/extensions/relay.py +93 -0
  20. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/subagents.py +96 -22
  21. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/tool_output.py +8 -7
  22. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/permission/__init__.py +47 -15
  23. zettcode-0.1.10/src/zettcode/app/agent/permission/sandbox.py +124 -0
  24. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/runtime.py +97 -17
  25. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/metadata.py +7 -2
  26. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/records.py +3 -1
  27. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/store.py +47 -0
  28. zettcode-0.1.10/src/zettcode/app/agent/tools/__init__.py +7 -0
  29. zettcode-0.1.10/src/zettcode/app/agent/tools/boundary.py +51 -0
  30. zettcode-0.1.10/src/zettcode/app/agent/tools/files.py +267 -0
  31. zettcode-0.1.10/src/zettcode/app/agent/tools/images.py +32 -0
  32. zettcode-0.1.10/src/zettcode/app/agent/tools/output.py +74 -0
  33. zettcode-0.1.10/src/zettcode/app/agent/tools/shell.py +171 -0
  34. zettcode-0.1.10/src/zettcode/app/agent/tools/workspace.py +268 -0
  35. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/app.py +38 -3
  36. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/bars.py +1 -1
  37. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/config_form.py +63 -8
  38. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/keys.py +28 -0
  39. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/notices.py +3 -3
  40. zettcode-0.1.10/src/zettcode/app/ui/providers.py +614 -0
  41. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/settings.py +5 -3
  42. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/shell.py +1 -1
  43. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/__init__.py +14 -0
  44. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/approval_page.py +9 -9
  45. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/completer.py +3 -4
  46. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/follow_up.py +4 -6
  47. zettcode-0.1.10/src/zettcode/app/ui/widgets/form_page.py +380 -0
  48. zettcode-0.1.10/src/zettcode/app/ui/widgets/model_settings_page.py +654 -0
  49. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/pet.py +44 -19
  50. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/statusline_page.py +31 -18
  51. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/transcript.py +47 -1
  52. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/usage_page.py +25 -7
  53. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/cli.py +150 -20
  54. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/config.py +136 -9
  55. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/config_edit.py +27 -1
  56. zettcode-0.1.10/src/zettcode/oneshot.py +200 -0
  57. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/paths.py +4 -0
  58. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/builtin_agent.py +92 -3
  59. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/builtin_commands.py +46 -5
  60. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/services.py +1 -0
  61. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/state.py +2 -2
  62. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/providers.py +6 -3
  63. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/client.py +25 -3
  64. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/models.py +11 -3
  65. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/types.py +5 -0
  66. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/__init__.py +3 -1
  67. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/capabilities.py +6 -0
  68. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/theme.py +8 -0
  69. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/renderer.py +45 -7
  70. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/theme_file.py +17 -7
  71. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/textarea.py +1 -1
  72. zettcode-0.1.9/src/zettcode/app/agent/extensions/capabilities.py +0 -50
  73. zettcode-0.1.9/src/zettcode/app/ui/providers.py +0 -567
  74. {zettcode-0.1.9 → zettcode-0.1.10}/.gitignore +0 -0
  75. {zettcode-0.1.9 → zettcode-0.1.10}/README.md +0 -0
  76. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/_compat.py +0 -0
  77. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/__init__.py +0 -0
  78. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/__init__.py +0 -0
  79. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/__init__.py +0 -0
  80. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/base.py +0 -0
  81. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/commands.py +0 -0
  82. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/context.py +0 -0
  83. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/exports.py +0 -0
  84. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/interaction.py +0 -0
  85. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/prompt.py +0 -0
  86. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/sessions.py +0 -0
  87. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/turns.py +0 -0
  88. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/__init__.py +0 -0
  89. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/changes.py +0 -0
  90. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/effort_gauge.py +0 -0
  91. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/entries.py +0 -0
  92. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/pulse.py +0 -0
  93. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/shimmer.py +0 -0
  94. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/stops.py +0 -0
  95. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
  96. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/fold.py +0 -0
  97. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
  98. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
  99. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/turn.py +0 -0
  100. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/__init__.py +0 -0
  101. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/mentions.py +0 -0
  102. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/relay.py +0 -0
  103. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/title.py +0 -0
  104. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/errors.py +0 -0
  105. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/export/__init__.py +0 -0
  106. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/export/trace/script.js +0 -0
  107. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/export/trace/style.css +0 -0
  108. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/export/trace/template.html +0 -0
  109. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/__init__.py +0 -0
  110. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/compaction.py +0 -0
  111. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/context.py +0 -0
  112. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/mcp.py +0 -0
  113. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/mcp_notice.py +0 -0
  114. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/messaging.py +0 -0
  115. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/messenger.py +0 -0
  116. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/usage.py +0 -0
  117. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/permission/approval.py +0 -0
  118. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/permission/readonly.py +0 -0
  119. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/__init__.py +0 -0
  120. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/layout.py +0 -0
  121. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/ledger.py +0 -0
  122. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/locks.py +0 -0
  123. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/brand.py +0 -0
  124. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commandline/__init__.py +0 -0
  125. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commandline/arguments.py +0 -0
  126. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commandline/completion.py +0 -0
  127. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commandline/history.py +0 -0
  128. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commandline/runner.py +0 -0
  129. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commands.py +0 -0
  130. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/__init__.py +0 -0
  131. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/cat.py +0 -0
  132. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/chibi.py +0 -0
  133. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/dog.py +0 -0
  134. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/kangaroo.py +0 -0
  135. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/model.py +0 -0
  136. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/penguin.py +0 -0
  137. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/polarbear.py +0 -0
  138. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/scarfy.py +0 -0
  139. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/seagull.py +0 -0
  140. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/slime.py +0 -0
  141. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/snowbear.py +0 -0
  142. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/sprout.py +0 -0
  143. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/tiger.py +0 -0
  144. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/whale.py +0 -0
  145. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pipeline.py +0 -0
  146. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/registry.py +0 -0
  147. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/__init__.py +0 -0
  148. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/boot.py +0 -0
  149. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/clipboard.py +0 -0
  150. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/commands.py +0 -0
  151. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/demo.py +0 -0
  152. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/followups.py +0 -0
  153. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/gc_policy.py +0 -0
  154. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/labels.py +0 -0
  155. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/messaging.py +0 -0
  156. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/sessions.py +0 -0
  157. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/updating.py +0 -0
  158. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
  159. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/bash_page.py +0 -0
  160. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/composer.py +0 -0
  161. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/context_charts.py +0 -0
  162. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/context_page.py +0 -0
  163. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/effort_page.py +0 -0
  164. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/input_page.py +0 -0
  165. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/model_page.py +0 -0
  166. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/panel.py +0 -0
  167. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/pet_page.py +0 -0
  168. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/root.py +0 -0
  169. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/self_compact_page.py +0 -0
  170. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
  171. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
  172. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/steering.py +0 -0
  173. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/update_page.py +0 -0
  174. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/usage_charts.py +0 -0
  175. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/welcome.py +0 -0
  176. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/__init__.py +0 -0
  177. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/__main__.py +0 -0
  178. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/client.py +0 -0
  179. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/files.py +0 -0
  180. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/finder.py +0 -0
  181. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/hub.py +0 -0
  182. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/log.py +0 -0
  183. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/models.py +0 -0
  184. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/process.py +0 -0
  185. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/protocol.py +0 -0
  186. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/roster.py +0 -0
  187. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/runner.py +0 -0
  188. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/server.py +0 -0
  189. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/text.py +0 -0
  190. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/web.py +0 -0
  191. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/collector.py +0 -0
  192. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/doctor.py +0 -0
  193. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/filelock.py +0 -0
  194. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/__init__.py +0 -0
  195. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/builtins.py +0 -0
  196. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/container.py +0 -0
  197. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/extension.py +0 -0
  198. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/loader.py +0 -0
  199. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/mixins.py +0 -0
  200. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/plugin.py +0 -0
  201. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/py.typed +0 -0
  202. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/__init__.py +0 -0
  203. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/_translate.py +0 -0
  204. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/errors.py +0 -0
  205. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/events.py +0 -0
  206. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/handlers.py +0 -0
  207. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/session.py +0 -0
  208. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/statusline.py +0 -0
  209. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/clipboard.py +0 -0
  210. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/console.py +0 -0
  211. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/__init__.py +0 -0
  212. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/app.py +0 -0
  213. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/events.py +0 -0
  214. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/focus.py +0 -0
  215. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/geometry.py +0 -0
  216. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/host.py +0 -0
  217. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/keymap.py +0 -0
  218. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/scheduler.py +0 -0
  219. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/screen.py +0 -0
  220. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/widget.py +0 -0
  221. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/gallery.py +0 -0
  222. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/glyphs.py +0 -0
  223. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/__init__.py +0 -0
  224. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/bridge.py +0 -0
  225. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/decoder.py +0 -0
  226. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/events.py +0 -0
  227. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/keys.py +0 -0
  228. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/reader.py +0 -0
  229. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/__init__.py +0 -0
  230. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/box.py +0 -0
  231. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/overlay.py +0 -0
  232. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/scroll.py +0 -0
  233. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/solver.py +0 -0
  234. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/spacing.py +0 -0
  235. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/__init__.py +0 -0
  236. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/ansi.py +0 -0
  237. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/canvas.py +0 -0
  238. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/code.py +0 -0
  239. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/color.py +0 -0
  240. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/rich_text.py +0 -0
  241. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/style.py +0 -0
  242. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/sweep.py +0 -0
  243. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/text.py +0 -0
  244. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/runner.py +0 -0
  245. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/terminal.py +0 -0
  246. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/testing/__init__.py +0 -0
  247. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/testing/harness.py +0 -0
  248. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/testing/snapshot.py +0 -0
  249. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/ayu-mirage.toml +0 -0
  250. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/catppuccin-latte.toml +0 -0
  251. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/catppuccin-mocha.toml +0 -0
  252. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/dracula.toml +0 -0
  253. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/github-dimmed.toml +0 -0
  254. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/github-light.toml +0 -0
  255. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/gruvbox-dark.toml +0 -0
  256. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/gruvbox-light.toml +0 -0
  257. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/kanagawa.toml +0 -0
  258. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/nord.toml +0 -0
  259. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/one-dark.toml +0 -0
  260. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/rose-pine-dawn.toml +0 -0
  261. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/rose-pine.toml +0 -0
  262. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/solarized-dark.toml +0 -0
  263. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/solarized-light.toml +0 -0
  264. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/tokyo-night.toml +0 -0
  265. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/__init__.py +0 -0
  266. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/collapsible.py +0 -0
  267. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/completion.py +0 -0
  268. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/console.py +0 -0
  269. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/dialog.py +0 -0
  270. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/diff.py +0 -0
  271. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/list.py +0 -0
  272. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/list_page.py +0 -0
  273. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/markdown.py +0 -0
  274. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/progress.py +0 -0
  275. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/rich_text.py +0 -0
  276. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/status.py +0 -0
  277. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/table.py +0 -0
  278. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/tasks.py +0 -0
  279. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/text.py +0 -0
  280. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/toast.py +0 -0
  281. {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/update.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: zettcode
3
- Version: 0.1.9
3
+ Version: 0.1.10
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
@@ -32,7 +32,7 @@ Requires-Dist: truststore<1,>=0.10
32
32
  Requires-Dist: uvicorn<1,>=0.35
33
33
  Requires-Dist: wcwidth<1,>=0.2
34
34
  Requires-Dist: websockets<17,>=15
35
- Requires-Dist: zett-agent<0.2,>=0.1.17
35
+ Requires-Dist: zett-agent<0.3,>=0.2.0
36
36
  Description-Content-Type: text/markdown
37
37
 
38
38
  # ZettCode
@@ -53,7 +53,7 @@ dependencies = [
53
53
  # than it did. An exact pin would make zettcode fight every other tool that
54
54
  # shares the dependency — including an upgrade of zett-agent itself, which pip
55
55
  # would downgrade to satisfy it.
56
- "zett-agent>=0.1.17,<0.2",
56
+ "zett-agent>=0.2.0,<0.3",
57
57
  ]
58
58
 
59
59
  [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.9"
10
+ __version__ = "0.1.10"
11
11
 
12
12
  #: Where each public name lives; read by :func:`__getattr__` on first use.
13
13
  _EXPORTS = {
@@ -14,6 +14,7 @@ from zettcode.config_edit import (
14
14
  remove_model,
15
15
  replace_model,
16
16
  set_pet,
17
+ set_pet_position,
17
18
  set_self_compaction,
18
19
  set_statusline,
19
20
  set_theme,
@@ -208,6 +209,29 @@ class SettingsMixin(AgentBase):
208
209
  self.runtime.config = replace(self.runtime.config, pet=name)
209
210
  return self.config_file
210
211
 
212
+ def remember_pet_position(self, across: float, down: float) -> Path:
213
+ """Write ``[ui] pet_position`` into the config file, so the next start puts the pet back where it was left.
214
+
215
+ The file is global to the reader (``~/.zettcode/config.toml``), not to a project: a pet is the reader's
216
+ companion, and it does not move house when they open another folder. Only that line changes, and a
217
+ config file that does not exist is not created for it.
218
+
219
+ Returns:
220
+ The file written.
221
+
222
+ Raises:
223
+ ConfigEditError: When there is no config file, it cannot be read or edited safely, or it cannot be
224
+ written. Nothing is written then.
225
+ """
226
+ text = set_pet_position(self._read_config_file(), across, down, where=self.config_file)
227
+ try:
228
+ write_text_atomically(self.config_file, text)
229
+ except OSError as error:
230
+ raise ConfigEditError(f"{self.config_file} could not be written: {error}") from error
231
+ position = (round(min(1.0, max(0.0, across)), 3), round(min(1.0, max(0.0, down)), 3))
232
+ self.runtime.config = replace(self.runtime.config, pet_position=position)
233
+ return self.config_file
234
+
211
235
  @property
212
236
  def statusline_hidden(self) -> frozenset[str]:
213
237
  """Return the names of the header and status-line parts the reader switched off."""
@@ -322,6 +346,15 @@ class SettingsMixin(AgentBase):
322
346
  """Return the permission levels, strictest first."""
323
347
  return tuple(level.value for level in PERMISSION_LEVELS)
324
348
 
325
- def use_permission(self, name: str | Permission) -> Permission:
326
- """Set how much of the shell may run without asking, for the rest of this run."""
327
- return self.runtime.use_permission(name)
349
+ def use_permission(self, name: str | Permission, *, remember: bool = True) -> Permission:
350
+ """Set how much of the shell may run without asking, and by default remember it for this session."""
351
+ return self.runtime.use_permission(name, remember=remember)
352
+
353
+ @property
354
+ def harness(self) -> bool:
355
+ """Return whether the just-in-time harness is on for the next request."""
356
+ return self.runtime.harness.on
357
+
358
+ def use_harness(self, on: bool) -> bool:
359
+ """Turn the just-in-time harness on or off for later requests, for the rest of this run."""
360
+ return self.runtime.use_harness(on)
@@ -106,6 +106,8 @@ TOOL_ROLES = {
106
106
  "view_image": "image",
107
107
  "glob": "search",
108
108
  "grep": "search",
109
+ "web_search": "web-search",
110
+ "ask_user": "ask-user",
109
111
  "write_file": "write",
110
112
  "edit": "write",
111
113
  "replace_in_file": "write", # what a session stored before the rename called it
@@ -120,8 +122,12 @@ TOOL_ROLES = {
120
122
 
121
123
 
122
124
  def tool_color(tool: str, theme: Theme) -> str:
123
- """Return the colour one tool paints its rows with."""
124
- return getattr(theme.tools, TOOL_ROLES.get(tool, ""), theme.accent)
125
+ """Return the colour one tool paints its rows with.
126
+
127
+ A role spelled with a hyphen (``web-search``) names the theme field it stands for (``web_search``).
128
+ """
129
+ role = TOOL_ROLES.get(tool, "").replace("-", "_")
130
+ return getattr(theme.tools, role, theme.accent)
125
131
 
126
132
 
127
133
  #: The entry fields that are not part of how a row looks: its identity, and the
@@ -575,7 +581,12 @@ class ToolPresenter(Presenter[T]):
575
581
  }.get(entry.status, STATUS)
576
582
  )
577
583
  hue = tool_color(entry.tool, theme)
578
- colour = theme.error if entry.status is EntryStatus.FAILED else hue
584
+ # A failure is the error colour, and a row that did not happen at all — a skipped tool, a cancelled
585
+ # question — is a caution rather than a result, so it is amber and not the tool's own hue.
586
+ colour = {
587
+ EntryStatus.FAILED: theme.error,
588
+ EntryStatus.SKIPPED: theme.warning,
589
+ }.get(entry.status, hue)
579
590
  marker = Style(foreground=colour)
580
591
  label = Style(foreground=colour)
581
592
  muted = Style(foreground=theme.muted)
@@ -10,11 +10,12 @@ from zett_agent.events import AgentEvent
10
10
  from zett_agent.extensions.shell_approval import SHELL_APPROVAL_EVENT_NAME
11
11
  from zett_agent.messages import ToolMessage
12
12
 
13
- from ..dialogue.ask import AskUserQuestion, question_from
13
+ from ..dialogue.ask import AskUserQuestion, declined_answer, question_from
14
14
  from ..dialogue.relay import Relayed
15
15
  from ..extensions.mcp_notice import MCP_NOTICE_EVENT_NAME, notice_from
16
16
  from ..extensions.usage import USAGE_EVENT_NAME
17
17
  from .entries import EntryStatus
18
+ from .server_tools import ServerToolRun, describe_output
18
19
  from .stops import stop_text
19
20
  from .transcript import Transcript
20
21
 
@@ -82,6 +83,10 @@ class TranscriptProjector(AgentEventDispatcher):
82
83
  self.on_usage = on_usage
83
84
  self.on_steering_started = on_steering_started
84
85
  self.on_steering_interrupted = on_steering_interrupted
86
+ #: What a hosted tool was called with while it streams: a start may carry the whole input, or only its
87
+ #: opening and then fragments. Kept per request and dropped when the call reports back.
88
+ self._server_inputs: dict[str, dict[str, object]] = {}
89
+ self._server_fragments: dict[str, str] = {}
85
90
 
86
91
  def begin_turn(self, prompt: str, *, side: bool = False, relayed: Relayed | None = None) -> None:
87
92
  """Echo the prompt into the transcript before the run starts.
@@ -171,9 +176,19 @@ class TranscriptProjector(AgentEventDispatcher):
171
176
  self.transcript.start_tool(call.id, call.name, call.arguments)
172
177
 
173
178
  async def on_tool_completed_event(self, event: AgentEvent) -> None:
174
- """Attach the returned message to the row of the call it answers."""
179
+ """Attach the returned message to the row of the call it answers.
180
+
181
+ A question the reader cancelled comes back as a result, not a failure: the model reads that it was declined
182
+ and carries on. The row is marked skipped so it does not look like a question that was answered.
183
+ """
175
184
  if isinstance(event.message, ToolMessage):
176
- self.transcript.complete_tool(event.message.tool_call_id, tool_output(event.message))
185
+ output = tool_output(event.message)
186
+ declined = event.message.name == "ask_user" and declined_answer(output)
187
+ self.transcript.complete_tool(
188
+ event.message.tool_call_id,
189
+ output,
190
+ status=EntryStatus.SKIPPED if declined else EntryStatus.COMPLETED,
191
+ )
177
192
 
178
193
  async def on_tool_failed_event(self, event: AgentEvent) -> None:
179
194
  """Fail every call in the batch and record the error text."""
@@ -188,23 +203,56 @@ class TranscriptProjector(AgentEventDispatcher):
188
203
  self.transcript.complete_tool(call.id, output, status=EntryStatus.SKIPPED)
189
204
 
190
205
  async def on_server_tool_started_event(self, event: AgentEvent) -> None:
191
- """Open a row for a tool the server runs on the agent's behalf."""
192
- if event.server_tool_call is not None:
193
- self.transcript.start_tool(event.server_tool_call.id, event.server_tool_call.name, {})
206
+ """Open a row for a tool the server runs on the agent's behalf, with what it was asked."""
207
+ call = event.server_tool_call
208
+ if call is None:
209
+ return
210
+ arguments = dict(call.input or {})
211
+ self._server_inputs[call.id] = arguments
212
+ self._server_fragments.pop(call.id, None)
213
+ self.transcript.start_tool(call.id, call.name, arguments)
214
+
215
+ async def on_server_tool_input_delta_event(self, event: AgentEvent) -> None:
216
+ """Keep the fragments of a hosted call's input; the query a search ran is in there."""
217
+ delta = event.server_tool_input_delta
218
+ if delta is not None:
219
+ self._server_fragments[delta.call_id] = self._server_fragments.get(delta.call_id, "") + delta.delta
220
+
221
+ def _server_arguments(self, call_id: str) -> dict[str, object]:
222
+ """Return what a hosted call turned out to be called with: its opening input and its fragments."""
223
+ arguments = dict(self._server_inputs.pop(call_id, {}))
224
+ fragments = self._server_fragments.pop(call_id, "")
225
+ if fragments:
226
+ try:
227
+ parsed = json.loads(fragments)
228
+ except ValueError:
229
+ parsed = None
230
+ if isinstance(parsed, dict):
231
+ arguments.update(parsed)
232
+ return arguments
194
233
 
195
234
  async def on_server_tool_completed_event(self, event: AgentEvent) -> None:
196
- """Attach a server tool result to its row."""
235
+ """Attach a hosted tool's result to its row: the pages it found, never the provider's payload."""
197
236
  result = event.server_tool_result
198
237
  if result is not None:
199
- self.transcript.complete_tool(result.call_id, serialize(result.output))
238
+ arguments = self._server_arguments(result.call_id)
239
+ if arguments:
240
+ # The row opened before its input had streamed in; now it knows what was searched for.
241
+ self.transcript.restate_tool(result.call_id, arguments)
242
+ run = ServerToolRun(result.call_id, result.name, arguments, output=result.output, error=result.error_code)
243
+ self.transcript.complete_tool(result.call_id, describe_output(run) or serialize(result.output))
200
244
 
201
245
  async def on_server_tool_failed_event(self, event: AgentEvent) -> None:
202
- """Attach a server tool failure, preferring the error code over the payload."""
246
+ """Attach a hosted tool's failure, preferring the error code over the payload."""
203
247
  result = event.server_tool_result
204
248
  if result is not None:
249
+ arguments = self._server_arguments(result.call_id)
250
+ if arguments:
251
+ self.transcript.restate_tool(result.call_id, arguments)
252
+ run = ServerToolRun(result.call_id, result.name, arguments, output=result.output, error=result.error_code)
205
253
  self.transcript.complete_tool(
206
254
  result.call_id,
207
- result.error_code or serialize(result.output),
255
+ describe_output(run) or serialize(result.output),
208
256
  status=EntryStatus.FAILED,
209
257
  )
210
258
 
@@ -34,7 +34,7 @@ from zettcode.tui import ELLIPSIS, SEPARATOR
34
34
  from zettcode.tui.render import GENERIC, language_for
35
35
 
36
36
  from .entries import DeleteEntry, EditEntry, MessageEntry, ReadEntry, ToolEntry, WriteEntry
37
- from .text import limit_output
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.
40
40
  TOOL = "tool"
@@ -386,8 +386,8 @@ class WriteTool(ToolHandler):
386
386
  names = frozenset({"write_file"})
387
387
 
388
388
  def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
389
- """Show the file being written."""
390
- return ToolRow(f"Wrote {argument(arguments, 'path', '(unknown file)')}")
389
+ """Show the file being written: the row is the call, so it says what the call does, not what it did."""
390
+ return ToolRow(f"Write {argument(arguments, 'path', '(unknown file)')}")
391
391
 
392
392
  def open_tool(
393
393
  self, name: str, arguments: Mapping[str, object], *, id: int, call_id: str, started_at: float
@@ -497,6 +497,60 @@ class SearchTool(ToolHandler):
497
497
  return ToolRow(f"Searched {pattern or 'the workspace'}" + (f" in {where}" if where else ""))
498
498
 
499
499
 
500
+ class WebSearchTool(ToolHandler):
501
+ """``web_search``: a search the endpoint ran, so the row says what was searched for."""
502
+
503
+ names = frozenset({"web_search"})
504
+
505
+ def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
506
+ """Name the query; the call is the endpoint's, so nothing else of it is ours to show."""
507
+ query = argument(arguments, "query")
508
+ return ToolRow(f'Searched the web for "{query}"' if query else "Searched the web")
509
+
510
+
511
+ class AskUserTool(ToolHandler):
512
+ """``ask_user``: the one row that waited for a person, so it is named as a thing, not as a verb.
513
+
514
+ The body is what the person did, not the protocol: the tool answers with the payload the shell sent, and that
515
+ JSON is neither readable nor useful in a transcript.
516
+ """
517
+
518
+ names = frozenset({"ask_user"})
519
+
520
+ def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
521
+ """Show the question that was put, under the name the shell gives that tool."""
522
+ question = argument(arguments, "question")
523
+ return ToolRow(f"Ask-User: {question}" if question else "Ask-User")
524
+
525
+ def body(self, name: str, output: str) -> str:
526
+ """Say what the person did: declined, answered, or answered with a picture.
527
+
528
+ Anything that is not the answer payload is left exactly as it came, so a failure or another tool's result
529
+ is never rewritten by this row.
530
+ """
531
+ payload = _ask_payload(output)
532
+ if payload is None:
533
+ return output
534
+ if payload.get("declined") is True:
535
+ return DECLINED_NOTE
536
+ answer = payload.get("answer")
537
+ if isinstance(answer, str) and answer.strip():
538
+ return f"You answered: {answer.strip()}"
539
+ if isinstance(payload.get("parts"), list) and payload["parts"]:
540
+ return "You answered with a picture."
541
+ return output
542
+
543
+
544
+ def _ask_payload(output: str) -> Mapping[str, object] | None:
545
+ """Return the payload of an ``ask_user`` result, or ``None`` when the text is not one."""
546
+ try:
547
+ result = json.loads(output)
548
+ except ValueError:
549
+ return None
550
+ payload = result.get("payload") if isinstance(result, Mapping) else None
551
+ return payload if isinstance(payload, Mapping) else None
552
+
553
+
500
554
  class TodoTool(ToolHandler):
501
555
  """``todo_write``: a plan, so the number of steps is the story."""
502
556
 
@@ -645,6 +699,8 @@ DEFAULT_RENDERERS = Renderers(
645
699
  DeleteTool(),
646
700
  ShellTool(),
647
701
  SearchTool(),
702
+ WebSearchTool(),
703
+ AskUserTool(),
648
704
  TodoTool(),
649
705
  SkillTool(),
650
706
  TaskTool(),
@@ -19,6 +19,7 @@ from zett_agent.messages import AnyMessage, AssistantMessage, SystemMessage, Too
19
19
 
20
20
  from zettcode.config import DEFAULT_TRANSCRIPT_MAX_ENTRIES
21
21
 
22
+ from ..dialogue.ask import declined_answer
22
23
  from ..dialogue.relay import relay_of
23
24
  from ..dialogue.side import is_side_line
24
25
  from ..extensions.usage import UsageSnapshot
@@ -26,6 +27,7 @@ from ..storage import MessageLine, Session
26
27
  from .entries import EntryStatus
27
28
  from .presenters import DEFAULT_PRESENTERS, Presenters
28
29
  from .rendering import DEFAULT_RENDERERS, Renderers
30
+ from .server_tools import ServerToolRun, answer_after_tools, describe_output, runs_in_blocks
29
31
  from .stops import stop_text
30
32
  from .transcript import Transcript
31
33
 
@@ -75,7 +77,13 @@ def entries_added_by(message: AnyMessage) -> int:
75
77
  return 1
76
78
  case AssistantMessage():
77
79
  stopped = message.stop is not None and not message.stop.ok
78
- return bool(message.reasoning) + bool(message.content) + len(message.tool_calls) + stopped
80
+ return (
81
+ bool(message.reasoning)
82
+ + bool(message.content)
83
+ + len(message.tool_calls)
84
+ + len(runs_in_blocks(message.replay_blocks))
85
+ + stopped
86
+ )
79
87
  case _:
80
88
  return 0
81
89
 
@@ -152,7 +160,26 @@ def replay(
152
160
  case AssistantMessage():
153
161
  if message.reasoning:
154
162
  transcript.restore_thinking(message.reasoning, line.timing.reasoning_duration_ns)
155
- if message.content:
163
+ runs = runs_in_blocks(message.replay_blocks)
164
+
165
+ def hosted(rows: Sequence[ServerToolRun] = runs) -> None:
166
+ """Add the rows of the tools the endpoint ran, each closed by its own result."""
167
+ for run in rows:
168
+ transcript.start_tool(run.call_id, run.name, run.arguments)
169
+ transcript.complete_tool(
170
+ run.call_id,
171
+ describe_output(run),
172
+ status=EntryStatus.FAILED if run.error else EntryStatus.COMPLETED,
173
+ wait=False,
174
+ )
175
+
176
+ # A hosted tool has no message of its own: its call and result are both in the answer's blocks, kept
177
+ # because the provider needs them back. Where the answer's text sits in those blocks is where the row
178
+ # goes relative to it, so a reopened session reads in the order the run did.
179
+ if not answer_after_tools(message.replay_blocks) and message.content:
180
+ transcript.append_answer(message.content)
181
+ hosted()
182
+ if answer_after_tools(message.replay_blocks) and message.content:
156
183
  transcript.append_answer(message.content)
157
184
  for call in message.tool_calls:
158
185
  transcript.start_tool(call.id, call.name, call.arguments)
@@ -160,6 +187,9 @@ def replay(
160
187
  transcript.error(stop_text(message.stop))
161
188
  case ToolMessage():
162
189
  status = EntryStatus.COMPLETED if message.success else EntryStatus.FAILED
190
+ if message.name == "ask_user" and declined_answer(message.text):
191
+ # A cancelled question is an answer of its own, not a failure.
192
+ status = EntryStatus.SKIPPED
163
193
  transcript.complete_tool(message.tool_call_id, message.text, status=status, wait=False)
164
194
  case _:
165
195
  continue
@@ -0,0 +1,157 @@
1
+ """What a provider-run tool left behind: the rows for a hosted search, live and replayed.
2
+
3
+ A hosted tool is not called by the agent: the endpoint runs it, and the framework reports the call and its result as
4
+ events. Those events are not messages, so the store never sees them; but the provider's own blocks are kept as the
5
+ answer's opaque ``replay_blocks``, which the store writes and reads back. This module is the one place that turns
6
+ either of those into the words of a row, so a resumed session shows what the live one showed.
7
+
8
+ The result of a search is a list of pages, each with a title, a URL and an opaque encrypted payload that must never be
9
+ put in front of the reader or sent back in a prompt as text: only the title and the URL are shown here.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Iterable, Mapping
15
+ from dataclasses import dataclass
16
+ from typing import Any
17
+
18
+ #: The most pages one row lists. A row is a note that a search happened, not the search's own page.
19
+ MAX_RESULTS = 5
20
+
21
+ #: Result blocks whose payload is the provider's own and is never shown as text.
22
+ _SEARCH_RESULT_TYPES = frozenset({"web_search_result", "search_result"})
23
+
24
+ #: Block types that record a hosted call rather than text.
25
+ _CALL_TYPES = frozenset({"server_tool_use"})
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class ServerToolRun:
30
+ """One hosted tool call as the transcript needs it.
31
+
32
+ Attributes:
33
+ call_id: The provider's identifier for the call.
34
+ name: The tool's name, such as ``web_search``.
35
+ arguments: What it was asked, when the provider said; ``{"query": ...}`` for a search.
36
+ output: What it returned, as the provider reported it.
37
+ error: The provider's error code when the tool failed.
38
+ """
39
+
40
+ call_id: str
41
+ name: str
42
+ arguments: Mapping[str, Any]
43
+ output: Any = None
44
+ error: str | None = None
45
+
46
+
47
+ def _results_in(output: Any) -> list[Mapping[str, Any]]:
48
+ """Return the page blocks of a search result, wherever the provider put them.
49
+
50
+ The adapter hands a hosted tool's output over as the block's own ``content``: a list of pages for a search, with
51
+ the provider's encrypted payload beside each title and URL.
52
+ """
53
+ match output:
54
+ case bool() | str() | int() | float() | None:
55
+ return []
56
+ case Mapping():
57
+ content = output.get("content")
58
+ if isinstance(content, Iterable) and not isinstance(content, str):
59
+ return [item for item in content if isinstance(item, Mapping)]
60
+ if output.get("type") in _SEARCH_RESULT_TYPES:
61
+ return [output]
62
+ return []
63
+ case Iterable():
64
+ return [item for item in output if isinstance(item, Mapping)]
65
+ case _:
66
+ return []
67
+
68
+
69
+ def describe_search(output: Any) -> str:
70
+ """Return a search result as the few lines a row shows: how many pages, and which.
71
+
72
+ Only a page's title and URL are shown; the encrypted payload the provider returns beside them is its own business
73
+ and is neither printed nor kept.
74
+ """
75
+ pages = _results_in(output)
76
+ if not pages:
77
+ return "No pages"
78
+ lines = [f"{len(pages)} page{'s' if len(pages) != 1 else ''}"]
79
+ for page in pages[:MAX_RESULTS]:
80
+ title = page.get("title")
81
+ url = page.get("url")
82
+ named = title.strip() if isinstance(title, str) and title.strip() else ""
83
+ address = url.strip() if isinstance(url, str) and url.strip() else ""
84
+ lines.append(f"- {named} \u2014 {address}" if named and address else f"- {named or address}")
85
+ if len(pages) > MAX_RESULTS:
86
+ lines.append(f"\u2026 and {len(pages) - MAX_RESULTS} more")
87
+ return "\n".join(lines)
88
+
89
+
90
+ def describe_output(run: ServerToolRun) -> str:
91
+ """Return what one hosted tool said, in the words of its own kind."""
92
+ if run.error:
93
+ return run.error
94
+ if run.name == "web_search" or _results_in(run.output):
95
+ return describe_search(run.output)
96
+ # Another provider's hosted tool: its payload is already text or a plain structure.
97
+ return run.output if isinstance(run.output, str) else ""
98
+
99
+
100
+ def runs_in_blocks(blocks: Iterable[Mapping[str, Any]]) -> list[ServerToolRun]:
101
+ """Return the hosted tool calls one answer's ``replay_blocks`` record, results paired with their calls.
102
+
103
+ A block that is not a hosted call is skipped: a thinking block, or the answer's text. A call whose result is not
104
+ in the same answer keeps an empty output, which reads as a search that said nothing.
105
+ """
106
+ calls: dict[str, ServerToolRun] = {}
107
+ order: list[str] = []
108
+ for block in blocks:
109
+ match block.get("type"):
110
+ case "server_tool_use":
111
+ call_id = block.get("id")
112
+ name = block.get("name")
113
+ if not isinstance(call_id, str) or not isinstance(name, str):
114
+ continue
115
+ call_input = block.get("input")
116
+ arguments = dict(call_input) if isinstance(call_input, Mapping) else {}
117
+ calls[call_id] = ServerToolRun(call_id, name, arguments)
118
+ order.append(call_id)
119
+ case "web_search_tool_result" | "server_tool_result" | "web_fetch_tool_result":
120
+ found = block.get("tool_use_id") or block.get("call_id")
121
+ if not isinstance(found, str) or found not in calls:
122
+ continue
123
+ error = block.get("error_code")
124
+ calls[found] = ServerToolRun(
125
+ found,
126
+ calls[found].name,
127
+ calls[found].arguments,
128
+ output=block,
129
+ error=str(error) if isinstance(error, str) and error else None,
130
+ )
131
+ return [calls[call_id] for call_id in order]
132
+
133
+
134
+ def answer_after_tools(blocks: Iterable[Mapping[str, Any]]) -> bool:
135
+ """Return whether the answer's text came after its hosted tool, as the provider streamed them.
136
+
137
+ Anthropic runs a hosted search before it writes the answer, so the row belongs above it; a provider that writes
138
+ first and searches after (DeepSeek does, when it announces the search) has it below. The blocks record which.
139
+ """
140
+ first_tool: int | None = None
141
+ for index, block in enumerate(blocks):
142
+ kind = block.get("type")
143
+ if first_tool is None and kind in _CALL_TYPES:
144
+ first_tool = index
145
+ elif kind == "text" and isinstance(block.get("text"), str) and block["text"].strip():
146
+ return first_tool is not None
147
+ return False
148
+
149
+
150
+ __all__ = [
151
+ "MAX_RESULTS",
152
+ "ServerToolRun",
153
+ "answer_after_tools",
154
+ "describe_output",
155
+ "describe_search",
156
+ "runs_in_blocks",
157
+ ]
@@ -45,6 +45,9 @@ PROCESSING = "Processing"
45
45
  #: reserved for it.
46
46
  STOP_HINT = "^C/Esc to interrupt"
47
47
 
48
+ #: What the row of a question the reader cancelled says under its title.
49
+ DECLINED_NOTE = "You declined; the model was told and carries on."
50
+
48
51
  # Matches both escape families a tool can smuggle into its output: CSI
49
52
  # (``ESC [`` parameters and a final byte) and OSC (``ESC ]`` up to BEL or ST).
50
53
  # Stripping them keeps a coloured compiler message from repainting the canvas.
@@ -15,6 +15,8 @@ from __future__ import annotations
15
15
  import random
16
16
  from collections.abc import Callable, MutableSequence, Sequence
17
17
 
18
+ from zettcode.tui import OPTION
19
+
18
20
  #: What the reader may be told, in the words they will see.
19
21
  TIPS: tuple[str, ...] = (
20
22
  "Type while it works: your message can steer this request, or wait and follow it.",
@@ -35,6 +37,7 @@ TIPS: tuple[str, ...] = (
35
37
  "Ctrl-R searches what you have sent before; Up recalls the last message.",
36
38
  "Ctrl-V attaches an image from the clipboard, for a model that takes images.",
37
39
  "Page Up scrolls the conversation, and Esc jumps back to the newest line.",
40
+ f"{OPTION.capitalize()}-\u2191/\u2193 (or Ctrl-\u2191/\u2193) jumps to the previous or next message you sent.",
38
41
  "Drag in the conversation to copy text. Hold Shift or Alt to use your terminal's own selection.",
39
42
  "Put project instructions in AGENTS.md and they are given to the model with every request.",
40
43
  "A SKILL.md in ~/.zettcode/skills teaches ZettCode a job it can repeat.",
@@ -44,7 +47,7 @@ TIPS: tuple[str, ...] = (
44
47
  "/self-compact lets the model decide for itself when to compact this session.",
45
48
  "zettcode resume <id> opens a stored session; zettcode update installs the newest release.",
46
49
  "zettcode doctor checks every configured model from a shell, with no session started.",
47
- "zettcode --auto runs shell commands without asking for that run; /permission can change it later.",
50
+ "zettcode --full skips every shell question and the sandbox for that run; /permission can change it later.",
48
51
  "Press Esc to stop a request; /continue picks it up again where it left off.",
49
52
  "A plugin is a Python package: install it and ZettCode gains its commands, tools, themes or pets.",
50
53
  "These tips can be turned off: tips = false under [transcript] in the configuration.",
@@ -128,6 +128,24 @@ class StreamMixin(TurnMixin):
128
128
  # tool's business and this opens every one the same way.
129
129
  self._add(self.renderers.open_tool(name, arguments, id=self._next_id, call_id=call_id, started_at=self.clock()))
130
130
 
131
+ def restate_tool(self, call_id: str, arguments: Mapping[str, object]) -> None:
132
+ """Phrase a row again with arguments that arrived after it opened.
133
+
134
+ A hosted tool's input may stream in fragments after the call started, so the row opens knowing only the tool's
135
+ name. This is where the query it really ran appears, before the row is closed.
136
+
137
+ Args:
138
+ call_id: Identifier the matching result will carry.
139
+ arguments: What the call turned out to be, which the chain phrases.
140
+ """
141
+ entry = next(
142
+ (item for item in reversed(self.entries) if isinstance(item, ToolEntry) and item.call_id == call_id), None
143
+ )
144
+ if entry is None:
145
+ return
146
+ entry.title = self.renderers.describe(entry.tool, arguments).title
147
+ self._changed(entry)
148
+
131
149
  def complete_tool(
132
150
  self, call_id: str, output: str, *, status: EntryStatus = EntryStatus.COMPLETED, wait: bool = True
133
151
  ) -> None:
@@ -8,6 +8,7 @@ panel never reads a raw payload and the agent never invents one.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import json
11
12
  from dataclasses import dataclass
12
13
  from typing import Any
13
14
 
@@ -19,6 +20,7 @@ __all__ = [
19
20
  "AskUserQuestion",
20
21
  "answer_payload",
21
22
  "decline_payload",
23
+ "declined_answer",
22
24
  "question_from",
23
25
  ]
24
26
 
@@ -94,3 +96,18 @@ def decline_payload(question: AskUserQuestion) -> dict[str, Any]:
94
96
  "declined": True,
95
97
  "reason": "the user cancelled the question",
96
98
  }
99
+
100
+
101
+ def declined_answer(text: str) -> bool:
102
+ """Return whether a tool result is the one a cancelled question leaves behind.
103
+
104
+ The tool answers with the payload the shell sent, serialized as JSON. A cancelled question carries
105
+ ``"declined": true`` and no answer; a row that has one is not a row that failed, it is a question nobody
106
+ needed to answer, and the transcript says so.
107
+ """
108
+ try:
109
+ result = json.loads(text)
110
+ except ValueError:
111
+ return False
112
+ payload = result.get("payload") if isinstance(result, dict) else None
113
+ return isinstance(payload, dict) and payload.get("declined") is True