zettcode 0.1.8__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 (285) hide show
  1. {zettcode-0.1.8 → zettcode-0.1.10}/.gitignore +3 -0
  2. zettcode-0.1.10/PKG-INFO +60 -0
  3. zettcode-0.1.10/README.md +23 -0
  4. {zettcode-0.1.8 → zettcode-0.1.10}/pyproject.toml +3 -48
  5. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/__init__.py +1 -1
  6. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/_compat.py +1 -1
  7. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/agent/settings.py +36 -3
  8. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/presenters.py +14 -3
  9. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/projection.py +58 -10
  10. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/rendering.py +59 -3
  11. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/replay.py +32 -2
  12. zettcode-0.1.10/src/zettcode/app/agent/conversation/server_tools.py +157 -0
  13. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/text.py +3 -0
  14. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/tips.py +4 -1
  15. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/stream.py +18 -0
  16. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/ask.py +17 -0
  17. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/side.py +11 -2
  18. zettcode-0.1.10/src/zettcode/app/agent/extensions/capabilities.py +68 -0
  19. zettcode-0.1.10/src/zettcode/app/agent/extensions/files.py +101 -0
  20. zettcode-0.1.10/src/zettcode/app/agent/extensions/harness.py +89 -0
  21. zettcode-0.1.10/src/zettcode/app/agent/extensions/harness_switch.py +18 -0
  22. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/mcp.py +14 -6
  23. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/mcp_notice.py +15 -3
  24. zettcode-0.1.10/src/zettcode/app/agent/extensions/relay.py +93 -0
  25. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/subagents.py +96 -22
  26. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/tool_output.py +8 -7
  27. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/permission/__init__.py +47 -15
  28. zettcode-0.1.10/src/zettcode/app/agent/permission/sandbox.py +124 -0
  29. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/runtime.py +97 -17
  30. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/storage/metadata.py +7 -2
  31. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/storage/records.py +3 -1
  32. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/storage/store.py +47 -0
  33. zettcode-0.1.10/src/zettcode/app/agent/tools/__init__.py +7 -0
  34. zettcode-0.1.10/src/zettcode/app/agent/tools/boundary.py +51 -0
  35. zettcode-0.1.10/src/zettcode/app/agent/tools/files.py +267 -0
  36. zettcode-0.1.10/src/zettcode/app/agent/tools/images.py +32 -0
  37. zettcode-0.1.10/src/zettcode/app/agent/tools/output.py +74 -0
  38. zettcode-0.1.10/src/zettcode/app/agent/tools/shell.py +171 -0
  39. zettcode-0.1.10/src/zettcode/app/agent/tools/workspace.py +268 -0
  40. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/app.py +38 -3
  41. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/bars.py +1 -1
  42. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/config_form.py +63 -8
  43. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/keys.py +28 -0
  44. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/notices.py +3 -3
  45. zettcode-0.1.10/src/zettcode/app/ui/providers.py +614 -0
  46. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/settings.py +5 -3
  47. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/shell.py +1 -1
  48. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/__init__.py +14 -0
  49. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/approval_page.py +9 -9
  50. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/completer.py +3 -4
  51. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/follow_up.py +4 -6
  52. zettcode-0.1.10/src/zettcode/app/ui/widgets/form_page.py +380 -0
  53. zettcode-0.1.10/src/zettcode/app/ui/widgets/model_settings_page.py +654 -0
  54. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/pet.py +44 -19
  55. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/statusline_page.py +31 -18
  56. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/transcript.py +47 -1
  57. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/usage_page.py +25 -7
  58. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/cli.py +150 -20
  59. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/config.py +183 -11
  60. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/config_edit.py +27 -1
  61. zettcode-0.1.10/src/zettcode/oneshot.py +200 -0
  62. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/paths.py +4 -0
  63. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/builtin_agent.py +92 -3
  64. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/builtin_commands.py +46 -5
  65. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/services.py +1 -0
  66. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/state.py +2 -2
  67. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/providers.py +6 -3
  68. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/sdk/__init__.py +38 -1
  69. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/sdk/_translate.py +89 -5
  70. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/sdk/client.py +161 -4
  71. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/sdk/events.py +3 -0
  72. zettcode-0.1.10/src/zettcode/sdk/models.py +148 -0
  73. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/sdk/session.py +92 -22
  74. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/sdk/types.py +188 -3
  75. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/__init__.py +3 -1
  76. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/capabilities.py +6 -0
  77. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/theme.py +8 -0
  78. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/render/renderer.py +45 -7
  79. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/theme_file.py +17 -7
  80. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/textarea.py +1 -1
  81. zettcode-0.1.8/LICENSE +0 -21
  82. zettcode-0.1.8/PKG-INFO +0 -149
  83. zettcode-0.1.8/README.md +0 -112
  84. zettcode-0.1.8/README.zh-CN.md +0 -106
  85. zettcode-0.1.8/src/zettcode/app/agent/extensions/capabilities.py +0 -50
  86. zettcode-0.1.8/src/zettcode/app/ui/providers.py +0 -567
  87. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/__init__.py +0 -0
  88. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/__init__.py +0 -0
  89. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/agent/__init__.py +0 -0
  90. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/agent/base.py +0 -0
  91. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/agent/commands.py +0 -0
  92. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/agent/context.py +0 -0
  93. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/agent/exports.py +0 -0
  94. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/agent/interaction.py +0 -0
  95. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/agent/prompt.py +0 -0
  96. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/agent/sessions.py +0 -0
  97. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/agent/turns.py +0 -0
  98. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/__init__.py +0 -0
  99. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/changes.py +0 -0
  100. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/effort_gauge.py +0 -0
  101. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/entries.py +0 -0
  102. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/pulse.py +0 -0
  103. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/shimmer.py +0 -0
  104. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/stops.py +0 -0
  105. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
  106. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/fold.py +0 -0
  107. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
  108. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
  109. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/turn.py +0 -0
  110. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/__init__.py +0 -0
  111. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/mentions.py +0 -0
  112. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/relay.py +0 -0
  113. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/title.py +0 -0
  114. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/errors.py +0 -0
  115. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/export/__init__.py +0 -0
  116. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/export/trace/script.js +0 -0
  117. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/export/trace/style.css +0 -0
  118. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/export/trace/template.html +0 -0
  119. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/__init__.py +0 -0
  120. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/compaction.py +0 -0
  121. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/context.py +0 -0
  122. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/messaging.py +0 -0
  123. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/messenger.py +0 -0
  124. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/usage.py +0 -0
  125. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/permission/approval.py +0 -0
  126. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/permission/readonly.py +0 -0
  127. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/storage/__init__.py +0 -0
  128. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/storage/layout.py +0 -0
  129. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/storage/ledger.py +0 -0
  130. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/agent/storage/locks.py +0 -0
  131. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/brand.py +0 -0
  132. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/commandline/__init__.py +0 -0
  133. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/commandline/arguments.py +0 -0
  134. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/commandline/completion.py +0 -0
  135. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/commandline/history.py +0 -0
  136. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/commandline/runner.py +0 -0
  137. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/commands.py +0 -0
  138. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/__init__.py +0 -0
  139. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/cat.py +0 -0
  140. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/chibi.py +0 -0
  141. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/dog.py +0 -0
  142. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/kangaroo.py +0 -0
  143. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/model.py +0 -0
  144. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/penguin.py +0 -0
  145. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/polarbear.py +0 -0
  146. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/scarfy.py +0 -0
  147. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/seagull.py +0 -0
  148. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/slime.py +0 -0
  149. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/snowbear.py +0 -0
  150. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/sprout.py +0 -0
  151. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/tiger.py +0 -0
  152. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pets/whale.py +0 -0
  153. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/pipeline.py +0 -0
  154. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/registry.py +0 -0
  155. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/__init__.py +0 -0
  156. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/boot.py +0 -0
  157. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/clipboard.py +0 -0
  158. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/commands.py +0 -0
  159. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/demo.py +0 -0
  160. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/followups.py +0 -0
  161. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/gc_policy.py +0 -0
  162. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/labels.py +0 -0
  163. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/messaging.py +0 -0
  164. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/sessions.py +0 -0
  165. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/updating.py +0 -0
  166. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
  167. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/bash_page.py +0 -0
  168. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/composer.py +0 -0
  169. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/context_charts.py +0 -0
  170. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/context_page.py +0 -0
  171. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/effort_page.py +0 -0
  172. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/input_page.py +0 -0
  173. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/model_page.py +0 -0
  174. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/panel.py +0 -0
  175. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/pet_page.py +0 -0
  176. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/root.py +0 -0
  177. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/self_compact_page.py +0 -0
  178. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
  179. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
  180. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/steering.py +0 -0
  181. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/update_page.py +0 -0
  182. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/usage_charts.py +0 -0
  183. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/welcome.py +0 -0
  184. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/__init__.py +0 -0
  185. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/__main__.py +0 -0
  186. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/client.py +0 -0
  187. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/files.py +0 -0
  188. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/finder.py +0 -0
  189. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/hub.py +0 -0
  190. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/log.py +0 -0
  191. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/models.py +0 -0
  192. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/process.py +0 -0
  193. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/protocol.py +0 -0
  194. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/roster.py +0 -0
  195. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/runner.py +0 -0
  196. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/server.py +0 -0
  197. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/text.py +0 -0
  198. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/broker/web.py +0 -0
  199. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/collector.py +0 -0
  200. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/doctor.py +0 -0
  201. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/filelock.py +0 -0
  202. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/__init__.py +0 -0
  203. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/builtins.py +0 -0
  204. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/container.py +0 -0
  205. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/extension.py +0 -0
  206. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/loader.py +0 -0
  207. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/mixins.py +0 -0
  208. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/plugins/plugin.py +0 -0
  209. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/py.typed +0 -0
  210. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/sdk/errors.py +0 -0
  211. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/sdk/handlers.py +0 -0
  212. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/statusline.py +0 -0
  213. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/clipboard.py +0 -0
  214. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/console.py +0 -0
  215. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/__init__.py +0 -0
  216. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/app.py +0 -0
  217. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/events.py +0 -0
  218. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/focus.py +0 -0
  219. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/geometry.py +0 -0
  220. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/host.py +0 -0
  221. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/keymap.py +0 -0
  222. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/scheduler.py +0 -0
  223. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/screen.py +0 -0
  224. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/core/widget.py +0 -0
  225. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/gallery.py +0 -0
  226. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/glyphs.py +0 -0
  227. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/input/__init__.py +0 -0
  228. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/input/bridge.py +0 -0
  229. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/input/decoder.py +0 -0
  230. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/input/events.py +0 -0
  231. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/input/keys.py +0 -0
  232. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/input/reader.py +0 -0
  233. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/layout/__init__.py +0 -0
  234. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/layout/box.py +0 -0
  235. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/layout/overlay.py +0 -0
  236. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/layout/scroll.py +0 -0
  237. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/layout/solver.py +0 -0
  238. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/layout/spacing.py +0 -0
  239. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/render/__init__.py +0 -0
  240. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/render/ansi.py +0 -0
  241. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/render/canvas.py +0 -0
  242. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/render/code.py +0 -0
  243. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/render/color.py +0 -0
  244. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/render/rich_text.py +0 -0
  245. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/render/style.py +0 -0
  246. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/render/sweep.py +0 -0
  247. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/render/text.py +0 -0
  248. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/runner.py +0 -0
  249. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/terminal.py +0 -0
  250. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/testing/__init__.py +0 -0
  251. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/testing/harness.py +0 -0
  252. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/testing/snapshot.py +0 -0
  253. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/ayu-mirage.toml +0 -0
  254. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/catppuccin-latte.toml +0 -0
  255. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/catppuccin-mocha.toml +0 -0
  256. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/dracula.toml +0 -0
  257. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/github-dimmed.toml +0 -0
  258. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/github-light.toml +0 -0
  259. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/gruvbox-dark.toml +0 -0
  260. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/gruvbox-light.toml +0 -0
  261. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/kanagawa.toml +0 -0
  262. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/nord.toml +0 -0
  263. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/one-dark.toml +0 -0
  264. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/rose-pine-dawn.toml +0 -0
  265. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/rose-pine.toml +0 -0
  266. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/solarized-dark.toml +0 -0
  267. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/solarized-light.toml +0 -0
  268. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/themes/tokyo-night.toml +0 -0
  269. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/__init__.py +0 -0
  270. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/collapsible.py +0 -0
  271. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/completion.py +0 -0
  272. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/console.py +0 -0
  273. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/dialog.py +0 -0
  274. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/diff.py +0 -0
  275. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/list.py +0 -0
  276. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/list_page.py +0 -0
  277. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/markdown.py +0 -0
  278. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/progress.py +0 -0
  279. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/rich_text.py +0 -0
  280. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/status.py +0 -0
  281. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/table.py +0 -0
  282. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/tasks.py +0 -0
  283. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/text.py +0 -0
  284. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/tui/widgets/toast.py +0 -0
  285. {zettcode-0.1.8 → zettcode-0.1.10}/src/zettcode/update.py +0 -0
