zettcode 0.1.12__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. {zettcode-0.1.12 → zettcode-0.3.0}/.gitignore +4 -0
  2. {zettcode-0.1.12 → zettcode-0.3.0}/PKG-INFO +2 -2
  3. {zettcode-0.1.12 → zettcode-0.3.0}/pyproject.toml +11 -2
  4. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/__init__.py +1 -1
  5. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/agent/commands.py +3 -3
  6. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/entries.py +7 -1
  7. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/presenters.py +12 -1
  8. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/rendering.py +158 -77
  9. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/transcript/stream.py +5 -1
  10. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/commands.py +12 -7
  11. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/model.py +1 -1
  12. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/plugins/__init__.py +24 -22
  13. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/plugins/builtin_commands.py +333 -34
  14. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/plugins/builtins.py +38 -21
  15. zettcode-0.1.12/src/zettcode/app/plugins/container.py → zettcode-0.3.0/src/zettcode/app/plugins/groups.py +48 -31
  16. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/plugins/loader.py +102 -32
  17. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/plugins/mixins.py +1 -1
  18. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/plugins/plugin.py +11 -6
  19. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/app.py +39 -9
  20. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/followups.py +2 -2
  21. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/keys.py +6 -1
  22. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/messaging.py +28 -2
  23. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/notices.py +1 -0
  24. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/sessions.py +18 -7
  25. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/shell.py +9 -5
  26. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/updating.py +57 -3
  27. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/__init__.py +10 -1
  28. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/approval_page.py +11 -2
  29. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/composer.py +20 -1
  30. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/model_settings_page.py +2 -3
  31. zettcode-0.3.0/src/zettcode/app/ui/widgets/plugins_page.py +428 -0
  32. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/self_compact_page.py +9 -54
  33. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/statusline_page.py +3 -33
  34. zettcode-0.3.0/src/zettcode/app/ui/widgets/switch.py +87 -0
  35. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/update_page.py +4 -1
  36. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/client.py +104 -6
  37. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/hub.py +76 -2
  38. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/models.py +5 -0
  39. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/protocol.py +14 -3
  40. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/roster.py +7 -1
  41. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/config.py +30 -1
  42. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/config_edit.py +17 -0
  43. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/daemon/cli.py +76 -0
  44. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/daemon/run.py +27 -5
  45. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/agent/turns.py +7 -5
  46. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/core.py +13 -3
  47. zettcode-0.3.0/src/zettcode/engine/extensions/harness.py +199 -0
  48. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/mcp_notice.py +8 -2
  49. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/subagents.py +4 -3
  50. zettcode-0.3.0/src/zettcode/engine/extensions/truncation.py +156 -0
  51. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/messenger.py +78 -4
  52. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/permission/__init__.py +18 -0
  53. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/permission/sandbox.py +19 -3
  54. zettcode-0.3.0/src/zettcode/engine/plugins/__init__.py +73 -0
  55. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/plugins/builtin_agent.py +144 -91
  56. zettcode-0.3.0/src/zettcode/engine/plugins/commands.py +131 -0
  57. zettcode-0.1.12/src/zettcode/engine/plugins/container.py → zettcode-0.3.0/src/zettcode/engine/plugins/context.py +31 -8
  58. zettcode-0.3.0/src/zettcode/engine/plugins/groups.py +80 -0
  59. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/plugins/loader.py +64 -30
  60. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/plugins/plugin.py +55 -11
  61. zettcode-0.3.0/src/zettcode/engine/plugins/switches.py +412 -0
  62. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/server_tools.py +24 -2
  63. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/storage/__init__.py +1 -1
  64. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/tools/boundary.py +7 -2
  65. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/tools/files.py +42 -47
  66. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/tools/output.py +12 -5
  67. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/tools/shell.py +20 -10
  68. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/tools/workspace.py +80 -77
  69. zettcode-0.3.0/src/zettcode/gateway/__init__.py +22 -0
  70. zettcode-0.3.0/src/zettcode/gateway/app.py +389 -0
  71. zettcode-0.3.0/src/zettcode/gateway/backend.py +291 -0
  72. zettcode-0.3.0/src/zettcode/gateway/chat.py +128 -0
  73. zettcode-0.3.0/src/zettcode/gateway/cli.py +138 -0
  74. zettcode-0.3.0/src/zettcode/gateway/service.py +811 -0
  75. zettcode-0.3.0/src/zettcode/gateway/settings.py +62 -0
  76. zettcode-0.3.0/src/zettcode/gateway/static/app.css +753 -0
  77. zettcode-0.3.0/src/zettcode/gateway/static/app.js +3 -0
  78. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
  79. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
  80. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
  81. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
  82. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
  83. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
  84. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
  85. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
  86. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
  87. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
  88. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
  89. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
  90. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
  91. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
  92. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
  93. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
  94. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
  95. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
  96. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
  97. zettcode-0.3.0/src/zettcode/gateway/static/assets/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
  98. zettcode-0.3.0/src/zettcode/gateway/static/chunks/chunk-Y6SLVHK3.js +1 -0
  99. zettcode-0.3.0/src/zettcode/gateway/static/chunks/render-G6G3TSB3.js +341 -0
  100. zettcode-0.3.0/src/zettcode/gateway/static/icon.png +0 -0
  101. zettcode-0.3.0/src/zettcode/gateway/static/index.html +23 -0
  102. zettcode-0.3.0/src/zettcode/gateway/static/logo.svg +205 -0
  103. zettcode-0.3.0/src/zettcode/gateway/static/manifest.webmanifest +11 -0
  104. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/paths.py +4 -0
  105. zettcode-0.3.0/src/zettcode/plugin_tui.py +104 -0
  106. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/relay.py +3 -0
  107. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/sdk/__init__.py +11 -0
  108. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/sdk/client.py +299 -10
  109. zettcode-0.3.0/src/zettcode/sdk/commands.py +9 -0
  110. zettcode-0.3.0/src/zettcode/sdk/daemons.py +153 -0
  111. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/sdk/errors.py +13 -0
  112. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/sdk/events.py +10 -2
  113. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/sdk/session.py +71 -25
  114. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/sdk/translation.py +25 -2
  115. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/sdk/types.py +184 -0
  116. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/server/app.py +67 -2
  117. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/server/runner.py +41 -0
  118. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/server/schemas.py +90 -1
  119. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/server/serialize.py +7 -1
  120. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/theme.py +15 -7
  121. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/ayu-mirage.toml +1 -1
  122. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/catppuccin-latte.toml +1 -1
  123. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/catppuccin-mocha.toml +1 -1
  124. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/dracula.toml +1 -1
  125. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/github-dimmed.toml +1 -1
  126. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/github-light.toml +1 -1
  127. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/gruvbox-dark.toml +1 -1
  128. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/gruvbox-light.toml +1 -1
  129. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/kanagawa.toml +1 -1
  130. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/nord.toml +1 -1
  131. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/one-dark.toml +1 -1
  132. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/rose-pine-dawn.toml +1 -1
  133. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/rose-pine.toml +1 -1
  134. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/solarized-dark.toml +1 -1
  135. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/solarized-light.toml +1 -1
  136. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/themes/tokyo-night.toml +1 -1
  137. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/update.py +31 -15
  138. zettcode-0.1.12/src/zettcode/engine/extensions/harness.py +0 -89
  139. zettcode-0.1.12/src/zettcode/engine/plugins/__init__.py +0 -64
  140. {zettcode-0.1.12 → zettcode-0.3.0}/README.md +0 -0
  141. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/_compat.py +0 -0
  142. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/__init__.py +0 -0
  143. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/__init__.py +0 -0
  144. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/agent/__init__.py +0 -0
  145. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/agent/settings.py +0 -0
  146. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/__init__.py +0 -0
  147. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/changes.py +0 -0
  148. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/effort_gauge.py +0 -0
  149. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/projection.py +0 -0
  150. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/pulse.py +0 -0
  151. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/replay.py +0 -0
  152. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/shimmer.py +0 -0
  153. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/stops.py +0 -0
  154. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/text.py +0 -0
  155. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/tips.py +0 -0
  156. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
  157. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/transcript/fold.py +0 -0
  158. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
  159. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
  160. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/agent/conversation/transcript/turn.py +0 -0
  161. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/brand.py +0 -0
  162. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/commandline/__init__.py +0 -0
  163. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/commandline/arguments.py +0 -0
  164. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/commandline/completion.py +0 -0
  165. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/commandline/history.py +0 -0
  166. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/commandline/runner.py +0 -0
  167. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/__init__.py +0 -0
  168. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/cat.py +0 -0
  169. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/chibi.py +0 -0
  170. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/dog.py +0 -0
  171. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/kangaroo.py +0 -0
  172. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/penguin.py +0 -0
  173. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/polarbear.py +0 -0
  174. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/scarfy.py +0 -0
  175. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/seagull.py +0 -0
  176. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/slime.py +0 -0
  177. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/snowbear.py +0 -0
  178. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/sprout.py +0 -0
  179. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/tiger.py +0 -0
  180. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/pets/whale.py +0 -0
  181. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/plugins/host.py +0 -0
  182. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/plugins/state.py +0 -0
  183. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/__init__.py +0 -0
  184. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/bars.py +0 -0
  185. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/boot.py +0 -0
  186. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/clipboard.py +0 -0
  187. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/commands.py +0 -0
  188. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/config_form.py +0 -0
  189. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/demo.py +0 -0
  190. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/gc_policy.py +0 -0
  191. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/labels.py +0 -0
  192. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/providers.py +0 -0
  193. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/settings.py +0 -0
  194. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/turns.py +0 -0
  195. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
  196. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/bash_page.py +0 -0
  197. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/completer.py +0 -0
  198. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/context_charts.py +0 -0
  199. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/context_page.py +0 -0
  200. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/effort_page.py +0 -0
  201. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/follow_up.py +0 -0
  202. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/form_page.py +0 -0
  203. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/input_page.py +0 -0
  204. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/model_page.py +0 -0
  205. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/panel.py +0 -0
  206. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/pet.py +0 -0
  207. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/pet_page.py +0 -0
  208. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/root.py +0 -0
  209. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
  210. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
  211. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/steering.py +0 -0
  212. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/transcript.py +0 -0
  213. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/usage_charts.py +0 -0
  214. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/usage_page.py +0 -0
  215. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/app/ui/widgets/welcome.py +0 -0
  216. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/__init__.py +0 -0
  217. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/__main__.py +0 -0
  218. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/files.py +0 -0
  219. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/finder.py +0 -0
  220. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/log.py +0 -0
  221. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/process.py +0 -0
  222. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/runner.py +0 -0
  223. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/server.py +0 -0
  224. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/text.py +0 -0
  225. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/broker/web.py +0 -0
  226. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/budget.py +0 -0
  227. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/cli.py +0 -0
  228. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/collector.py +0 -0
  229. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/daemon/__init__.py +0 -0
  230. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/daemon/__main__.py +0 -0
  231. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/daemon/process.py +0 -0
  232. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/daemon/state.py +0 -0
  233. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/doctor.py +0 -0
  234. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/__init__.py +0 -0
  235. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/agent/__init__.py +0 -0
  236. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/agent/base.py +0 -0
  237. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/agent/context.py +0 -0
  238. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/agent/exports.py +0 -0
  239. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/agent/interaction.py +0 -0
  240. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/agent/sessions.py +0 -0
  241. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/agent/settings.py +0 -0
  242. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/dialogue/__init__.py +0 -0
  243. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/dialogue/ask.py +0 -0
  244. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/dialogue/side.py +0 -0
  245. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/dialogue/title.py +0 -0
  246. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/export/__init__.py +0 -0
  247. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/export/trace/script.js +0 -0
  248. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/export/trace/style.css +0 -0
  249. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/export/trace/template.html +0 -0
  250. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/__init__.py +0 -0
  251. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/capabilities.py +0 -0
  252. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/compaction.py +0 -0
  253. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/context.py +0 -0
  254. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/files.py +0 -0
  255. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/harness_switch.py +0 -0
  256. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/mcp.py +0 -0
  257. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/messaging.py +0 -0
  258. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/relay.py +0 -0
  259. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/tool_output.py +0 -0
  260. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/extensions/usage.py +0 -0
  261. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/permission/approval.py +0 -0
  262. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/permission/readonly.py +0 -0
  263. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/pipeline.py +0 -0
  264. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/plugins/extension.py +0 -0
  265. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/plugins/mixins.py +0 -0
  266. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/plugins/services.py +0 -0
  267. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/plugins/state.py +0 -0
  268. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/storage/layout.py +0 -0
  269. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/storage/ledger.py +0 -0
  270. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/storage/locks.py +0 -0
  271. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/storage/metadata.py +0 -0
  272. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/storage/records.py +0 -0
  273. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/storage/store.py +0 -0
  274. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/tools/__init__.py +0 -0
  275. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/engine/tools/images.py +0 -0
  276. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/error_text.py +0 -0
  277. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/filelock.py +0 -0
  278. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/mentions.py +0 -0
  279. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/oneshot.py +0 -0
  280. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/prompt.py +0 -0
  281. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/providers.py +0 -0
  282. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/py.typed +0 -0
  283. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/registry.py +0 -0
  284. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/sdk/handlers.py +0 -0
  285. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/sdk/models.py +0 -0
  286. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/server/__init__.py +0 -0
  287. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/server/cli.py +0 -0
  288. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/server/settings.py +0 -0
  289. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/statusline.py +0 -0
  290. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/__init__.py +0 -0
  291. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/capabilities.py +0 -0
  292. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/clipboard.py +0 -0
  293. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/console.py +0 -0
  294. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/__init__.py +0 -0
  295. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/app.py +0 -0
  296. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/events.py +0 -0
  297. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/focus.py +0 -0
  298. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/geometry.py +0 -0
  299. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/host.py +0 -0
  300. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/keymap.py +0 -0
  301. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/scheduler.py +0 -0
  302. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/screen.py +0 -0
  303. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/core/widget.py +0 -0
  304. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/gallery.py +0 -0
  305. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/glyphs.py +0 -0
  306. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/input/__init__.py +0 -0
  307. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/input/bridge.py +0 -0
  308. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/input/decoder.py +0 -0
  309. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/input/events.py +0 -0
  310. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/input/keys.py +0 -0
  311. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/input/reader.py +0 -0
  312. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/layout/__init__.py +0 -0
  313. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/layout/box.py +0 -0
  314. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/layout/overlay.py +0 -0
  315. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/layout/scroll.py +0 -0
  316. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/layout/solver.py +0 -0
  317. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/layout/spacing.py +0 -0
  318. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/render/__init__.py +0 -0
  319. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/render/ansi.py +0 -0
  320. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/render/canvas.py +0 -0
  321. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/render/code.py +0 -0
  322. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/render/color.py +0 -0
  323. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/render/renderer.py +0 -0
  324. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/render/rich_text.py +0 -0
  325. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/render/style.py +0 -0
  326. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/render/sweep.py +0 -0
  327. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/render/text.py +0 -0
  328. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/runner.py +0 -0
  329. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/terminal.py +0 -0
  330. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/testing/__init__.py +0 -0
  331. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/testing/harness.py +0 -0
  332. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/testing/snapshot.py +0 -0
  333. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/theme_file.py +0 -0
  334. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/__init__.py +0 -0
  335. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/collapsible.py +0 -0
  336. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/completion.py +0 -0
  337. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/console.py +0 -0
  338. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/dialog.py +0 -0
  339. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/diff.py +0 -0
  340. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/list.py +0 -0
  341. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/list_page.py +0 -0
  342. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/markdown.py +0 -0
  343. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/progress.py +0 -0
  344. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/rich_text.py +0 -0
  345. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/status.py +0 -0
  346. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/table.py +0 -0
  347. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/tasks.py +0 -0
  348. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/text.py +0 -0
  349. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/textarea.py +0 -0
  350. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/tui/widgets/toast.py +0 -0
  351. {zettcode-0.1.12 → zettcode-0.3.0}/src/zettcode/wording.py +0 -0
