tau-coding-agent 0.1.0__py3-none-any.whl

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 (283) hide show
  1. tau/__init__.py +0 -0
  2. tau/agent/__init__.py +11 -0
  3. tau/agent/prompt/__init__.py +10 -0
  4. tau/agent/prompt/builder.py +302 -0
  5. tau/agent/prompt/types.py +33 -0
  6. tau/agent/service.py +369 -0
  7. tau/agent/types.py +61 -0
  8. tau/auth/manager.py +247 -0
  9. tau/auth/storage.py +82 -0
  10. tau/auth/types.py +41 -0
  11. tau/builtins/__init__.py +4 -0
  12. tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
  13. tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
  14. tau/builtins/commands/__init__.py +41 -0
  15. tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  16. tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
  17. tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
  18. tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
  19. tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
  20. tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
  21. tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
  22. tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
  23. tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
  24. tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
  25. tau/builtins/commands/clear.py +16 -0
  26. tau/builtins/commands/compact.py +28 -0
  27. tau/builtins/commands/reload.py +27 -0
  28. tau/builtins/commands/session.py +19 -0
  29. tau/builtins/extensions/footer/__init__.py +76 -0
  30. tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
  31. tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
  32. tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
  33. tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
  34. tau/builtins/extensions/footer/git.py +26 -0
  35. tau/builtins/extensions/footer/model.py +69 -0
  36. tau/builtins/extensions/footer/utils.py +44 -0
  37. tau/builtins/extensions/header/__init__.py +18 -0
  38. tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
  39. tau/builtins/models/__init__.py +0 -0
  40. tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
  41. tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
  42. tau/builtins/models/audio.py +43 -0
  43. tau/builtins/models/image.py +43 -0
  44. tau/builtins/models/text.py +482 -0
  45. tau/builtins/models/video.py +40 -0
  46. tau/builtins/prompts/commit.md +7 -0
  47. tau/builtins/prompts/docs.md +7 -0
  48. tau/builtins/prompts/explain.md +7 -0
  49. tau/builtins/prompts/fix.md +7 -0
  50. tau/builtins/prompts/refactor.md +7 -0
  51. tau/builtins/prompts/review.md +7 -0
  52. tau/builtins/prompts/test.md +7 -0
  53. tau/builtins/providers/__init__.py +0 -0
  54. tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  55. tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
  56. tau/builtins/providers/audio.py +10 -0
  57. tau/builtins/providers/image.py +9 -0
  58. tau/builtins/providers/text.py +33 -0
  59. tau/builtins/providers/video.py +6 -0
  60. tau/builtins/skills/code-review/SKILL.md +4 -0
  61. tau/builtins/skills/debug/SKILL.md +4 -0
  62. tau/builtins/skills/git-commit/SKILL.md +4 -0
  63. tau/builtins/themes/dark.yaml +1 -0
  64. tau/builtins/themes/light.yaml +46 -0
  65. tau/builtins/tools/__init__.py +73 -0
  66. tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  67. tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  68. tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
  69. tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
  70. tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
  71. tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
  72. tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
  73. tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
  74. tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
  75. tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
  76. tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
  77. tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
  78. tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
  79. tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
  80. tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
  81. tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
  82. tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
  83. tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
  84. tau/builtins/tools/edit.py +215 -0
  85. tau/builtins/tools/glob.py +112 -0
  86. tau/builtins/tools/grep.py +146 -0
  87. tau/builtins/tools/ls.py +135 -0
  88. tau/builtins/tools/read.py +122 -0
  89. tau/builtins/tools/terminal.py +150 -0
  90. tau/builtins/tools/write.py +105 -0
  91. tau/commands/__init__.py +10 -0
  92. tau/commands/registry.py +71 -0
  93. tau/commands/types.py +33 -0
  94. tau/console/__init__.py +0 -0
  95. tau/console/cli.py +266 -0
  96. tau/console/commands/__init__.py +0 -0
  97. tau/console/commands/auth.py +193 -0
  98. tau/console/commands/packages.py +104 -0
  99. tau/console/commands/update.py +76 -0
  100. tau/core/__init__.py +0 -0
  101. tau/core/registry.py +102 -0
  102. tau/engine/__init__.py +47 -0
  103. tau/engine/service.py +768 -0
  104. tau/engine/types.py +163 -0
  105. tau/extensions/__init__.py +28 -0
  106. tau/extensions/api.py +928 -0
  107. tau/extensions/context.py +462 -0
  108. tau/extensions/events.py +70 -0
  109. tau/extensions/loader.py +386 -0
  110. tau/extensions/runtime.py +184 -0
  111. tau/extensions/settings.py +137 -0
  112. tau/hooks/__init__.py +112 -0
  113. tau/hooks/engine.py +237 -0
  114. tau/hooks/inference.py +21 -0
  115. tau/hooks/runtime.py +126 -0
  116. tau/hooks/service.py +121 -0
  117. tau/hooks/session.py +117 -0
  118. tau/hooks/tui.py +61 -0
  119. tau/hooks/types.py +72 -0
  120. tau/inference/__init__.py +180 -0
  121. tau/inference/api/__init__.py +0 -0
  122. tau/inference/api/audio/__init__.py +0 -0
  123. tau/inference/api/audio/base.py +29 -0
  124. tau/inference/api/audio/builtins.py +15 -0
  125. tau/inference/api/audio/elevenlabs_audio.py +183 -0
  126. tau/inference/api/audio/gemini_audio.py +95 -0
  127. tau/inference/api/audio/openai_audio.py +159 -0
  128. tau/inference/api/audio/registry.py +15 -0
  129. tau/inference/api/audio/sarvam_audio.py +163 -0
  130. tau/inference/api/audio/service.py +103 -0
  131. tau/inference/api/audio/utils.py +47 -0
  132. tau/inference/api/image/__init__.py +0 -0
  133. tau/inference/api/image/base.py +17 -0
  134. tau/inference/api/image/builtins.py +8 -0
  135. tau/inference/api/image/gemini_image.py +77 -0
  136. tau/inference/api/image/openai_image.py +103 -0
  137. tau/inference/api/image/openrouter.py +144 -0
  138. tau/inference/api/image/registry.py +15 -0
  139. tau/inference/api/image/service.py +71 -0
  140. tau/inference/api/registry.py +82 -0
  141. tau/inference/api/text/__init__.py +0 -0
  142. tau/inference/api/text/anthropic_claude_code.py +222 -0
  143. tau/inference/api/text/anthropic_messages.py +196 -0
  144. tau/inference/api/text/base.py +40 -0
  145. tau/inference/api/text/builtins.py +19 -0
  146. tau/inference/api/text/gemini_generate.py +234 -0
  147. tau/inference/api/text/github_copilot_chat.py +172 -0
  148. tau/inference/api/text/google_antigravity.py +522 -0
  149. tau/inference/api/text/mistral_chat.py +284 -0
  150. tau/inference/api/text/ollama_chat.py +200 -0
  151. tau/inference/api/text/openai_codex_responses.py +497 -0
  152. tau/inference/api/text/openai_completions.py +227 -0
  153. tau/inference/api/text/openai_responses.py +235 -0
  154. tau/inference/api/text/registry.py +50 -0
  155. tau/inference/api/text/service.py +297 -0
  156. tau/inference/api/text/types.py +7 -0
  157. tau/inference/api/text/utils.py +228 -0
  158. tau/inference/api/video/__init__.py +0 -0
  159. tau/inference/api/video/base.py +26 -0
  160. tau/inference/api/video/builtins.py +7 -0
  161. tau/inference/api/video/fal_video.py +119 -0
  162. tau/inference/api/video/openrouter_video.py +142 -0
  163. tau/inference/api/video/registry.py +15 -0
  164. tau/inference/api/video/service.py +72 -0
  165. tau/inference/model/__init__.py +0 -0
  166. tau/inference/model/registry.py +102 -0
  167. tau/inference/model/types.py +65 -0
  168. tau/inference/provider/__init__.py +0 -0
  169. tau/inference/provider/oauth/__init__.py +35 -0
  170. tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
  171. tau/inference/provider/oauth/github_copilot.py +333 -0
  172. tau/inference/provider/oauth/google_antigravity.py +258 -0
  173. tau/inference/provider/oauth/openai_codex.py +309 -0
  174. tau/inference/provider/oauth/pkce.py +14 -0
  175. tau/inference/provider/oauth/types.py +46 -0
  176. tau/inference/provider/oauth/utils.py +154 -0
  177. tau/inference/provider/registry.py +141 -0
  178. tau/inference/provider/types.py +114 -0
  179. tau/inference/types.py +549 -0
  180. tau/inference/utils.py +219 -0
  181. tau/message/__init__.py +0 -0
  182. tau/message/types.py +482 -0
  183. tau/message/utils.py +178 -0
  184. tau/packages/__init__.py +11 -0
  185. tau/packages/manager.py +190 -0
  186. tau/packages/types.py +20 -0
  187. tau/packages/utils.py +67 -0
  188. tau/prompts/expand.py +58 -0
  189. tau/prompts/loader.py +69 -0
  190. tau/prompts/registry.py +45 -0
  191. tau/prompts/types.py +24 -0
  192. tau/rpc/__init__.py +8 -0
  193. tau/rpc/mode.py +783 -0
  194. tau/rpc/types.py +252 -0
  195. tau/runtime/service.py +759 -0
  196. tau/runtime/types.py +303 -0
  197. tau/session/branch_summarization.py +312 -0
  198. tau/session/compaction.py +646 -0
  199. tau/session/manager.py +652 -0
  200. tau/session/types.py +188 -0
  201. tau/session/utils.py +233 -0
  202. tau/settings/manager.py +1077 -0
  203. tau/settings/paths.py +150 -0
  204. tau/settings/storage.py +63 -0
  205. tau/settings/types.py +173 -0
  206. tau/settings/utils.py +25 -0
  207. tau/skills/loader.py +91 -0
  208. tau/skills/registry.py +70 -0
  209. tau/skills/types.py +25 -0
  210. tau/themes/loader.py +238 -0
  211. tau/themes/registry.py +108 -0
  212. tau/themes/types.py +19 -0
  213. tau/tool/__init__.py +3 -0
  214. tau/tool/registry.py +117 -0
  215. tau/tool/render.py +21 -0
  216. tau/tool/types.py +244 -0
  217. tau/trust/__init__.py +13 -0
  218. tau/trust/manager.py +80 -0
  219. tau/trust/types.py +14 -0
  220. tau/trust/utils.py +72 -0
  221. tau/tui/__init__.py +54 -0
  222. tau/tui/agent_hooks.py +346 -0
  223. tau/tui/ansi.py +330 -0
  224. tau/tui/app.py +540 -0
  225. tau/tui/autocomplete.py +33 -0
  226. tau/tui/capabilities.py +119 -0
  227. tau/tui/commands/__init__.py +3 -0
  228. tau/tui/commands/appearance.py +498 -0
  229. tau/tui/commands/auth.py +232 -0
  230. tau/tui/commands/context.py +38 -0
  231. tau/tui/commands/misc.py +82 -0
  232. tau/tui/commands/model.py +118 -0
  233. tau/tui/commands/session.py +464 -0
  234. tau/tui/component.py +268 -0
  235. tau/tui/components/__init__.py +0 -0
  236. tau/tui/components/autocomplete_manager.py +267 -0
  237. tau/tui/components/autocomplete_picker.py +143 -0
  238. tau/tui/components/box.py +90 -0
  239. tau/tui/components/command_palette.py +144 -0
  240. tau/tui/components/dynamic_border.py +19 -0
  241. tau/tui/components/file_picker.py +233 -0
  242. tau/tui/components/image.py +181 -0
  243. tau/tui/components/inline_selector.py +71 -0
  244. tau/tui/components/layout.py +1194 -0
  245. tau/tui/components/message_list.py +692 -0
  246. tau/tui/components/modal.py +97 -0
  247. tau/tui/components/model_palette.py +204 -0
  248. tau/tui/components/picker_overlay.py +174 -0
  249. tau/tui/components/prompt_overlay.py +236 -0
  250. tau/tui/components/resume_modal.py +372 -0
  251. tau/tui/components/select_list.py +222 -0
  252. tau/tui/components/settings_modal.py +274 -0
  253. tau/tui/components/settings_schema.py +203 -0
  254. tau/tui/components/spinner.py +119 -0
  255. tau/tui/components/text_input.py +396 -0
  256. tau/tui/components/text_prompt.py +82 -0
  257. tau/tui/components/tree_select_list.py +580 -0
  258. tau/tui/components/trust_screen.py +97 -0
  259. tau/tui/diff.py +114 -0
  260. tau/tui/fuzzy.py +99 -0
  261. tau/tui/input.py +496 -0
  262. tau/tui/input_handler.py +716 -0
  263. tau/tui/keybindings.py +87 -0
  264. tau/tui/markdown.py +286 -0
  265. tau/tui/message_renderers.py +31 -0
  266. tau/tui/overlay.py +326 -0
  267. tau/tui/renderer.py +378 -0
  268. tau/tui/terminal.py +499 -0
  269. tau/tui/theme.py +148 -0
  270. tau/tui/tui.py +544 -0
  271. tau/tui/ui_context.py +768 -0
  272. tau/tui/utils.py +20 -0
  273. tau/utils/__init__.py +0 -0
  274. tau/utils/http_proxy.py +221 -0
  275. tau/utils/image_processing.py +172 -0
  276. tau/utils/secrets.py +59 -0
  277. tau/utils/version_check.py +60 -0
  278. tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
  279. tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
  280. tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
  281. tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  282. tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  283. tau_coding_agent-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,692 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, TYPE_CHECKING