@@ -21,6 +21,9 @@ docs/.vitepress/dist/
21
21
  docs/.vitepress/cache/
22
22
  docs/.vitepress/.temp/
23
23
 
24
+ # The TypeScript client is built from its sources; only dist/ is published.
25
+ clients/typescript/dist/
26
+
24
27
  # The palette gallery is generated by `make palettes`: every widget painted in
25
28
  # every palette, written by docs/scripts/palette_gallery.py. Regenerate it
26
29
  # rather than committing a 2 MB copy that goes stale the moment a colour moves.
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.5
2
+ Name: zettcode
3
+ Version: 0.1.10
4
+ Summary: A focused terminal coding agent powered by zett-agent
5
+ Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
6
+ Project-URL: Issues, https://github.com/Chang-LeHung/zettcode/issues
7
+ Author: Chang-LeHung
8
+ License-Expression: MIT
9
+ Keywords: agent,coding-agent,llm,terminal,tui
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 10
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3.15
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: exceptiongroup<2,>=1.2; python_version < '3.11'
24
+ Requires-Dist: httpx<1,>=0.27
25
+ Requires-Dist: pydantic<3,>=2
26
+ Requires-Dist: pyperclip<2,>=1.9
27
+ Requires-Dist: setproctitle<2,>=1.3.7; os_name == 'posix'
28
+ Requires-Dist: starlette<2,>=0.48
29
+ Requires-Dist: tiktoken<1,>=0.12
30
+ Requires-Dist: tomli<3,>=2; python_version < '3.11'
31
+ Requires-Dist: truststore<1,>=0.10
32
+ Requires-Dist: uvicorn<1,>=0.35
33
+ Requires-Dist: wcwidth<1,>=0.2
34
+ Requires-Dist: websockets<17,>=15
35
+ Requires-Dist: zett-agent<0.3,>=0.2.0
36
+ Description-Content-Type: text/markdown
37
+
38
+ # ZettCode
39
+
40
+ A focused terminal coding agent powered by [zett-agent](https://pypi.org/project/zett-agent/): it reads and edits your
41
+ code, runs commands with your permission, and is driven by a model you choose. The repository also ships a Python SDK
42
+ and an HTTP server.
43
+
44
+ ```bash
45
+ pip install zettcode
46
+ zettcode
47
+ ```
48
+
49
+ - [The guide](https://chang-lehung.github.io/zettcode/) — commands, keys, plugins, configuration
50
+ - [The SDK](https://chang-lehung.github.io/zettcode/sdk/) — drive the agent from Python
51
+ - [The server](https://chang-lehung.github.io/zettcode/server/) — drive it over HTTP
52
+
53
+ The full README, with the screenshots, is at the root of the repository.
54
+
55
+ ```python
56
+ import zettcode.sdk as zc
57
+
58
+ result = await zc.prompt("summarize the README", permission="deny")
59
+ print(result.text)
60
+ ```
@@ -0,0 +1,23 @@
1
+ # ZettCode
2
+
3
+ A focused terminal coding agent powered by [zett-agent](https://pypi.org/project/zett-agent/): it reads and edits your
4
+ code, runs commands with your permission, and is driven by a model you choose. The repository also ships a Python SDK
5
+ and an HTTP server.
6
+
7
+ ```bash
8
+ pip install zettcode
9
+ zettcode
10
+ ```
11
+
12
+ - [The guide](https://chang-lehung.github.io/zettcode/) — commands, keys, plugins, configuration
13
+ - [The SDK](https://chang-lehung.github.io/zettcode/sdk/) — drive the agent from Python
14
+ - [The server](https://chang-lehung.github.io/zettcode/server/) — drive it over HTTP
15
+
16
+ The full README, with the screenshots, is at the root of the repository.
17
+
18
+ ```python
19
+ import zettcode.sdk as zc
20
+
21
+ result = await zc.prompt("summarize the README", permission="deny")
22
+ print(result.text)
23
+ ```
@@ -5,7 +5,6 @@ readme = "README.md"
5
5
  requires-python = ">=3.10"
6
6
  dynamic = ["version"]
7
7
  license = "MIT"
8
- license-files = ["LICENSE"]
9
8
  authors = [{name = "Chang-LeHung"}]
10
9
  keywords = ["agent", "llm", "tui", "terminal", "coding-agent"]
11
10
  classifiers = [
@@ -19,6 +18,7 @@ classifiers = [
19
18
  "Programming Language :: Python :: 3.12",
20
19
  "Programming Language :: Python :: 3.13",
21
20
  "Programming Language :: Python :: 3.14",
21
+ "Programming Language :: Python :: 3.15",
22
22
  "Typing :: Typed",
23
23
  ]
24
24
  dependencies = [
@@ -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]
@@ -75,53 +75,8 @@ path = "src/zettcode/__init__.py"
75
75
  packages = ["src/zettcode"]
76
76
 
77
77
  [tool.hatch.build.targets.sdist]
78
- include = ["/src", "/README.md", "/README.zh-CN.md", "/LICENSE", "/pyproject.toml"]
79
-
80
- [dependency-groups]
81
- dev = [
82
- # Unlike the conditional runtime deps, these are installed on every
83
- # interpreter so mypy can check the 3.10 branch of ``_compat`` for real.
84
- "exceptiongroup>=1.2,<2",
85
- "mypy>=1.14,<2",
86
- "pytest>=8,<9",
87
- "pytest-asyncio>=0.26,<2",
88
- "pytest-cov>=7.1.0",
89
- "ruff>=0.11,<1",
90
- "tomli>=2,<3",
91
- "viztracer>=1.1.1",
92
- ]
78
+ include = ["/src", "/README.md", "/pyproject.toml"]
93
79
 
94
80
  [tool.pytest.ini_options]
95
81
  testpaths = ["tests"]
96
82
  asyncio_mode = "auto"
97
-
98
- [tool.mypy]
99
- python_version = "3.10"
100
- files = ["src", "perf"]
101
- # Check the pydantic storage models as models, not as plain classes.
102
- plugins = ["pydantic.mypy"]
103
- warn_unused_ignores = true
104
- warn_redundant_casts = true
105
- no_implicit_optional = true
106
-
107
- [[tool.mypy.overrides]]
108
- # Ships no stubs; the runner only calls the two documented functions.
109
- module = ["pyperclip"]
110
- ignore_missing_imports = true
111
-
112
- [tool.ruff]
113
- line-length = 120
114
- target-version = "py310"
115
- src = ["src", "tests", "perf"]
116
-
117
- [tool.ruff.lint]
118
- select = ["E", "F", "I", "UP", "B"]
119
- ignore = ["E501"]
120
-
121
- [tool.ruff.lint.isort]
122
- known-first-party = ["zettcode"]
123
-
124
- [tool.ruff.format]
125
- quote-style = "double"
126
- indent-style = "space"
127
- line-ending = "lf"
@@ -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.8"
10
+ __version__ = "0.1.10"
11
11
 
12
12
  #: Where each public name lives; read by :func:`__getattr__` on first use.
13
13
  _EXPORTS = {
@@ -1,6 +1,6 @@
1
1
  """Small shims for the language versions this project supports.
2
2
 
3
- ZettCode runs on Python 3.10 through 3.14, so the few things the standard
3
+ ZettCode runs on Python 3.10 through 3.15, so the few things the standard
4
4
  library grew in between live here instead of behind version checks at each use.
5
5
  """
6
6
 
@@ -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