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
tau/tui/app.py ADDED
@@ -0,0 +1,540 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Callable, TYPE_CHECKING
5
+
6
+ from tau.tui.input import KeyEvent, InputEvent
7
+ from tau.tui.components.layout import Layout
8
+ from tau.tui.keybindings import KeyMap, configure_keybindings
9
+ from tau.tui.theme import LayoutTheme
10
+ from tau.tui.tui import TUI
11
+ from tau.tui.commands.context import CommandContext
12
+ from tau.tui.agent_hooks import AgentHookHandler
13
+ from tau.tui.input_handler import InputHandler
14
+ from tau.extensions import ExtensionContext
15
+
16
+ if TYPE_CHECKING:
17
+ from tau.runtime.service import Runtime
18
+ from tau.runtime.types import RuntimeConfig
19
+
20
+
21
+ class App:
22
+ """
23
+ Wires the TUI layout to the agent runtime.
24
+
25
+ Delegates to focused collaborators:
26
+ - AgentHookHandler — subscribes to agent events, drives spinner/messages
27
+ - InputHandler — submit, paste, clipboard, steer, history
28
+ - tau.tui.commands.* — slash command logic, each receiving a CommandContext
29
+
30
+ Usage::
31
+
32
+ config = RuntimeConfig(cwd=Path.cwd(), model_id="claude-sonnet-4-6")
33
+ app = await App.create(config)
34
+ await app.run()
35
+ """
36
+
37
+ def __init__(self, runtime: Runtime, tui: TUI, layout: Layout) -> None:
38
+ self._runtime = runtime
39
+ self._tui = tui
40
+ self._layout = layout
41
+ self._input = InputHandler(runtime, layout, tui)
42
+ self._hooks = AgentHookHandler(
43
+ runtime, layout, tui,
44
+ on_palette_refresh=self.refresh_palette,
45
+ on_turn_content=self._input.mark_turn_content,
46
+ )
47
+ self._unsubs: list[Callable[[], None]] = []
48
+ self._last_ctrl_c: float = 0.0
49
+ self._last_escape: float = 0.0
50
+
51
+ # -------------------------------------------------------------------------
52
+ # Factory
53
+ # -------------------------------------------------------------------------
54
+
55
+ @classmethod
56
+ async def create(
57
+ cls,
58
+ runtime: Runtime,
59
+ theme: LayoutTheme | str | None = None,
60
+ keybindings: KeyMap | None = None,
61
+ ) -> App:
62
+ """Build the TUI around an already-constructed Runtime."""
63
+ from tau.themes.registry import theme_registry
64
+
65
+ cwd = runtime.session_manager.cwd if runtime.session_manager is not None else None
66
+ theme_registry.load_external(cwd=cwd)
67
+
68
+ from tau.prompts.registry import prompt_registry
69
+ prompt_registry.load_external(cwd=cwd)
70
+
71
+ resolved_theme: LayoutTheme | None
72
+ theme_name = "default"
73
+ if isinstance(theme, str):
74
+ theme_name = theme
75
+ resolved_theme = theme_registry.get(theme_name)
76
+ elif theme is None:
77
+ sm = runtime.settings_manager
78
+ theme_name = (sm.get_theme() if sm is not None else None) or "default"
79
+ try:
80
+ resolved_theme = theme_registry.get(theme_name)
81
+ except ValueError:
82
+ theme_name = "default"
83
+ resolved_theme = theme_registry.get(theme_name)
84
+ else:
85
+ resolved_theme = theme
86
+
87
+ sm = runtime.settings_manager
88
+ picker_max_visible = 8
89
+ autocomplete_max_visible = 5
90
+ if sm is not None:
91
+ resolved_theme.message.show_thinking = sm.get_show_thinking()
92
+ resolved_theme.message.show_tool_calls = sm.get_show_tool_calls()
93
+ resolved_theme.message.show_images = sm.get_show_images()
94
+ picker_max_visible = sm.get_picker_max_visible()
95
+ autocomplete_max_visible = sm.get_autocomplete_max_visible()
96
+
97
+ if keybindings:
98
+ configure_keybindings(keybindings)
99
+
100
+ show_hardware_cursor = False
101
+ editor_padding_x = 0
102
+ if sm is not None:
103
+ show_hardware_cursor = sm.get_show_hardware_cursor()
104
+ editor_padding_x = sm.get_editor_padding_x()
105
+
106
+ tui = TUI(show_hardware_cursor=show_hardware_cursor)
107
+ layout = Layout(
108
+ tui,
109
+ theme=resolved_theme,
110
+ picker_max_visible=picker_max_visible,
111
+ autocomplete_max_visible=autocomplete_max_visible,
112
+ editor_padding_x=editor_padding_x,
113
+ )
114
+ tui.set_focus(layout)
115
+ app = cls(runtime, tui, layout)
116
+
117
+ # ESC clears the editor only while idle; mid-stream it must fall through
118
+ # to the global key handler so it can abort the run.
119
+ layout.set_busy_check(
120
+ lambda: (a := runtime.agent) is not None and not a.is_idle()
121
+ )
122
+
123
+ runtime.set_layout(layout)
124
+
125
+ tool_registry = getattr(getattr(runtime, "_context", None), "tool_registry", None)
126
+ if tool_registry is not None:
127
+ layout.messages.set_tool_lookup(tool_registry.get)
128
+
129
+ ext = runtime.extension_runtime
130
+ if ext is not None:
131
+ from tau.tui.message_renderers import message_renderer_registry
132
+ for ctype, fn in ext.get_message_renderers().items():
133
+ message_renderer_registry.register(ctype, fn)
134
+ for provider in ext.get_autocomplete_providers():
135
+ layout.register_autocomplete_provider(provider)
136
+ return app
137
+
138
+ @classmethod
139
+ async def from_config(
140
+ cls,
141
+ config: RuntimeConfig,
142
+ theme: LayoutTheme | str | None = None,
143
+ keybindings: KeyMap | None = None,
144
+ ) -> App:
145
+ """Convenience: build Runtime from config then attach the TUI."""
146
+ from tau.runtime.service import Runtime
147
+ runtime = await Runtime.create(config)
148
+ return await cls.create(runtime, theme=theme, keybindings=keybindings)
149
+
150
+ # -------------------------------------------------------------------------
151
+ # Command context
152
+ # -------------------------------------------------------------------------
153
+
154
+ def _ctx(self) -> CommandContext:
155
+ return CommandContext(
156
+ runtime=self._runtime,
157
+ layout=self._layout,
158
+ tui=self._tui,
159
+ on_palette_refresh=self.refresh_palette,
160
+ )
161
+
162
+ def _get_current_model_key(self) -> str:
163
+ agent = self._runtime.agent
164
+ if agent is None:
165
+ return ""
166
+ llm = getattr(getattr(agent, "_engine", None), "llm", None)
167
+ model = getattr(llm, "model", None) if llm is not None else None
168
+ if model is None:
169
+ return ""
170
+ return f"{model.provider}/{model.id}"
171
+
172
+ def _on_model_palette_commit(self, model_id: str, provider: str) -> None:
173
+ import asyncio
174
+ from tau.tui.commands import model as cmd_model
175
+ asyncio.ensure_future(cmd_model._apply_model(self._ctx(), model_id, provider))
176
+
177
+ # -------------------------------------------------------------------------
178
+ # UI command registration
179
+ # -------------------------------------------------------------------------
180
+
181
+ def _register_ui_commands(self) -> None:
182
+ from tau.commands.types import CommandInfo
183
+ from tau.tui.commands import model as cmd_model
184
+ from tau.tui.commands import appearance as cmd_appearance
185
+ from tau.tui.commands import session as cmd_session
186
+ from tau.tui.commands import auth as cmd_auth
187
+ from tau.tui.commands import misc as cmd_misc
188
+
189
+ reg = [
190
+ CommandInfo(name="model", description="Switch the active model (interactive picker).",
191
+ call=lambda _r, _a: cmd_model.open_model_selector(self._ctx()),
192
+ ),
193
+ CommandInfo(name="effort", description="Set the thinking effort level for the current model.",
194
+ call=lambda _r, _a: cmd_model.open_effort_selector(self._ctx())),
195
+ CommandInfo(name="theme", description="Change the UI theme (interactive picker).",
196
+ call=lambda _r, _a: cmd_appearance.open_theme_selector(self._ctx())),
197
+ CommandInfo(name="settings", description="Show current settings.",
198
+ call=lambda _r, _a: cmd_appearance.open_settings_panel(self._ctx())),
199
+ CommandInfo(name="resume", description="Browse and resume a past session interactively.",
200
+ call=lambda _r, _a: cmd_session.open_resume_selector(self._ctx())),
201
+ CommandInfo(name="tree", description="Navigate the session tree and switch to a different branch.",
202
+ call=lambda _r, _a: cmd_session.open_tree_selector(self._ctx())),
203
+ CommandInfo(name="clone", description="Duplicate the current session at the current position.",
204
+ call=lambda _r, _a: cmd_session.cmd_clone(self._ctx())),
205
+ CommandInfo(name="session", description="Show session info and stats.",
206
+ call=lambda _r, _a: cmd_session.cmd_session(self._ctx())),
207
+ CommandInfo(name="login", description="Save an API key for a provider.",
208
+ call=lambda _r, _a: cmd_auth.open_login_selector(self._ctx())),
209
+ CommandInfo(name="logout", description="Remove stored credentials for a provider.",
210
+ call=lambda _r, _a: cmd_auth.open_logout_selector(self._ctx())),
211
+ CommandInfo(name="copy", description="Copy the last assistant message to the clipboard.",
212
+ call=lambda _r, _a: cmd_misc.cmd_copy(self._ctx())),
213
+ CommandInfo(name="help", description="List all commands and keyboard shortcuts.",
214
+ call=lambda _r, _a: cmd_misc.show_help(self._ctx()),
215
+ aliases=["?"]),
216
+ CommandInfo(name="quit", description="Exit tau.",
217
+ call=lambda _r, _a: self._tui.stop(),
218
+ aliases=["q", "exit"]),
219
+ ]
220
+ for info in reg:
221
+ self._runtime.commands.register(info)
222
+
223
+ # -------------------------------------------------------------------------
224
+ # Lifecycle
225
+ # -------------------------------------------------------------------------
226
+
227
+ def _redirect_logging_off_terminal(self) -> None:
228
+ """Keep all logging off the terminal while the TUI owns the screen.
229
+
230
+ The renderer tracks the screen with a differential model; any bytes
231
+ written to the terminal by something other than the renderer desync it
232
+ and leave stale lines (e.g. a stranded spinner). Without an explicit
233
+ handler, Python's ``logging.lastResort`` writes WARNING+ records to
234
+ stderr — and the LSP client logs the language server's stderr at WARNING
235
+ on every read. Route everything to a log file instead and neutralise the
236
+ stderr fallback so nothing reaches the TTY.
237
+ """
238
+ import logging
239
+ import sys
240
+ from tau.settings.paths import get_logs_dir
241
+ from tau.session.utils import create_session_id
242
+
243
+ root = logging.getLogger()
244
+ # Drop any handler that writes to the live terminal (e.g. --debug's
245
+ # basicConfig stderr handler) — it would corrupt the renderer.
246
+ for h in list(root.handlers):
247
+ if isinstance(h, logging.StreamHandler) and getattr(h, "stream", None) in (sys.stdout, sys.stderr):
248
+ root.removeHandler(h)
249
+ # Unconfigured loggers must never fall back to the stderr last-resort.
250
+ logging.lastResort = logging.NullHandler()
251
+ # One log file per run, named by the active session id so logs don't grow
252
+ # unbounded in a single file. Fall back to a fresh id if no session yet.
253
+ sm = self._runtime.session_manager
254
+ log_id = (sm.session_id if sm is not None else None) or create_session_id()
255
+ try:
256
+ logs_dir = get_logs_dir()
257
+ logs_dir.mkdir(parents=True, exist_ok=True)
258
+ fh = logging.FileHandler(logs_dir / f"{log_id}.log")
259
+ fh.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
260
+ root.addHandler(fh)
261
+ if root.level == logging.NOTSET or root.level > logging.WARNING:
262
+ root.setLevel(logging.WARNING)
263
+ except OSError:
264
+ # Couldn't open the log file — at least keep logs off the terminal.
265
+ root.addHandler(logging.NullHandler())
266
+
267
+ async def run(self) -> None:
268
+ """Set up hooks, replay session, then run the TUI loop."""
269
+ self._redirect_logging_off_terminal()
270
+ self._hooks.subscribe()
271
+
272
+ sm = self._runtime.settings_manager
273
+ if sm is None or not sm.get_quiet_startup():
274
+ self._replay_session()
275
+
276
+ self._hooks._refresh_model_badge()
277
+ self._input.load_history()
278
+
279
+ self._register_ui_commands()
280
+ self._layout.set_commands(self._build_palette_entries())
281
+ self._layout.set_model_callbacks(
282
+ commit_cb=self._on_model_palette_commit,
283
+ current_key_cb=self._get_current_model_key,
284
+ )
285
+
286
+ sm = self._runtime.session_manager
287
+ if sm is not None:
288
+ self._layout.set_cwd(sm.cwd)
289
+
290
+ self._input.bind()
291
+ self._tui.on_input(self._on_global_key)
292
+ self._register_extension_shortcuts()
293
+
294
+ # Fire tui_ready so extensions can run initial UI setup now that the
295
+ # layout exists (session_start fires earlier, before the layout is set).
296
+ from tau.hooks.tui import TuiReadyEvent, TuiStartEvent, TuiExitEvent
297
+ await self._runtime.hooks.emit(TuiReadyEvent())
298
+
299
+ # If the project needs a trust decision, replace the root with TrustScreen
300
+ # before the loop starts so the layout never renders until the user acts.
301
+ trust_needed = self._setup_trust_screen_if_needed()
302
+
303
+ asyncio.ensure_future(self._announce_update())
304
+
305
+ await self._runtime.hooks.emit(TuiStartEvent())
306
+ try:
307
+ await self._tui.run()
308
+ finally:
309
+ await self._runtime.hooks.emit(TuiExitEvent())
310
+ self._cleanup()
311
+
312
+ # -------------------------------------------------------------------------
313
+ # Project trust prompt
314
+ # -------------------------------------------------------------------------
315
+
316
+ def _setup_trust_screen_if_needed(self) -> bool:
317
+ """If the project needs a trust decision, swap the TUI root to TrustScreen.
318
+
319
+ Returns True if the trust screen was installed (caller can ignore the value).
320
+ The trust screen schedules its own async resolution and swaps back to the
321
+ normal layout once the user acts.
322
+ """
323
+ sm = self._runtime.settings_manager
324
+ if sm is None or sm.is_project_trusted():
325
+ return False
326
+ session_mgr = self._runtime.session_manager
327
+ if session_mgr is None:
328
+ return False
329
+ cwd = session_mgr.cwd
330
+
331
+ from tau.trust.manager import has_project_trust_inputs, get_trust_options, trust_store, TrustOption
332
+ if not has_project_trust_inputs(cwd):
333
+ return False
334
+
335
+ options = get_trust_options(cwd, session_only=True)
336
+
337
+ def _on_commit(chosen: TrustOption | None) -> None:
338
+ if chosen is None or not chosen.trusted:
339
+ # User declined trust (or cancelled) — exit instead of
340
+ # falling through to the normal agent layout.
341
+ self._tui.stop()
342
+ return
343
+
344
+ # Restore the normal layout now that the project is trusted
345
+ self._tui.clear()
346
+ self._layout.attach(self._tui)
347
+ self._tui.set_focus(self._layout)
348
+ self._tui.request_render()
349
+
350
+ trust_store.apply_option(chosen)
351
+ sm.set_project_trusted(True)
352
+
353
+ # Reload extensions so project config takes effect
354
+ import asyncio as _asyncio
355
+ async def _reload() -> None:
356
+ await self._runtime.reload_extensions()
357
+ _asyncio.ensure_future(_reload())
358
+
359
+ from tau.tui.components.trust_screen import TrustScreen
360
+ screen = TrustScreen(str(cwd), options, _on_commit)
361
+ self._layout.detach(self._tui)
362
+ self._tui.add_child(screen)
363
+ self._tui.set_focus(screen)
364
+ return True
365
+
366
+ # -------------------------------------------------------------------------
367
+ # Global key handler
368
+ # -------------------------------------------------------------------------
369
+
370
+ def _on_global_key(self, event: InputEvent) -> None:
371
+ if not isinstance(event, KeyEvent):
372
+ return
373
+
374
+ if event.matches("escape"):
375
+ import time
376
+ agent = self._runtime.agent
377
+ if agent is not None and not agent.is_idle():
378
+ self._input.escape_abort()
379
+ self._last_escape = 0.0
380
+ elif not self._layout.input.text:
381
+ # Double-escape on empty editor: perform the configured action
382
+ now = time.monotonic()
383
+ if now - self._last_escape < 0.5:
384
+ self._last_escape = 0.0
385
+ self._do_double_escape()
386
+ else:
387
+ self._last_escape = now
388
+ else:
389
+ self._last_escape = 0.0
390
+ return
391
+
392
+ if event.matches("ctrl+c"):
393
+ import time
394
+ agent = self._runtime.agent
395
+ if agent is not None and not agent.is_idle():
396
+ agent.abort()
397
+ else:
398
+ now = time.monotonic()
399
+ if now - self._last_ctrl_c < 0.5:
400
+ self._tui.stop()
401
+ else:
402
+ self._last_ctrl_c = now
403
+ self._layout.input.clear()
404
+ self._tui.request_render()
405
+ return
406
+
407
+ if event.matches("ctrl+o"):
408
+ self._layout.messages.toggle_tool_results_expanded()
409
+ self._tui.request_render()
410
+ return
411
+
412
+ if event.matches("ctrl+e"):
413
+ self._layout.messages.toggle_invocations_expanded()
414
+ self._tui.request_render()
415
+ return
416
+
417
+ if event.matches("ctrl+d"):
418
+ self._tui.stop()
419
+
420
+ def _do_double_escape(self) -> None:
421
+ """Execute the action configured for double-Escape on an empty editor."""
422
+ import asyncio
423
+ sm = self._runtime.settings_manager
424
+ action = sm.get_double_escape_action() if sm is not None else "fork"
425
+ match action:
426
+ case "none":
427
+ return
428
+ case "tree":
429
+ from tau.tui.commands import session as cmd_session
430
+ cmd_session.open_tree_selector(self._ctx())
431
+ case "fork" | _:
432
+ from tau.tui.commands import session as cmd_session
433
+ cmd_session.cmd_clone(self._ctx())
434
+
435
+ # -------------------------------------------------------------------------
436
+ # Extension shortcuts
437
+ # -------------------------------------------------------------------------
438
+
439
+ def _register_extension_shortcuts(self) -> None:
440
+ runtime = self._runtime
441
+ for shortcut in runtime.extension_shortcuts:
442
+ key = shortcut.key
443
+ handler = shortcut.handler
444
+
445
+ def _make_handler(k, h):
446
+ def on_input(event: object) -> None:
447
+ if not isinstance(event, KeyEvent) or not event.matches(k):
448
+ return
449
+ ctx = ExtensionContext.from_runtime(runtime)
450
+ result = h(ctx)
451
+ if asyncio.iscoroutine(result):
452
+ asyncio.ensure_future(result) # type: ignore[arg-type]
453
+ return on_input
454
+
455
+ self._unsubs.append(self._tui.on_input(_make_handler(key, handler)))
456
+
457
+ # -------------------------------------------------------------------------
458
+ # Startup helpers
459
+ # -------------------------------------------------------------------------
460
+
461
+ def _build_palette_entries(self):
462
+ from tau.commands.types import CommandInfo
463
+ from tau.prompts.registry import prompt_registry
464
+
465
+ # Commands whose feature is currently switched off are hidden from the
466
+ # palette (and treated as unavailable) for this session.
467
+ sm = self._runtime.settings_manager
468
+ hidden: set[str] = set()
469
+ if sm is not None and not sm.is_compaction_enabled():
470
+ hidden.add("compact")
471
+
472
+ overrides = self._palette_dynamic_descriptions()
473
+ entries = []
474
+ for cmd in self._runtime.commands.list():
475
+ if cmd.name in hidden:
476
+ continue
477
+ if cmd.name in overrides:
478
+ from dataclasses import replace
479
+ entries.append(replace(cmd, description=overrides[cmd.name]))
480
+ else:
481
+ entries.append(cmd)
482
+ for tmpl in prompt_registry.list():
483
+ hint = f" {tmpl.argument_hint}" if tmpl.argument_hint else ""
484
+ entries.append(CommandInfo(
485
+ name=tmpl.name,
486
+ description=tmpl.description + hint,
487
+ call=lambda _r, _a: None,
488
+ argument_hint=tmpl.argument_hint,
489
+ ))
490
+ return entries
491
+
492
+ def _palette_dynamic_descriptions(self) -> dict[str, str]:
493
+ from tau.tui.commands import model as cmd_model
494
+ from tau.tui.commands import auth as cmd_auth
495
+ overrides = cmd_model.get_palette_overrides(self._runtime.agent)
496
+ overrides.update(cmd_auth.get_palette_overrides())
497
+ return overrides
498
+
499
+ def refresh_palette(self) -> None:
500
+ self._layout.set_commands(self._build_palette_entries())
501
+
502
+ def _replay_session(self) -> None:
503
+ sm = self._runtime.session_manager
504
+ if sm is None:
505
+ return
506
+ ctx = sm.build_session_context()
507
+ for msg in ctx.messages:
508
+ self._layout.add_message(msg)
509
+
510
+ # -------------------------------------------------------------------------
511
+ # Cleanup
512
+ # -------------------------------------------------------------------------
513
+
514
+ async def _announce_update(self) -> None:
515
+ task = self._runtime.version_check_task
516
+ if task is None:
517
+ return
518
+ latest = await task
519
+ if latest is None:
520
+ return
521
+ from tau.tui.ansi import BOLD, BRIGHT_YELLOW, DIM, RESET
522
+ from tau.tui.component import Column, StaticComponent
523
+ from tau.tui.components.dynamic_border import DynamicBorder
524
+ color = lambda s: BRIGHT_YELLOW + s + RESET # noqa: E731
525
+ banner = Column([
526
+ DynamicBorder(color),
527
+ StaticComponent([
528
+ f" {BRIGHT_YELLOW}⚡{RESET} Update available: {BOLD}v{latest}{RESET}"
529
+ f"{DIM} · run: tau update{RESET}",
530
+ ]),
531
+ DynamicBorder(color),
532
+ ])
533
+ self._layout.set_widget("version_update", banner, placement="above_editor")
534
+
535
+ def _cleanup(self) -> None:
536
+ self._input.save_history()
537
+ self._hooks.unsubscribe()
538
+ for unsub in self._unsubs:
539
+ unsub()
540
+ self._unsubs.clear()
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Awaitable, Callable
5
+
6
+
7
+ @dataclass
8
+ class AutocompleteItem:
9
+ """A single completion suggestion returned by a provider."""
10
+ label: str
11
+ description: str = ""
12
+ # Text inserted into the editor. When None the label itself is inserted.
13
+ insert_text: str | None = None
14
+
15
+
16
+ @dataclass
17
+ class AutocompleteContext:
18
+ """Snapshot passed to a provider's get_items() call."""
19
+ text: str # full editor text at the moment of the call
20
+ cursor_pos: int # character index of the cursor in text
21
+ trigger: str # the trigger character that activated this provider (e.g. "#")
22
+ query: str # text typed after the trigger up to the cursor (no spaces)
23
+
24
+
25
+ @dataclass
26
+ class AutocompleteRegistration:
27
+ """A provider registered by an extension via tau.add_autocomplete_provider()."""
28
+ trigger: str
29
+ get_items: Callable[
30
+ [AutocompleteContext],
31
+ list[AutocompleteItem] | Awaitable[list[AutocompleteItem]],
32
+ ]
33
+ description: str = ""
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import Literal
6
+
7
+ ImageProtocol = Literal["kitty", "iterm2"] | None
8
+
9
+
10
+ @dataclass
11
+ class TerminalCapabilities:
12
+ images: ImageProtocol
13
+ truecolor: bool
14
+ hyperlinks: bool
15
+
16
+
17
+ @dataclass
18
+ class CellDimensions:
19
+ width_px: int
20
+ height_px: int
21
+
22
+
23
+ _cached: TerminalCapabilities | None = None
24
+ _cell_dims = CellDimensions(width_px=9, height_px=18)
25
+
26
+
27
+ def get_cell_dimensions() -> CellDimensions:
28
+ return _cell_dims
29
+
30
+
31
+ def set_cell_dimensions(dims: CellDimensions) -> None:
32
+ global _cell_dims
33
+ _cell_dims = dims
34
+
35
+
36
+ def _probe_cell_dimensions() -> CellDimensions:
37
+ """Read pixel and cell sizes from the terminal via TIOCGWINSZ."""
38
+ try:
39
+ import fcntl
40
+ import struct
41
+ import termios
42
+
43
+ buf = struct.pack("HHHH", 0, 0, 0, 0)
44
+ res = fcntl.ioctl(1, termios.TIOCGWINSZ, buf)
45
+ rows, cols, width_px, height_px = struct.unpack("HHHH", res)
46
+ if rows > 0 and cols > 0 and width_px > 0 and height_px > 0:
47
+ return CellDimensions(width_px=width_px // cols, height_px=height_px // rows)
48
+ except Exception:
49
+ pass
50
+ return CellDimensions(width_px=9, height_px=18)
51
+
52
+
53
+ def _tmux_forwards_hyperlinks() -> bool:
54
+ try:
55
+ import subprocess
56
+ result = subprocess.run(
57
+ ["tmux", "display-message", "-p", "#{client_termfeatures}"],
58
+ capture_output=True, text=True, timeout=0.25,
59
+ )
60
+ return "hyperlinks" in result.stdout.split(",")
61
+ except Exception:
62
+ return False
63
+
64
+
65
+ def detect_capabilities() -> TerminalCapabilities:
66
+ term = os.environ.get("TERM", "").lower()
67
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
68
+ terminal_emulator = os.environ.get("TERMINAL_EMULATOR", "").lower()
69
+ color_term = os.environ.get("COLORTERM", "").lower()
70
+ truecolor = color_term in ("truecolor", "24bit")
71
+
72
+ if os.environ.get("TMUX") or term.startswith("tmux"):
73
+ return TerminalCapabilities(images=None, truecolor=truecolor, hyperlinks=_tmux_forwards_hyperlinks())
74
+
75
+ if term.startswith("screen"):
76
+ return TerminalCapabilities(images=None, truecolor=truecolor, hyperlinks=False)
77
+
78
+ if os.environ.get("KITTY_WINDOW_ID") or term_program == "kitty":
79
+ return TerminalCapabilities(images="kitty", truecolor=True, hyperlinks=True)
80
+
81
+ if term_program == "ghostty" or "ghostty" in term or os.environ.get("GHOSTTY_RESOURCES_DIR"):
82
+ return TerminalCapabilities(images="kitty", truecolor=True, hyperlinks=True)
83
+
84
+ if os.environ.get("WEZTERM_PANE") or term_program == "wezterm":
85
+ return TerminalCapabilities(images="kitty", truecolor=True, hyperlinks=True)
86
+
87
+ if os.environ.get("ITERM_SESSION_ID") or term_program == "iterm.app":
88
+ return TerminalCapabilities(images="iterm2", truecolor=True, hyperlinks=True)
89
+
90
+ if os.environ.get("WT_SESSION"):
91
+ return TerminalCapabilities(images=None, truecolor=True, hyperlinks=True)
92
+
93
+ if term_program == "vscode":
94
+ return TerminalCapabilities(images=None, truecolor=True, hyperlinks=True)
95
+
96
+ if term_program == "alacritty":
97
+ return TerminalCapabilities(images=None, truecolor=True, hyperlinks=False)
98
+
99
+ if terminal_emulator == "jetbrains-jediterm":
100
+ return TerminalCapabilities(images=None, truecolor=True, hyperlinks=False)
101
+
102
+ return TerminalCapabilities(images=None, truecolor=truecolor, hyperlinks=False)
103
+
104
+
105
+ def get_capabilities() -> TerminalCapabilities:
106
+ global _cached
107
+ if _cached is None:
108
+ _cached = detect_capabilities()
109
+ set_cell_dimensions(_probe_cell_dimensions())
110
+ return _cached
111
+
112
+
113
+ def reset_capabilities_cache() -> None:
114
+ global _cached
115
+ _cached = None
116
+
117
+
118
+ def is_image_line(line: str) -> bool:
119
+ return "\x1b_G" in line or "\x1b]1337;" in line
@@ -0,0 +1,3 @@
1
+ from tau.tui.commands.context import CommandContext
2
+
3
+ __all__ = ["CommandContext"]