@@ -24,6 +24,10 @@ docs/.vitepress/.temp/
24
24
  # The TypeScript client is built from its sources; only dist/ is published.
25
25
  clients/typescript/dist/
26
26
 
27
+ # The gateway's page is built from clients/gateway into the package when a distribution is made (`make gateway-ui`);
28
+ # the build output is not kept in the repository. pyproject's `artifacts` puts it in the wheel all the same.
29
+ zettcode/src/zettcode/gateway/static/
30
+
27
31
  # The palette gallery is generated by `make palettes`: every widget painted in
28
32
  # every palette, written by docs/scripts/palette_gallery.py. Regenerate it
29
33
  # rather than committing a 2 MB copy that goes stale the moment a colour moves.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: zettcode
3
- Version: 0.1.12
3
+ Version: 0.3.0
4
4
  Summary: A focused terminal coding agent powered by zett-agent
5
5
  Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
6
6
  Project-URL: Issues, https://github.com/Chang-LeHung/zettcode/issues
@@ -35,7 +35,7 @@ Requires-Dist: truststore<1,>=0.10
35
35
  Requires-Dist: uvicorn<1,>=0.35
36
36
  Requires-Dist: wcwidth<1,>=0.2
37
37
  Requires-Dist: websockets<17,>=15
38
- Requires-Dist: zett-agent<0.3,>=0.2.0
38
+ Requires-Dist: zett-agent<0.3,>=0.2.2
39
39
  Description-Content-Type: text/markdown
