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.
- tau/__init__.py +0 -0
- tau/agent/__init__.py +11 -0
- tau/agent/prompt/__init__.py +10 -0
- tau/agent/prompt/builder.py +302 -0
- tau/agent/prompt/types.py +33 -0
- tau/agent/service.py +369 -0
- tau/agent/types.py +61 -0
- tau/auth/manager.py +247 -0
- tau/auth/storage.py +82 -0
- tau/auth/types.py +41 -0
- tau/builtins/__init__.py +4 -0
- tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__init__.py +41 -0
- tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
- tau/builtins/commands/clear.py +16 -0
- tau/builtins/commands/compact.py +28 -0
- tau/builtins/commands/reload.py +27 -0
- tau/builtins/commands/session.py +19 -0
- tau/builtins/extensions/footer/__init__.py +76 -0
- tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/git.py +26 -0
- tau/builtins/extensions/footer/model.py +69 -0
- tau/builtins/extensions/footer/utils.py +44 -0
- tau/builtins/extensions/header/__init__.py +18 -0
- tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__init__.py +0 -0
- tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/models/audio.py +43 -0
- tau/builtins/models/image.py +43 -0
- tau/builtins/models/text.py +482 -0
- tau/builtins/models/video.py +40 -0
- tau/builtins/prompts/commit.md +7 -0
- tau/builtins/prompts/docs.md +7 -0
- tau/builtins/prompts/explain.md +7 -0
- tau/builtins/prompts/fix.md +7 -0
- tau/builtins/prompts/refactor.md +7 -0
- tau/builtins/prompts/review.md +7 -0
- tau/builtins/prompts/test.md +7 -0
- tau/builtins/providers/__init__.py +0 -0
- tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/providers/audio.py +10 -0
- tau/builtins/providers/image.py +9 -0
- tau/builtins/providers/text.py +33 -0
- tau/builtins/providers/video.py +6 -0
- tau/builtins/skills/code-review/SKILL.md +4 -0
- tau/builtins/skills/debug/SKILL.md +4 -0
- tau/builtins/skills/git-commit/SKILL.md +4 -0
- tau/builtins/themes/dark.yaml +1 -0
- tau/builtins/themes/light.yaml +46 -0
- tau/builtins/tools/__init__.py +73 -0
- tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
- tau/builtins/tools/edit.py +215 -0
- tau/builtins/tools/glob.py +112 -0
- tau/builtins/tools/grep.py +146 -0
- tau/builtins/tools/ls.py +135 -0
- tau/builtins/tools/read.py +122 -0
- tau/builtins/tools/terminal.py +150 -0
- tau/builtins/tools/write.py +105 -0
- tau/commands/__init__.py +10 -0
- tau/commands/registry.py +71 -0
- tau/commands/types.py +33 -0
- tau/console/__init__.py +0 -0
- tau/console/cli.py +266 -0
- tau/console/commands/__init__.py +0 -0
- tau/console/commands/auth.py +193 -0
- tau/console/commands/packages.py +104 -0
- tau/console/commands/update.py +76 -0
- tau/core/__init__.py +0 -0
- tau/core/registry.py +102 -0
- tau/engine/__init__.py +47 -0
- tau/engine/service.py +768 -0
- tau/engine/types.py +163 -0
- tau/extensions/__init__.py +28 -0
- tau/extensions/api.py +928 -0
- tau/extensions/context.py +462 -0
- tau/extensions/events.py +70 -0
- tau/extensions/loader.py +386 -0
- tau/extensions/runtime.py +184 -0
- tau/extensions/settings.py +137 -0
- tau/hooks/__init__.py +112 -0
- tau/hooks/engine.py +237 -0
- tau/hooks/inference.py +21 -0
- tau/hooks/runtime.py +126 -0
- tau/hooks/service.py +121 -0
- tau/hooks/session.py +117 -0
- tau/hooks/tui.py +61 -0
- tau/hooks/types.py +72 -0
- tau/inference/__init__.py +180 -0
- tau/inference/api/__init__.py +0 -0
- tau/inference/api/audio/__init__.py +0 -0
- tau/inference/api/audio/base.py +29 -0
- tau/inference/api/audio/builtins.py +15 -0
- tau/inference/api/audio/elevenlabs_audio.py +183 -0
- tau/inference/api/audio/gemini_audio.py +95 -0
- tau/inference/api/audio/openai_audio.py +159 -0
- tau/inference/api/audio/registry.py +15 -0
- tau/inference/api/audio/sarvam_audio.py +163 -0
- tau/inference/api/audio/service.py +103 -0
- tau/inference/api/audio/utils.py +47 -0
- tau/inference/api/image/__init__.py +0 -0
- tau/inference/api/image/base.py +17 -0
- tau/inference/api/image/builtins.py +8 -0
- tau/inference/api/image/gemini_image.py +77 -0
- tau/inference/api/image/openai_image.py +103 -0
- tau/inference/api/image/openrouter.py +144 -0
- tau/inference/api/image/registry.py +15 -0
- tau/inference/api/image/service.py +71 -0
- tau/inference/api/registry.py +82 -0
- tau/inference/api/text/__init__.py +0 -0
- tau/inference/api/text/anthropic_claude_code.py +222 -0
- tau/inference/api/text/anthropic_messages.py +196 -0
- tau/inference/api/text/base.py +40 -0
- tau/inference/api/text/builtins.py +19 -0
- tau/inference/api/text/gemini_generate.py +234 -0
- tau/inference/api/text/github_copilot_chat.py +172 -0
- tau/inference/api/text/google_antigravity.py +522 -0
- tau/inference/api/text/mistral_chat.py +284 -0
- tau/inference/api/text/ollama_chat.py +200 -0
- tau/inference/api/text/openai_codex_responses.py +497 -0
- tau/inference/api/text/openai_completions.py +227 -0
- tau/inference/api/text/openai_responses.py +235 -0
- tau/inference/api/text/registry.py +50 -0
- tau/inference/api/text/service.py +297 -0
- tau/inference/api/text/types.py +7 -0
- tau/inference/api/text/utils.py +228 -0
- tau/inference/api/video/__init__.py +0 -0
- tau/inference/api/video/base.py +26 -0
- tau/inference/api/video/builtins.py +7 -0
- tau/inference/api/video/fal_video.py +119 -0
- tau/inference/api/video/openrouter_video.py +142 -0
- tau/inference/api/video/registry.py +15 -0
- tau/inference/api/video/service.py +72 -0
- tau/inference/model/__init__.py +0 -0
- tau/inference/model/registry.py +102 -0
- tau/inference/model/types.py +65 -0
- tau/inference/provider/__init__.py +0 -0
- tau/inference/provider/oauth/__init__.py +35 -0
- tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
- tau/inference/provider/oauth/github_copilot.py +333 -0
- tau/inference/provider/oauth/google_antigravity.py +258 -0
- tau/inference/provider/oauth/openai_codex.py +309 -0
- tau/inference/provider/oauth/pkce.py +14 -0
- tau/inference/provider/oauth/types.py +46 -0
- tau/inference/provider/oauth/utils.py +154 -0
- tau/inference/provider/registry.py +141 -0
- tau/inference/provider/types.py +114 -0
- tau/inference/types.py +549 -0
- tau/inference/utils.py +219 -0
- tau/message/__init__.py +0 -0
- tau/message/types.py +482 -0
- tau/message/utils.py +178 -0
- tau/packages/__init__.py +11 -0
- tau/packages/manager.py +190 -0
- tau/packages/types.py +20 -0
- tau/packages/utils.py +67 -0
- tau/prompts/expand.py +58 -0
- tau/prompts/loader.py +69 -0
- tau/prompts/registry.py +45 -0
- tau/prompts/types.py +24 -0
- tau/rpc/__init__.py +8 -0
- tau/rpc/mode.py +783 -0
- tau/rpc/types.py +252 -0
- tau/runtime/service.py +759 -0
- tau/runtime/types.py +303 -0
- tau/session/branch_summarization.py +312 -0
- tau/session/compaction.py +646 -0
- tau/session/manager.py +652 -0
- tau/session/types.py +188 -0
- tau/session/utils.py +233 -0
- tau/settings/manager.py +1077 -0
- tau/settings/paths.py +150 -0
- tau/settings/storage.py +63 -0
- tau/settings/types.py +173 -0
- tau/settings/utils.py +25 -0
- tau/skills/loader.py +91 -0
- tau/skills/registry.py +70 -0
- tau/skills/types.py +25 -0
- tau/themes/loader.py +238 -0
- tau/themes/registry.py +108 -0
- tau/themes/types.py +19 -0
- tau/tool/__init__.py +3 -0
- tau/tool/registry.py +117 -0
- tau/tool/render.py +21 -0
- tau/tool/types.py +244 -0
- tau/trust/__init__.py +13 -0
- tau/trust/manager.py +80 -0
- tau/trust/types.py +14 -0
- tau/trust/utils.py +72 -0
- tau/tui/__init__.py +54 -0
- tau/tui/agent_hooks.py +346 -0
- tau/tui/ansi.py +330 -0
- tau/tui/app.py +540 -0
- tau/tui/autocomplete.py +33 -0
- tau/tui/capabilities.py +119 -0
- tau/tui/commands/__init__.py +3 -0
- tau/tui/commands/appearance.py +498 -0
- tau/tui/commands/auth.py +232 -0
- tau/tui/commands/context.py +38 -0
- tau/tui/commands/misc.py +82 -0
- tau/tui/commands/model.py +118 -0
- tau/tui/commands/session.py +464 -0
- tau/tui/component.py +268 -0
- tau/tui/components/__init__.py +0 -0
- tau/tui/components/autocomplete_manager.py +267 -0
- tau/tui/components/autocomplete_picker.py +143 -0
- tau/tui/components/box.py +90 -0
- tau/tui/components/command_palette.py +144 -0
- tau/tui/components/dynamic_border.py +19 -0
- tau/tui/components/file_picker.py +233 -0
- tau/tui/components/image.py +181 -0
- tau/tui/components/inline_selector.py +71 -0
- tau/tui/components/layout.py +1194 -0
- tau/tui/components/message_list.py +692 -0
- tau/tui/components/modal.py +97 -0
- tau/tui/components/model_palette.py +204 -0
- tau/tui/components/picker_overlay.py +174 -0
- tau/tui/components/prompt_overlay.py +236 -0
- tau/tui/components/resume_modal.py +372 -0
- tau/tui/components/select_list.py +222 -0
- tau/tui/components/settings_modal.py +274 -0
- tau/tui/components/settings_schema.py +203 -0
- tau/tui/components/spinner.py +119 -0
- tau/tui/components/text_input.py +396 -0
- tau/tui/components/text_prompt.py +82 -0
- tau/tui/components/tree_select_list.py +580 -0
- tau/tui/components/trust_screen.py +97 -0
- tau/tui/diff.py +114 -0
- tau/tui/fuzzy.py +99 -0
- tau/tui/input.py +496 -0
- tau/tui/input_handler.py +716 -0
- tau/tui/keybindings.py +87 -0
- tau/tui/markdown.py +286 -0
- tau/tui/message_renderers.py +31 -0
- tau/tui/overlay.py +326 -0
- tau/tui/renderer.py +378 -0
- tau/tui/terminal.py +499 -0
- tau/tui/theme.py +148 -0
- tau/tui/tui.py +544 -0
- tau/tui/ui_context.py +768 -0
- tau/tui/utils.py +20 -0
- tau/utils/__init__.py +0 -0
- tau/utils/http_proxy.py +221 -0
- tau/utils/image_processing.py +172 -0
- tau/utils/secrets.py +59 -0
- tau/utils/version_check.py +60 -0
- tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
- tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
- tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
- tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|