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