40
40
 
41
41
  # ZettCode
@@ -57,13 +57,17 @@ dependencies = [
57
57
  # tiktoken, and httpx behind their first use, so warming a runtime costs less
58
58
  # than it did. An exact pin would make zettcode fight every other tool that
59
59
  # shares the dependency — including an upgrade of zett-agent itself, which pip
60
- # would downgrade to satisfy it.
61
- "zett-agent>=0.2.0,<0.3",
60
+ # would downgrade to satisfy it. 0.2.2 is the floor for one reason this program
61
+ # now leans on: a hosted tool the provider left open is closed with
62
+ # ``output={"status": "not_reported"}`` instead of leaving the step waiting, and
63
+ # `engine/server_tools.describe_output` reads that to say so.
64
+ "zett-agent>=0.2.2,<0.3",
62
65
  ]
63
66
 
64
67
  [project.scripts]
65
68
  zettcode = "zettcode.cli:main"
66
69
  zettcode-server = "zettcode.server.cli:main"
70
+ zettcode-gateway = "zettcode.gateway.cli:main"
67
71
 
68
72
  [project.urls]
69
73
  Repository = "https://github.com/Chang-LeHung/zettcode"
@@ -77,6 +81,11 @@ build-backend = "hatchling.build"
77
81
  # Single source of truth: the version is written once, in the package.
