symphony-code 0.0.1__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.
- coding_agent/__init__.py +20 -0
- coding_agent/_lazy.py +17 -0
- coding_agent/addons/__init__.py +35 -0
- coding_agent/addons/approvals.py +224 -0
- coding_agent/addons/compaction/__init__.py +36 -0
- coding_agent/addons/compaction/addon.py +88 -0
- coding_agent/addons/compaction/compactor.py +138 -0
- coding_agent/addons/compaction/prompts.py +20 -0
- coding_agent/addons/compaction/transcript.py +118 -0
- coding_agent/addons/evaluation/__init__.py +68 -0
- coding_agent/addons/evaluation/addon.py +199 -0
- coding_agent/addons/evaluation/evaluators.py +245 -0
- coding_agent/addons/evaluation/handlers.py +66 -0
- coding_agent/addons/evaluation/policy.py +277 -0
- coding_agent/addons/evaluation/protocol.py +192 -0
- coding_agent/addons/evaluation/state.py +284 -0
- coding_agent/addons/langfuse/__init__.py +24 -0
- coding_agent/addons/langfuse/addon.py +465 -0
- coding_agent/addons/langfuse/install.py +79 -0
- coding_agent/addons/langfuse/serialize.py +158 -0
- coding_agent/addons/learning/__init__.py +33 -0
- coding_agent/addons/learning/addon.py +181 -0
- coding_agent/addons/learning/loop.py +247 -0
- coding_agent/addons/learning/prompts.py +26 -0
- coding_agent/addons/learning/sanitize.py +64 -0
- coding_agent/addons/learning/store.py +522 -0
- coding_agent/addons/manager/__init__.py +18 -0
- coding_agent/addons/manager/bridge.py +74 -0
- coding_agent/addons/manager/discovery.py +34 -0
- coding_agent/addons/manager/report.py +33 -0
- coding_agent/addons/manager/runtime.py +76 -0
- coding_agent/addons/manager/tool_changes.py +16 -0
- coding_agent/addons/persistence/__init__.py +41 -0
- coding_agent/addons/persistence/active.py +132 -0
- coding_agent/addons/persistence/artifacts.py +110 -0
- coding_agent/addons/persistence/collection.py +166 -0
- coding_agent/addons/persistence/compatibility.py +30 -0
- coding_agent/addons/persistence/images.py +133 -0
- coding_agent/addons/persistence/jsonl.py +1195 -0
- coding_agent/addons/persistence/presentation.py +104 -0
- coding_agent/addons/plugins/__init__.py +19 -0
- coding_agent/addons/plugins/install.py +37 -0
- coding_agent/addons/plugins/manager.py +140 -0
- coding_agent/addons/plugins/models.py +50 -0
- coding_agent/addons/skills/__init__.py +28 -0
- coding_agent/addons/skills/addon.py +26 -0
- coding_agent/addons/skills/args.py +78 -0
- coding_agent/addons/skills/bundled_skills/addon-authoring/SKILL.md +77 -0
- coding_agent/addons/skills/bundled_skills/context-budget/SKILL.md +12 -0
- coding_agent/addons/skills/bundled_skills/orient-first/SKILL.md +16 -0
- coding_agent/addons/skills/bundled_skills/security-boundary/SKILL.md +16 -0
- coding_agent/addons/skills/bundled_skills/skill-discipline/SKILL.md +13 -0
- coding_agent/addons/skills/bundled_skills/surgical-edit/SKILL.md +11 -0
- coding_agent/addons/skills/bundled_skills/systematic-debug/SKILL.md +18 -0
- coding_agent/addons/skills/bundled_skills/verify-before-done/SKILL.md +11 -0
- coding_agent/addons/skills/frontmatter.py +57 -0
- coding_agent/addons/skills/models.py +57 -0
- coding_agent/addons/skills/registry.py +109 -0
- coding_agent/addons/skills/tool.py +54 -0
- coding_agent/addons/voice/__init__.py +5 -0
- coding_agent/addons/voice/addon.py +106 -0
- coding_agent/addons/voice/credentials.py +42 -0
- coding_agent/addons/voice/playback.py +42 -0
- coding_agent/addons/voice/speak.py +287 -0
- coding_agent/addons/voice/stream.py +651 -0
- coding_agent/addons/voice/summary.py +126 -0
- coding_agent/agent.py +523 -0
- coding_agent/bench/__init__.py +1 -0
- coding_agent/bench/cli.py +200 -0
- coding_agent/bench/patch.py +78 -0
- coding_agent/commands/__init__.py +5 -0
- coding_agent/commands/catalog.py +179 -0
- coding_agent/commands/jev_argument.py +40 -0
- coding_agent/config/__init__.py +51 -0
- coding_agent/config/config.py +376 -0
- coding_agent/config/credentials.py +153 -0
- coding_agent/config/personalities.json +34 -0
- coding_agent/config/personalities.py +170 -0
- coding_agent/config/prompts.py +67 -0
- coding_agent/protocols/__init__.py +5 -0
- coding_agent/protocols/acp/README.md +36 -0
- coding_agent/protocols/acp/__init__.py +27 -0
- coding_agent/protocols/acp/mapping.py +164 -0
- coding_agent/protocols/acp/server.py +261 -0
- coding_agent/protocols/acp/transport.py +72 -0
- coding_agent/protocols/stdio/README.md +36 -0
- coding_agent/protocols/stdio/__init__.py +37 -0
- coding_agent/protocols/stdio/catalog.py +49 -0
- coding_agent/protocols/stdio/commands.py +218 -0
- coding_agent/protocols/stdio/server.py +181 -0
- coding_agent/protocols/stdio/transport.py +71 -0
- coding_agent/py.typed +0 -0
- coding_agent/resources/__init__.py +5 -0
- coding_agent/resources/usage.py +264 -0
- coding_agent/run/__init__.py +1 -0
- coding_agent/run/cli.py +99 -0
- coding_agent/run/detach.py +37 -0
- coding_agent/run/interrupted.py +67 -0
- coding_agent/run/log_sink.py +46 -0
- coding_agent/tools/__init__.py +73 -0
- coding_agent/tools/ask_user.py +61 -0
- coding_agent/tools/base.py +136 -0
- coding_agent/tools/bash.py +288 -0
- coding_agent/tools/bash_jobs.py +116 -0
- coding_agent/tools/generate_image.py +123 -0
- coding_agent/tools/memory.py +100 -0
- coding_agent/tools/patch.py +218 -0
- coding_agent/tools/read_file.py +167 -0
- coding_agent/tools/search.py +433 -0
- coding_agent/tools/web_search.py +151 -0
- coding_agent/tools/write_file.py +40 -0
- coding_agent/tui/EFFICIENCY_PLAN.md +25 -0
- coding_agent/tui/__init__.py +22 -0
- coding_agent/tui/__main__.py +122 -0
- coding_agent/tui/app.py +456 -0
- coding_agent/tui/chrome/__init__.py +33 -0
- coding_agent/tui/chrome/footer.py +274 -0
- coding_agent/tui/chrome/topbar.py +259 -0
- coding_agent/tui/commands/__init__.py +45 -0
- coding_agent/tui/commands/catalog.py +35 -0
- coding_agent/tui/commands/manager.py +557 -0
- coding_agent/tui/composer/__init__.py +18 -0
- coding_agent/tui/composer/input.py +219 -0
- coding_agent/tui/composer/slash_menu.py +341 -0
- coding_agent/tui/composer/surface.py +330 -0
- coding_agent/tui/composer/voice.py +267 -0
- coding_agent/tui/composer/voice_mode.py +491 -0
- coding_agent/tui/runtime/__init__.py +23 -0
- coding_agent/tui/runtime/events.py +728 -0
- coding_agent/tui/runtime/sink.py +115 -0
- coding_agent/tui/runtime/state.py +145 -0
- coding_agent/tui/runtime/subagent.py +531 -0
- coding_agent/tui/runtime/turn.py +206 -0
- coding_agent/tui/screens/__init__.py +54 -0
- coding_agent/tui/screens/ask.py +69 -0
- coding_agent/tui/screens/context.py +195 -0
- coding_agent/tui/screens/dashboard.py +91 -0
- coding_agent/tui/screens/diff.py +302 -0
- coding_agent/tui/screens/extensions.py +68 -0
- coding_agent/tui/screens/file_selector.py +167 -0
- coding_agent/tui/screens/history.py +458 -0
- coding_agent/tui/screens/langfuse.py +142 -0
- coding_agent/tui/screens/learning.py +113 -0
- coding_agent/tui/screens/modal.py +131 -0
- coding_agent/tui/screens/onboard.py +455 -0
- coding_agent/tui/screens/resume.py +163 -0
- coding_agent/tui/theme/__init__.py +49 -0
- coding_agent/tui/theme/load.py +325 -0
- coding_agent/tui/theme/style.py +95 -0
- coding_agent/tui/theme/theme.toml +1687 -0
- coding_agent/tui/tools/__init__.py +53 -0
- coding_agent/tui/tools/activity.py +184 -0
- coding_agent/tui/tools/calls.py +560 -0
- coding_agent/tui/tools/code_view.py +52 -0
- coding_agent/tui/tools/images.py +250 -0
- coding_agent/tui/tools/labels.py +135 -0
- coding_agent/tui/tools/modal.py +137 -0
- coding_agent/tui/tools/partial_args.py +19 -0
- coding_agent/tui/tools/snapshots.py +382 -0
- coding_agent/tui/transcript/__init__.py +36 -0
- coding_agent/tui/transcript/messages.py +408 -0
- coding_agent/tui/transcript/process.py +504 -0
- coding_agent/tui/transcript/surface.py +345 -0
- coding_agent/tui/transcript/thinking_markdown.py +314 -0
- symphony_code-0.0.1.dist-info/METADATA +66 -0
- symphony_code-0.0.1.dist-info/RECORD +169 -0
- symphony_code-0.0.1.dist-info/WHEEL +4 -0
- symphony_code-0.0.1.dist-info/entry_points.txt +5 -0
- symphony_code-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""Tool and thought snapshots, and the Explored group row that displays them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Mapping, Sequence
|
|
7
|
+
|
|
8
|
+
from rich.style import Style
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from textual import events
|
|
11
|
+
|
|
12
|
+
from coding_agent.tui.tools.activity import (
|
|
13
|
+
choose_completion_verb,
|
|
14
|
+
explored_activity,
|
|
15
|
+
explored_count_label,
|
|
16
|
+
explored_title,
|
|
17
|
+
format_explored_duration,
|
|
18
|
+
parse_activity,
|
|
19
|
+
same_activity_group,
|
|
20
|
+
)
|
|
21
|
+
from coding_agent.tui.tools.labels import header_target, patch_path, tool_detail, tool_label
|
|
22
|
+
from coding_agent.tui.transcript.messages import (
|
|
23
|
+
SelectableStatic,
|
|
24
|
+
clip_text,
|
|
25
|
+
return_composer_focus,
|
|
26
|
+
)
|
|
27
|
+
from coding_agent.tui.transcript.thinking_markdown import thinking_plain_text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ThoughtSnapshot:
|
|
32
|
+
"""Display data retained after a completed reasoning widget is folded."""
|
|
33
|
+
|
|
34
|
+
title: str
|
|
35
|
+
content: str = ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ToolCallSnapshot:
|
|
40
|
+
"""Display data retained after a live tool card is folded away."""
|
|
41
|
+
|
|
42
|
+
call_id: str
|
|
43
|
+
tool_name: str = "tool"
|
|
44
|
+
label: str = "Tool"
|
|
45
|
+
detail: str = ""
|
|
46
|
+
status: str = "done"
|
|
47
|
+
result: str = ""
|
|
48
|
+
activity_verb: str = ""
|
|
49
|
+
activity_reason: str = ""
|
|
50
|
+
activity_group: str = ""
|
|
51
|
+
duration: float | None = None
|
|
52
|
+
|
|
53
|
+
def as_text(self) -> str:
|
|
54
|
+
marker = "×" if self.status == "failed" else "✓"
|
|
55
|
+
line = f"{marker} {self.label}"
|
|
56
|
+
if self.detail:
|
|
57
|
+
line = f"{line} {self.detail}"
|
|
58
|
+
if self.result:
|
|
59
|
+
line = f"{line}\n {self.result}"
|
|
60
|
+
return line
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def snapshot_from_call(
|
|
64
|
+
*,
|
|
65
|
+
call_id: str,
|
|
66
|
+
tool_name: str,
|
|
67
|
+
arguments: Mapping[str, Any] | None = None,
|
|
68
|
+
raw_arguments: str = "",
|
|
69
|
+
status: str = "done",
|
|
70
|
+
result: str = "",
|
|
71
|
+
activity: Mapping[str, Any] | None = None,
|
|
72
|
+
) -> ToolCallSnapshot:
|
|
73
|
+
label, _icon = tool_label(tool_name)
|
|
74
|
+
display_arguments = dict(arguments or {})
|
|
75
|
+
nested_activity = display_arguments.pop("activity", None)
|
|
76
|
+
parsed = parse_activity(activity if isinstance(activity, Mapping) else nested_activity)
|
|
77
|
+
detail = tool_detail(tool_name, display_arguments, raw_arguments)
|
|
78
|
+
if tool_name == "patch" and not detail:
|
|
79
|
+
detail = patch_path(display_arguments, raw_arguments, result)
|
|
80
|
+
return ToolCallSnapshot(
|
|
81
|
+
call_id=call_id,
|
|
82
|
+
tool_name=tool_name,
|
|
83
|
+
label=label,
|
|
84
|
+
detail=clip_text(detail, 300),
|
|
85
|
+
status=status,
|
|
86
|
+
result=clip_text(result, 260) if result else "",
|
|
87
|
+
activity_verb=parsed.verb,
|
|
88
|
+
activity_reason=parsed.reason,
|
|
89
|
+
activity_group=parsed.group,
|
|
90
|
+
duration=None,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
NESTED_GUIDE = "tool-call-summary--guide"
|
|
95
|
+
NESTED_VERB = "tool-call-summary--verb"
|
|
96
|
+
NESTED_ARGS = "tool-call-summary--args"
|
|
97
|
+
NESTED_DETAIL = "tool-call-summary--detail"
|
|
98
|
+
NESTED_VERB_FAILED = "tool-call-summary--verb-failed"
|
|
99
|
+
NESTED_VERB_RUNNING = "tool-call-summary--verb-running"
|
|
100
|
+
LIVE_STATUSES = frozenset({"preparing", "running"})
|
|
101
|
+
# fx (vercel-labs/fx) draws a tree guide in front of grouped tool rows:
|
|
102
|
+
# "├" for every row but the last, "└" for the last, and "│" under a
|
|
103
|
+
# non-final row when it continues onto a second line.
|
|
104
|
+
GUIDE_BRANCH = "├"
|
|
105
|
+
GUIDE_LAST = "└"
|
|
106
|
+
GUIDE_CONTINUE = "│"
|
|
107
|
+
GUIDE_END = " "
|
|
108
|
+
# Used before the widget is mounted (no stylesheet yet); TCSS supplies the
|
|
109
|
+
# themed colours once it is.
|
|
110
|
+
UNMOUNTED_NESTED_STYLE = Style()
|
|
111
|
+
# Nested rows stay regular weight while the focused/hovered header is bold.
|
|
112
|
+
NESTED_WEIGHT = Style(bold=False)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def verb_component(status: str) -> str:
|
|
116
|
+
"""Return the verb's component class: danger when failed, warning while live."""
|
|
117
|
+
if status == "failed":
|
|
118
|
+
return NESTED_VERB_FAILED
|
|
119
|
+
if status in LIVE_STATUSES:
|
|
120
|
+
return NESTED_VERB_RUNNING
|
|
121
|
+
return NESTED_VERB
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def detail_preview(content: str, limit: int) -> str:
|
|
125
|
+
"""A thought's markdown as one clipped line of plain text, markers stripped."""
|
|
126
|
+
return clip_text(thinking_plain_text(content), limit)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def guide_glyphs(is_last: bool) -> tuple[str, str]:
|
|
130
|
+
"""Return the guide for a row's first line and for its continuation."""
|
|
131
|
+
if is_last:
|
|
132
|
+
return GUIDE_LAST, GUIDE_END
|
|
133
|
+
return GUIDE_BRANCH, GUIDE_CONTINUE
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def append_nested_row(
|
|
137
|
+
text: Text, styles: Mapping[str, Style], guide: str, status: str, verb: str, args: str
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Append one row: guide, verb (coloured by status), then arguments."""
|
|
140
|
+
text.append("\n")
|
|
141
|
+
text.append(guide, style=styles[NESTED_GUIDE])
|
|
142
|
+
text.append(" ")
|
|
143
|
+
text.append(verb, style=styles[verb_component(status)])
|
|
144
|
+
if args:
|
|
145
|
+
text.append(f" {args}", style=styles[NESTED_ARGS])
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def append_continuation(text: Text, styles: Mapping[str, Style], guide: str, content: str) -> None:
|
|
149
|
+
"""Append a row's second line under the guide's continuation glyph."""
|
|
150
|
+
text.append("\n")
|
|
151
|
+
text.append(guide, style=styles[NESTED_GUIDE])
|
|
152
|
+
text.append(f" {detail_preview(content, 220)}", style=styles[NESTED_DETAIL])
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ToolCallSummary(SelectableStatic, can_focus=True):
|
|
156
|
+
"""One stretch of non-interactive tool calls, grouped at render time.
|
|
157
|
+
|
|
158
|
+
The live transcript mounts a group once and updates its rows in place:
|
|
159
|
+
``update_call`` swaps a call's snapshot, so running → done is a text
|
|
160
|
+
change. A live group starts expanded; ``close`` folds it when the stretch
|
|
161
|
+
ends unless the reader opened it themselves.
|
|
162
|
+
|
|
163
|
+
Expanded rows start at the header's left edge behind an fx-style tree
|
|
164
|
+
guide, one line each: guide, verb, then arguments. The
|
|
165
|
+
``tool-call-summary--*`` component classes in theme.toml colour each part.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
COMPONENT_CLASSES = {
|
|
169
|
+
NESTED_GUIDE,
|
|
170
|
+
NESTED_VERB,
|
|
171
|
+
NESTED_ARGS,
|
|
172
|
+
NESTED_DETAIL,
|
|
173
|
+
NESTED_VERB_FAILED,
|
|
174
|
+
NESTED_VERB_RUNNING,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
calls: Sequence[ToolCallSnapshot | ThoughtSnapshot] | None = None,
|
|
180
|
+
*,
|
|
181
|
+
expanded: bool = False,
|
|
182
|
+
) -> None:
|
|
183
|
+
self.calls: list[ToolCallSnapshot] = []
|
|
184
|
+
self.entries: list[ToolCallSnapshot | ThoughtSnapshot] = []
|
|
185
|
+
self.is_expanded = expanded
|
|
186
|
+
# Set when the reader toggles the group; ``close`` never folds it then.
|
|
187
|
+
self.user_expanded = False
|
|
188
|
+
super().__init__(classes="tool-call-summary", markup=False)
|
|
189
|
+
# Restored/completed runs can contain thousands of entries. Build once
|
|
190
|
+
# rather than rescanning the growing group for every snapshot.
|
|
191
|
+
seen: set[str] = set()
|
|
192
|
+
for call in calls or ():
|
|
193
|
+
if isinstance(call, ThoughtSnapshot):
|
|
194
|
+
self.entries.append(call)
|
|
195
|
+
elif call.call_id not in seen:
|
|
196
|
+
seen.add(call.call_id)
|
|
197
|
+
self.calls.append(call)
|
|
198
|
+
self.entries.append(call)
|
|
199
|
+
self.title = self._summary_title()
|
|
200
|
+
self.set_class(any(item.status == "failed" for item in self.calls), "has-failures")
|
|
201
|
+
|
|
202
|
+
def _thought_count(self) -> int:
|
|
203
|
+
return sum(isinstance(entry, ThoughtSnapshot) for entry in self.entries)
|
|
204
|
+
|
|
205
|
+
def _summary_title(self) -> str:
|
|
206
|
+
return explored_title(self.calls, thought_count=self._thought_count())
|
|
207
|
+
|
|
208
|
+
def accepts(self, snapshot: ToolCallSnapshot) -> bool:
|
|
209
|
+
"""Return whether a call belongs in this activity's group."""
|
|
210
|
+
if not self.calls:
|
|
211
|
+
return True
|
|
212
|
+
return same_activity_group(explored_activity(self.calls), snapshot)
|
|
213
|
+
|
|
214
|
+
def rejects_last(self, snapshot: ToolCallSnapshot) -> bool:
|
|
215
|
+
"""Whether this group's newest call no longer matches the calls before it."""
|
|
216
|
+
if len(self.calls) < 2 or self.calls[-1].call_id != snapshot.call_id:
|
|
217
|
+
return False
|
|
218
|
+
return not same_activity_group(explored_activity(self.calls[:-1]), snapshot)
|
|
219
|
+
|
|
220
|
+
def render(self) -> Text:
|
|
221
|
+
activity = explored_activity(self.calls)
|
|
222
|
+
heading = activity.verb or activity.reason or "Explored"
|
|
223
|
+
text = Text()
|
|
224
|
+
text.append(clip_text(heading, 96), style="bold #8ab4cf")
|
|
225
|
+
duration = format_explored_duration(self.calls)
|
|
226
|
+
timing = f" for {duration}" if duration else ""
|
|
227
|
+
count = explored_count_label(
|
|
228
|
+
tool_count=self.count, thought_count=self._thought_count()
|
|
229
|
+
)
|
|
230
|
+
text.append(f" · {count}{timing}", style="#a2adb8")
|
|
231
|
+
failed = sum(call.status == "failed" for call in self.calls)
|
|
232
|
+
if failed:
|
|
233
|
+
text.append(f" · {failed} failed", style="bold #d66b73")
|
|
234
|
+
if not self.is_expanded:
|
|
235
|
+
thought = next(
|
|
236
|
+
(entry for entry in self.entries if isinstance(entry, ThoughtSnapshot)),
|
|
237
|
+
None,
|
|
238
|
+
)
|
|
239
|
+
if thought is not None and thought.content:
|
|
240
|
+
self._append_detail(text, thought.content, 96)
|
|
241
|
+
return text
|
|
242
|
+
self._append_nested_rows(text)
|
|
243
|
+
return text
|
|
244
|
+
|
|
245
|
+
def _nested_style(self, component: str) -> Style:
|
|
246
|
+
themed = self.get_component_rich_style(
|
|
247
|
+
component, partial=True, default=UNMOUNTED_NESTED_STYLE
|
|
248
|
+
)
|
|
249
|
+
return themed + NESTED_WEIGHT
|
|
250
|
+
|
|
251
|
+
def _append_detail(self, text: Text, content: str, limit: int) -> None:
|
|
252
|
+
text.append(f"\n{detail_preview(content, limit)}", style=self._nested_style(NESTED_DETAIL))
|
|
253
|
+
|
|
254
|
+
def _append_nested_rows(self, text: Text) -> None:
|
|
255
|
+
"""One compact line per entry behind the guide, flush with the header."""
|
|
256
|
+
styles = {component: self._nested_style(component) for component in self.COMPONENT_CLASSES}
|
|
257
|
+
last_index = len(self.entries) - 1
|
|
258
|
+
for index, entry in enumerate(self.entries):
|
|
259
|
+
guide, continuation = guide_glyphs(index == last_index)
|
|
260
|
+
if isinstance(entry, ThoughtSnapshot):
|
|
261
|
+
title = thinking_plain_text(entry.title)
|
|
262
|
+
append_nested_row(text, styles, guide, "done", title, "")
|
|
263
|
+
if entry.content:
|
|
264
|
+
append_continuation(text, styles, continuation, entry.content)
|
|
265
|
+
continue
|
|
266
|
+
append_nested_row(
|
|
267
|
+
text, styles, guide, entry.status, entry.label, header_target(entry.detail)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def call_ids(self) -> list[str]:
|
|
272
|
+
return [call.call_id for call in self.calls]
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def count(self) -> int:
|
|
276
|
+
return len(self.calls)
|
|
277
|
+
|
|
278
|
+
def add_call(self, snapshot: ToolCallSnapshot, *, layout: bool = True) -> None:
|
|
279
|
+
if snapshot.call_id not in self.call_ids:
|
|
280
|
+
self.calls.append(snapshot)
|
|
281
|
+
self.entries.append(snapshot)
|
|
282
|
+
self.title = self._summary_title()
|
|
283
|
+
self.set_class(any(item.status == "failed" for item in self.calls), "has-failures")
|
|
284
|
+
if layout and self.is_mounted:
|
|
285
|
+
self.refresh(layout=True)
|
|
286
|
+
|
|
287
|
+
def update_call(self, snapshot: ToolCallSnapshot) -> None:
|
|
288
|
+
"""Swap in a call's newer snapshot; a status change only re-renders text."""
|
|
289
|
+
index = self.call_ids.index(snapshot.call_id)
|
|
290
|
+
previous = self.calls[index]
|
|
291
|
+
self.calls[index] = snapshot
|
|
292
|
+
self.entries[self.entries.index(previous)] = snapshot
|
|
293
|
+
self.title = self._summary_title()
|
|
294
|
+
self.set_class(any(item.status == "failed" for item in self.calls), "has-failures")
|
|
295
|
+
reshaped = (previous.label, previous.detail) != (snapshot.label, snapshot.detail) or (
|
|
296
|
+
previous.status == "failed"
|
|
297
|
+
) != (snapshot.status == "failed")
|
|
298
|
+
self.refresh(layout=reshaped)
|
|
299
|
+
|
|
300
|
+
def remove_call(self, call_id: str) -> None:
|
|
301
|
+
index = self.call_ids.index(call_id)
|
|
302
|
+
self.entries.remove(self.calls.pop(index))
|
|
303
|
+
self.title = self._summary_title()
|
|
304
|
+
self.refresh(layout=True)
|
|
305
|
+
|
|
306
|
+
def add_thought(
|
|
307
|
+
self,
|
|
308
|
+
title: str,
|
|
309
|
+
content: str = "",
|
|
310
|
+
*,
|
|
311
|
+
layout: bool = True,
|
|
312
|
+
) -> None:
|
|
313
|
+
self.entries.append(ThoughtSnapshot(title=title, content=content))
|
|
314
|
+
self.title = self._summary_title()
|
|
315
|
+
if layout:
|
|
316
|
+
self.refresh(layout=True)
|
|
317
|
+
|
|
318
|
+
def toggle(self) -> None:
|
|
319
|
+
"""The reader's expand/collapse; a group they opened stays open."""
|
|
320
|
+
self.is_expanded = not self.is_expanded
|
|
321
|
+
self.user_expanded = self.is_expanded
|
|
322
|
+
self.refresh(layout=True)
|
|
323
|
+
|
|
324
|
+
def close(self) -> None:
|
|
325
|
+
"""Fold a finished live stretch unless the reader opened it."""
|
|
326
|
+
if self.is_expanded and not self.user_expanded:
|
|
327
|
+
self.is_expanded = False
|
|
328
|
+
self.refresh(layout=True)
|
|
329
|
+
|
|
330
|
+
def on_click(self, event: events.Click) -> None:
|
|
331
|
+
event.stop()
|
|
332
|
+
self.toggle()
|
|
333
|
+
self.app.call_after_refresh(return_composer_focus, self)
|
|
334
|
+
|
|
335
|
+
def on_key(self, event: events.Key) -> None:
|
|
336
|
+
if event.key in {"enter", "space"}:
|
|
337
|
+
event.stop()
|
|
338
|
+
event.prevent_default()
|
|
339
|
+
self.toggle()
|
|
340
|
+
|
|
341
|
+
def snapshot_text(self) -> str:
|
|
342
|
+
return "\n\n".join(call.as_text() for call in self.calls)
|
|
343
|
+
|
|
344
|
+
def archive_text(self) -> str:
|
|
345
|
+
"""Keep archived/explore transcripts as compact tool names only."""
|
|
346
|
+
return "\n".join(
|
|
347
|
+
entry.title if isinstance(entry, ThoughtSnapshot) else entry.label
|
|
348
|
+
for entry in self.entries
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class CompletedRunSummary(ToolCallSummary):
|
|
353
|
+
"""A folded collection covering the work that happened before the final reply."""
|
|
354
|
+
|
|
355
|
+
def __init__(
|
|
356
|
+
self,
|
|
357
|
+
calls: Sequence[ToolCallSnapshot] | None = None,
|
|
358
|
+
*,
|
|
359
|
+
verb: str = "",
|
|
360
|
+
duration: str = "",
|
|
361
|
+
detail: str = "",
|
|
362
|
+
) -> None:
|
|
363
|
+
self._verb = verb.strip() or choose_completion_verb()
|
|
364
|
+
self._duration = duration
|
|
365
|
+
self._detail = detail
|
|
366
|
+
super().__init__(calls)
|
|
367
|
+
|
|
368
|
+
def _summary_title(self) -> str:
|
|
369
|
+
timing = f" for {self._duration}" if self._duration else ""
|
|
370
|
+
detail = f" · {self._detail}" if self._detail else ""
|
|
371
|
+
return f"{self._verb}{timing}{detail}"
|
|
372
|
+
|
|
373
|
+
def render(self) -> Text:
|
|
374
|
+
text = Text()
|
|
375
|
+
text.append(self._verb, style="bold #8ab4cf")
|
|
376
|
+
if self._duration:
|
|
377
|
+
text.append(f" for {self._duration}", style="#a2adb8")
|
|
378
|
+
if self._detail:
|
|
379
|
+
text.append(f" · {self._detail}", style="#a2adb8")
|
|
380
|
+
if self.is_expanded:
|
|
381
|
+
self._append_nested_rows(text)
|
|
382
|
+
return text
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Conversation transcript widgets for the coding-agent TUI."""
|
|
2
|
+
|
|
3
|
+
from coding_agent.tui.transcript.messages import (
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
Notice,
|
|
6
|
+
RunSummary,
|
|
7
|
+
UserMessage,
|
|
8
|
+
Welcome,
|
|
9
|
+
clip_text,
|
|
10
|
+
compact_json,
|
|
11
|
+
preview_text,
|
|
12
|
+
)
|
|
13
|
+
from coding_agent.tui.transcript.process import (
|
|
14
|
+
ReasoningHeader,
|
|
15
|
+
ReasoningWidget,
|
|
16
|
+
RunProcess,
|
|
17
|
+
ThinkingStatus,
|
|
18
|
+
)
|
|
19
|
+
from coding_agent.tui.transcript.surface import TranscriptScroll, TranscriptSurface
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AssistantMessage",
|
|
23
|
+
"Notice",
|
|
24
|
+
"RunSummary",
|
|
25
|
+
"ReasoningHeader",
|
|
26
|
+
"ReasoningWidget",
|
|
27
|
+
"RunProcess",
|
|
28
|
+
"ThinkingStatus",
|
|
29
|
+
"TranscriptScroll",
|
|
30
|
+
"TranscriptSurface",
|
|
31
|
+
"UserMessage",
|
|
32
|
+
"Welcome",
|
|
33
|
+
"clip_text",
|
|
34
|
+
"compact_json",
|
|
35
|
+
"preview_text",
|
|
36
|
+
]
|