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.
Files changed (169) hide show
  1. coding_agent/__init__.py +20 -0
  2. coding_agent/_lazy.py +17 -0
  3. coding_agent/addons/__init__.py +35 -0
  4. coding_agent/addons/approvals.py +224 -0
  5. coding_agent/addons/compaction/__init__.py +36 -0
  6. coding_agent/addons/compaction/addon.py +88 -0
  7. coding_agent/addons/compaction/compactor.py +138 -0
  8. coding_agent/addons/compaction/prompts.py +20 -0
  9. coding_agent/addons/compaction/transcript.py +118 -0
  10. coding_agent/addons/evaluation/__init__.py +68 -0
  11. coding_agent/addons/evaluation/addon.py +199 -0
  12. coding_agent/addons/evaluation/evaluators.py +245 -0
  13. coding_agent/addons/evaluation/handlers.py +66 -0
  14. coding_agent/addons/evaluation/policy.py +277 -0
  15. coding_agent/addons/evaluation/protocol.py +192 -0
  16. coding_agent/addons/evaluation/state.py +284 -0
  17. coding_agent/addons/langfuse/__init__.py +24 -0
  18. coding_agent/addons/langfuse/addon.py +465 -0
  19. coding_agent/addons/langfuse/install.py +79 -0
  20. coding_agent/addons/langfuse/serialize.py +158 -0
  21. coding_agent/addons/learning/__init__.py +33 -0
  22. coding_agent/addons/learning/addon.py +181 -0
  23. coding_agent/addons/learning/loop.py +247 -0
  24. coding_agent/addons/learning/prompts.py +26 -0
  25. coding_agent/addons/learning/sanitize.py +64 -0
  26. coding_agent/addons/learning/store.py +522 -0
  27. coding_agent/addons/manager/__init__.py +18 -0
  28. coding_agent/addons/manager/bridge.py +74 -0
  29. coding_agent/addons/manager/discovery.py +34 -0
  30. coding_agent/addons/manager/report.py +33 -0
  31. coding_agent/addons/manager/runtime.py +76 -0
  32. coding_agent/addons/manager/tool_changes.py +16 -0
  33. coding_agent/addons/persistence/__init__.py +41 -0
  34. coding_agent/addons/persistence/active.py +132 -0
  35. coding_agent/addons/persistence/artifacts.py +110 -0
  36. coding_agent/addons/persistence/collection.py +166 -0
  37. coding_agent/addons/persistence/compatibility.py +30 -0
  38. coding_agent/addons/persistence/images.py +133 -0
  39. coding_agent/addons/persistence/jsonl.py +1195 -0
  40. coding_agent/addons/persistence/presentation.py +104 -0
  41. coding_agent/addons/plugins/__init__.py +19 -0
  42. coding_agent/addons/plugins/install.py +37 -0
  43. coding_agent/addons/plugins/manager.py +140 -0
  44. coding_agent/addons/plugins/models.py +50 -0
  45. coding_agent/addons/skills/__init__.py +28 -0
  46. coding_agent/addons/skills/addon.py +26 -0
  47. coding_agent/addons/skills/args.py +78 -0
  48. coding_agent/addons/skills/bundled_skills/addon-authoring/SKILL.md +77 -0
  49. coding_agent/addons/skills/bundled_skills/context-budget/SKILL.md +12 -0
  50. coding_agent/addons/skills/bundled_skills/orient-first/SKILL.md +16 -0
  51. coding_agent/addons/skills/bundled_skills/security-boundary/SKILL.md +16 -0
  52. coding_agent/addons/skills/bundled_skills/skill-discipline/SKILL.md +13 -0
  53. coding_agent/addons/skills/bundled_skills/surgical-edit/SKILL.md +11 -0
  54. coding_agent/addons/skills/bundled_skills/systematic-debug/SKILL.md +18 -0
  55. coding_agent/addons/skills/bundled_skills/verify-before-done/SKILL.md +11 -0
  56. coding_agent/addons/skills/frontmatter.py +57 -0
  57. coding_agent/addons/skills/models.py +57 -0
  58. coding_agent/addons/skills/registry.py +109 -0
  59. coding_agent/addons/skills/tool.py +54 -0
  60. coding_agent/addons/voice/__init__.py +5 -0
  61. coding_agent/addons/voice/addon.py +106 -0
  62. coding_agent/addons/voice/credentials.py +42 -0
  63. coding_agent/addons/voice/playback.py +42 -0
  64. coding_agent/addons/voice/speak.py +287 -0
  65. coding_agent/addons/voice/stream.py +651 -0
  66. coding_agent/addons/voice/summary.py +126 -0
  67. coding_agent/agent.py +523 -0
  68. coding_agent/bench/__init__.py +1 -0
  69. coding_agent/bench/cli.py +200 -0
  70. coding_agent/bench/patch.py +78 -0
  71. coding_agent/commands/__init__.py +5 -0
  72. coding_agent/commands/catalog.py +179 -0
  73. coding_agent/commands/jev_argument.py +40 -0
  74. coding_agent/config/__init__.py +51 -0
  75. coding_agent/config/config.py +376 -0
  76. coding_agent/config/credentials.py +153 -0
  77. coding_agent/config/personalities.json +34 -0
  78. coding_agent/config/personalities.py +170 -0
  79. coding_agent/config/prompts.py +67 -0
  80. coding_agent/protocols/__init__.py +5 -0
  81. coding_agent/protocols/acp/README.md +36 -0
  82. coding_agent/protocols/acp/__init__.py +27 -0
  83. coding_agent/protocols/acp/mapping.py +164 -0
  84. coding_agent/protocols/acp/server.py +261 -0
  85. coding_agent/protocols/acp/transport.py +72 -0
  86. coding_agent/protocols/stdio/README.md +36 -0
  87. coding_agent/protocols/stdio/__init__.py +37 -0
  88. coding_agent/protocols/stdio/catalog.py +49 -0
  89. coding_agent/protocols/stdio/commands.py +218 -0
  90. coding_agent/protocols/stdio/server.py +181 -0
  91. coding_agent/protocols/stdio/transport.py +71 -0
  92. coding_agent/py.typed +0 -0
  93. coding_agent/resources/__init__.py +5 -0
  94. coding_agent/resources/usage.py +264 -0
  95. coding_agent/run/__init__.py +1 -0
  96. coding_agent/run/cli.py +99 -0
  97. coding_agent/run/detach.py +37 -0
  98. coding_agent/run/interrupted.py +67 -0
  99. coding_agent/run/log_sink.py +46 -0
  100. coding_agent/tools/__init__.py +73 -0
  101. coding_agent/tools/ask_user.py +61 -0
  102. coding_agent/tools/base.py +136 -0
  103. coding_agent/tools/bash.py +288 -0
  104. coding_agent/tools/bash_jobs.py +116 -0
  105. coding_agent/tools/generate_image.py +123 -0
  106. coding_agent/tools/memory.py +100 -0
  107. coding_agent/tools/patch.py +218 -0
  108. coding_agent/tools/read_file.py +167 -0
  109. coding_agent/tools/search.py +433 -0
  110. coding_agent/tools/web_search.py +151 -0
  111. coding_agent/tools/write_file.py +40 -0
  112. coding_agent/tui/EFFICIENCY_PLAN.md +25 -0
  113. coding_agent/tui/__init__.py +22 -0
  114. coding_agent/tui/__main__.py +122 -0
  115. coding_agent/tui/app.py +456 -0
  116. coding_agent/tui/chrome/__init__.py +33 -0
  117. coding_agent/tui/chrome/footer.py +274 -0
  118. coding_agent/tui/chrome/topbar.py +259 -0
  119. coding_agent/tui/commands/__init__.py +45 -0
  120. coding_agent/tui/commands/catalog.py +35 -0
  121. coding_agent/tui/commands/manager.py +557 -0
  122. coding_agent/tui/composer/__init__.py +18 -0
  123. coding_agent/tui/composer/input.py +219 -0
  124. coding_agent/tui/composer/slash_menu.py +341 -0
  125. coding_agent/tui/composer/surface.py +330 -0
  126. coding_agent/tui/composer/voice.py +267 -0
  127. coding_agent/tui/composer/voice_mode.py +491 -0
  128. coding_agent/tui/runtime/__init__.py +23 -0
  129. coding_agent/tui/runtime/events.py +728 -0
  130. coding_agent/tui/runtime/sink.py +115 -0
  131. coding_agent/tui/runtime/state.py +145 -0
  132. coding_agent/tui/runtime/subagent.py +531 -0
  133. coding_agent/tui/runtime/turn.py +206 -0
  134. coding_agent/tui/screens/__init__.py +54 -0
  135. coding_agent/tui/screens/ask.py +69 -0
  136. coding_agent/tui/screens/context.py +195 -0
  137. coding_agent/tui/screens/dashboard.py +91 -0
  138. coding_agent/tui/screens/diff.py +302 -0
  139. coding_agent/tui/screens/extensions.py +68 -0
  140. coding_agent/tui/screens/file_selector.py +167 -0
  141. coding_agent/tui/screens/history.py +458 -0
  142. coding_agent/tui/screens/langfuse.py +142 -0
  143. coding_agent/tui/screens/learning.py +113 -0
  144. coding_agent/tui/screens/modal.py +131 -0
  145. coding_agent/tui/screens/onboard.py +455 -0
  146. coding_agent/tui/screens/resume.py +163 -0
  147. coding_agent/tui/theme/__init__.py +49 -0
  148. coding_agent/tui/theme/load.py +325 -0
  149. coding_agent/tui/theme/style.py +95 -0
  150. coding_agent/tui/theme/theme.toml +1687 -0
  151. coding_agent/tui/tools/__init__.py +53 -0
  152. coding_agent/tui/tools/activity.py +184 -0
  153. coding_agent/tui/tools/calls.py +560 -0
  154. coding_agent/tui/tools/code_view.py +52 -0
  155. coding_agent/tui/tools/images.py +250 -0
  156. coding_agent/tui/tools/labels.py +135 -0
  157. coding_agent/tui/tools/modal.py +137 -0
  158. coding_agent/tui/tools/partial_args.py +19 -0
  159. coding_agent/tui/tools/snapshots.py +382 -0
  160. coding_agent/tui/transcript/__init__.py +36 -0
  161. coding_agent/tui/transcript/messages.py +408 -0
  162. coding_agent/tui/transcript/process.py +504 -0
  163. coding_agent/tui/transcript/surface.py +345 -0
  164. coding_agent/tui/transcript/thinking_markdown.py +314 -0
  165. symphony_code-0.0.1.dist-info/METADATA +66 -0
  166. symphony_code-0.0.1.dist-info/RECORD +169 -0
  167. symphony_code-0.0.1.dist-info/WHEEL +4 -0
  168. symphony_code-0.0.1.dist-info/entry_points.txt +5 -0
  169. 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
+ ]