78
82
  path = "src/zettcode/__init__.py"
79
83
 
84
+ # The gateway's page is built (make gateway-ui) into the package and is not in git; hatchling skips what git ignores
85
+ # unless it is named here.
86
+ [tool.hatch.build]
87
+ artifacts = ["src/zettcode/gateway/static/**"]
88
+
80
89
  [tool.hatch.build.targets.wheel]
81
90
  packages = ["src/zettcode"]
82
91
 
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
7
7
 
8
8
  #: The one place the version is written. ``pyproject.toml`` reads it from here
9
9
  #: and the release workflow checks the tag against it, so a bump is one edit.
10
- __version__ = "0.1.12"
10
+ __version__ = "0.3.0"
11
11
 
12
12
  #: Where each public name lives; read by :func:`__getattr__` on first use.
13
13
  _EXPORTS = {
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  from zettcode.app.commands import Command, CommandContext, CommandHost, CommandResult
11
11
  from zettcode.app.pets import PetCatalog, bundled_pets
12
12
  from zettcode.app.plugins.host import HOST_LINK, HostLink
13
- from zettcode.app.plugins.plugin import TerminalPlugins
13
+ from zettcode.app.plugins.plugin import TuiPlugins
14
14
  from zettcode.app.plugins.state import TurnSegment, UiRow
15
15
  from zettcode.engine.agent.exports import ExportsMixin
16
16
  from zettcode.providers import BUILTIN_PROVIDERS, Provider, merge_providers
@@ -28,10 +28,10 @@ class CommandsMixin(AppearanceMixin, ExportsMixin):
28
28
  """The ``/new``, ``/compact``, and ``/export`` commands, and the plugins' commands, mentions, and rows."""
29
29
 
30
30
  @property
31
- def terminal_plugins(self) -> TerminalPlugins:
31
+ def terminal_plugins(self) -> TuiPlugins:
32
32
  """Return what the plugins contributed to the terminal; the engine was built with the terminal's loader."""
33
33
  plugins = self.runtime.plugins
34
- if not isinstance(plugins, TerminalPlugins):
34
+ if not isinstance(plugins, TuiPlugins):
35
35
  raise TypeError("This agent's engine was not built with the terminal's plugin loader")
36
36
  return plugins
37
37
 
@@ -243,10 +243,16 @@ class ToolEntry(TimedEntry):
243
243
  class ReadEntry(ToolEntry):
244
244
  """A file that was read: its body is source code, drawn as the language it is.
245
245
 
246
- ``language`` names the scanner for the output, or is ``None`` for a file that is not code.
246
+ ``language`` names the scanner for the output, or is ``None`` for a file that is not code. ``lines`` is how many
247
+ lines came back, once a completed read has one; it is a field of the row rather than part of its title, so the
248
+ title being restated or cut at the width cannot lose it. ``picture`` marks a path that looks like a picture — the suffix the read tool
249
+ sends as an image — since a row is opened from the arguments, before the result exists; a file whose content is an
250
+ image under another name is counted as text until the result itself says otherwise.
247
251
  """
248
252
 
249
253
  language: str | None = None
254
+ lines: int | None = None
255
+ picture: bool = False
250
256
 
251
257
 
252
258
  @dataclass(slots=True, kw_only=True)
@@ -680,10 +680,21 @@ class SessionMessagePresenter(ToolPresenter[MessageEntry]):
680
680
 
681
681
 
682
682
  class ReadPresenter(ToolPresenter[ReadEntry]):
683
- """A file that was read: the body is highlighted as the language the file is in."""
683
+ """A file that was read: the body is highlighted as the language the file is in, and the row says how much."""
684
684
 
685
685
  entry_type = ReadEntry
686
686
 
687
+ def after_title(self, entry: ReadEntry, theme: Theme) -> list[Span]:
688
+ """Say how many lines came back, beside the title rather than inside it.
689
+
690
+ A field of the row survives what a title does not: `restate_tool` rewrites the title from the arguments, and
691
+ the count is drawn from the entry, the way an edit's ``+N -N`` is. A row too narrow for the title and the
692
+ count together cuts the count, as it cuts any tail — an edit's counts go the same way.
693
+ """
694
+ if entry.lines is None:
695
+ return []
696
+ return [Span(f" \u00b7 {entry.lines} line" + ("" if entry.lines == 1 else "s"), Style(foreground=theme.subtle))]
697
+
687
698
  def body_lines(self, entry: ReadEntry, width: int, theme: Theme, muted: Style) -> list[TextLine]:
688
699
  """Show the output as source code when the file is."""
689
700
  return self.output_lines(entry, width, theme, muted, language=entry.language)
@@ -33,7 +33,7 @@ from pathlib import Path
33
33
  from zettcode.tui import ELLIPSIS, SEPARATOR
34
34
  from zettcode.tui.render import GENERIC, language_for
35
35
 
36
- from .entries import DeleteEntry, EditEntry, MessageEntry, ReadEntry, ToolEntry, WriteEntry
36
+ from .entries import DeleteEntry, EditEntry, EntryStatus, MessageEntry, ReadEntry, ToolEntry, WriteEntry
37
37
  from .text import DECLINED_NOTE, limit_output
38
38
 
39
39
  #: Output kinds the chain shapes; a link claims one or more of them.
@@ -41,9 +41,25 @@ TOOL = "tool"
41
41
  THINKING = "thinking"
42
42
  ANSWER = "answer"
43
43
 
44
+ #: Suffixes that look like a picture, copied from the read tool's accepted set: a row is opened from the arguments,
45
+ #: before the result exists, so the suffix is all it has. The durable answer is a boolean on the finished event, taken
46
+ #: from the result's own content — see :func:`is_picture_path`.
47
+ PICTURE_SUFFIXES = frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp"})
48
+
49
+
50
+ def is_picture_path(path: str) -> bool:
51
+ """Return whether ``path`` looks like a picture by its suffix.
52
+
53
+ A guess, and named like one: the read tool decides by the file's signature, so a PNG copied to ``.bin`` is counted
54
+ as text and a text file named ``notes.png`` is not counted. Both are a line about a row nobody is reading closely;
55
+ the exact answer needs the boolean the result will carry (see :data:`PICTURE_SUFFIXES`).
56
+ """
57
+ return Path(path).suffix.lower() in PICTURE_SUFFIXES
58
+
59
+
44
60
  #: The hint a bounded read leaves behind; it is advice for the model, not the
45
61
  #: reader, and the row already shows the text it applies to.
46
- CONTINUATION = re.compile(r"\n?\.\.\. \[(?:line truncated|more lines)[^\]]*\]\s*\Z")
62
+ CONTINUATION = re.compile(r"\n?\.\.\. \[(?:line truncated|output truncated|more lines)[^\]]*\]\s*\Z")
47
63
 
48
64
  #: Wrapper tags a model may leave around its reasoning channel.
49
65
  REASONING_TAGS = re.compile(r"</?(?:think(?:ing)?|reasoning)>|<\|(?:thinking|reasoning)\|>", re.IGNORECASE)
@@ -128,6 +144,16 @@ class RenderHandler(ABC):
128
144
  """Return the text under a tool row; only a tool link changes this."""
129
145
  return output
130
146
 
147
+ def finished(self, entry: ToolEntry, text: str, status: EntryStatus) -> None:
148
+ """Adjust a row now that its result is in — a count, a size — and change nothing by default.
149
+
150
+ Args:
151
+ entry: The row, whose body is already set and bounded.
152
+ text: The body as it came back, after :meth:`body` shaped it and before it was bounded.
153
+ status: Whether the call completed, failed or was skipped: a row may only count what worked.
154
+ """
155
+ return None
156
+
131
157
  def text(self, text: str, *, opening: bool) -> str:
132
158
  """Return one streamed fragment, unchanged unless a link shapes it.
133
159
 
@@ -183,6 +209,12 @@ class Renderers:
183
209
  handler = self._claim(TOOL, name)
184
210
  return output if handler is None else handler.body(name, output)
185
211
 
212
+ def finished(self, entry: ToolEntry, text: str, status: EntryStatus) -> None:
213
+ """Let the claiming link adjust a row once its result is in."""
214
+ handler = self._claim(TOOL, entry.tool)
215
+ if handler is not None:
216
+ handler.finished(entry, text, status)
217
+
186
218
  def text(self, kind: str, text: str, *, opening: bool = False) -> str:
187
219
  """Return one streamed fragment of ``kind``, shaped by the claiming link."""
188
220
  handler = self._claim(kind)
@@ -197,38 +229,48 @@ def argument(arguments: Mapping[str, object], key: str, default: str = "") -> st
197
229
  return default
198
230
 
199
231
 
200
- def edited_lines(edits: object) -> tuple[int, int]:
232
+ def edit_changes(arguments: Mapping[str, object]) -> tuple[tuple[str, str], ...]:
233
+ """Return the text pairs an ``edit`` call replaces: one pair now, a list of them from an older transcript.
234
+
235
+ A call carries ``old_string`` and ``new_string``. A session stored before that shape gave the arguments an
236
+ ``edits`` list, and it is read the same way so the row of an old transcript still shows its diff. Anything
237
+ that is not two strings is dropped rather than guessed at.
238
+ """
239
+ legacy = arguments.get("edits")
240
+ if isinstance(legacy, Sequence) and not isinstance(legacy, str):
241
+ return tuple(
242
+ (edit["old_text"], edit["new_text"])
243
+ for edit in legacy
244
+ if isinstance(edit, Mapping)
245
+ and isinstance(edit.get("old_text"), str)
246
+ and isinstance(edit.get("new_text"), str)
247
+ )
248
+ old_string, new_string = arguments.get("old_string"), arguments.get("new_string")
249
+ if isinstance(old_string, str) and isinstance(new_string, str):
250
+ return ((old_string, new_string),)
251
+ return ()
252
+
253
+
254
+ def edited_lines(changes: Sequence[tuple[str, str]]) -> tuple[int, int]:
201
255
  """Return the lines an ``edit`` call adds and removes.
202
256
 
203
- The tool answers ``Replaced 2 occurrences in app.py``, which says how many
204
- edits landed but not how large they were: a two-line fix and a two-hundred-line
205
- rewrite read the same. The edits carry both texts, so the difference is counted
206
- here — with :class:`difflib.SequenceMatcher` rather than by subtracting lengths,
207
- because a replacement that keeps the lines around it should not read as a
208
- rewrite, and that is what a reader comparing two rows is looking at.
257
+ The tool answers ``Replaced 1 occurrence in app.py``, which says that a change landed but not how large it was:
258
+ a two-line fix and a two-hundred-line rewrite read the same. The call carries both texts, so the difference is
259
+ counted here — with :class:`difflib.SequenceMatcher` rather than by subtracting lengths, because a replacement
260
+ that keeps the lines around it should not read as a rewrite, and that is what a reader comparing two rows is
261
+ looking at.
209
262
 
210
263
  Args:
211
- edits: The call's ``edits`` argument, as the model sent it: a sequence of
212
- mappings with ``old_text`` and ``new_text``. Anything else is ignored
213
- rather than guessed at.
264
+ changes: The old and new text of each change, as :func:`edit_changes` reads them.
214
265
 
215
266
  Returns:
216
- ``(added, removed)`` across every edit; ``(0, 0)`` when there is nothing
217
- countable, which is also what a call with no edits at all answers.
267
+ ``(added, removed)`` across every change; ``(0, 0)`` when the call carried nothing countable.
218
268
 
219
- An edit that replaces every occurrence counts once, not once per occurrence:
220
- how many the file holds is something only the file knows, and the tool's own
221
- answer is where that number appears.
269
+ A change that replaces every occurrence counts once, not once per occurrence: how many the file holds is
270
+ something only the file knows, and the tool's own answer is where that number appears.
222
271
  """
223
- if not isinstance(edits, Sequence) or isinstance(edits, str):
224
- return (0, 0)
225
272
  added = removed = 0
226
- for edit in edits:
227
- if not isinstance(edit, Mapping):
228
- continue
229
- old_text, new_text = edit.get("old_text"), edit.get("new_text")
230
- if not isinstance(old_text, str) or not isinstance(new_text, str):
231
- continue
273
+ for old_text, new_text in changes:
232
274
  matcher = difflib.SequenceMatcher(None, old_text.splitlines(), new_text.splitlines())
233
275
  for tag, old_start, old_end, new_start, new_end in matcher.get_opcodes():
234
276
  if tag in ("replace", "delete"):
@@ -238,31 +280,24 @@ def edited_lines(edits: object) -> tuple[int, int]:
238
280
  return (added, removed)
239
281
 
240
282
 
241
- def edit_diff(edits: object, *, limit: int = MAX_DIFF_ROWS) -> tuple[DiffRow, ...]:
283
+ def edit_diff(changes: Sequence[tuple[str, str]], *, limit: int = MAX_DIFF_ROWS) -> tuple[DiffRow, ...]:
242
284
  """Return the rows of the change an ``edit`` call makes.
243
285
 
244
- Each edit carries the text it replaces and the text it puts there, so the change can be shown
286
+ Each change carries the text it replaces and the text it puts there, so the change can be shown
245
287
  without reading the file: the lines that differ, and one unchanged line around each so it reads in
246
- place. Edits are told apart by a labelled break when there is more than one, and what is kept is
247
- cut at ``limit`` rows with a last row saying so, because the arguments can be a whole file.
288
+ place. Changes are told apart by a labelled break when there is more than one — which only an older
289
+ transcript has — and what is kept is cut at ``limit`` rows with a last row saying so, because the
290
+ arguments can be a whole file.
248
291
 
249
292
  Args:
250
- edits: The call's ``edits`` argument as the model sent it; anything that is not a mapping with
251
- two texts is skipped rather than guessed at, exactly as :func:`edited_lines` does.
293
+ changes: The old and new text of each change, as :func:`edit_changes` reads them.
252
294
  limit: Most rows to keep.
253
295
  """
254
- if not isinstance(edits, Sequence) or isinstance(edits, str):
255
- return ()
256
- usable = [
257
- edit
258
- for edit in edits
259
- if isinstance(edit, Mapping) and isinstance(edit.get("old_text"), str) and isinstance(edit.get("new_text"), str)
260
- ]
261
296
  rows: list[DiffRow] = []
262
- for number, edit in enumerate(usable, start=1):
263
- if len(usable) > 1:
264
- rows.append(("@", f"edit {number} of {len(usable)}"))
265
- old_lines, new_lines = edit["old_text"].splitlines(), edit["new_text"].splitlines()
297
+ for number, (old_text, new_text) in enumerate(changes, start=1):
298
+ if len(changes) > 1:
299
+ rows.append(("@", f"edit {number} of {len(changes)}"))
300
+ old_lines, new_lines = old_text.splitlines(), new_text.splitlines()
266
301
  opcodes = difflib.SequenceMatcher(None, old_lines, new_lines).get_opcodes()
267
302
  for index, (tag, old_start, old_end, new_start, new_end) in enumerate(opcodes):
268
303
  if tag == "equal":
@@ -350,6 +385,15 @@ def _value_text(value: object) -> str:
350
385
  return str(value)
351
386
 
352
387
 
388
+ def file_path(arguments: Mapping[str, object], default: str = "") -> str:
389
+ """Return the path a file tool was called for.
390
+
391
+ ``file_path`` is the argument now; a session stored before the rename carries ``path``, and is still shown the
392
+ file it changed rather than as an unknown one.
393
+ """
394
+ return argument(arguments, "file_path") or argument(arguments, "path") or default
395
+
396
+
353
397
  def language_for_path(path: str) -> str | None:
354
398
  """Return the scanner for a file's contents, or ``None`` when it is not code."""
355
399
  language = language_for(Path(path).suffix.lstrip("."))
@@ -363,22 +407,40 @@ class ReadTool(ToolHandler):
363
407
 
364
408
  def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
365
409
  """Show the file, and highlight the body as the language it is."""
366
- path = argument(arguments, "path", "(unknown file)")
410
+ path = file_path(arguments, "(unknown file)")
367
411
  return ToolRow(f"Read {path}", language_for_path(path))
368
412
 
369
413
  def open_tool(
370
414
  self, name: str, arguments: Mapping[str, object], *, id: int, call_id: str, started_at: float
371
415
  ) -> ToolEntry:
372
- """A read row, which knows the language its body is in."""
416
+ """A read row, which knows the language its body is in, and whether the file is a picture."""
373
417
  row = self.describe(name, arguments)
418
+ path = file_path(arguments)
374
419
  return ReadEntry(
375
- id=id, call_id=call_id, tool=name, title=row.title, language=row.language, started_at=started_at
420
+ id=id,
421
+ call_id=call_id,
422
+ tool=name,
423
+ title=row.title,
424
+ language=row.language,
425
+ picture=is_picture_path(path),
426
+ started_at=started_at,
376
427
  )
377
428
 
378
429
  def body(self, name: str, output: str) -> str:
379
430
  """Drop the continuation hint; the row bounds the text it applies to."""
380
431
  return CONTINUATION.sub("", output).rstrip()
381
432
 
433
+ def finished(self, entry: ToolEntry, text: str, status: EntryStatus) -> None:
434
+ """Count what the read returned, on the row, if it returned text and the call worked.
435
+
436
+ The count is of what came back — a range asked for is not the same number — and of the whole of it, not of
437
+ the preview the row keeps, so a read past the output limit is counted in full. A picture carries no lines,
438
+ and a failed call's body is the error message rather than the file, so neither is counted.
439
+ """
440
+ if status is not EntryStatus.COMPLETED or not isinstance(entry, ReadEntry) or entry.picture:
441
+ return
442
+ entry.lines = len(text.splitlines())
443
+
382
444
 
383
445
  class WriteTool(ToolHandler):
384
446
  """``write_file``: the file being written."""
@@ -387,7 +449,7 @@ class WriteTool(ToolHandler):
387
449
 
388
450
  def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
389
451
  """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)')}")
452
+ return ToolRow(f"Write {file_path(arguments, '(unknown file)')}")
391
453
 
392
454
  def open_tool(
393
455
  self, name: str, arguments: Mapping[str, object], *, id: int, call_id: str, started_at: float
@@ -399,7 +461,7 @@ class WriteTool(ToolHandler):
399
461
  call_id=call_id,
400
462
  tool=name,
401
463
  title=self.describe(name, arguments).title,
402
- path=argument(arguments, "path"),
464
+ path=file_path(arguments),
403
465
  added=written_lines(content),
404
466
  diff=write_diff(content),
405
467
  started_at=started_at,
@@ -407,25 +469,21 @@ class WriteTool(ToolHandler):
407
469
 
408
470
 
409
471
  class EditTool(ToolHandler):
410
- """``edit``: the file and how many edits the call carries.
472
+ """``edit``: the file, and the size of the one change the call makes.
411
473
 
412
- A session stored before the tool was renamed calls it ``replace_in_file``, and is still shown the same way.
474
+ A session stored before the tool was renamed calls it ``replace_in_file``, and is still shown the same way; one
475
+ stored before a call carried a single change gave it a list under ``edits``, and that list is read too.
413
476
  """
414
477
 
415
478
  names = frozenset({"edit", "replace_in_file"})
416
479
 
417
480
  def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
418
- """Count the edits, and the lines they add and remove, before they land.
481
+ """Show the file; the size of the change is drawn beside the title, before it lands.
419
482
 
420
- A multi-edit call and a one-line call are both worth seeing in advance, and
421
- so is the size of the change: `+12 -3` beside the file says whether this is
422
- a tweak or a rewrite, which is the question a reader asks the row.
483
+ `+12 -3` beside the file says whether this is a tweak or a rewrite, which is the question a reader asks
484
+ the row. One change a call is why the title counts nothing: a count of one is noise on every row.
423
485
  """
424
- path = argument(arguments, "path", "(unknown file)")
425
- edits = arguments.get("edits")
426
- count = len(edits) if isinstance(edits, Sequence) and not isinstance(edits, str) else 0
427
- suffix = f" ({count} edit{'' if count == 1 else 's'})" if count else ""
428
- return ToolRow(f"Edited {path}{suffix}")
486
+ return ToolRow(f"Edit {file_path(arguments, '(unknown file)')}")
429
487
 
430
488
  def open_tool(
431
489
  self, name: str, arguments: Mapping[str, object], *, id: int, call_id: str, started_at: float
@@ -435,17 +493,17 @@ class EditTool(ToolHandler):
435
493
  The counts ride outside the title: they are drawn in the colours an addition and a
436
494
  deletion get, which a title made of one string cannot be.
437
495
  """
438
- edits = arguments.get("edits")
439
- added, removed = edited_lines(edits)
496
+ changes = edit_changes(arguments)
497
+ added, removed = edited_lines(changes)
440
498
  return EditEntry(
441
499
  id=id,
442
500
  call_id=call_id,
443
501
  tool=name,
444
502
  title=self.describe(name, arguments).title,
445
- path=argument(arguments, "path"),
503
+ path=file_path(arguments),
446
504
  added=added,
447
505
  removed=removed,
448
- diff=edit_diff(edits),
506
+ diff=edit_diff(changes),
449
507
  started_at=started_at,
450
508
  )
451
509
 
@@ -457,7 +515,7 @@ class DeleteTool(ToolHandler):
457
515
 
458
516
  def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
459
517
  """Show the file being removed."""
460
- return ToolRow(f"Deleted {argument(arguments, 'path', '(unknown file)')}")
518
+ return ToolRow(f"Deleted {file_path(arguments, '(unknown file)')}")
461
519
 
462
520
  def open_tool(
463
521
  self, name: str, arguments: Mapping[str, object], *, id: int, call_id: str, started_at: float
@@ -468,19 +526,21 @@ class DeleteTool(ToolHandler):
468
526
  call_id=call_id,
469
527
  tool=name,
470
528
  title=self.describe(name, arguments).title,
471
- path=argument(arguments, "path"),
529
+ path=file_path(arguments),
472
530
  started_at=started_at,
473
531
  )
474
532
 
475
533
 
476
534
  class ShellTool(ToolHandler):
477
- """``bash``: the command itself, which is the whole story."""
535
+ """``bash``: the command itself, and the directory it runs in when the call names one."""
478
536
 
479
537
  names = frozenset({"bash"})
480
538
 
481
539
  def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
482
- """Show the command, not a JSON copy of it."""
483
- return ToolRow(f"Ran {argument(arguments, 'command', '(unknown command)')}")
540
+ """Show the command, not a JSON copy of it, and where it runs when that is not the workspace."""
541
+ command = argument(arguments, "command", "(unknown command)")
542
+ where = argument(arguments, "work_dir")
543
+ return ToolRow(f"Bash {command}" + (f" {SEPARATOR} {where}" if where else ""))
484
544
 
485
545
 
486
546
  class SearchTool(ToolHandler):
@@ -493,7 +553,7 @@ class SearchTool(ToolHandler):
493
553
  pattern = argument(arguments, "pattern")
494
554
  if name == "glob":
495
555
  return ToolRow(f"Listed {pattern}" if pattern else "Listed files")
496
- where = argument(arguments, "path")
556
+ where = argument(arguments, "file_pattern") or argument(arguments, "path")
497
557
  return ToolRow(f"Searched {pattern or 'the workspace'}" + (f" in {where}" if where else ""))
498
558
 
499
559
 
@@ -631,6 +691,33 @@ class MessageTool(ToolHandler):
631
691
  )
