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