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,232 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from tau.tui.commands.context import CommandContext
6
+
7
+
8
+ def open_login_selector(ctx: CommandContext) -> None:
9
+ """Step 1 — choose auth type: subscription (OAuth) or API key."""
10
+ from tau.inference.api.text.service import TextLLM
11
+ from tau.inference.provider.types import OAuthProvider, APIProvider
12
+ from tau.tui.components.select_list import SelectItem
13
+
14
+ has_oauth = any(isinstance(p, OAuthProvider) for p in TextLLM._providers.list())
15
+ has_api = any(isinstance(p, APIProvider) for p in TextLLM._providers.list())
16
+
17
+ if has_oauth and has_api:
18
+ items = [
19
+ SelectItem(label="Subscription", description="OAuth — GitHub Copilot, OpenAI Codex, etc.", value="oauth"),
20
+ SelectItem(label="API key", description="A static key, $ENV_VAR, or !command for a provider", value="api_key"),
21
+ ]
22
+ def on_type(auth_type: str) -> None:
23
+ if auth_type == "oauth":
24
+ open_oauth_provider_selector(ctx)
25
+ else:
26
+ open_api_key_provider_selector(ctx)
27
+ ctx.layout.open_tree_selector(items, on_type, lambda: ctx.notify("Login cancelled."))
28
+ elif has_oauth:
29
+ open_oauth_provider_selector(ctx)
30
+ else:
31
+ open_api_key_provider_selector(ctx)
32
+
33
+
34
+ def _provider_items(providers: list) -> list:
35
+ from tau.tui.components.select_list import SelectItem
36
+ return [SelectItem(label=p.name, description=p.id, value=p.id) for p in providers]
37
+
38
+
39
+ def open_oauth_provider_selector(ctx: CommandContext) -> None:
40
+ """Step 2 (OAuth path) — pick which OAuth provider to log in to."""
41
+ from tau.inference.api.text.service import TextLLM
42
+ from tau.inference.provider.types import OAuthProvider
43
+
44
+ providers = [p for p in TextLLM._providers.list() if isinstance(p, OAuthProvider)]
45
+ if not providers:
46
+ ctx.notify("No subscription providers available.")
47
+ return
48
+
49
+ items = _provider_items(providers)
50
+
51
+ def on_pick(provider_id: str) -> None:
52
+ asyncio.ensure_future(run_oauth_login(ctx, provider_id))
53
+
54
+ ctx.layout.open_tree_selector(items, on_pick, lambda: ctx.notify("Login cancelled."))
55
+
56
+
57
+ async def run_oauth_login(ctx: CommandContext, provider_id: str) -> None:
58
+ """Run the full OAuth login flow, wiring callbacks to the TUI."""
59
+ import webbrowser
60
+ from tau.tui.ansi import BOLD, DIM, RESET
61
+ from tau.inference.api.text.service import TextLLM
62
+ from tau.inference.provider.types import OAuthProvider
63
+ from tau.inference.provider.oauth.types import OAuthLoginCallbacks, OAuthPrompt, OAuthAuthInfo
64
+
65
+ provider = next(
66
+ (p for p in TextLLM._providers.list()
67
+ if isinstance(p, OAuthProvider) and p.id == provider_id),
68
+ None,
69
+ )
70
+ if provider is None:
71
+ ctx.notify(f"Provider '{provider_id}' not found.")
72
+ return
73
+
74
+ ctx.layout.open_oauth_status([
75
+ f" {BOLD}Logging in to {provider.name}{RESET} {DIM}(Esc not available during flow){RESET}",
76
+ ])
77
+
78
+ _prompt_future: asyncio.Future[str] | None = None
79
+
80
+ def on_auth(info: OAuthAuthInfo) -> None:
81
+ ctx.layout.open_oauth_status([
82
+ f" {BOLD}Logging in to {provider.name}{RESET}",
83
+ "",
84
+ f" {DIM}Open this URL in your browser:{RESET}",
85
+ f" {info.url}",
86
+ ])
87
+ if info.instructions:
88
+ ctx.layout.update_oauth_status(f" {DIM}{info.instructions}{RESET}")
89
+ webbrowser.open(info.url)
90
+
91
+ async def on_prompt(prompt: OAuthPrompt) -> str:
92
+ nonlocal _prompt_future
93
+ loop = asyncio.get_event_loop()
94
+ _prompt_future = loop.create_future()
95
+ label = prompt.message
96
+ if prompt.placeholder:
97
+ label += f" ({DIM}{prompt.placeholder}{RESET})"
98
+ ctx.layout.close_oauth_status()
99
+ ctx.layout.open_prompt(
100
+ label=label,
101
+ on_commit=lambda val: _prompt_future.set_result(val) if not _prompt_future.done() else None,
102
+ on_cancel=lambda: _prompt_future.set_exception(asyncio.CancelledError()) if not _prompt_future.done() else None,
103
+ secret=False,
104
+ )
105
+ try:
106
+ return await _prompt_future
107
+ except asyncio.CancelledError:
108
+ raise ValueError("Login cancelled")
109
+
110
+ def on_progress(message: str) -> None:
111
+ ctx.layout.open_oauth_status([
112
+ f" {BOLD}Logging in to {provider.name}{RESET}",
113
+ f" {DIM}{message}{RESET}",
114
+ ])
115
+
116
+ callbacks = OAuthLoginCallbacks(
117
+ on_auth=on_auth,
118
+ on_prompt=on_prompt,
119
+ on_progress=on_progress,
120
+ )
121
+
122
+ try:
123
+ await TextLLM._auth_manager.login(provider_id, callbacks)
124
+ ctx.layout.close_oauth_status()
125
+ ctx.notify(f"Logged in to {provider.name}. Credentials saved.")
126
+ if ctx.on_palette_refresh is not None:
127
+ ctx.on_palette_refresh()
128
+ except asyncio.CancelledError:
129
+ ctx.layout.close_oauth_status()
130
+ ctx.notify("Login cancelled.")
131
+ except Exception as exc:
132
+ ctx.layout.close_oauth_status()
133
+ msg = str(exc)
134
+ if msg and msg.lower() != "login cancelled":
135
+ ctx.notify(f"Login failed: {msg}")
136
+
137
+
138
+ def open_api_key_provider_selector(ctx: CommandContext) -> None:
139
+ """Step 2 (API key path) — pick which provider to save a key for."""
140
+ from tau.inference.api.text.service import TextLLM
141
+ from tau.inference.provider.types import APIProvider
142
+
143
+ providers = [p for p in TextLLM._providers.list() if isinstance(p, APIProvider)]
144
+ if not providers:
145
+ ctx.notify("No API key providers available.")
146
+ return
147
+
148
+ items = _provider_items(providers)
149
+
150
+ def on_pick(provider_id: str) -> None:
151
+ provider = next((p for p in providers if p.id == provider_id), None)
152
+ if provider is None:
153
+ return
154
+ ctx.layout.open_prompt(
155
+ label=f"API key for {provider.name} (literal, $ENV_VAR, or !command):",
156
+ on_commit=lambda key: _save_api_key(ctx, provider_id, provider.name, key),
157
+ on_cancel=lambda: ctx.notify("Login cancelled."),
158
+ secret=True,
159
+ )
160
+
161
+ ctx.layout.open_tree_selector(items, on_pick, lambda: ctx.notify("Login cancelled."))
162
+
163
+
164
+ def _save_api_key(ctx: CommandContext, provider_id: str, provider_name: str, key: str) -> None:
165
+ from tau.inference.api.text.service import TextLLM
166
+ from tau.auth.types import APICredential
167
+
168
+ key = key.strip()
169
+ if not key:
170
+ ctx.notify("API key cannot be empty.")
171
+ return
172
+ TextLLM._auth_manager.set(provider_id, APICredential(key=key))
173
+ # $ENV_VAR / !command references are stored as-is and resolved at runtime.
174
+ ref = " (resolved at runtime)" if key[:1] in ("$", "!") else ""
175
+ ctx.notify(f"API key saved for {provider_name}.{ref}")
176
+ if ctx.on_palette_refresh is not None:
177
+ ctx.on_palette_refresh()
178
+
179
+
180
+ def get_palette_overrides() -> dict[str, str]:
181
+ """Return dynamic palette description overrides for /login and /logout."""
182
+ overrides: dict[str, str] = {}
183
+ try:
184
+ from tau.inference.api.text.service import TextLLM
185
+ auth = TextLLM._auth_manager
186
+ auth.reload()
187
+ logged_in = auth.list()
188
+ if logged_in:
189
+ providers_str = ", ".join(logged_in)
190
+ overrides["login"] = f"Add credentials · active: {providers_str}"
191
+ overrides["logout"] = f"Remove credentials · active: {providers_str}"
192
+ else:
193
+ overrides["login"] = "Add credentials · none active"
194
+ overrides["logout"] = "Remove credentials · none active"
195
+ except Exception:
196
+ pass
197
+ return overrides
198
+
199
+
200
+ def open_logout_selector(ctx: CommandContext) -> None:
201
+ from tau.inference.api.text.service import TextLLM
202
+ from tau.tui.components.select_list import SelectItem
203
+
204
+ TextLLM._auth_manager.reload()
205
+ stored = TextLLM._auth_manager.list()
206
+ if not stored:
207
+ ctx.notify(
208
+ "No stored credentials. /logout only removes keys saved by /login — "
209
+ "environment variables are unchanged."
210
+ )
211
+ return
212
+
213
+ providers = TextLLM._providers.list()
214
+ provider_map = {p.id: p for p in providers}
215
+
216
+ items = [
217
+ SelectItem(
218
+ label=provider_map[pid].name if pid in provider_map else pid,
219
+ description=pid,
220
+ value=pid,
221
+ )
222
+ for pid in stored
223
+ ]
224
+
225
+ def on_pick(provider_id: str) -> None:
226
+ name = provider_map[provider_id].name if provider_id in provider_map else provider_id
227
+ TextLLM._auth_manager.remove(provider_id)
228
+ ctx.notify(f"Removed stored credentials for {name}.")
229
+ if ctx.on_palette_refresh is not None:
230
+ ctx.on_palette_refresh()
231
+
232
+ ctx.layout.open_tree_selector(items, on_pick, lambda: ctx.notify("Logout cancelled."))
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass, field
5
+ from typing import Callable, TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from tau.runtime.service import Runtime
9
+ from tau.tui.components.layout import Layout
10
+ from tau.tui.tui import TUI
11
+
12
+
13
+ @dataclass
14
+ class CommandContext:
15
+ """Minimal context injected into every TUI command handler.
16
+
17
+ This is the single seam between App (orchestrator) and the command
18
+ modules (feature logic). Commands receive this instead of holding a
19
+ reference to the full App, so they can be called from keyboard
20
+ shortcuts, extension APIs, or tests without instantiating App.
21
+ """
22
+ runtime: "Runtime"
23
+ layout: "Layout"
24
+ tui: "TUI"
25
+ on_palette_refresh: Callable[[], None] | None = field(default=None)
26
+
27
+ def notify(self, text: str) -> None:
28
+ """Post a system status note to the chat stream."""
29
+ from tau.message.types import CustomMessage, LinesContent
30
+ lines = text.splitlines() + [""]
31
+ contents = [LinesContent(lines=lines)]
32
+ msg = CustomMessage(
33
+ custom_type="system",
34
+ timestamp=time.time(),
35
+ contents=contents,
36
+ )
37
+ self.layout.add_message(msg)
38
+ self.tui.request_render()
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import sys
5
+
6
+ from tau.tui.commands.context import CommandContext
7
+
8
+
9
+ def cmd_copy(ctx: CommandContext) -> None:
10
+ from tau.session.types import MessageEntry as SessionMessageEntry
11
+ from tau.message.types import AssistantMessage
12
+
13
+ sm = ctx.runtime.session_manager
14
+ if sm is None:
15
+ ctx.notify("No active session.")
16
+ return
17
+
18
+ text = ""
19
+ for entry in reversed(sm.get_branch()):
20
+ if isinstance(entry, SessionMessageEntry):
21
+ msg = entry.message
22
+ if isinstance(msg, AssistantMessage):
23
+ text = msg.text_content()
24
+ break
25
+
26
+ if not text:
27
+ ctx.notify("No assistant messages to copy.")
28
+ return
29
+
30
+ try:
31
+ copy_to_clipboard(text)
32
+ ctx.notify("Copied last assistant message to clipboard.")
33
+ except Exception as exc:
34
+ ctx.notify(f"Copy failed: {exc}")
35
+
36
+
37
+ def copy_to_clipboard(text: str) -> None:
38
+ if sys.platform == "darwin":
39
+ subprocess.run(["pbcopy"], input=text.encode(), check=True)
40
+ else:
41
+ for cmd in [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]]:
42
+ try:
43
+ subprocess.run(cmd, input=text.encode(), check=True, capture_output=True)
44
+ return
45
+ except (FileNotFoundError, subprocess.CalledProcessError):
46
+ continue
47
+ raise RuntimeError("No clipboard tool found. Install xclip, xsel, or wl-copy.")
48
+
49
+
50
+ def show_help(ctx: CommandContext) -> None:
51
+ from tau.prompts.registry import prompt_registry
52
+
53
+ cmds = sorted(ctx.runtime.commands.list(), key=lambda c: c.name)
54
+ cmd_lines = "\n".join(f" /{c.name:<14} {c.description}" for c in cmds)
55
+
56
+ tmpls = sorted(prompt_registry.list(), key=lambda t: t.name)
57
+ tmpl_lines = "\n".join(
58
+ f" /{t.name:<14} {t.description}" + (f" {t.argument_hint}" if t.argument_hint else "")
59
+ for t in tmpls
60
+ )
61
+
62
+ shortcuts = (
63
+ " Enter Submit / steer mid-task when agent is busy\n"
64
+ " Alt+Enter Queue as follow-up (waits for agent to finish)\n"
65
+ " Alt+↑ Restore queued messages into editor\n"
66
+ " Esc Abort running agent (restores queued messages)\n"
67
+ " Ctrl+C Abort running agent / quit when idle\n"
68
+ " Page Up Enter scroll mode (Esc / End to exit)\n"
69
+ " @<path> Attach a file — browse with ↑↓, Tab to select\n"
70
+ " /<command> Run a slash command — executes immediately\n"
71
+ " /name [args] Expand a prompt template and send to agent\n"
72
+ " !<cmd> Run a shell command — executes immediately"
73
+ )
74
+
75
+ ext_shortcuts = ctx.runtime.extension_shortcuts
76
+ ext_lines = ""
77
+ if ext_shortcuts:
78
+ rows = "\n".join(f" {s.key:<14} {s.description or ''}" for s in ext_shortcuts)
79
+ ext_lines = f"\n\nExtension shortcuts:\n{rows}"
80
+
81
+ tmpl_section = f"\n\nPrompt templates:\n{tmpl_lines}" if tmpl_lines else ""
82
+ ctx.notify(f"Commands:\n{cmd_lines}{tmpl_section}\n\nKeyboard shortcuts:\n{shortcuts}{ext_lines}")
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from tau.tui.commands.context import CommandContext
6
+
7
+
8
+ def open_model_selector(ctx: CommandContext) -> None:
9
+ """Open the model selector modal."""
10
+ import asyncio
11
+
12
+ try:
13
+ from tau.inference.api.text.service import TextLLM
14
+ models = TextLLM.list_available()
15
+ except Exception as exc:
16
+ ctx.notify(f"Failed to load models: {exc}")
17
+ return
18
+
19
+ if not models:
20
+ ctx.notify("No models available. Use /login to add providers.")
21
+ return
22
+
23
+ llm = getattr(getattr(ctx.runtime.agent, "_engine", None), "llm", None) if ctx.runtime.agent else None
24
+ model = getattr(llm, "model", None) if llm is not None else None
25
+ current_key = f"{model.provider}/{model.id}" if model is not None else ""
26
+
27
+ def commit(value: tuple[str, str]) -> None:
28
+ model_id, provider = value
29
+ asyncio.ensure_future(_apply_model(ctx, model_id, provider))
30
+
31
+ ctx.layout.open_model_selector(models, current_key, commit, lambda: ctx.notify("Model selection cancelled."))
32
+
33
+
34
+ async def _apply_model(ctx: CommandContext, model_id: str, provider: str) -> None:
35
+ try:
36
+ await ctx.runtime.set_model(model_id, provider)
37
+ ctx.notify(f"Model set to {provider}/{model_id}")
38
+ if ctx.on_palette_refresh is not None:
39
+ ctx.on_palette_refresh()
40
+ except Exception as exc:
41
+ ctx.notify(f"Failed to set model: {exc}")
42
+
43
+
44
+ def open_effort_selector(ctx: CommandContext) -> None:
45
+ from tau.inference.types import ThinkingLevel
46
+
47
+ agent = ctx.runtime.agent
48
+ model = None
49
+ if agent is not None:
50
+ model = getattr(agent._engine.llm, "model", None)
51
+
52
+ if model is None or not getattr(model, "thinking", False):
53
+ ctx.notify("Model doesn't support thinking")
54
+ return
55
+
56
+ current_level = None
57
+ if agent is not None:
58
+ current_level = agent._engine.llm.api.options.thinking_level
59
+ current = current_level.value if current_level else ThinkingLevel.Off.value
60
+
61
+ levels = [lv.value for lv in ThinkingLevel]
62
+
63
+ def commit(level_val: str) -> None:
64
+ asyncio.ensure_future(_apply_effort(ctx, level_val))
65
+
66
+ ctx.layout.open_effort_selector(levels, current, commit, lambda: ctx.notify("Effort selection cancelled."))
67
+
68
+
69
+ def get_palette_overrides(agent: object) -> dict[str, str]:
70
+ """Return dynamic palette description overrides for /model and /effort."""
71
+ overrides: dict[str, str] = {}
72
+
73
+ llm = getattr(getattr(agent, "_engine", None), "llm", None) if agent is not None else None
74
+ model = getattr(llm, "model", None) if llm is not None else None
75
+
76
+ if model is not None:
77
+ provider = getattr(model, "provider", "") or ""
78
+ model_id = getattr(model, "id", "") or ""
79
+ overrides["model"] = f"{provider}/{model_id}" if provider else model_id
80
+ else:
81
+ overrides["model"] = "Switch the active model"
82
+
83
+ effort_val = None
84
+ if llm is not None:
85
+ opts = getattr(getattr(llm, "api", None), "options", None)
86
+ if opts is not None:
87
+ level = getattr(opts, "thinking_level", None)
88
+ if level is not None:
89
+ effort_val = getattr(level, "value", str(level))
90
+ overrides["effort"] = effort_val if effort_val is not None else "Set thinking effort level"
91
+
92
+ return overrides
93
+
94
+
95
+ async def _apply_effort(ctx: CommandContext, level_val: str) -> None:
96
+ from tau.hooks.tui import ThinkingLevelSelectEvent
97
+ from tau.inference.types import ThinkingLevel
98
+
99
+ agent = ctx.runtime.agent
100
+ if agent is None:
101
+ return
102
+ previous_level = agent._engine.llm.api.options.thinking_level
103
+ level = ThinkingLevel(level_val)
104
+ agent._engine.llm.api.options.thinking_level = None if level == ThinkingLevel.Off else level
105
+
106
+ sm = ctx.runtime.session_manager
107
+ if sm is not None:
108
+ sm.append_thinking_level_change(level)
109
+
110
+ settings = ctx.runtime.settings_manager
111
+ if settings is not None:
112
+ settings.set_thinking_level(level)
113
+
114
+ await ctx.runtime.hooks.emit(ThinkingLevelSelectEvent(level=level, previous_level=previous_level))
115
+
116
+ ctx.notify(f"Effort set to {level_val}")
117
+ if ctx.on_palette_refresh is not None:
118
+ ctx.on_palette_refresh()