632
692
 
633
693
 
694
+ class NamespacedTool(ToolHandler):
695
+ """A tool whose name carries where it came from: ``server__tool``, so the row names the server.
696
+
697
+ The server is part of what the call was: the same ``search`` in two servers is two different tools, with
698
+ different reach and different answers, and a row that drops the server makes them look like one.
699
+
700
+ The name is all a renderer knows, so the row says what the name says — ``github/create_issue`` — and not that
701
+ the tool speaks MCP: a plugin registers a namespaced name of its own with the same shape, and the runtime does
702
+ not mark its remote tools here. A name without both parts is shown as it arrived.
703
+ """
704
+
705
+ def handles(self, kind: str, name: str = "") -> bool:
706
+ """Claim a namespaced name that no earlier link claimed."""
707
+ return kind == TOOL and "__" in name.strip("_") and not name.startswith("__")
708
+
709
+ def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
710
+ """Show ``server/tool``, so the row names where the capability came from.
711
+
712
+ The runtime names a remote tool ``<server>__<tool>``; a name that prefixed the family with ``mcp`` —
713
+ ``mcp__github__create_issue`` — is read the same way, and one without both parts keeps its own name.
714
+ """
715
+ remote = name[5:] if name.startswith("mcp__") else name
716
+ server, _, tool = remote.partition("__")
717
+ label = f"{server}/{tool}" if server and tool else name
718
+ return ToolRow(f"{label} {summarise(arguments)}".rstrip())
719
+
720
+
634
721
  class FallbackTool(ToolHandler):
