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/component.py ADDED
@@ -0,0 +1,268 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from tau.tui.input import InputEvent
8
+
9
+
10
+ class Component(ABC):
11
+ """
12
+ Base class for all TUI components.
13
+
14
+ A component produces a list of pre-rendered lines for a given terminal
15
+ width. The renderer calls render() on each frame and only writes lines
16
+ that changed since the last frame.
17
+ """
18
+
19
+ @abstractmethod
20
+ def render(self, width: int) -> list[str]:
21
+ """
22
+ Return the component's visual representation as a list of strings.
23
+
24
+ Each string is one terminal line. Lines must not contain newline
25
+ characters — the renderer handles line breaks. ANSI escape codes
26
+ are allowed; the renderer uses ansi.visible_width() for comparisons.
27
+
28
+ Args:
29
+ width: Available terminal columns.
30
+
31
+ Returns:
32
+ List of strings, one per line.
33
+ """
34
+ ...
35
+
36
+ def handle_input(self, event: InputEvent) -> bool: # noqa: ARG002
37
+ """
38
+ Handle a keyboard / mouse / paste event.
39
+
40
+ Returns True if the event was consumed (stops propagation).
41
+ Default: not handled.
42
+ """
43
+ return False
44
+
45
+ def invalidate(self) -> None:
46
+ """
47
+ Clear any cached render state.
48
+
49
+ Called by the renderer after a terminal resize or when the component
50
+ needs to be fully re-rendered on the next frame.
51
+ """
52
+
53
+
54
+ class Focusable(ABC):
55
+ """
56
+ Mixin for components that want explicit keyboard focus.
57
+
58
+ When TUI.set_focus(component) is called, TUI sets ``focused = True``
59
+ on the component and routes handle_input() calls to it exclusively
60
+ until focus changes. Components that display a text cursor or need
61
+ IME positioning should implement this interface.
62
+
63
+ Example::
64
+
65
+ class MyInput(Component, Focusable):
66
+ def render(self, width):
67
+ cursor = "█" if self.focused else ""
68
+ return [f"> {self._text}{cursor}"]
69
+ """
70
+
71
+ focused: bool = False
72
+
73
+
74
+ class Container(Component):
75
+ """
76
+ An ordered list of child components rendered top-to-bottom.
77
+
78
+ An ordered list of child components rendered top-to-bottom.
79
+ Children are rendered in insertion order; each child gets the full
80
+ available width.
81
+
82
+ Usage::
83
+
84
+ header = Container()
85
+ header.add_child(Banner())
86
+ header.add_child(Spacer(1))
87
+
88
+ tui.add_child(header)
89
+ tui.add_child(chat)
90
+ tui.add_child(editor)
91
+ """
92
+
93
+ def __init__(self) -> None:
94
+ self.children: list[Component] = []
95
+
96
+ def add_child(self, component: Component) -> None:
97
+ """Append a component to the bottom of this container."""
98
+ self.children.append(component)
99
+
100
+ def remove_child(self, component: Component) -> None:
101
+ """Remove a component; no-op if not present."""
102
+ try:
103
+ self.children.remove(component)
104
+ except ValueError:
105
+ pass
106
+
107
+ def clear(self) -> None:
108
+ """Remove all children."""
109
+ self.children.clear()
110
+
111
+ # -------------------------------------------------------------------------
112
+ # Component
113
+ # -------------------------------------------------------------------------
114
+
115
+ def render(self, width: int) -> list[str]:
116
+ lines: list[str] = []
117
+ for child in self.children:
118
+ lines.extend(child.render(width))
119
+ return lines
120
+
121
+ def handle_input(self, event: InputEvent) -> bool:
122
+ for child in self.children:
123
+ if child.handle_input(event):
124
+ return True
125
+ return False
126
+
127
+ def invalidate(self) -> None:
128
+ for child in self.children:
129
+ child.invalidate()
130
+
131
+
132
+ class StaticComponent(Component):
133
+ """
134
+ A component backed by a fixed list of pre-rendered lines.
135
+ Useful for testing and simple static content.
136
+ """
137
+
138
+ def __init__(self, lines: list[str]) -> None:
139
+ self._lines = lines
140
+
141
+ def render(self, width: int) -> list[str]: # noqa: ARG002
142
+ return self._lines
143
+
144
+
145
+ class Column(Component):
146
+ """
147
+ Renders children top-to-bottom, each getting the full width.
148
+
149
+ Fixed counterpart to ``Container`` — children are supplied at construction
150
+ time. Use ``Container`` when you need to add/remove children at runtime.
151
+
152
+ Usage::
153
+
154
+ col = Column([Banner(), Divider(), ChatArea()])
155
+ """
156
+
157
+ def __init__(self, children: list[Component]) -> None:
158
+ self.children = list(children)
159
+
160
+ def render(self, width: int) -> list[str]:
161
+ lines: list[str] = []
162
+ for child in self.children:
163
+ lines.extend(child.render(width))
164
+ return lines
165
+
166
+ def handle_input(self, event: InputEvent) -> bool:
167
+ for child in reversed(self.children):
168
+ if child.handle_input(event):
169
+ return True
170
+ return False
171
+
172
+ def invalidate(self) -> None:
173
+ for child in self.children:
174
+ child.invalidate()
175
+
176
+
177
+ # Backwards-compatible alias
178
+ VerticalStack = Column
179
+
180
+
181
+ class Row(Component):
182
+ """
183
+ Renders children side-by-side in a single terminal line.
184
+
185
+ Each child is assigned a slot — ``"left"``, ``"center"``, or ``"right"``
186
+ — and the Row distributes the available width so that:
187
+
188
+ - left content is flush-left
189
+ - right content is flush-right
190
+ - center content sits in the middle (best-effort)
191
+
192
+ Each child's ``render()`` is called with its measured slot width and only
193
+ the **first line** of the result is used. This keeps Row a single-line
194
+ primitive; stack multiple Rows inside a Column/Container for multi-line
195
+ horizontal layouts.
196
+
197
+ Usage::
198
+
199
+ row = Row([
200
+ (GitBadge(), "left"),
201
+ (StatusBadge(),"center"),
202
+ (ModelBadge(), "right"),
203
+ ])
204
+ """
205
+
206
+ def __init__(self, slots: list[tuple[Component, str]] | None = None) -> None:
207
+ self._slots: list[tuple[Component, str]] = list(slots) if slots else []
208
+
209
+ def add_slot(self, component: Component, align: str = "left") -> None:
210
+ """Append a component with the given alignment (``"left"``, ``"center"``, ``"right"``)."""
211
+ self._slots.append((component, align))
212
+
213
+ def render(self, width: int) -> list[str]:
214
+ from tau.tui.ansi import visible_width, truncate
215
+
216
+ left_parts: list[str] = []
217
+ center_parts: list[str] = []
218
+ right_parts: list[str] = []
219
+
220
+ for component, align in self._slots:
221
+ lines = component.render(width)
222
+ text = lines[0] if lines else ""
223
+ if align == "right":
224
+ right_parts.append(text)
225
+ elif align == "center":
226
+ center_parts.append(text)
227
+ else:
228
+ left_parts.append(text)
229
+
230
+ left = " ".join(left_parts)
231
+ center = " ".join(center_parts)
232
+ right = " ".join(right_parts)
233
+
234
+ lw = visible_width(left)
235
+ cw = visible_width(center)
236
+ rw = visible_width(right)
237
+
238
+ if center:
239
+ # left | center (centered) | right
240
+ center_start = max(lw + 1, (width - cw) // 2)
241
+ right_start = width - rw
242
+ if center_start + cw > right_start:
243
+ center_start = max(lw + 1, right_start - cw - 1)
244
+ line = left
245
+ line += " " * max(0, center_start - lw)
246
+ line += center
247
+ cur = center_start + cw
248
+ line += " " * max(0, right_start - cur)
249
+ line += right
250
+ else:
251
+ # left | right
252
+ gap = width - lw - rw
253
+ if gap >= 0:
254
+ line = left + " " * gap + right
255
+ else:
256
+ line = truncate(left, max(0, width - rw)) + right
257
+
258
+ return [line]
259
+
260
+ def handle_input(self, event: InputEvent) -> bool:
261
+ for component, _ in self._slots:
262
+ if component.handle_input(event):
263
+ return True
264
+ return False
265
+
266
+ def invalidate(self) -> None:
267
+ for component, _ in self._slots:
268
+ component.invalidate()
File without changes
@@ -0,0 +1,267 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ from typing import Callable, TYPE_CHECKING
6
+
7
+ from tau.tui.components.autocomplete_picker import AutocompletePicker
8
+ from tau.tui.input import InputEvent, KeyEvent
9
+
10
+ if TYPE_CHECKING:
11
+ from tau.commands.types import CommandInfo
12
+ from tau.tui.autocomplete import AutocompleteRegistration
13
+
14
+
15
+ class AutocompleteManager:
16
+ """
17
+ Owns both inline autocomplete pickers and all their async fetch state.
18
+
19
+ Previously this logic was spread across ~130 lines inside Layout.
20
+ Layout now creates one instance and delegates via three calls:
21
+
22
+ sync(text, cursor, commands) — called after every keystroke
23
+ handle_input(event, text, cursor) -> (consumed, new_text | None)
24
+ render(width) -> list[str]
25
+
26
+ Two pickers are managed internally:
27
+ - Extension autocomplete (_ac_picker) — trigger chars registered by providers
28
+ - Command arg completions (_cmd_arg_picker) — activated by '/cmd <space>'
29
+ """
30
+
31
+ def __init__(self, max_visible: int, request_render: Callable[[], None]) -> None:
32
+ self._ac_picker = AutocompletePicker(max_visible=max_visible)
33
+ self._ac_providers: list[AutocompleteRegistration] = []
34
+ self._ac_trigger_pos: int = -1
35
+ self._ac_active_trigger: str = ""
36
+ self._ac_pending_task: asyncio.Task | None = None # type: ignore[type-arg]
37
+
38
+ self._cmd_arg_picker = AutocompletePicker(max_visible=max_visible)
39
+ self._cmd_arg_active: str = ""
40
+ self._cmd_arg_pending_task: asyncio.Task | None = None # type: ignore[type-arg]
41
+
42
+ self._request_render = request_render
43
+
44
+ # -------------------------------------------------------------------------
45
+ # Public API
46
+ # -------------------------------------------------------------------------
47
+
48
+ @property
49
+ def active(self) -> bool:
50
+ return self._ac_picker.active or self._cmd_arg_picker.active
51
+
52
+ def register_provider(self, reg: AutocompleteRegistration) -> None:
53
+ self._ac_providers.append(reg)
54
+
55
+ def clear(self) -> None:
56
+ """Dismiss both pickers — called when another picker takes over."""
57
+ self._ac_picker.clear()
58
+ self._cmd_arg_picker.clear()
59
+ self._ac_active_trigger = ""
60
+ self._ac_trigger_pos = -1
61
+ self._cmd_arg_active = ""
62
+
63
+ def sync(self, text: str, cursor: int, all_commands: list[CommandInfo]) -> None:
64
+ """
65
+ Called after every keystroke when neither the file picker nor the
66
+ command palette is active. Updates whichever picker applies.
67
+ """
68
+ # Command argument completions: '/cmd <args>'
69
+ if text.startswith("/"):
70
+ space_idx = text.find(" ")
71
+ if space_idx != -1:
72
+ cmd_name = text[1:space_idx]
73
+ arg_prefix = text[space_idx + 1:]
74
+ cmd = next(
75
+ (c for c in all_commands
76
+ if c.name == cmd_name or cmd_name in (c.aliases or [])),
77
+ None,
78
+ )
79
+ if cmd is not None and cmd.get_argument_completions is not None:
80
+ if cmd_name != self._cmd_arg_active:
81
+ self._cmd_arg_active = cmd_name
82
+ self._cmd_arg_picker.clear()
83
+ self._start_cmd_arg(cmd, arg_prefix)
84
+ self._ac_picker.clear()
85
+ return
86
+ self._cmd_arg_picker.clear()
87
+ self._cmd_arg_active = ""
88
+
89
+ # Extension autocomplete: trigger character registered by a provider
90
+ ac_info = self._find_trigger(text, cursor)
91
+ if ac_info is not None:
92
+ trigger, query, trigger_pos = ac_info
93
+ if trigger != self._ac_active_trigger or trigger_pos != self._ac_trigger_pos:
94
+ self._ac_active_trigger = trigger
95
+ self._ac_trigger_pos = trigger_pos
96
+ self._ac_picker.clear()
97
+ self._start_ac(trigger, query, text, cursor)
98
+ else:
99
+ self._ac_picker.set_query(query)
100
+ else:
101
+ self._ac_active_trigger = ""
102
+ self._ac_trigger_pos = -1
103
+ self._ac_picker.clear()
104
+
105
+ def handle_input(
106
+ self,
107
+ event: InputEvent,
108
+ text: str,
109
+ cursor: int,
110
+ ) -> tuple[bool, str | None]:
111
+ """
112
+ Handle a key event when a picker is active.
113
+
114
+ Returns (consumed, new_text):
115
+ consumed — True if the event was handled (prevents further dispatch)
116
+ new_text — the full replacement text when an item was accepted, else None
117
+ """
118
+ if not isinstance(event, KeyEvent):
119
+ return False, None
120
+
121
+ if self._ac_picker.active:
122
+ match event.key:
123
+ case "up":
124
+ self._ac_picker.move_up()
125
+ return True, None
126
+ case "down":
127
+ self._ac_picker.move_down()
128
+ return True, None
129
+ case "tab" | "enter":
130
+ return True, self._accept_ac(text, cursor)
131
+ case "escape":
132
+ self._ac_picker.clear()
133
+ return True, None
134
+
135
+ if self._cmd_arg_picker.active:
136
+ match event.key:
137
+ case "up":
138
+ self._cmd_arg_picker.move_up()
139
+ return True, None
140
+ case "down":
141
+ self._cmd_arg_picker.move_down()
142
+ return True, None
143
+ case "tab" | "enter":
144
+ return True, self._accept_cmd_arg(text)
145
+ case "escape":
146
+ self._cmd_arg_picker.clear()
147
+ return True, None
148
+
149
+ return False, None
150
+
151
+ def render(self, width: int) -> list[str]:
152
+ lines: list[str] = []
153
+ lines.extend(self._ac_picker.render(width))
154
+ lines.extend(self._cmd_arg_picker.render(width))
155
+ return lines
156
+
157
+ # -------------------------------------------------------------------------
158
+ # Trigger detection
159
+ # -------------------------------------------------------------------------
160
+
161
+ def _find_trigger(self, text: str, cursor: int) -> tuple[str, str, int] | None:
162
+ """Scan rightward from cursor for the nearest registered trigger char."""
163
+ if not self._ac_providers:
164
+ return None
165
+ before = text[:cursor]
166
+ triggers = {p.trigger for p in self._ac_providers}
167
+ for i in range(len(before) - 1, -1, -1):
168
+ ch = before[i]
169
+ if ch == " ":
170
+ break
171
+ if ch in triggers:
172
+ return ch, before[i + 1:], i
173
+ return None
174
+
175
+ # -------------------------------------------------------------------------
176
+ # Async fetch helpers
177
+ # -------------------------------------------------------------------------
178
+
179
+ def _start_ac(self, trigger: str, query: str, text: str, cursor: int) -> None:
180
+ provider = next((p for p in self._ac_providers if p.trigger == trigger), None)
181
+ if provider is None:
182
+ return
183
+
184
+ from tau.tui.autocomplete import AutocompleteContext
185
+ ctx = AutocompleteContext(text=text, cursor_pos=cursor, trigger=trigger, query=query)
186
+
187
+ if self._ac_pending_task is not None:
188
+ self._ac_pending_task.cancel()
189
+ self._ac_pending_task = None
190
+
191
+ result = provider.get_items(ctx)
192
+ if inspect.isawaitable(result):
193
+ async def _fetch() -> None:
194
+ try:
195
+ items = await result # type: ignore[misc]
196
+ if self._ac_active_trigger == trigger:
197
+ self._ac_picker.set_items(items)
198
+ self._ac_picker.set_query(query)
199
+ self._request_render()
200
+ except asyncio.CancelledError:
201
+ pass
202
+ except Exception:
203
+ pass
204
+ self._ac_pending_task = asyncio.ensure_future(_fetch())
205
+ else:
206
+ self._ac_picker.set_items(result) # type: ignore[arg-type]
207
+ self._ac_picker.set_query(query)
208
+
209
+ def _start_cmd_arg(self, cmd: CommandInfo, prefix: str) -> None:
210
+ if cmd.get_argument_completions is None:
211
+ return
212
+ if self._cmd_arg_pending_task is not None:
213
+ self._cmd_arg_pending_task.cancel()
214
+ self._cmd_arg_pending_task = None
215
+
216
+ result = cmd.get_argument_completions(prefix)
217
+ if inspect.isawaitable(result):
218
+ active = self._cmd_arg_active
219
+
220
+ async def _fetch() -> None:
221
+ try:
222
+ items = await result # type: ignore[misc]
223
+ if self._cmd_arg_active == active:
224
+ self._cmd_arg_picker.set_items(items)
225
+ self._cmd_arg_picker.set_query(prefix)
226
+ self._request_render()
227
+ except asyncio.CancelledError:
228
+ pass
229
+ except Exception:
230
+ pass
231
+ self._cmd_arg_pending_task = asyncio.ensure_future(_fetch())
232
+ else:
233
+ self._cmd_arg_picker.set_items(result) # type: ignore[arg-type]
234
+ self._cmd_arg_picker.set_query(prefix)
235
+
236
+ # -------------------------------------------------------------------------
237
+ # Accept helpers
238
+ # -------------------------------------------------------------------------
239
+
240
+ def _accept_ac(self, text: str, cursor: int) -> str | None:
241
+ item = self._ac_picker.selected
242
+ if item is None:
243
+ return None
244
+ insert = item.insert_text if item.insert_text is not None else item.label
245
+ new_text = (
246
+ text[: self._ac_trigger_pos]
247
+ + self._ac_active_trigger
248
+ + insert
249
+ + " "
250
+ + text[cursor:]
251
+ )
252
+ self._ac_picker.clear()
253
+ self._ac_active_trigger = ""
254
+ self._ac_trigger_pos = -1
255
+ return new_text
256
+
257
+ def _accept_cmd_arg(self, text: str) -> str | None:
258
+ item = self._cmd_arg_picker.selected
259
+ if item is None:
260
+ return None
261
+ space_idx = text.find(" ")
262
+ if space_idx == -1:
263
+ return None
264
+ insert = item.insert_text if item.insert_text is not None else item.label
265
+ new_text = text[: space_idx + 1] + insert + " "
266
+ self._cmd_arg_picker.clear()
267
+ return new_text
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from tau.tui.ansi import RESET, BOLD, BRIGHT_BLACK, BRIGHT_WHITE
6
+ from tau.tui.component import Component
7
+ from tau.tui.fuzzy import fuzzy_filter
8
+ from tau.tui.input import InputEvent, KeyEvent
9
+
10
+ if TYPE_CHECKING:
11
+ from tau.tui.autocomplete import AutocompleteItem
12
+
13
+ _DEFAULT_VISIBLE_ROWS = 5
14
+
15
+
16
+ class AutocompletePicker(Component):
17
+ """
18
+ Fuzzy-filtered inline dropdown for extension autocomplete providers.
19
+
20
+ Shown above the input (like the command palette) when an extension trigger
21
+ character is detected in the editor text. Navigation is handled here;
22
+ commit (Tab/Enter) and dismiss (Escape) are handled by Layout.
23
+ """
24
+
25
+ def __init__(self, max_visible: int = _DEFAULT_VISIBLE_ROWS) -> None:
26
+ self._all_items: list[AutocompleteItem] = []
27
+ self._items: list[AutocompleteItem] = []
28
+ self._selected: int = 0
29
+ self._query: str = ""
30
+ self._active: bool = False
31
+ self._max_visible = max_visible
32
+
33
+ # -------------------------------------------------------------------------
34
+ # Public API
35
+ # -------------------------------------------------------------------------
36
+
37
+ @property
38
+ def active(self) -> bool:
39
+ return self._active and bool(self._items)
40
+
41
+ @property
42
+ def selected(self) -> AutocompleteItem | None:
43
+ return self._items[self._selected] if self._items else None
44
+
45
+ @property
46
+ def line_count(self) -> int:
47
+ return min(self._max_visible, len(self._items))
48
+
49
+ def set_items(self, items: list[AutocompleteItem]) -> None:
50
+ self._all_items = list(items)
51
+ self._active = True
52
+ self._apply_filter(self._query)
53
+
54
+ def set_query(self, query: str) -> None:
55
+ if query == self._query:
56
+ return
57
+ self._query = query
58
+ self._apply_filter(query)
59
+
60
+ def clear(self) -> None:
61
+ self._all_items = []
62
+ self._items = []
63
+ self._selected = 0
64
+ self._query = ""
65
+ self._active = False
66
+
67
+ def move_up(self) -> None:
68
+ if self._items:
69
+ self._selected = (self._selected - 1) % len(self._items)
70
+
71
+ def move_down(self) -> None:
72
+ if self._items:
73
+ self._selected = (self._selected + 1) % len(self._items)
74
+
75
+ # -------------------------------------------------------------------------
76
+ # Component
77
+ # -------------------------------------------------------------------------
78
+
79
+ def render(self, width: int) -> list[str]:
80
+ if not self.active:
81
+ return []
82
+
83
+ count = len(self._items)
84
+ visible = min(self._max_visible, count)
85
+ start = max(0, min(self._selected - visible + 1, count - visible))
86
+
87
+ label_w = max(8, min(
88
+ max(len(item.label) for item in self._items[start : start + visible]),
89
+ 24,
90
+ ))
91
+ desc_w = max(0, width - label_w - 4)
92
+
93
+ lines: list[str] = []
94
+
95
+ if start > 0:
96
+ lines.append(BRIGHT_BLACK + f" ↑ {start} more" + RESET)
97
+
98
+ for i in range(start, start + visible):
99
+ item = self._items[i]
100
+ is_sel = i == self._selected
101
+ label = item.label[:label_w].ljust(label_w)
102
+ desc = item.description[:desc_w] if desc_w > 0 else ""
103
+
104
+ if is_sel:
105
+ row = " " + BOLD + BRIGHT_WHITE + label + RESET + " " + BRIGHT_BLACK + desc + RESET
106
+ else:
107
+ row = " " + BRIGHT_BLACK + label + " " + desc + RESET
108
+
109
+ lines.append(row)
110
+
111
+ remaining = count - (start + visible)
112
+ if remaining > 0:
113
+ lines.append(BRIGHT_BLACK + f" ↓ {remaining} more" + RESET)
114
+
115
+ return lines
116
+
117
+ def handle_input(self, event: InputEvent) -> bool:
118
+ if not isinstance(event, KeyEvent):
119
+ return False
120
+ key = str(event)
121
+ if key in ("up", "ctrl+p"):
122
+ self.move_up()
123
+ return True
124
+ if key in ("down", "ctrl+n"):
125
+ self.move_down()
126
+ return True
127
+ return False
128
+
129
+ # -------------------------------------------------------------------------
130
+ # Internal
131
+ # -------------------------------------------------------------------------
132
+
133
+ def _apply_filter(self, query: str) -> None:
134
+ q = query.strip()
135
+ if not q:
136
+ self._items = list(self._all_items)
137
+ else:
138
+ self._items = fuzzy_filter(
139
+ self._all_items,
140
+ q,
141
+ lambda item: item.label + " " + item.description,
142
+ )
143
+ self._selected = min(self._selected, len(self._items) - 1) if self._items else 0