4
+
5
+ from tau.tui.ansi import visible_width, wrap, RESET, BOLD, DIM
6
+ from tau.tui.component import Component
7
+ from tau.tui.diff import _is_diff
8
+ from tau.tui.input import InputEvent, KeyEvent
9
+ from tau.tui.markdown import render_markdown
10
+ from tau.tui.theme import MessageTheme
11
+
12
+ if TYPE_CHECKING:
13
+ from tau.tool.types import Tool
14
+
15
+ _TOOL_INDENT = " "
16
+ _RESULT_INDENT = " "
17
+
18
+
19
+ def apply_render_shell(lines: list[str], theme: Any, color_fn: Any = None) -> list[str]:
20
+ """Apply the standard └ framing to a pre-rendered list of lines.
21
+
22
+ First line gets ' └ <line>', subsequent lines get ' <line>'.
23
+ Optional color_fn is applied to the first line (e.g. theme.error for red).
24
+ Shared by tool results and notify so any style change propagates everywhere.
25
+ """
26
+ if not lines:
27
+ return []
28
+ first = color_fn(lines[0]) if color_fn else lines[0]
29
+ out = [f"{_RESULT_INDENT}{theme.dim('└')} {first}"]
30
+ out.extend(f"{_RESULT_INDENT} {l}" for l in lines[1:])
31
+ return out
32
+
33
+
34
+ # ── MessageBlock ──────────────────────────────────────────────────────────────
35
+
36
+ class MessageBlock:
37
+ """
38
+ Cached rendering of a single message (any type).
39
+
40
+ Pass a MessageTheme to control all colours. Call invalidate() whenever
41
+ the underlying message changes (e.g. a streaming token arrives).
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ message: object,
47
+ streaming: bool = False,
48
+ theme: MessageTheme | None = None,
49
+ user_prefix: str = "❯ ",
50
+ tool_lookup: Callable[[str], "Tool | None"] | None = None,
51
+ ) -> None:
52
+ self._message = message
53
+ self._streaming = streaming
54
+ self._expanded = False
55
+ self._theme = theme or MessageTheme()
56
+ self._user_prefix = user_prefix
57
+ self._tool_lookup = tool_lookup
58
+ self._cached: list[str] | None = None
59
+ self._cached_width = 0
60
+ # Keyed by (content_idx, image_idx) — persisted so Kitty image IDs stay stable
61
+ self._image_components: dict[tuple[int, int], Any] = {}
62
+ self._tool_results_cache: list[str] | None = None
63
+ self._tool_results_message: object | None = None
64
+ self._tool_results_width = 0
65
+
66
+ # -------------------------------------------------------------------------
67
+ # Public API
68
+ # -------------------------------------------------------------------------
69
+
70
+ def invalidate(self) -> None:
71
+ self._cached = None
72
+ self._tool_results_cache = None
73
+ self._tool_results_message = None
74
+
75
+ def toggle_expanded(self) -> None:
76
+ self._expanded = not self._expanded
77
+ self.invalidate()
78
+
79
+ def set_streaming(self, value: bool) -> None:
80
+ if self._streaming != value:
81
+ self._streaming = value
82
+ self.invalidate()
83
+
84
+ def set_theme(self, theme: MessageTheme) -> None:
85
+ self._theme = theme
86
+ self.invalidate()
87
+
88
+ def set_user_prefix(self, prefix: str) -> None:
89
+ if self._user_prefix != prefix:
90
+ self._user_prefix = prefix
91
+ self.invalidate()
92
+
93
+ def set_tool_lookup(self, fn: Callable[[str], "Tool | None"] | None) -> None:
94
+ self._tool_lookup = fn
95
+ self.invalidate()
96
+
97
+ def _render_image(self, key: tuple[int, int], b64: str, mime: str, width: int) -> list[str]:
98
+ if not self._theme.show_images:
99
+ from tau.tui.components.image import Image
100
+ return [Image(b64, mime)._fallback_text()]
101
+ if key not in self._image_components:
102
+ from tau.tui.components.image import Image
103
+ self._image_components[key] = Image(b64, mime)
104
+ return self._image_components[key].render(width)
105
+
106
+ @property
107
+ def message(self) -> object:
108
+ return self._message
109
+
110
+ # -------------------------------------------------------------------------
111
+ # Rendering
112
+ # -------------------------------------------------------------------------
113
+
114
+ def render(self, width: int) -> list[str]:
115
+ if self._cached is not None and self._cached_width == width:
116
+ return self._cached
117
+ self._cached = self._build(width)
118
+ self._cached_width = width
119
+ return self._cached
120
+
121
+ def render_with_tool_results(self, tool_message: object, width: int) -> list[str]:
122
+ from tau.message.types import AssistantMessage, ToolMessage, ToolResultContent
123
+
124
+ if not isinstance(self._message, AssistantMessage) or not isinstance(tool_message, ToolMessage):
125
+ return self.render(width)
126
+ if (
127
+ self._tool_results_cache is not None
128
+ and self._tool_results_message is tool_message
129
+ and self._tool_results_width == width
130
+ ):
131
+ return self._tool_results_cache
132
+
133
+ results = {
134
+ item.id: item
135
+ for item in tool_message.contents
136
+ if isinstance(item, ToolResultContent)
137
+ }
138
+ lines = self._render_assistant(self._message, width, results)
139
+
140
+ matched_ids = {
141
+ item.id
142
+ for item in self._message.tool_calls()
143
+ if item.id in results
144
+ }
145
+ for item in tool_message.contents:
146
+ if isinstance(item, ToolResultContent) and item.id not in matched_ids:
147
+ lines.extend(self._render_tool_result(item, width, item.tool_name))
148
+
149
+ lines.append("")
150
+ self._tool_results_cache = lines
151
+ self._tool_results_message = tool_message
152
+ self._tool_results_width = width
153
+ return lines
154
+
155
+ def _build(self, width: int) -> list[str]:
156
+ from tau.message.types import (
157
+ AssistantMessage, UserMessage, ToolMessage,
158
+ CustomMessage, TerminalExecutionMessage,
159
+ )
160
+
161
+ msg = self._message
162
+ lines: list[str] = []
163
+
164
+ if isinstance(msg, UserMessage):
165
+ lines.extend(self._render_user(msg, width))
166
+ elif isinstance(msg, AssistantMessage):
167
+ lines.extend(self._render_assistant(msg, width))
168
+ elif isinstance(msg, ToolMessage):
169
+ lines.extend(self._render_tool_message(msg, width))
170
+ elif isinstance(msg, TerminalExecutionMessage):
171
+ lines.extend(self._render_terminal(msg, width))
172
+ elif isinstance(msg, CustomMessage):
173
+ lines.extend(self._render_custom(msg, width))
174
+ else:
175
+ from tau.message.types import TemplateInvocationMessage, SkillInvocationMessage
176
+ if isinstance(msg, TemplateInvocationMessage):
177
+ lines.extend(self._render_template_invocation(msg, width))
178
+ elif isinstance(msg, SkillInvocationMessage):
179
+ lines.extend(self._render_skill_invocation(msg, width))
180
+ else:
181
+ lines.append(self._theme.dim(str(msg)))
182
+
183
+ from tau.message.types import (
184
+ TemplateInvocationMessage, SkillInvocationMessage, UserMessage, TextContent,
185
+ )
186
+ is_command = (
187
+ isinstance(msg, UserMessage)
188
+ and any(
189
+ isinstance(c, TextContent) and c.content.lstrip().startswith("/")
190
+ for c in msg.contents
191
+ )
192
+ )
193
+ if not isinstance(msg, (CustomMessage, TemplateInvocationMessage, SkillInvocationMessage)) and not is_command:
194
+ lines.append("") # blank separator after each message
195
+ return lines
196
+
197
+ # -------------------------------------------------------------------------
198
+ # Per-type renderers
199
+ # -------------------------------------------------------------------------
200
+
201
+ def _render_user(self, msg: Any, width: int) -> list[str]:
202
+ from tau.message.types import UserMessage, TextContent, ImageContent
203
+ if not isinstance(msg, UserMessage):
204
+ return []
205
+ t = self._theme
206
+ prefix = self._user_prefix
207
+ inner_width = max(1, width - visible_width(prefix))
208
+ lines: list[str] = []
209
+ for c_idx, item in enumerate(msg.contents):
210
+ if isinstance(item, TextContent) and item.content:
211
+ for line in wrap(item.content.rstrip(), inner_width):
212
+ lead = t.you_label(prefix) if not lines else " "
213
+ lines.append(lead + line)
214
+ elif isinstance(item, ImageContent):
215
+ for i_idx, (b64, mime) in enumerate(item.to_base64()):
216
+ lines.extend(self._render_image((c_idx, i_idx), b64, mime, inner_width))
217
+ return lines
218
+
219
+ def _render_assistant(
220
+ self,
221
+ msg: Any,
222
+ width: int,
223
+ tool_results: dict[str, Any] | None = None,
224
+ ) -> list[str]:
225
+ from tau.message.types import (
226
+ AssistantMessage, TextContent, ThinkingContent, ToolCallContent,
227
+ )
228
+ from tau.inference.types import StopReason
229
+ if not isinstance(msg, AssistantMessage):
230
+ return []
231
+
232
+ t = self._theme
233
+ inner_width = max(1, width - 2)
234
+ tool_indent_w = max(1, width - len(_TOOL_INDENT) - 2)
235
+ lines: list[str] = []
236
+
237
+ has_content = any(
238
+ (isinstance(c, TextContent) and c.content)
239
+ or isinstance(c, (ThinkingContent, ToolCallContent))
240
+ for c in msg.contents
241
+ )
242
+
243
+ if not has_content and msg.stop_reason == StopReason.Error and msg.error:
244
+ lines.append(t.error_label("error"))
245
+ for line in wrap(msg.error, inner_width):
246
+ lines.append(" " + line)
247
+ return lines
248
+
249
+ # No "assistant" label — the content speaks for itself.
250
+ from tau.message.types import ImageContent as _ImageContent
251
+ for idx, item in enumerate(msg.contents):
252
+ if isinstance(item, ThinkingContent):
253
+ if t.show_thinking:
254
+ if item.content:
255
+ for line in wrap(item.content.rstrip(), inner_width):
256
+ lines.append(" " + t.thinking(line))
257
+ else:
258
+ lines.append(" " + t.thinking(t.thinking_label))
259
+ next_item = msg.contents[idx + 1] if idx + 1 < len(msg.contents) else None
260
+ if not isinstance(next_item, ThinkingContent):
261
+ lines.append("")
262
+
263
+ elif isinstance(item, TextContent) and item.content:
264
+ for line in render_markdown(item.content.rstrip(), inner_width, t.markdown):
265
+ lines.append(" " + line)
266
+
267
+ elif isinstance(item, _ImageContent):
268
+ for i_idx, (b64, mime) in enumerate(item.to_base64()):
269
+ lines.extend(self._render_image((idx, i_idx), b64, mime, inner_width))
270
+
271
+ elif isinstance(item, ToolCallContent):
272
+ if t.show_tool_calls:
273
+ tool = self._tool_lookup(item.name) if self._tool_lookup else None
274
+ if tool is not None and tool.render_call is not None:
275
+ custom = tool.render_call(item.args, self._streaming)
276
+ if custom:
277
+ lines.extend(custom)
278
+ else:
279
+ from tau.tool.render import display_name, call_line
280
+ if item.args:
281
+ first_val = next(iter(item.args.values()), "")
282
+ lines.extend(call_line(item.name, str(first_val) if first_val else ""))
283
+ else:
284
+ lines.append(f"{_TOOL_INDENT}{BOLD}{display_name(item.name)}{RESET}")
285
+ if tool_results is not None and item.id in tool_results:
286
+ lines.extend(self._render_tool_result(tool_results[item.id], width, item.name))
287
+ # Separate consecutive tool-call blocks with a blank line so
288
+ # they don't render flush against each other (mirrors how
289
+ # ThinkingContent spaces itself above).
290
+ next_item = msg.contents[idx + 1] if idx + 1 < len(msg.contents) else None
291
+ if isinstance(next_item, ToolCallContent):
292
+ lines.append("")
293
+
294
+ if self._streaming:
295
+ cursor = t.stream_cursor("▋")
296
+ if lines:
297
+ lines[-1] = lines[-1] + cursor
298
+ else:
299
+ lines.append(" " + cursor)
300
+ elif msg.stop_reason == StopReason.Abort:
301
+ lines.append(" " + t.dim("┌ User Interrupted"))
302
+
303
+ return lines
304
+
305
+ def _render_terminal(self, msg: Any, width: int) -> list[str]:
306
+ from tau.message.types import TerminalExecutionMessage
307
+ from tau.tui.ansi import BRIGHT_RED
308
+ if not isinstance(msg, TerminalExecutionMessage):
309
+ return []
310
+ t = self._theme
311
+ label = t.dim("$ " + msg.command)
312
+ if msg.cancelled:
313
+ label += " " + BRIGHT_RED + "(cancelled)" + RESET
314
+ elif msg.exit_code is not None and msg.exit_code != 0:
315
+ label += " " + BRIGHT_RED + f"(exit {msg.exit_code})" + RESET
316
+ lines = [label]
317
+ if msg.output:
318
+ for line in msg.output.rstrip().split("\n"):
319
+ lines.append(" " + t.dim(line))
320
+ if self._streaming:
321
+ lines.append(" " + t.stream_cursor("▋"))
322
+ return lines
323
+
324
+ def _render_custom(self, msg: Any, width: int) -> list[str]:
325
+ from tau.message.types import CustomMessage, TextContent, LinesContent
326
+ if not isinstance(msg, CustomMessage):
327
+ return []
328
+ from tau.tui.message_renderers import message_renderer_registry
329
+ custom = message_renderer_registry.render(msg, self._theme, width)
330
+ if custom is not None:
331
+ return custom
332
+ t = self._theme
333
+ for item in msg.contents:
334
+ if isinstance(item, LinesContent):
335
+ color_fn = t.tool_result_err if item.notify_type == "error" else None
336
+ return apply_render_shell(item.lines, t, color_fn)
337
+ if isinstance(item, TextContent) and item.content:
338
+ lines = wrap(item.content.rstrip(), max(1, width - visible_width(_RESULT_INDENT) - 4))
339
+ return apply_render_shell([l for l in lines], t)
340
+ return []
341
+
342
+ def _render_template_invocation(self, msg: Any, width: int) -> list[str]:
343
+ from tau.message.types import TemplateInvocationMessage
344
+ from tau.tui.ansi import BOLD, RESET, DIM
345
+ if not isinstance(msg, TemplateInvocationMessage):
346
+ return []
347
+ t = self._theme
348
+ label = BOLD + t.tool_arrow("[template]") + RESET
349
+ if msg.expanded:
350
+ lines = [f" {label}"]
351
+ header = f" {BOLD}{msg.name}{RESET}"
352
+ if msg.args:
353
+ header += t.dim(f" {msg.args}")
354
+ lines.append(header)
355
+ lines.append("")
356
+ for line in msg.expanded_content.splitlines():
357
+ lines.append(f" {t.dim(line) if line.strip() == '' else line}")
358
+ lines.append("")
359
+ else:
360
+ hint = t.dim(" (ctrl+o to expand)")
361
+ name_args = msg.name + (f" {msg.args}" if msg.args else "")
362
+ lines = [f" {label} {name_args}{hint}"]
363
+ return lines
364
+
365
+ def _render_skill_invocation(self, msg: Any, width: int) -> list[str]:
366
+ from tau.message.types import SkillInvocationMessage
367
+ from tau.tui.ansi import BOLD, RESET
368
+ if not isinstance(msg, SkillInvocationMessage):
369
+ return []
370
+ t = self._theme
371
+ label = BOLD + t.tool_arrow("[skill]") + RESET
372
+ if msg.expanded:
373
+ lines = [f" {label}"]
374
+ header = f" {BOLD}{msg.name}{RESET}"
375
+ if msg.args:
376
+ header += t.dim(f" {msg.args}")
377
+ lines.append(header)
378
+ lines.append("")
379
+ for line in msg.content.splitlines():
380
+ lines.append(f" {line}")
381
+ lines.append("")
382
+ else:
383
+ hint = t.dim(" (ctrl+o to expand)")
384
+ name_args = msg.name + (f" {msg.args}" if msg.args else "")
385
+ lines = [f" {label} {name_args}{hint}"]
386
+ return lines
387
+
388
+ def _render_tool_message(self, msg: Any, width: int) -> list[str]:
389
+ from tau.message.types import ToolMessage, ToolResultContent
390
+ if not isinstance(msg, ToolMessage):
391
+ return []
392
+ lines: list[str] = []
393
+
394
+ for item in msg.contents:
395
+ if isinstance(item, ToolResultContent):
396
+ lines.extend(self._render_tool_result(item, width, item.tool_name))
397
+
398
+ return lines
399
+
400
+ def _render_tool_result(self, item: Any, width: int, tool_name: str = "") -> list[str]:
401
+ from tau.message.types import ToolResultContent
402
+ if not isinstance(item, ToolResultContent):
403
+ return []
404
+
405
+ tool = self._tool_lookup(tool_name) if (self._tool_lookup and tool_name) else None
406
+ if tool is not None and tool.render_result is not None:
407
+ from tau.tool.types import ToolRenderOptions
408
+ opts = ToolRenderOptions(
409
+ is_error=item.is_error,
410
+ expanded=self._expanded,
411
+ is_partial=self._streaming,
412
+ metadata=item.metadata,
413
+ )
414
+ custom = tool.render_result(item.content, opts)
415
+ if custom:
416
+ # A custom renderer must return one terminal line per element.
417
+ # Defensively flatten any embedded newlines so the differential
418
+ # renderer's per-line height accounting stays correct — a single
419
+ # element spanning multiple rows otherwise corrupts the diff.
420
+ if any("\n" in str(c) for c in custom):
421
+ custom = [seg for c in custom for seg in str(c).split("\n")]
422
+ if tool.render_shell == "default":
423
+ t = self._theme
424
+ color_fn = t.tool_result_err if item.is_error else t.tool_result_ok
425
+ framed = list(custom)
426
+ framed[0] = color_fn(framed[0])
427
+ lines = apply_render_shell(framed, t)
428
+ else:
429
+ lines = list(custom)
430
+ lines.extend(_render_extra_blocks(item.metadata, self._expanded, self._theme))
431
+ return lines
432
+
433
+ t = self._theme
434
+ color_fn = t.tool_result_err if item.is_error else t.tool_result_ok
435
+ content = item.content.strip() if item.content else ""
436
+ all_lines = content.split("\n") if content else []
437
+ if not all_lines:
438
+ rendered = [color_fn("(no output)")]
439
+ elif not item.is_error and _is_diff(content):
440
+ from tau.tui.diff import render_diff
441
+ diff_lines = render_diff(
442
+ content,
443
+ added=t.diff_added,
444
+ removed=t.diff_removed,
445
+ context=t.diff_context,
446
+ hunk=t.diff_hunk,
447
+ inverse=t.diff_inverse,
448
+ )
449
+ if self._expanded or len(diff_lines) <= 3:
450
+ rendered = diff_lines or [color_fn("(empty diff)")]
451
+ else:
452
+ rendered = diff_lines[:3] + [t.dim(f"({len(diff_lines)} lines — ctrl+o to expand)")]
453
+ elif self._expanded or len(all_lines) == 1:
454
+ rendered = [color_fn(all_lines[0])] + [t.dim(l) for l in all_lines[1:]]
455
+ else:
456
+ rendered = [color_fn(all_lines[0]), t.dim(f"({len(all_lines)} lines)")]
457
+ lines = apply_render_shell(rendered, t)
458
+ lines.extend(_render_extra_blocks(item.metadata, self._expanded, self._theme))
459
+ return lines
460
+
461
+
462
+ def _render_extra_blocks(metadata: dict, expanded: bool, theme: Any) -> list[str]:
463
+ """Render generic extension blocks appended below any tool result."""
464
+ blocks = (metadata or {}).get("_extra_blocks")
465
+ if not blocks:
466
+ return []
467
+ lines: list[str] = []
468
+ for block in blocks:
469
+ block_lines: list[str] = block.get("expanded" if expanded else "collapsed") or []
470
+ if not block_lines:
471
+ continue
472
+ lines.append(f"{_RESULT_INDENT}{theme.dim('└')} {block_lines[0]}")
473
+ lines.extend(f"{_RESULT_INDENT} {l}" for l in block_lines[1:])
474
+ return lines
475
+
476
+
477
+ # ── MessageList ───────────────────────────────────────────────────────────────
478
+
479
+ class MessageList(Component):
480
+ """
481
+ Scrollable list of MessageBlock objects rendered inside a fixed-height
482
+ viewport. Pass a MessageTheme to MessageList to apply it to all new blocks.
483
+ """
484
+
485
+ def __init__(
486
+ self,
487
+ height: int = 20,
488
+ theme: MessageTheme | None = None,
489
+ user_prefix: str = "❯ ",
490
+ ) -> None:
491
+ self._blocks: list[MessageBlock] = []
492
+ self._height = height
493
+ self._scroll = 0
494
+ self._auto_scroll = True
495
+ self._focused = False
496
+ self._theme = theme or MessageTheme()
497
+ self._user_prefix = user_prefix
498
+ self._tool_lookup: Callable[[str], "Tool | None"] | None = None
499
+
500
+ # -------------------------------------------------------------------------
501
+ # Public API
502
+ # -------------------------------------------------------------------------
503
+
504
+ def set_height(self, height: int) -> None:
505
+ self._height = max(1, height)
506
+
507
+ def set_theme(self, theme: MessageTheme) -> None:
508
+ self._theme = theme
509
+ for block in self._blocks:
510
+ block.set_theme(theme)
511
+
512
+ def set_user_prefix(self, prefix: str) -> None:
513
+ self._user_prefix = prefix
514
+ for block in self._blocks:
515
+ block.set_user_prefix(prefix)
516
+
517
+ def set_tool_lookup(self, fn: Callable[[str], "Tool | None"] | None) -> None:
518
+ self._tool_lookup = fn
519
+ for block in self._blocks:
520
+ block.set_tool_lookup(fn)
521
+
522
+ def toggle_tool_results_expanded(self) -> None:
523
+ """Ctrl+E — toggle expanded/collapsed view for all tool result blocks."""
524
+ from tau.message.types import AssistantMessage, ToolMessage
525
+ targets = [
526
+ b for b in self._blocks
527
+ if isinstance(b.message, (AssistantMessage, ToolMessage))
528
+ ]
529
+ if not targets:
530
+ return
531
+ new_state = not targets[-1]._expanded # type: ignore[union-attr]
532
+ for b in targets:
533
+ b._expanded = new_state
534
+ b.invalidate()
535
+
536
+ def toggle_invocations_expanded(self) -> None:
537
+ """Ctrl+O — toggle expand/collapse for all template and skill invocation blocks."""
538
+ from tau.message.types import TemplateInvocationMessage, SkillInvocationMessage
539
+ targets = [
540
+ b for b in self._blocks
541
+ if isinstance(b.message, (TemplateInvocationMessage, SkillInvocationMessage))
542
+ ]
543
+ if not targets:
544
+ return
545
+ new_state = not targets[-1].message.expanded # type: ignore[union-attr]
546
+ for b in targets:
547
+ b.message.expanded = new_state # type: ignore[union-attr]
548
+ b.invalidate()
549
+
550
+ def add_block(self, block: MessageBlock) -> None:
551
+ self._blocks.append(block)
552
+ if self._auto_scroll:
553
+ self._scroll = 0
554
+
555
+ def remove_last(self) -> bool:
556
+ """Remove the last block (used to undo a user message on pre-stream abort)."""
557
+ if self._blocks:
558
+ self._blocks.pop()
559
+ return True
560
+ return False
561
+
562
+ def remove_pending_user_turn(self) -> bool:
563
+ """Pop trailing blocks up to and including the most recent user message.
564
+
565
+ Used to undo a pre-stream abort. ``message_start`` may have already added
566
+ an empty assistant placeholder block (the model began a message but no
567
+ token arrived yet), so removing only the last block would drop that
568
+ placeholder and leave the user message visible. This removes both.
569
+ Returns True if a user message was removed.
570
+ """
571
+ from tau.message.types import UserMessage
572
+ while self._blocks:
573
+ block = self._blocks.pop()
574
+ if isinstance(block.message, UserMessage):
575
+ return True
576
+ return False
577
+
578
+ def clear(self) -> None:
579
+ self._blocks.clear()
580
+ self._scroll = 0
581
+ self._auto_scroll = True
582
+
583
+ def add_message(self, message: object, streaming: bool = False) -> MessageBlock:
584
+ block = MessageBlock(
585
+ message,
586
+ streaming=streaming,
587
+ theme=self._theme,
588
+ user_prefix=self._user_prefix,
589
+ tool_lookup=self._tool_lookup,
590
+ )
591
+ self.add_block(block)
592
+ return block
593
+
594
+ def set_focused(self, focused: bool) -> None:
595
+ self._focused = focused
596
+
597
+ def scroll_up(self, n: int = 1) -> None:
598
+ self._scroll += n
599
+ self._auto_scroll = False
600
+
601
+ def scroll_down(self, n: int = 1) -> None:
602
+ self._scroll = max(0, self._scroll - n)
603
+ if self._scroll == 0:
604
+ self._auto_scroll = True
605
+
606
+ def scroll_to_bottom(self) -> None:
607
+ self._scroll = 0
608
+ self._auto_scroll = True
609
+
610
+ def scroll_to_top(self) -> None:
611
+ self._auto_scroll = False
612
+ self._scroll = 999_999
613
+
614
+ @property
615
+ def at_bottom(self) -> bool:
616
+ return self._scroll == 0
617
+
618
+ # -------------------------------------------------------------------------
619
+ # Component
620
+ # -------------------------------------------------------------------------
621
+
622
+ def all_lines(self, width: int) -> list[str]:
623
+ return self._render_blocks(width)
624
+
625
+ def render(self, width: int) -> list[str]:
626
+ # In scrollback mode the terminal's own buffer handles scrolling.
627
+ # Return all rendered lines without any clipping or top-padding so that
628
+ # (a) no blank lines appear above content when the list is short, and
629
+ # (b) old messages naturally flow into the terminal's scrollback buffer
630
+ # as new content pushes them off the visible viewport.
631
+ return self._render_blocks(width)
632
+
633
+ def _render_blocks(self, width: int) -> list[str]:
634
+ from tau.message.types import AssistantMessage, ToolCallContent, ToolMessage
635
+
636
+ lines: list[str] = []
637
+ index = 0
638
+ while index < len(self._blocks):
639
+ block = self._blocks[index]
640
+ next_message = (
641
+ self._blocks[index + 1].message
642
+ if index + 1 < len(self._blocks)
643
+ else None
644
+ )
645
+ message = block.message
646
+ followed_by_tool_result = (
647
+ isinstance(message, AssistantMessage)
648
+ and any(isinstance(item, ToolCallContent) for item in message.contents)
649
+ and isinstance(next_message, ToolMessage)
650
+ )
651
+ if followed_by_tool_result:
652
+ lines.extend(block.render_with_tool_results(next_message, width))
653
+ index += 2
654
+ continue
655
+
656
+ lines.extend(block.render(width))
657
+ index += 1
658
+ return lines
659
+
660
+ def handle_input(self, event: InputEvent) -> bool:
661
+ if not self._focused or not isinstance(event, KeyEvent):
662
+ return False
663
+ match str(event):
664
+ case "page_up" | "b": self.scroll_up(self._height)
665
+ case "page_down" | "space": self.scroll_down(self._height)
666
+ case "up" | "k": self.scroll_up(1)
667
+ case "down" | "j": self.scroll_down(1)
668
+ case "end" | "shift+g": self.scroll_to_bottom()
669
+ case "home" | "g": self.scroll_to_top()
670
+ case _: return False
671
+ return True
672
+
673
+ def invalidate(self) -> None:
674
+ for block in self._blocks:
675
+ block.invalidate()
676
+
677
+
678
+ # ── Arg formatter ─────────────────────────────────────────────────────────────
679
+
680
+ def _format_args(args: dict, max_width: int) -> str:
681
+ if not args:
682
+ return ""
683
+ parts = []
684
+ for k, v in args.items():
685
+ v_str = str(v)
686
+ if len(v_str) > 40:
687
+ v_str = v_str[:37] + "…"
688
+ parts.append(f"{k}={v_str}")
689
+ result = " ".join(parts)
690
+ if len(result) > max_width:
691
+ result = result[: max_width - 1] + "…"
692
+ return result