635
722
  """Anything no other link claims: its name and flat arguments, never JSON."""
636
723
 
@@ -639,15 +726,8 @@ class FallbackTool(ToolHandler):
639
726
  return kind == TOOL
640
727
 
641
728
  def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
642
- """Show ``name key=value`` so an unknown tool still reads as a sentence.
643
-
644
- An MCP tool is namespaced ``<server>__<tool>``; the server is worth
645
- keeping because it says where the capability comes from, but it reads
646
- better after the tool than in front of it.
647
- """
648
- server, _, tool = name.partition("__")
649
- label = f"{tool} (via {server})" if tool else name
650
- return ToolRow(f"{label} {summarise(arguments)}".rstrip())
729
+ """Show ``name key=value`` so an unknown tool still reads as a sentence."""
730
+ return ToolRow(f"{name} {summarise(arguments)}".rstrip())
651
731
 
652
732
 
653
733
  class ReasoningHandler(RenderHandler):
@@ -705,6 +785,7 @@ DEFAULT_RENDERERS = Renderers(
705
785
  SkillTool(),
706
786
  TaskTool(),
707
787
  MessageTool(),
788
+ NamespacedTool(),
708
789
  FallbackTool(),
709
790
  ReasoningHandler(),
710
791
  AnswerHandler(),
@@ -164,7 +164,11 @@ class StreamMixin(TurnMixin):
164
164
  return
165
165
  # An unknown status is refused before the row is touched at all.
166
166
  outcome = EntryStatus(status)
167
- entry.text = limit_output(self.renderers.body(entry.tool, output))
167
+ body = self.renderers.body(entry.tool, output)
168
+ entry.text = limit_output(body)
169
+ # The result, unbounded: a count is of what came back, not of the preview the row keeps, and what it says about
170
+ # the body depends on whether the call worked.
171
+ self.renderers.finished(entry, body, outcome)
168
172
  entry.finish(outcome, self.clock())
169
173
  self._changed(entry)
170
174
  # The loop calls the model again once the batch has produced its
@@ -101,19 +101,24 @@ class CommandHost(Protocol):
101
101
  async def run_side_question(self, question: str) -> None:
102
102
  """Answer a question that is kept out of the conversation."""
103
103
 
104
- async def prompt(self, text: str) -> bool:
104
+ async def prompt(self, text: str, *, shown: str | None = None) -> bool:
105
105
  """Run ``text`` as one turn of this conversation, exactly as if the reader had typed it.
106
106
 
107
- The text reaches the model the way a draft does: echoed in the transcript as the reader's own message,
108
- stored in the session, streamed as it is answered, and settled by the same row. ``@`` references are
109
- resolved; pictures the text cannot carry are not part of it.
107
+ The text reaches the model the way a draft does: stored in the session, streamed as it is answered, and
108
+ settled by the same row. ``@`` references are resolved.
109
+
110
+ A command whose instructions are longer than what it wants to show passes the two apart: ``text`` is what
111
+ the model reads, ``shown`` is what the transcript draws in its place. A reader who typed ``/code-review``
112
+ sees that, not the brief it stands for — while the model reads the brief with that turn, then and in every
113
+ later turn, because the brief is part of the message the session keeps. Only its display is short.
110
114
 
111
115
  The turn is awaited, so the command that asked for it holds the request slot until it has been answered,
112
116
  and the row the shell shows is the command's own.
113
117
 
114
118
  Args:
115
- text: What to ask. Its own text is what the reader sees, so a command that builds the text should
116
- write it as a reader would have.
119
+ text: What to ask. With no ``shown``, this is also what the reader sees.
120
+ shown: What the transcript shows, when it differs from ``text``. The message the session keeps is
121
+ the same either way: ``text`` is in it, and ``shown`` is what a reader is shown of it.
117
122
 
118
123
  Returns:
119
124
  Whether a turn was started and has ended. A request already running, a blank text and a session held
@@ -247,7 +252,7 @@ class Command:
247
252
  its sessions, or the model, and is handled by
248
253
  :class:`~zettcode.app.agent.TerminalAgent`; ``"plugin"`` comes from
249
254
  an installed plugin, registered through
250
- :class:`~zettcode.app.plugins.TerminalContainer`.
255
+ :class:`~zettcode.engine.plugins.context.PluginContext`.
251
256
  handler: Runs the command with a :class:`CommandContext` — the trimmed
252
257
  argument and the UI surfaces it may write to while it works — and
253
258
  returns a :class:`CommandResult` describing anything left to show.
@@ -7,7 +7,7 @@ it, moves it, and picks the mood.
7
7
 
8
8
  Pets are offered the way palettes are: a :class:`PetSource` names one and builds it only when it is asked for,
9
9
  the built-in plugin offers the ones that ship, and any other plugin offers its own through
10
- ``PluginContainer.register_pet``. :class:`PetCatalog` is what ``/pet`` and the start-up read.
10
+ ``ctx.tui.register_pet``. :class:`PetCatalog` is what ``/pet`` and the start-up read.
11
11
 
12
12
  Every frame of a pet is the same size, and its height is an even number of pixel rows: two pixels fit in
13
13
  one terminal cell.