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
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from tau.tui.commands.context import CommandContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def open_resume_selector(ctx: CommandContext) -> None:
|
|
9
|
+
from tau.session.manager import SessionManager
|
|
10
|
+
|
|
11
|
+
sm = ctx.runtime.session_manager
|
|
12
|
+
cwd = sm.cwd if sm is not None else None
|
|
13
|
+
current_path = sm.session_file if sm is not None else None
|
|
14
|
+
|
|
15
|
+
current_sessions = SessionManager.list(cwd) if cwd is not None else []
|
|
16
|
+
|
|
17
|
+
def all_loader() -> list:
|
|
18
|
+
return SessionManager.list_all()
|
|
19
|
+
|
|
20
|
+
def commit(path: object) -> None:
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
asyncio.ensure_future(_apply_resume(ctx, Path(str(path))))
|
|
23
|
+
|
|
24
|
+
ctx.layout.open_resume_selector(
|
|
25
|
+
sessions=current_sessions,
|
|
26
|
+
on_commit=commit,
|
|
27
|
+
on_cancel=lambda: ctx.notify("Resume cancelled."),
|
|
28
|
+
all_sessions_loader=all_loader,
|
|
29
|
+
current_session_path=current_path,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def _apply_resume(ctx: CommandContext, path: object) -> None:
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
p = Path(str(path))
|
|
36
|
+
try:
|
|
37
|
+
await ctx.runtime.resume_session(p)
|
|
38
|
+
ctx.notify(f"Resumed session {p.stem[:32]}")
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
ctx.notify(f"Failed to resume: {exc}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _message_snippet(message: object) -> tuple[str, str]:
|
|
44
|
+
"""Return (role_label, text_snippet) for any AgentMessage variant."""
|
|
45
|
+
from tau.message.types import (
|
|
46
|
+
TerminalExecutionMessage,
|
|
47
|
+
BranchSummaryMessage,
|
|
48
|
+
CompactionSummaryMessage,
|
|
49
|
+
CustomMessage,
|
|
50
|
+
SkillInvocationMessage,
|
|
51
|
+
TemplateInvocationMessage,
|
|
52
|
+
TextContent,
|
|
53
|
+
ThinkingContent,
|
|
54
|
+
ToolCallContent,
|
|
55
|
+
ToolResultContent,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
role_attr = getattr(message, "role", "")
|
|
59
|
+
role = getattr(role_attr, "value", role_attr) or type(message).__name__
|
|
60
|
+
|
|
61
|
+
if isinstance(message, TerminalExecutionMessage):
|
|
62
|
+
return "terminal", message.command
|
|
63
|
+
if isinstance(message, CompactionSummaryMessage):
|
|
64
|
+
return "compaction", message.summary
|
|
65
|
+
if isinstance(message, BranchSummaryMessage):
|
|
66
|
+
return "branch_summary", message.summary
|
|
67
|
+
if isinstance(message, SkillInvocationMessage):
|
|
68
|
+
return "skill", f"{message.name} {message.content}".strip()
|
|
69
|
+
if isinstance(message, TemplateInvocationMessage):
|
|
70
|
+
return "template", message.name
|
|
71
|
+
|
|
72
|
+
contents = getattr(message, "contents", None)
|
|
73
|
+
if isinstance(contents, list):
|
|
74
|
+
parts: list[str] = []
|
|
75
|
+
for c in contents:
|
|
76
|
+
if isinstance(c, TextContent):
|
|
77
|
+
parts.append(c.content)
|
|
78
|
+
elif isinstance(c, ThinkingContent):
|
|
79
|
+
parts.append(f"(thinking) {c.content}")
|
|
80
|
+
elif isinstance(c, ToolCallContent):
|
|
81
|
+
parts.append(f"[tool: {c.name}]")
|
|
82
|
+
elif isinstance(c, ToolResultContent):
|
|
83
|
+
prefix = "[error] " if c.is_error else ""
|
|
84
|
+
parts.append(f"{prefix}{c.content}")
|
|
85
|
+
text = " ".join(p for p in parts if p)
|
|
86
|
+
if isinstance(message, CustomMessage):
|
|
87
|
+
role = f"custom:{message.custom_type}"
|
|
88
|
+
return role, text
|
|
89
|
+
|
|
90
|
+
return role, ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _message_selectable(message: object) -> bool:
|
|
94
|
+
"""False for an assistant turn with unanswered tool calls (would create a dangling tool_call)."""
|
|
95
|
+
from tau.message.types import AssistantMessage
|
|
96
|
+
return not (isinstance(message, AssistantMessage) and message.tool_calls())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def open_tree_selector(ctx: CommandContext) -> None:
|
|
100
|
+
from tau.session.types import (
|
|
101
|
+
BranchSummaryEntry,
|
|
102
|
+
CompactionEntry,
|
|
103
|
+
CustomInfoEntry,
|
|
104
|
+
CustomMessageEntry,
|
|
105
|
+
LabelEntry,
|
|
106
|
+
MessageEntry,
|
|
107
|
+
ModelChangeEntry,
|
|
108
|
+
ThinkingLevelChangeEntry,
|
|
109
|
+
)
|
|
110
|
+
from tau.message.types import TextContent
|
|
111
|
+
from tau.tui.components.tree_select_list import TreeRow
|
|
112
|
+
|
|
113
|
+
sm = ctx.runtime.session_manager
|
|
114
|
+
if sm is None:
|
|
115
|
+
ctx.notify("No active session.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
nodes = sm.get_tree()
|
|
119
|
+
if not nodes:
|
|
120
|
+
ctx.notify("Session tree is empty.")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
current_leaf = sm.get_leaf_id()
|
|
124
|
+
rows: list[TreeRow[str]] = []
|
|
125
|
+
|
|
126
|
+
# Flatten once to map id -> parent_id, so we can walk current_leaf's
|
|
127
|
+
# ancestor chain and mark the active path (independent of tree nesting).
|
|
128
|
+
parent_of: dict[str, str | None] = {}
|
|
129
|
+
|
|
130
|
+
def _index(node_list: list) -> None:
|
|
131
|
+
for node in node_list:
|
|
132
|
+
parent_of[node.entry.id] = node.entry.parent_id
|
|
133
|
+
_index(node.children)
|
|
134
|
+
|
|
135
|
+
_index(nodes)
|
|
136
|
+
|
|
137
|
+
active_ids: set[str] = set()
|
|
138
|
+
cur = current_leaf
|
|
139
|
+
while cur is not None and cur in parent_of:
|
|
140
|
+
active_ids.add(cur)
|
|
141
|
+
cur = parent_of[cur]
|
|
142
|
+
|
|
143
|
+
def _entry_role_text(entry: object) -> tuple[str, str] | None:
|
|
144
|
+
if isinstance(entry, MessageEntry):
|
|
145
|
+
return _message_snippet(entry.message)
|
|
146
|
+
if isinstance(entry, CompactionEntry):
|
|
147
|
+
return "compaction", entry.summary
|
|
148
|
+
if isinstance(entry, BranchSummaryEntry):
|
|
149
|
+
return "branch_summary", entry.summary
|
|
150
|
+
if isinstance(entry, CustomMessageEntry):
|
|
151
|
+
text = " ".join(c.content for c in entry.content if isinstance(c, TextContent))
|
|
152
|
+
return f"custom:{entry.custom_type}", text
|
|
153
|
+
if isinstance(entry, LabelEntry):
|
|
154
|
+
return ("label", entry.label) if entry.label else None
|
|
155
|
+
if isinstance(entry, ModelChangeEntry):
|
|
156
|
+
return "model", f"{entry.provider_id}/{entry.model_id}"
|
|
157
|
+
if isinstance(entry, ThinkingLevelChangeEntry):
|
|
158
|
+
return "thinking_level", str(entry.thinking_level)
|
|
159
|
+
if isinstance(entry, CustomInfoEntry):
|
|
160
|
+
return f"info:{entry.custom_type}", ""
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
disabled_ids: set[str] = set()
|
|
164
|
+
|
|
165
|
+
def _contains_active(node: object) -> bool:
|
|
166
|
+
"""True if node or any descendant is on the active path."""
|
|
167
|
+
stack = [node]
|
|
168
|
+
while stack:
|
|
169
|
+
n = stack.pop()
|
|
170
|
+
if n.entry.id in active_ids: # type: ignore[attr-defined]
|
|
171
|
+
return True
|
|
172
|
+
stack.extend(n.children) # type: ignore[attr-defined]
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def _build_prefix(
|
|
176
|
+
gutters: list[tuple[int, bool]],
|
|
177
|
+
show_connector: bool,
|
|
178
|
+
is_last: bool,
|
|
179
|
+
display_indent: int,
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Char-by-char prefix: gutters (│) + connector (├─/└─) + spaces."""
|
|
182
|
+
if display_indent == 0:
|
|
183
|
+
return ""
|
|
184
|
+
connector_pos = display_indent - 1
|
|
185
|
+
chars: list[str] = []
|
|
186
|
+
for ci in range(display_indent * 3):
|
|
187
|
+
level = ci // 3
|
|
188
|
+
pos = ci % 3
|
|
189
|
+
gutter_show = next((s for lv, s in gutters if lv == level), None)
|
|
190
|
+
if gutter_show is not None:
|
|
191
|
+
chars.append("│" if pos == 0 and gutter_show else " ")
|
|
192
|
+
elif show_connector and level == connector_pos:
|
|
193
|
+
if pos == 0:
|
|
194
|
+
chars.append("└" if is_last else "├")
|
|
195
|
+
elif pos == 1:
|
|
196
|
+
chars.append("─")
|
|
197
|
+
else:
|
|
198
|
+
chars.append(" ")
|
|
199
|
+
else:
|
|
200
|
+
chars.append(" ")
|
|
201
|
+
return "".join(chars)
|
|
202
|
+
|
|
203
|
+
def _walk(
|
|
204
|
+
node_list: list,
|
|
205
|
+
gutters: list[tuple[int, bool]],
|
|
206
|
+
display_indent: int,
|
|
207
|
+
just_branched: bool,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Tree walk:
|
|
211
|
+
- Connectors (├─/└─) only when multiple siblings exist.
|
|
212
|
+
- Linear single-child chains stay flat (no indent increase, no connector).
|
|
213
|
+
- Gutters (│) track open branch lines for descendants.
|
|
214
|
+
- just_branched: parent had multiple children → first gen after branch also indents.
|
|
215
|
+
"""
|
|
216
|
+
n = len(node_list)
|
|
217
|
+
is_branching = n > 1 # multiple siblings → show connectors
|
|
218
|
+
|
|
219
|
+
for i, node in enumerate(node_list):
|
|
220
|
+
is_last = i == n - 1
|
|
221
|
+
entry = node.entry
|
|
222
|
+
role_text = _entry_role_text(entry)
|
|
223
|
+
|
|
224
|
+
if role_text is not None:
|
|
225
|
+
role, text = role_text
|
|
226
|
+
show_connector = is_branching and display_indent > 0
|
|
227
|
+
prefix = _build_prefix(gutters, show_connector, is_last, display_indent)
|
|
228
|
+
selectable = not isinstance(entry, MessageEntry) or _message_selectable(entry.message)
|
|
229
|
+
if not selectable:
|
|
230
|
+
disabled_ids.add(entry.id)
|
|
231
|
+
rows.append(TreeRow(
|
|
232
|
+
prefix=prefix,
|
|
233
|
+
role=role,
|
|
234
|
+
text=text[:80].replace("\n", " ").replace("\t", " "),
|
|
235
|
+
on_active_path=entry.id in active_ids,
|
|
236
|
+
is_current=entry.id == current_leaf,
|
|
237
|
+
selectable=selectable,
|
|
238
|
+
value=entry.id,
|
|
239
|
+
parent_value=getattr(entry, "parent_id", None),
|
|
240
|
+
has_children=len(node.children) > 0,
|
|
241
|
+
))
|
|
242
|
+
|
|
243
|
+
# Sort children so the branch containing the active leaf comes first
|
|
244
|
+
children = node.children
|
|
245
|
+
if len(children) > 1:
|
|
246
|
+
children = sorted(children, key=lambda n: 0 if _contains_active(n) else 1)
|
|
247
|
+
|
|
248
|
+
# Child indent rules:
|
|
249
|
+
# - node has multiple children → +1 (they will branch)
|
|
250
|
+
# - current level is branching AND not at root → +1 (just-branched grouping)
|
|
251
|
+
# - linear single-child chain → stay flat (no change)
|
|
252
|
+
multiple_children = len(children) > 1
|
|
253
|
+
if multiple_children:
|
|
254
|
+
child_indent = display_indent + 1
|
|
255
|
+
elif is_branching and display_indent > 0:
|
|
256
|
+
child_indent = display_indent + 1
|
|
257
|
+
else:
|
|
258
|
+
child_indent = display_indent
|
|
259
|
+
|
|
260
|
+
# Gutters: when this level branches, record a │ column for descendants
|
|
261
|
+
if is_branching and display_indent > 0:
|
|
262
|
+
child_gutters = gutters + [(display_indent - 1, not is_last)]
|
|
263
|
+
else:
|
|
264
|
+
child_gutters = gutters
|
|
265
|
+
|
|
266
|
+
_walk(children, child_gutters, child_indent, just_branched=is_branching)
|
|
267
|
+
|
|
268
|
+
_walk(nodes, [], 0, just_branched=False)
|
|
269
|
+
|
|
270
|
+
if not rows:
|
|
271
|
+
ctx.notify("No navigable branches found.")
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
def commit(entry_id: str) -> None:
|
|
275
|
+
if entry_id in disabled_ids:
|
|
276
|
+
ctx.notify("Can't branch from a pending tool call — pick the tool result or a later message instead.")
|
|
277
|
+
return
|
|
278
|
+
asyncio.ensure_future(_apply_tree_branch(ctx, entry_id))
|
|
279
|
+
|
|
280
|
+
ctx.layout.open_branch_tree_selector(rows, commit, lambda: ctx.notify("Branch navigation cancelled."))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _extract_user_message_text(message: object) -> str | None:
|
|
284
|
+
"""Return the text content of a UserMessage, or None if not a plain user message."""
|
|
285
|
+
from tau.message.types import UserMessage, TextContent
|
|
286
|
+
if not isinstance(message, UserMessage):
|
|
287
|
+
return None
|
|
288
|
+
contents = getattr(message, "contents", None)
|
|
289
|
+
if not isinstance(contents, list):
|
|
290
|
+
return None
|
|
291
|
+
parts = [c.content for c in contents if isinstance(c, TextContent)]
|
|
292
|
+
return " ".join(parts) if parts else None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
async def _apply_tree_branch(ctx: CommandContext, entry_id: str) -> None:
|
|
296
|
+
from tau.session.types import MessageEntry
|
|
297
|
+
sm = ctx.runtime.session_manager
|
|
298
|
+
settings = ctx.runtime.settings_manager
|
|
299
|
+
|
|
300
|
+
# No-op if already at this node
|
|
301
|
+
if sm is not None and sm.get_leaf_id() == entry_id:
|
|
302
|
+
ctx.notify("Already at this point.")
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
# Detect if the selected entry is a user message — if so, navigate to its
|
|
306
|
+
# parent and restore the message text into the editor instead of the history.
|
|
307
|
+
navigate_id = entry_id
|
|
308
|
+
restore_text: str | None = None
|
|
309
|
+
if sm is not None and entry_id in sm.by_id:
|
|
310
|
+
entry = sm.by_id[entry_id]
|
|
311
|
+
if isinstance(entry, MessageEntry):
|
|
312
|
+
user_text = _extract_user_message_text(entry.message)
|
|
313
|
+
if user_text is not None:
|
|
314
|
+
restore_text = user_text
|
|
315
|
+
navigate_id = entry.parent_id or entry_id
|
|
316
|
+
|
|
317
|
+
# Determine whether to ask about summarization
|
|
318
|
+
summary_enabled = settings.is_branch_summary_enabled() if settings is not None else True
|
|
319
|
+
skip_prompt = settings.get_branch_summary_skip_prompt() if settings is not None else False
|
|
320
|
+
|
|
321
|
+
summarize = False
|
|
322
|
+
if summary_enabled and not skip_prompt:
|
|
323
|
+
from tau.tui.components.select_list import SelectItem
|
|
324
|
+
summary_items: list[SelectItem[str]] = [
|
|
325
|
+
SelectItem(label="No summary", description="Switch branch without summarizing", value="none"),
|
|
326
|
+
SelectItem(label="Summarize", description="Generate a summary of the abandoned branch", value="yes"),
|
|
327
|
+
]
|
|
328
|
+
loop = asyncio.get_running_loop()
|
|
329
|
+
fut: asyncio.Future[str | None] = loop.create_future()
|
|
330
|
+
|
|
331
|
+
def _commit(value: str) -> None:
|
|
332
|
+
if not fut.done():
|
|
333
|
+
fut.set_result(value)
|
|
334
|
+
|
|
335
|
+
def _cancel() -> None:
|
|
336
|
+
if not fut.done():
|
|
337
|
+
fut.set_result(None)
|
|
338
|
+
|
|
339
|
+
ctx.layout.open_tree_selector(summary_items, _commit, _cancel)
|
|
340
|
+
choice = await fut
|
|
341
|
+
|
|
342
|
+
if choice is None:
|
|
343
|
+
return # user cancelled
|
|
344
|
+
|
|
345
|
+
summarize = choice == "yes"
|
|
346
|
+
|
|
347
|
+
# Show spinner label while summarizing
|
|
348
|
+
if summarize:
|
|
349
|
+
ctx.layout.spinner.set_label("Summarizing branch…")
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
# When restoring a user message, navigate to its parent (navigate_id may differ from entry_id)
|
|
353
|
+
if sm is not None and navigate_id != sm.get_leaf_id():
|
|
354
|
+
ok = await ctx.runtime.navigate_tree(navigate_id, summarize=summarize)
|
|
355
|
+
if not ok:
|
|
356
|
+
ctx.notify("Branch navigation cancelled.")
|
|
357
|
+
return
|
|
358
|
+
elif restore_text is None:
|
|
359
|
+
# Already at this node (assistant message case), covered above
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
if restore_text is not None:
|
|
363
|
+
ctx.layout.input.set_text(restore_text)
|
|
364
|
+
ctx.notify("Restored message to input.")
|
|
365
|
+
else:
|
|
366
|
+
ctx.notify(f"Switched to branch at {entry_id[:8]}")
|
|
367
|
+
except Exception as exc:
|
|
368
|
+
ctx.notify(f"Failed to switch branch: {exc}")
|
|
369
|
+
finally:
|
|
370
|
+
if summarize:
|
|
371
|
+
# Restore default spinner label
|
|
372
|
+
ctx.layout.spinner.set_label(ctx.layout.spinner._theme.label_thinking)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def cmd_clone(ctx: CommandContext) -> None:
|
|
376
|
+
asyncio.ensure_future(_apply_clone(ctx))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
async def _apply_clone(ctx: CommandContext) -> None:
|
|
380
|
+
try:
|
|
381
|
+
await ctx.runtime.clone_session()
|
|
382
|
+
sm = ctx.runtime.session_manager
|
|
383
|
+
name = sm.session_file.name[:40] if sm and sm.session_file else "new session"
|
|
384
|
+
ctx.notify(f"Cloned into {name}")
|
|
385
|
+
except Exception as exc:
|
|
386
|
+
ctx.notify(f"Failed to clone: {exc}")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def cmd_session(ctx: CommandContext) -> None:
|
|
390
|
+
from tau.tui.ansi import BOLD, DIM, RESET
|
|
391
|
+
from tau.session.types import MessageEntry as SessionMessageEntry
|
|
392
|
+
from tau.message.types import AssistantMessage, ToolMessage, UserMessage
|
|
393
|
+
|
|
394
|
+
sm = ctx.runtime.session_manager
|
|
395
|
+
if sm is None:
|
|
396
|
+
ctx.notify("No active session.")
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
branch = sm.get_branch()
|
|
400
|
+
|
|
401
|
+
user_count = 0
|
|
402
|
+
assistant_count = 0
|
|
403
|
+
tool_call_count = 0
|
|
404
|
+
tool_result_count = 0
|
|
405
|
+
input_tokens = 0
|
|
406
|
+
output_tokens = 0
|
|
407
|
+
cache_read_tokens = 0
|
|
408
|
+
cache_write_tokens = 0
|
|
409
|
+
total_cost = 0.0
|
|
410
|
+
|
|
411
|
+
for entry in branch:
|
|
412
|
+
if not isinstance(entry, SessionMessageEntry):
|
|
413
|
+
continue
|
|
414
|
+
msg = entry.message
|
|
415
|
+
if isinstance(msg, UserMessage):
|
|
416
|
+
user_count += 1
|
|
417
|
+
elif isinstance(msg, AssistantMessage):
|
|
418
|
+
assistant_count += 1
|
|
419
|
+
tool_call_count += len(msg.tool_calls())
|
|
420
|
+
input_tokens += msg.usage.input_tokens
|
|
421
|
+
output_tokens += msg.usage.output_tokens
|
|
422
|
+
cache_read_tokens += msg.usage.cache_read_tokens
|
|
423
|
+
cache_write_tokens += msg.usage.cache_write_tokens
|
|
424
|
+
total_cost += msg.usage.cost.total
|
|
425
|
+
elif isinstance(msg, ToolMessage):
|
|
426
|
+
tool_result_count += len(msg.contents)
|
|
427
|
+
|
|
428
|
+
total_messages = user_count + assistant_count + (1 if tool_result_count else 0)
|
|
429
|
+
total_tokens = input_tokens + output_tokens + cache_read_tokens + cache_write_tokens
|
|
430
|
+
|
|
431
|
+
session_name = sm.get_session_name()
|
|
432
|
+
session_file = sm.session_file
|
|
433
|
+
session_id = sm.session_id or ""
|
|
434
|
+
|
|
435
|
+
W = 14
|
|
436
|
+
lines: list[str] = []
|
|
437
|
+
lines.append(f"{BOLD}Session Info{RESET}")
|
|
438
|
+
lines.append("")
|
|
439
|
+
if session_name:
|
|
440
|
+
lines.append(f"{DIM}{'Name':<{W}}{RESET} {session_name}")
|
|
441
|
+
lines.append(f"{DIM}{'File':<{W}}{RESET} {session_file or 'in-memory'}")
|
|
442
|
+
lines.append(f"{DIM}{'ID':<{W}}{RESET} {session_id}")
|
|
443
|
+
lines.append("")
|
|
444
|
+
lines.append(f"{BOLD}Messages{RESET}")
|
|
445
|
+
lines.append(f"{DIM}{'User':<{W}}{RESET} {user_count}")
|
|
446
|
+
lines.append(f"{DIM}{'Assistant':<{W}}{RESET} {assistant_count}")
|
|
447
|
+
lines.append(f"{DIM}{'Tool calls':<{W}}{RESET} {tool_call_count}")
|
|
448
|
+
lines.append(f"{DIM}{'Tool results':<{W}}{RESET} {tool_result_count}")
|
|
449
|
+
lines.append(f"{DIM}{'Total':<{W}}{RESET} {total_messages}")
|
|
450
|
+
lines.append("")
|
|
451
|
+
lines.append(f"{BOLD}Tokens{RESET}")
|
|
452
|
+
lines.append(f"{DIM}{'Input':<{W}}{RESET} {input_tokens:,}")
|
|
453
|
+
lines.append(f"{DIM}{'Output':<{W}}{RESET} {output_tokens:,}")
|
|
454
|
+
if cache_read_tokens:
|
|
455
|
+
lines.append(f"{DIM}{'Cache read':<{W}}{RESET} {cache_read_tokens:,}")
|
|
456
|
+
if cache_write_tokens:
|
|
457
|
+
lines.append(f"{DIM}{'Cache write':<{W}}{RESET} {cache_write_tokens:,}")
|
|
458
|
+
lines.append(f"{DIM}{'Total':<{W}}{RESET} {total_tokens:,}")
|
|
459
|
+
if total_cost > 0:
|
|
460
|
+
lines.append("")
|
|
461
|
+
lines.append(f"{BOLD}Cost{RESET}")
|
|
462
|
+
lines.append(f"{DIM}{'Total':<{W}}{RESET} ${total_cost:.4f}")
|
|
463
|
+
|
|
464
|
+
ctx.notify("\n".join(lines))
|