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/ui_context.py ADDED
@@ -0,0 +1,768 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import weakref
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING, Any, Callable
7
+
8
+ if TYPE_CHECKING:
9
+ from tau.tui.component import Component
10
+ from tau.tui.components.layout import Layout
11
+ from tau.tui.theme import LayoutTheme
12
+ from tau.tui.input import InputEvent
13
+ from tau.settings.manager import SettingsManager
14
+
15
+
16
+ @dataclass
17
+ class FooterData:
18
+ """Live data passed to enhanced footer factories as the third argument.
19
+
20
+ Factories with signature ``(tui, theme, footer_data)`` receive this on
21
+ every render so they can display up-to-date stats without polling.
22
+ """
23
+ git_branch: str = ""
24
+ context_tokens: int | None = None
25
+ context_window: int = 0
26
+ context_percent: float | None = None
27
+ active_extensions: list[str] = field(default_factory=list)
28
+ model_id: str = ""
29
+ provider_id: str = ""
30
+
31
+
32
+ class _NullHandle:
33
+ """Returned by show_overlay when there is no active layout (headless mode)."""
34
+ def close(self) -> None:
35
+ pass
36
+ hide = close
37
+
38
+
39
+ class _InterceptComponent:
40
+ """Wraps a Component and runs an on_handle interceptor before its handle_input.
41
+
42
+ Intentionally does not subclass Component to avoid the ABC machinery —
43
+ duck-typing is sufficient since TUI only calls render/handle_input/invalidate.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ inner: "Component",
49
+ on_handle: Callable[["InputEvent"], bool | None],
50
+ ) -> None:
51
+ self._inner = inner
52
+ self._on_handle = on_handle
53
+
54
+ def render(self, width: int) -> list[str]:
55
+ return self._inner.render(width)
56
+
57
+ def handle_input(self, event: "InputEvent") -> bool:
58
+ if self._on_handle(event) is True:
59
+ return True
60
+ return self._inner.handle_input(event)
61
+
62
+ def invalidate(self) -> None:
63
+ self._inner.invalidate()
64
+
65
+
66
+ class UIContext:
67
+ """
68
+ Runtime TUI customization API — available as ``ctx.ui`` inside extension handlers.
69
+
70
+ Allows extensions to inject widgets, replace the footer, add status slots,
71
+ and replace the input editor — all on the live layout.
72
+ """
73
+
74
+ def __init__(self, layout: "Layout", settings: "SettingsManager | None" = None) -> None:
75
+ self._layout_ref: weakref.ref["Layout"] = weakref.ref(layout)
76
+ self._settings = settings
77
+
78
+ def _layout(self) -> "Layout | None":
79
+ return self._layout_ref()
80
+
81
+ # -------------------------------------------------------------------------
82
+ # Widgets
83
+ # -------------------------------------------------------------------------
84
+
85
+ def set_widget(
86
+ self,
87
+ id: str,
88
+ widget: "Component | list[str]",
89
+ placement: str = "above_editor",
90
+ ) -> None:
91
+ """Inject a widget above or below the editor. placement: 'above_editor' | 'below_editor'."""
92
+ layout = self._layout()
93
+ if layout is not None:
94
+ layout.set_widget(id, widget, placement)
95
+
96
+ def remove_widget(self, id: str) -> None:
97
+ """Remove a previously added widget by id."""
98
+ layout = self._layout()
99
+ if layout is not None:
100
+ layout.remove_widget(id)
101
+
102
+ # -------------------------------------------------------------------------
103
+ # Footer
104
+ # -------------------------------------------------------------------------
105
+
106
+ def set_footer(self, component_or_factory: "Component | Callable[..., Component]") -> None:
107
+ """Replace the footer with a custom component or factory.
108
+
109
+ Three factory signatures are supported:
110
+
111
+ - ``factory()`` — zero-argument
112
+ - ``factory(tui, theme)`` — receives TUI and theme objects
113
+ - ``factory(tui, theme, footer_data)`` — also receives live ``FooterData``
114
+
115
+ Pass ``None`` to revert to the built-in footer.
116
+ """
117
+ import inspect
118
+ layout = self._layout()
119
+ if layout is None:
120
+ return
121
+ if callable(component_or_factory):
122
+ try:
123
+ sig = inspect.signature(component_or_factory)
124
+ arity = len(sig.parameters)
125
+ except (ValueError, TypeError):
126
+ arity = 0
127
+ if arity >= 3:
128
+ footer_data = self._build_footer_data(layout)
129
+ tui = getattr(layout, "_tui", None)
130
+ theme = getattr(layout, "_theme", None)
131
+ component = component_or_factory(tui, theme, footer_data)
132
+ elif arity >= 2:
133
+ tui = getattr(layout, "_tui", None)
134
+ theme = getattr(layout, "_theme", None)
135
+ component = component_or_factory(tui, theme)
136
+ else:
137
+ component = component_or_factory()
138
+ else:
139
+ component = component_or_factory
140
+ layout.set_custom_footer(component)
141
+
142
+ @staticmethod
143
+ def _build_footer_data(layout: "Layout") -> FooterData:
144
+ """Collect live stats to populate a FooterData for enhanced footer factories."""
145
+ git_branch = ""
146
+ footer = getattr(layout, "footer", None)
147
+ if footer is not None:
148
+ git_branch = getattr(footer, "_git_branch", "") or ""
149
+
150
+ context_tokens: int | None = None
151
+ context_window = 0
152
+ context_percent: float | None = None
153
+ tui = getattr(layout, "_tui", None)
154
+ runtime = getattr(tui, "_runtime", None) if tui is not None else None
155
+ agent = getattr(runtime, "agent", None) if runtime is not None else None
156
+ if agent is not None:
157
+ usage_fn = getattr(agent, "get_context_usage", None)
158
+ if callable(usage_fn):
159
+ usage = usage_fn()
160
+ if usage is not None:
161
+ context_tokens = getattr(usage, "tokens", None)
162
+ context_window = getattr(usage, "context_window", 0) or 0
163
+ context_percent = getattr(usage, "percent", None)
164
+
165
+ model_id = ""
166
+ provider_id = ""
167
+ if agent is not None:
168
+ engine = getattr(agent, "_engine", None)
169
+ if engine is not None:
170
+ llm = getattr(engine, "llm", None)
171
+ if llm is not None:
172
+ model_id = getattr(getattr(llm, "model", None), "id", "") or ""
173
+ provider_id = getattr(llm, "provider_id", "") or ""
174
+
175
+ active_extensions: list[str] = []
176
+ if runtime is not None:
177
+ ext_mgr = getattr(runtime, "extension_manager", None)
178
+ if ext_mgr is not None:
179
+ exts = getattr(ext_mgr, "extensions", None) or []
180
+ active_extensions = [getattr(e, "name", "") for e in exts if getattr(e, "name", "")]
181
+
182
+ return FooterData(
183
+ git_branch=git_branch,
184
+ context_tokens=context_tokens,
185
+ context_window=context_window,
186
+ context_percent=context_percent,
187
+ active_extensions=active_extensions,
188
+ model_id=model_id,
189
+ provider_id=provider_id,
190
+ )
191
+
192
+ def restore_footer(self) -> None:
193
+ """Revert to the built-in FooterBar."""
194
+ layout = self._layout()
195
+ if layout is not None:
196
+ layout.set_custom_footer(None)
197
+
198
+ # -------------------------------------------------------------------------
199
+ # Editor replacement
200
+ # -------------------------------------------------------------------------
201
+
202
+ def set_editor_component(self, factory: "Callable[[Any, Any], Component] | None") -> None:
203
+ """Replace the input editor, or pass None to restore the default.
204
+
205
+ Factory receives ``(InputTheme, keybindings)`` and returns a Component.
206
+ """
207
+ layout = self._layout()
208
+ if layout is not None:
209
+ layout.set_custom_input(factory)
210
+
211
+ def get_editor_component(self) -> "Callable[[Any, Any], Component] | None":
212
+ """Return the currently installed custom editor factory, or None when using the default."""
213
+ layout = self._layout()
214
+ if layout is None:
215
+ return None
216
+ return getattr(layout, "_custom_input_factory", None)
217
+
218
+ # -------------------------------------------------------------------------
219
+ # Overlay / interactive dialogs (async — usable from async command handlers)
220
+ # -------------------------------------------------------------------------
221
+
222
+ async def select(self, title: str, options: list[str]) -> str | None:
223
+ """Show a picker and return the chosen option, or None if cancelled.
224
+
225
+ Usage::
226
+
227
+ async def handler(ctx, args):
228
+ choice = await ctx.ui.select("Pick one", ["Foo", "Bar"])
229
+ if choice:
230
+ ctx.notify(f"You picked {choice}")
231
+ """
232
+ layout = self._layout()
233
+ if layout is None:
234
+ return None
235
+ from tau.tui.components.select_list import SelectItem
236
+ items: list[SelectItem[str]] = [
237
+ SelectItem(label=o, description=title if i == 0 else "", value=o)
238
+ for i, o in enumerate(options)
239
+ ]
240
+ loop = asyncio.get_running_loop()
241
+ fut: asyncio.Future[str | None] = loop.create_future()
242
+
243
+ def _commit(value: str) -> None:
244
+ if not fut.done():
245
+ fut.set_result(value)
246
+
247
+ def _cancel() -> None:
248
+ if not fut.done():
249
+ fut.set_result(None)
250
+
251
+ layout.open_tree_selector(items, _commit, _cancel)
252
+ return await fut
253
+
254
+ async def confirm(self, title: str, message: str = "") -> bool:
255
+ """Show a Yes/No picker and return True if the user chose Yes.
256
+
257
+ Usage::
258
+
259
+ async def handler(ctx, args):
260
+ if await ctx.ui.confirm("Delete?", "This cannot be undone."):
261
+ do_delete()
262
+ """
263
+ label = f"{title}: {message}" if message else title
264
+ result = await self.select(label, ["Yes", "No"])
265
+ return result == "Yes"
266
+
267
+ async def prompt(self, label: str, *, secret: bool = False) -> str | None:
268
+ """Show a single-line text input overlay and return the entered text, or None if cancelled.
269
+
270
+ Usage::
271
+
272
+ async def handler(ctx, args):
273
+ key = await ctx.ui.prompt("Enter API key", secret=True)
274
+ if key:
275
+ save_key(key)
276
+ """
277
+ layout = self._layout()
278
+ if layout is None:
279
+ return None
280
+ loop = asyncio.get_running_loop()
281
+ fut: asyncio.Future[str | None] = loop.create_future()
282
+
283
+ def _commit(value: str) -> None:
284
+ if not fut.done():
285
+ fut.set_result(value)
286
+
287
+ def _cancel() -> None:
288
+ if not fut.done():
289
+ fut.set_result(None)
290
+
291
+ layout.open_prompt(label, _commit, _cancel, secret=secret)
292
+ return await fut
293
+
294
+ async def editor(self, title: str, prefill: str = "") -> str | None:
295
+ """Show a floating multi-line text editor. Returns the saved text, or None if cancelled.
296
+
297
+ The editor opens as a full-screen-width overlay. The user presses
298
+ ``Ctrl+S`` to confirm or ``Escape`` to cancel.
299
+
300
+ Usage::
301
+
302
+ async def handler(ctx, args):
303
+ text = await ctx.ui.editor("Edit instructions", prefill="Be concise.")
304
+ if text is not None:
305
+ ctx.runtime.settings_manager.set_custom_instructions(text)
306
+ """
307
+ layout = self._layout()
308
+ if layout is None:
309
+ return None
310
+ loop = asyncio.get_running_loop()
311
+ fut: asyncio.Future[str | None] = loop.create_future()
312
+
313
+ def _commit(value: str) -> None:
314
+ if not fut.done():
315
+ fut.set_result(value)
316
+
317
+ def _cancel() -> None:
318
+ if not fut.done():
319
+ fut.set_result(None)
320
+
321
+ layout.open_editor(title, prefill, _commit, _cancel)
322
+ return await fut
323
+
324
+ async def custom(
325
+ self,
326
+ factory: "Callable[..., Component]",
327
+ options: Any = None,
328
+ ) -> Any:
329
+ """Show a fully custom focusable component and wait for it to complete.
330
+
331
+ ``factory`` is called with ``(tui, theme, keybindings, done)`` and must
332
+ return a ``Component``. Call ``done(value)`` from inside the component
333
+ (or from a key handler) to resolve the awaitable and close the overlay.
334
+
335
+ ``options.on_handle`` — optional key interceptor that runs before the
336
+ component's own ``handle_input``; return ``True`` to consume the event.
337
+
338
+ Usage::
339
+
340
+ from tau.tui.overlay import CustomOptions, OverlayOptions
341
+
342
+ class CounterComponent(Component):
343
+ def __init__(self, done):
344
+ self._count = 0
345
+ self._done = done
346
+
347
+ def render(self, width):
348
+ return [f" Count: {self._count} (Enter to confirm, Esc to cancel)"]
349
+
350
+ def handle_input(self, event):
351
+ if event.matches("enter"):
352
+ self._done(self._count)
353
+ return True
354
+ if event.matches("escape"):
355
+ self._done(None)
356
+ return True
357
+ if event.matches("up"):
358
+ self._count += 1
359
+ return True
360
+ return False
361
+
362
+ result = await ctx.ui.custom(
363
+ lambda tui, theme, kb, done: CounterComponent(done),
364
+ CustomOptions(overlay_options=OverlayOptions(width="40%", anchor="center")),
365
+ )
366
+ """
367
+ layout = self._layout()
368
+ if layout is None:
369
+ return None
370
+
371
+ from tau.tui.overlay import CustomOptions as _CO, OverlayOptions
372
+ from tau.tui.keybindings import get_keybindings
373
+
374
+ opts = options if options is not None else _CO()
375
+ loop = asyncio.get_running_loop()
376
+ fut: asyncio.Future[Any] = loop.create_future()
377
+ handle_ref: list[Any] = [None]
378
+
379
+ def _done(value: Any) -> None:
380
+ if not fut.done():
381
+ fut.set_result(value)
382
+ if handle_ref[0] is not None:
383
+ handle_ref[0].close()
384
+ handle_ref[0] = None
385
+
386
+ component = factory(layout._tui, layout._theme, get_keybindings(), _done)
387
+
388
+ if opts.on_handle is not None:
389
+ component = _InterceptComponent(component, opts.on_handle) # type: ignore[assignment]
390
+
391
+ overlay_opts = opts.overlay_options
392
+ handle_ref[0] = layout._tui.show_overlay(component, overlay_opts) # type: ignore[arg-type]
393
+ layout._tui.request_render()
394
+
395
+ return await fut
396
+
397
+ def show_overlay(
398
+ self,
399
+ component: "Component",
400
+ width: int | str = "60%",
401
+ max_height: int | str = "80%",
402
+ anchor: str = "center",
403
+ non_capturing: bool = False,
404
+ ) -> "object":
405
+ """Show a floating overlay window and return an OverlayHandle.
406
+
407
+ The handle has a ``close()`` method to dismiss the overlay.
408
+
409
+ Usage::
410
+
411
+ from tau.tui.components.picker_overlay import TextOverlay
412
+
413
+ handle = ctx.ui.show_overlay(
414
+ TextOverlay(["Hello!", "Press Esc to close"]),
415
+ width="50%",
416
+ anchor="top-right",
417
+ non_capturing=True,
418
+ )
419
+ # dismiss later:
420
+ handle.close()
421
+ """
422
+ layout = self._layout()
423
+ if layout is None:
424
+ return _NullHandle()
425
+ from typing import cast as _cast
426
+ from tau.tui.overlay import OverlayOptions, OverlayAnchor
427
+ opts = OverlayOptions(
428
+ width=width,
429
+ max_height=max_height,
430
+ anchor=_cast(OverlayAnchor, anchor),
431
+ non_capturing=non_capturing,
432
+ )
433
+ return layout._tui.show_overlay(component, opts)
434
+
435
+ def notify(self, message: str | list[str], type: str = "info") -> None: # noqa: A002
436
+ """Show an inline system notification in the message list.
437
+
438
+ Pass a str for plain text or a list[str] of pre-rendered lines to get
439
+ the same └ framing used by tool results (apply_render_shell).
440
+
441
+ Usage::
442
+
443
+ ctx.ui.notify("Done!")
444
+ ctx.ui.notify(["connected", " pyright (./)"])
445
+ """
446
+ layout = self._layout()
447
+ if layout is None:
448
+ return
449
+ import time
450
+ from typing import cast
451
+ from tau.message.types import CustomMessage, TextContent, LinesContent, ImageContent
452
+ custom_type = "tool" if type == "tool" else "system"
453
+ _contents = (
454
+ [LinesContent(lines=message, notify_type=type)]
455
+ if isinstance(message, list)
456
+ else [TextContent(content=message)]
457
+ )
458
+ msg = CustomMessage(
459
+ custom_type=custom_type,
460
+ timestamp=time.time(),
461
+ contents=cast(list[TextContent | ImageContent | LinesContent], _contents),
462
+ )
463
+ layout.add_message(msg)
464
+ layout._tui.request_render()
465
+
466
+ def clear_messages(self) -> None:
467
+ """Clear all messages from the message list."""
468
+ layout = self._layout()
469
+ if layout is None:
470
+ return
471
+ layout.clear_messages()
472
+ layout._tui.request_render()
473
+
474
+ # -------------------------------------------------------------------------
475
+ # Header
476
+ # -------------------------------------------------------------------------
477
+
478
+ def set_header(
479
+ self,
480
+ factory: "Component | Callable[[], Component] | None",
481
+ ) -> None:
482
+ """Inject a component above the message list, or pass None to remove it.
483
+
484
+ Usage::
485
+
486
+ from tau.tui.component import StaticComponent
487
+ ctx.ui.set_header(StaticComponent(["── My Extension ──"]))
488
+ """
489
+ layout = self._layout()
490
+ if layout is not None:
491
+ layout.set_header(factory)
492
+
493
+ # -------------------------------------------------------------------------
494
+ # Terminal title
495
+ # -------------------------------------------------------------------------
496
+
497
+ def set_title(self, title: str) -> None:
498
+ """Set the terminal window/tab title.
499
+
500
+ Usage::
501
+
502
+ ctx.ui.set_title("My Agent – session 42")
503
+ """
504
+ layout = self._layout()
505
+ if layout is not None:
506
+ layout._tui.terminal.set_title(title)
507
+
508
+ # -------------------------------------------------------------------------
509
+ # Spinner / working indicator
510
+ # -------------------------------------------------------------------------
511
+
512
+ def set_working_message(self, msg: str | None = None) -> None:
513
+ """Override the spinner label while the agent is working.
514
+
515
+ Pass None to clear the override and revert to the default label.
516
+
517
+ Usage::
518
+
519
+ ctx.ui.set_working_message("Fetching results…")
520
+ """
521
+ layout = self._layout()
522
+ if layout is None:
523
+ return
524
+ if msg is None:
525
+ layout.spinner.set_label(layout.spinner._theme.label_thinking)
526
+ else:
527
+ layout.spinner.set_label(msg)
528
+ layout._tui.request_render()
529
+
530
+ def set_working_visible(self, visible: bool) -> None:
531
+ """Show or force-hide the working spinner.
532
+
533
+ Usage::
534
+
535
+ ctx.ui.set_working_visible(False) # hide spinner entirely
536
+ """
537
+ layout = self._layout()
538
+ if layout is not None:
539
+ layout.spinner.set_force_hidden(not visible)
540
+
541
+ def set_working_indicator(
542
+ self,
543
+ frames: list[str] | None = None,
544
+ interval_ms: int | None = None,
545
+ ) -> None:
546
+ """Customise the spinner animation frames and/or speed.
547
+
548
+ Pass None for either argument to revert that setting to the theme default.
549
+
550
+ Usage::
551
+
552
+ ctx.ui.set_working_indicator(["◐", "◓", "◑", "◒"], interval_ms=80)
553
+ """
554
+ layout = self._layout()
555
+ if layout is not None:
556
+ layout.spinner.set_custom_indicator(frames, interval_ms)
557
+
558
+ # -------------------------------------------------------------------------
559
+ # Thinking label
560
+ # -------------------------------------------------------------------------
561
+
562
+ def set_hidden_thinking_label(self, label: str | None = None) -> None:
563
+ """Change the collapsed thinking-block label, or pass None to reset.
564
+
565
+ Usage::
566
+
567
+ ctx.ui.set_hidden_thinking_label("reasoning…")
568
+ """
569
+ layout = self._layout()
570
+ if layout is None:
571
+ return
572
+ layout._theme.message.thinking_label = label if label is not None else "thinking…"
573
+ layout._tui.request_render()
574
+
575
+ # -------------------------------------------------------------------------
576
+ # Editor content
577
+ # -------------------------------------------------------------------------
578
+
579
+ def get_editor_text(self) -> str:
580
+ """Return the current text in the input editor.
581
+
582
+ Usage::
583
+
584
+ current = ctx.ui.get_editor_text()
585
+ """
586
+ layout = self._layout()
587
+ return layout.get_editor_text() if layout is not None else ""
588
+
589
+ def set_editor_text(self, text: str) -> None:
590
+ """Replace the input editor content with the given text.
591
+
592
+ Usage::
593
+
594
+ ctx.ui.set_editor_text("Summarise the file above")
595
+ """
596
+ layout = self._layout()
597
+ if layout is not None:
598
+ layout.set_editor_text(text)
599
+
600
+ def paste_to_editor(self, text: str) -> None:
601
+ """Insert text at the current cursor position in the editor.
602
+
603
+ Usage::
604
+
605
+ ctx.ui.paste_to_editor("@path/to/file.py ")
606
+ """
607
+ layout = self._layout()
608
+ if layout is not None:
609
+ layout.paste_to_editor(text)
610
+
611
+ # -------------------------------------------------------------------------
612
+ # Theme
613
+ # -------------------------------------------------------------------------
614
+
615
+ @property
616
+ def theme(self) -> "LayoutTheme | None":
617
+ """The active LayoutTheme instance (read-only reference).
618
+
619
+ Usage::
620
+
621
+ print(ctx.ui.theme.spinner.frames)
622
+ """
623
+ layout = self._layout()
624
+ return layout._theme if layout is not None else None
625
+
626
+ def get_all_themes(self) -> list[str]:
627
+ """Return the names of all registered themes.
628
+
629
+ Usage::
630
+
631
+ names = ctx.ui.get_all_themes()
632
+ """
633
+ from tau.themes.registry import theme_registry
634
+ return theme_registry.list()
635
+
636
+ def set_theme(self, theme: "str | LayoutTheme", *, persist: bool = False) -> bool:
637
+ """Switch the active theme by name or LayoutTheme instance. Returns True on success.
638
+
639
+ Pass a name string to look up a registered theme, or a ``LayoutTheme``
640
+ instance to apply a fully custom theme directly. Set ``persist=True``
641
+ (only meaningful with a name string) to also save the choice to settings
642
+ so it survives a restart.
643
+
644
+ Usage::
645
+
646
+ ctx.ui.set_theme("dracula")
647
+ ctx.ui.set_theme("dracula", persist=True)
648
+ ctx.ui.set_theme(LayoutTheme(divider_execute=lambda s: "\\x1b[32m" + s + "\\x1b[0m"))
649
+ """
650
+ from tau.tui.theme import LayoutTheme as _LT
651
+ layout = self._layout()
652
+ if layout is None:
653
+ return False
654
+
655
+ if isinstance(theme, str):
656
+ from tau.themes.registry import theme_registry
657
+ try:
658
+ new_theme = theme_registry.get(theme)
659
+ except ValueError:
660
+ return False
661
+ layout.set_theme(new_theme)
662
+ if persist and self._settings is not None:
663
+ self._settings.set_theme(theme)
664
+ return True
665
+
666
+ if isinstance(theme, _LT):
667
+ layout.set_theme(theme)
668
+ return True
669
+
670
+ return False
671
+
672
+ # -------------------------------------------------------------------------
673
+ # Tool calls visibility
674
+ # -------------------------------------------------------------------------
675
+
676
+ def get_tools_expanded(self) -> bool:
677
+ """Return whether tool-call blocks are currently shown in the message list.
678
+
679
+ Usage::
680
+
681
+ expanded = ctx.ui.get_tools_expanded()
682
+ """
683
+ layout = self._layout()
684
+ return layout._theme.message.show_tool_calls if layout is not None else True
685
+
686
+ def set_tools_expanded(self, expanded: bool) -> None:
687
+ """Show or hide tool-call blocks in the message list.
688
+
689
+ Usage::
690
+
691
+ ctx.ui.set_tools_expanded(False) # collapse all tool calls
692
+ """
693
+ layout = self._layout()
694
+ if layout is not None:
695
+ layout._theme.message.show_tool_calls = expanded
696
+ layout.messages.invalidate()
697
+ layout._tui.request_render()
698
+
699
+ def get_tool_results_expanded(self) -> bool:
700
+ """Return whether tool result blocks are currently in expanded mode.
701
+
702
+ Usage::
703
+
704
+ expanded = ctx.ui.get_tool_results_expanded()
705
+ """
706
+ layout = self._layout()
707
+ if layout is None:
708
+ return False
709
+ if not layout.messages._blocks:
710
+ return False
711
+ return layout.messages._blocks[-1]._expanded
712
+
713
+ def set_tool_results_expanded(self, expanded: bool) -> None:
714
+ """Expand or collapse all tool result blocks.
715
+
716
+ When expanded, ``render_result`` receives ``opts.expanded=True`` so
717
+ renderers can show full output instead of a one-line preview.
718
+
719
+ Usage::
720
+
721
+ ctx.ui.set_tool_results_expanded(True) # show full tool output
722
+ ctx.ui.set_tool_results_expanded(False) # back to preview
723
+ """
724
+ layout = self._layout()
725
+ if layout is None:
726
+ return
727
+ from tau.message.types import AssistantMessage, ToolMessage
728
+ for block in layout.messages._blocks:
729
+ if isinstance(block.message, (AssistantMessage, ToolMessage)):
730
+ block._expanded = expanded
731
+ block.invalidate()
732
+ layout._tui.request_render()
733
+
734
+ # -------------------------------------------------------------------------
735
+ # Raw terminal input subscription
736
+ # -------------------------------------------------------------------------
737
+
738
+ def on_terminal_input(self, handler: "Callable[[Any], bool | None]") -> "Callable[[], None]":
739
+ """Subscribe to raw terminal input events. Returns an unsubscribe callable.
740
+
741
+ The handler runs before the editor and any overlays. Return True to
742
+ consume the event — the editor and all other handlers are skipped for
743
+ that keystroke. Return None or False to let it propagate normally.
744
+
745
+ Usage::
746
+
747
+ def on_key(event):
748
+ if event.matches("ctrl+p"):
749
+ open_my_panel()
750
+ return True # consumed — editor won't see it
751
+
752
+ unsub = ctx.ui.on_terminal_input(on_key)
753
+ # later: unsub()
754
+ """
755
+ layout = self._layout()
756
+ if layout is None:
757
+ return lambda: None
758
+ return layout._tui.on_input_intercept(handler)
759
+
760
+ # -------------------------------------------------------------------------
761
+ # Render
762
+ # -------------------------------------------------------------------------
763
+
764
+ def request_render(self) -> None:
765
+ """Request a TUI re-render — call this after updating a widget's content."""
766
+ layout = self._layout()
767
+ if layout is not None:
768
+ layout._tui.request_render()