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,557 @@
1
+ """Slash-command handlers and dispatcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import uuid
7
+ from pathlib import Path
8
+ from typing import Any, Iterable
9
+
10
+ from core_ai import find_provider, get_model, get_provider
11
+ from core_ai.providers.catalog import PROVIDERS, configured_provider_ids
12
+ from coding_agent.agent import build_agent
13
+ from coding_agent.addons.evaluation.addon import JevAddon
14
+ from coding_agent.addons.manager import addons_report, tool_change_notice
15
+ from coding_agent.commands.jev_argument import parse_jev_argument
16
+ from coding_agent.addons.persistence.jsonl import JsonlPersistence
17
+ from coding_agent.addons.persistence.presentation import session_images
18
+ from coding_agent.config import ensure_spawn_settings
19
+ from coding_agent.config.credentials import OFFLINE_HINT, load_provider_env
20
+ from coding_agent.addons.langfuse import ensure_langfuse_installed
21
+ from textual.containers import Container
22
+ from textual.widgets import Static
23
+
24
+ from coding_agent.tui.screens.modal import ModalBase, ModalCloseButton, ModalScroll
25
+ from coding_agent.tui.theme import CONTENT_MODAL_CSS
26
+ from coding_agent.tui.tools.images import ImageAttachment, textual_image
27
+ from coding_agent.tui.screens import (
28
+ ContextModal,
29
+ DiffModal,
30
+ ExtensionsModal,
31
+ LangfuseSetupScreen,
32
+ LearningModal,
33
+ ProviderOnboardScreen,
34
+ )
35
+ from coding_agent.commands.catalog import (
36
+ EFFORT_CATALOG,
37
+ SLASH_COMMANDS,
38
+ EffortOption,
39
+ find_model,
40
+ find_personality,
41
+ model_options,
42
+ personality_options,
43
+ )
44
+
45
+ class SessionModal(ModalBase[None]):
46
+ """Durable session archive, including images saved beside the transcript."""
47
+
48
+ CSS = CONTENT_MODAL_CSS
49
+
50
+ def __init__(self, report: str, root: Path | None = None) -> None:
51
+ super().__init__()
52
+ self.report = report
53
+ self.root = root
54
+
55
+ def compose(self): # type: ignore[no-untyped-def]
56
+ with Container(id="content-pane", classes="modal-pane"):
57
+ yield ModalCloseButton("Esc", id="modal-close")
58
+ yield Static("Session archive", id="content-title")
59
+ with ModalScroll(id="content-body", classes="modal-body"):
60
+ yield Static(self.report, markup=False)
61
+ for index, path in enumerate(session_images(self.root) if self.root else [], 1):
62
+ yield Static(f"Image {index} · {path.name}", markup=False)
63
+ try:
64
+ image = ImageAttachment.from_path(path, f"[Image {index}]")
65
+ yield textual_image(image.decoded())
66
+ except OSError:
67
+ yield Static("Image is unavailable.")
68
+ yield Static("↑↓ scroll · Esc close", classes="modal-footer")
69
+
70
+
71
+ def open_langfuse_setup(app: Any) -> None:
72
+ """Install the optional SDK if needed, then collect Langfuse credentials."""
73
+ app.run_worker(_open_langfuse_setup(app), exclusive=False)
74
+
75
+
76
+ async def _open_langfuse_setup(app: Any) -> None:
77
+ result = await asyncio.to_thread(ensure_langfuse_installed)
78
+ if result.installed and not result.already_present:
79
+ app.add_notice("Langfuse SDK installed.", "success")
80
+ elif not result.installed:
81
+ app.add_notice(result.message, "error")
82
+ app.push_screen(LangfuseSetupScreen(), lambda saved: _on_langfuse_saved(app, saved))
83
+
84
+
85
+ def _on_langfuse_saved(app: Any, saved: bool | None) -> None:
86
+ app.query_one("#prompt").focus()
87
+ if not saved:
88
+ return
89
+ app.run_worker(_finish_langfuse_setup(app), exclusive=False)
90
+
91
+
92
+ async def _finish_langfuse_setup(app: Any) -> None:
93
+ """Re-check the optional SDK, then reload so the add-on can attach."""
94
+ app.add_notice("Langfuse saved · installing SDK if needed…")
95
+ result = await asyncio.to_thread(ensure_langfuse_installed)
96
+ if result.installed:
97
+ notice = (
98
+ "Langfuse saved · reloading configuration."
99
+ if result.already_present
100
+ else "Langfuse SDK installed · reloading configuration."
101
+ )
102
+ app.add_notice(notice, "success")
103
+ else:
104
+ app.add_notice(f"Langfuse keys saved, but tracing is off · {result.message}", "error")
105
+ await reload_project(app)
106
+
107
+
108
+ def toggle_jev(app: Any, argument: str = "") -> None:
109
+ """Enable or disable critic mode without switching the chat model."""
110
+ current = bool(getattr(getattr(app, "config", None), "evaluation", None) and app.config.evaluation.enabled)
111
+ change = parse_jev_argument(current, argument)
112
+ enabled, rule = change.enabled, change.rule
113
+ app.enable_jev = enabled
114
+ settings = {"enabled": enabled}
115
+ if rule is not None:
116
+ app.jev_rule = rule
117
+ app.config = ensure_spawn_settings(app.workspace, overrides={"evaluation": settings})
118
+ app.run_worker(reload_project(app), exclusive=False)
119
+ if enabled:
120
+ app.add_notice("Jev finish check on" + (f" · rule: {rule}" if rule else "") + ".", "success")
121
+ else:
122
+ app.add_notice("Jev critic mode off.", "success")
123
+
124
+
125
+ # --- model_switcher.py ---
126
+ def select_model(app: Any, argument: str) -> None:
127
+ agent = app._agent
128
+ assert agent is not None
129
+ selected = find_model(argument, app._model_options)
130
+ if selected is None:
131
+ app.add_notice(f"Unknown model: {argument}. Run /model to see available models.", "warning")
132
+ return
133
+ agent.harness.model_id = selected.id
134
+ app.config = ensure_spawn_settings(
135
+ app.workspace,
136
+ config=app.config,
137
+ overrides={"last_model": selected.id},
138
+ )
139
+ if not model_supports_effort(selected.id):
140
+ agent.harness.reasoning_effort = None
141
+ if agent.learning_loop is not None:
142
+ agent.learning_loop.model_id = selected.id
143
+ app.model_id = selected.id
144
+ app._ui_state.model_id = selected.id
145
+ app._ui_state.set_context_limit(agent.harness.state.context_limit(selected.id))
146
+ app.query_one("#topbar").set_context(app.workspace, selected.id)
147
+ app._set_status("")
148
+ app.add_update(f"Model switched to {selected.label} · {selected.id}")
149
+
150
+
151
+ def show_model_picker(app: Any) -> None:
152
+ prompt = app.query_one("#prompt")
153
+ prompt.value = "/model "
154
+ prompt.cursor_position = len(prompt.value)
155
+ app.query_one("#slash-menu").set_models(app._model_options, app._agent.harness.model_id)
156
+
157
+
158
+ def select_personality(app: Any, argument: str) -> None:
159
+ selected = find_personality(argument, personality_options())
160
+ if selected is None:
161
+ app.add_notice(
162
+ f"Unknown personality: {argument}. Run /personality to see the catalog.",
163
+ "warning",
164
+ )
165
+ return
166
+ resolved = ensure_spawn_settings(app.workspace, overrides={"personality": selected.id})
167
+ app.config = resolved
168
+ agent = getattr(app, "_agent", None)
169
+ if agent is not None:
170
+ agent.config = resolved
171
+ apply = getattr(agent, "apply_system_prompt", None)
172
+ if callable(apply):
173
+ apply()
174
+ app.add_update(f"Personality set to {selected.label}")
175
+
176
+
177
+ def show_personality_picker(app: Any) -> None:
178
+ prompt = app.query_one("#prompt")
179
+ prompt.value = "/personality "
180
+ prompt.cursor_position = len(prompt.value)
181
+ current = getattr(getattr(app, "config", None), "personality", None) or ""
182
+ app.query_one("#slash-menu").set_personalities(personality_options(), current)
183
+
184
+ # --- effort_switcher.py ---
185
+ def effort_matches(
186
+ value: str,
187
+ efforts: Iterable[EffortOption] = EFFORT_CATALOG,
188
+ ) -> tuple[EffortOption, ...]:
189
+ needle = value.strip().lower()
190
+ return tuple(
191
+ effort
192
+ for effort in efforts
193
+ if not needle
194
+ or needle in effort.id.lower()
195
+ or needle in effort.label.lower()
196
+ )
197
+
198
+
199
+ def effort_options_for_model(model_id: str) -> tuple[EffortOption, ...]:
200
+ """Return only effort levels advertised by the active model."""
201
+ model = _model_info(model_id)
202
+ if model is None or not model.thinking_level_map:
203
+ return ()
204
+
205
+ supported = {
206
+ "none" if level == "off" else level
207
+ for level, provider_value in model.thinking_level_map
208
+ if provider_value is not None
209
+ }
210
+ return tuple(
211
+ option
212
+ for option in EFFORT_CATALOG
213
+ if option.id == "default" or option.id in supported
214
+ )
215
+
216
+
217
+ def _model_info(model_id: str):
218
+ provider, separator, model_name = model_id.partition(":")
219
+ if not separator:
220
+ return None
221
+ return get_model(provider, model_name)
222
+
223
+
224
+ def model_supports_effort(model_id: str) -> bool:
225
+ return bool(effort_options_for_model(model_id))
226
+
227
+
228
+ def select_effort(app: Any, argument: str) -> None:
229
+ value = argument.strip().lower()
230
+ efforts = effort_options_for_model(app._agent.harness.model_id)
231
+ supported = {option.id for option in efforts}
232
+ if value not in supported:
233
+ choices = ", ".join(option.id for option in efforts)
234
+ app.add_notice(f"Unknown effort: {argument}. Choose: {choices}", "warning")
235
+ return
236
+ app._agent.harness.reasoning_effort = None if value == "default" else value
237
+ label = next(option.label for option in efforts if option.id == value)
238
+ app.add_update(f"Reasoning effort set to {label}")
239
+
240
+
241
+ def show_effort_picker(app: Any) -> None:
242
+ efforts = effort_options_for_model(app._agent.harness.model_id)
243
+ if not efforts:
244
+ app.add_notice("The active model does not support effort settings.", "warning")
245
+ return
246
+ prompt = app.query_one("#prompt")
247
+ prompt.value = "/effort "
248
+ prompt.cursor_position = len(prompt.value)
249
+ current = app._agent.harness.reasoning_effort or "default"
250
+ app.query_one("#slash-menu").set_efforts(efforts, current)
251
+
252
+
253
+ # --- reload.py ---
254
+ async def reload_project(app: Any) -> None:
255
+ """Reload environment-backed agent configuration in the current session."""
256
+ previous_agent = app._agent
257
+ previous_effort = getattr(
258
+ getattr(previous_agent, "harness", None),
259
+ "reasoning_effort",
260
+ None,
261
+ )
262
+ session_id = previous_agent.session_id if previous_agent is not None else app.session_id
263
+ try:
264
+ load_provider_env(app.workspace)
265
+ reloaded_config = ensure_spawn_settings(app.workspace)
266
+ reloaded_config.unattended = app.config.unattended
267
+ reloaded_agent = build_agent(
268
+ workspace=app.workspace,
269
+ sink=app.sink,
270
+ model_id=app.model_id,
271
+ session_id=session_id,
272
+ enable_learning=app.enable_learning,
273
+ enable_jev=getattr(app, "enable_jev", None),
274
+ config=reloaded_config,
275
+ client="tui",
276
+ )
277
+ from coding_agent.protocols.stdio.commands import load_state
278
+
279
+ load_state(reloaded_agent)
280
+ session_rule = getattr(app, "jev_rule", "")
281
+ jev_addon = reloaded_agent.harness.find_addon(JevAddon)
282
+ if jev_addon is not None:
283
+ jev_addon.config.rule = session_rule
284
+ reloaded_model = reloaded_agent.harness.model_id
285
+ reloaded_info = _model_info(reloaded_model)
286
+ reloaded_agent.harness.reasoning_effort = (
287
+ None
288
+ if reloaded_info is not None and not model_supports_effort(reloaded_model)
289
+ else previous_effort
290
+ )
291
+ if previous_agent is not None:
292
+ previous_addons = previous_agent.user_addons.surface.tool_names
293
+ previous_agent.dispose_user_addons()
294
+ else:
295
+ previous_addons = frozenset()
296
+ app._agent = reloaded_agent
297
+ app.config = reloaded_config
298
+ app._model_options = model_options(reloaded_agent.registry.namespaces())
299
+ app.session_id = reloaded_agent.session_id
300
+
301
+ if previous_agent is not None and previous_agent.learning_loop is not None:
302
+ previous_agent.learning_loop.cancel()
303
+
304
+ model_id = reloaded_agent.harness.model_id
305
+ app._ui_state.model_id = model_id
306
+ context_limit = reloaded_agent.harness.state.context_limit(model_id)
307
+ app._ui_state.set_context_limit(context_limit)
308
+ app.query_one("#topbar").set_context(app.workspace, model_id)
309
+ app._set_status("")
310
+ invalidate = getattr(app, "invalidate_workspace_caches", None)
311
+ if callable(invalidate):
312
+ invalidate()
313
+ app.add_update("Configuration reloaded.")
314
+ surface = reloaded_agent.user_addons.surface
315
+ app.add_notice(f"Addons: {tool_change_notice(previous_addons, surface.tool_names)}")
316
+ for notice in surface.notices:
317
+ app.add_notice(notice, "warning")
318
+ app.query_one("#prompt").focus()
319
+ except Exception as exc: # noqa: BLE001
320
+ app._agent = previous_agent
321
+ app.add_notice(f"Reload failed · {exc}", "error")
322
+
323
+
324
+ # --- session.py ---
325
+ def start_new_session(app: Any) -> None:
326
+ agent = app._agent
327
+ assert agent is not None
328
+ session_id = str(uuid.uuid4())
329
+ app.session_id = session_id
330
+ agent.session_id = session_id
331
+ agent.harness.session_id = session_id
332
+ store = getattr(agent, "learning_store", None)
333
+ if store is not None and hasattr(store, "bind_session"):
334
+ persistence = getattr(agent, "persistence", None)
335
+ session_dir = None
336
+ if persistence is not None and hasattr(persistence, "session_dir"):
337
+ session_dir = persistence.session_dir(session_id)
338
+ store.bind_session(session_id, session_dir)
339
+ register = getattr(app, "_register_active_session", None)
340
+ if callable(register):
341
+ register()
342
+ app._ui_state.reset_for_run(model_id=agent.harness.model_id)
343
+ app._ui_state.phase = "idle"
344
+ app._ui_state.detail = "ready"
345
+ app.action_clear_transcript()
346
+ app.add_notice(f"New conversation · {session_id[:8]}", "success")
347
+ app._set_status("")
348
+
349
+ # --- status.py ---
350
+ def show_status(app: Any) -> None:
351
+ if app._agent is None:
352
+ app.add_notice("Status · offline", "warning")
353
+ return
354
+ metrics = app._ui_state.metrics
355
+ context = "unknown"
356
+ if metrics.context_limit is not None and metrics.context_left is not None:
357
+ context = f"{metrics.context_left:,} / {metrics.context_limit:,} tokens left"
358
+ lines = [
359
+ "Status",
360
+ f"model {app._agent.harness.model_id}",
361
+ ]
362
+ if model_supports_effort(app._agent.harness.model_id):
363
+ lines.append(f"effort {app._agent.harness.reasoning_effort or 'default'}")
364
+ lines.extend(
365
+ [
366
+ f"personality {getattr(getattr(app, 'config', None), 'personality', None) or 'stock'}",
367
+ f"approval {app._agent.approval.approvals.mode}",
368
+ f"session {app._agent.session_id}",
369
+ f"context {context}",
370
+ f"current {metrics.tokens_used:,} tokens",
371
+ f"cumulative input {metrics.cumulative_tokens:,} tokens",
372
+ ]
373
+ )
374
+ namespaces = app._agent.registry.namespaces()
375
+ if namespaces:
376
+ labels = ", ".join(get_provider(provider_id).label for provider_id in namespaces)
377
+ lines.append(f"providers {labels}")
378
+ app.add_notice("\n".join(lines))
379
+
380
+
381
+ async def show_context(app: Any) -> None:
382
+ if app._agent is None:
383
+ app.add_notice("Context · offline", "warning")
384
+ return
385
+ report = await app._agent.context_report()
386
+ app.push_screen(ContextModal(report))
387
+
388
+
389
+ # --- compact.py ---
390
+ async def compact_context(app: Any) -> None:
391
+ """Run the harness-mounted compactor on the saved conversation.
392
+
393
+ The agent emits ``compaction_*`` events which the presenter turns into the
394
+ notice and footer metrics; the explicit refresh after the await keeps the
395
+ footer current even when no event changed the chrome snapshot.
396
+ """
397
+ try:
398
+ before, after = await app._agent.compact_conversation()
399
+ except RuntimeError as exc:
400
+ app.add_notice(f"Compact unavailable · {exc}", "warning")
401
+ return
402
+ if before == 0:
403
+ app.add_notice("There is no saved conversation to compact.")
404
+ elif before == after:
405
+ app.add_notice(f"Context is already compact · {after} messages")
406
+ if app._presenter is not None:
407
+ app._presenter.refresh_chrome()
408
+
409
+
410
+ # --- command_manager.py ---
411
+ class CommandManager:
412
+ """Parse slash commands and run the matching handler."""
413
+
414
+ def __init__(self, app: Any) -> None:
415
+ self.app = app
416
+
417
+
418
+ async def run(self, value: str) -> None:
419
+ command, _, argument = value[1:].partition(" ")
420
+ command = command.lower().strip()
421
+ argument = argument.strip()
422
+ app = self.app
423
+
424
+ if command in {"quit", "exit"}:
425
+ app.exit()
426
+ elif command == "clear":
427
+ app.action_clear_transcript()
428
+ elif command == "help":
429
+ commands = SLASH_COMMANDS
430
+ if app._agent is not None:
431
+ commands = (*commands, *app._agent.user_addons.surface.slash_commands)
432
+ if app._agent is not None and not model_supports_effort(
433
+ app._agent.harness.model_id
434
+ ):
435
+ commands = tuple(item for item in commands if item.name != "effort")
436
+ lines = [f"{item.usage:<24} {item.description}" for item in commands]
437
+ app.add_notice("Slash commands\n" + "\n".join(lines))
438
+ elif command == "status":
439
+ show_status(app)
440
+ elif command == "dashboard":
441
+ app.action_dashboard()
442
+ elif command == "context":
443
+ await show_context(app)
444
+ elif command == "session":
445
+ if app._agent is None:
446
+ app.add_notice(OFFLINE_HINT, "error")
447
+ else:
448
+ from coding_agent.addons.persistence.presentation import session_report
449
+
450
+ store = app._agent.persistence
451
+ root = store.session_dir(app._agent.session_id) if isinstance(store, JsonlPersistence) else None
452
+ app.push_screen(SessionModal(await session_report(app._agent), root))
453
+ elif command == "learning":
454
+ agent = getattr(app, "_agent", None)
455
+ try:
456
+ learned = agent.learning_store if agent is not None else None
457
+ except AttributeError:
458
+ learned = None
459
+ app.push_screen(LearningModal(
460
+ app.workspace,
461
+ session_id=getattr(agent, "session_id", None),
462
+ store=learned,
463
+ ))
464
+ elif command in {"installed", "extensions", "plugins", "skills"}:
465
+ agent = app._agent
466
+ skills, plugins = (agent.skill_registry.skills, agent.loaded_plugins) if agent else ((), ())
467
+ app.push_screen(ExtensionsModal(skills, plugins))
468
+ elif command == "effort":
469
+ if app._busy:
470
+ app.add_notice("/effort is unavailable while a turn is running.", "warning")
471
+ elif app._agent is None:
472
+ app.add_notice(OFFLINE_HINT, "error")
473
+ elif not model_supports_effort(app._agent.harness.model_id):
474
+ app.add_notice("The active model does not support effort settings.", "warning")
475
+ elif argument:
476
+ select_effort(app, argument)
477
+ else:
478
+ show_effort_picker(app)
479
+ elif command == "voice":
480
+ from coding_agent.tui.composer.voice_mode import toggle_voice
481
+
482
+ toggle_voice(app)
483
+ elif command == "addons":
484
+ if app._agent is None:
485
+ app.add_notice(OFFLINE_HINT, "error")
486
+ else:
487
+ runtime = app._agent.user_addons
488
+ app.add_notice(addons_report(runtime.surface, runtime.hook_failures))
489
+ elif command == "reload" and app._busy:
490
+ app._reload_queued = True
491
+ app.add_notice("Reload queued · it applies when the current run ends.")
492
+ elif app._busy:
493
+ app.add_notice(f"/{command} is unavailable while a turn is running.", "warning")
494
+ elif command == "reload":
495
+ await reload_project(app)
496
+ elif command == "provider":
497
+ open_provider_onboard(app, argument)
498
+ elif command == "langfuse":
499
+ open_langfuse_setup(app)
500
+ elif command == "jev":
501
+ toggle_jev(app, argument)
502
+ elif app._agent is None:
503
+ app.add_notice(OFFLINE_HINT, "error")
504
+ elif command == "new":
505
+ start_new_session(app)
506
+ elif command == "model":
507
+ select_model(app, argument) if argument else show_model_picker(app)
508
+ elif command == "personality":
509
+ select_personality(app, argument) if argument else show_personality_picker(app)
510
+ elif command == "compact":
511
+ await compact_context(app)
512
+ elif command == "diff":
513
+ app.push_screen(DiffModal(app.workspace))
514
+ elif app._agent.user_addons.surface.command(command) is not None:
515
+ app.add_notice(await app._agent.user_addons.run_command(command, argument) or "")
516
+ else:
517
+ app.add_notice(f"Unknown command: /{command}. Type /help to see commands.", "warning")
518
+
519
+
520
+ def open_provider_onboard(app: Any, argument: str = "") -> None:
521
+ initial = None
522
+ if argument:
523
+ selected = find_provider(argument)
524
+ if selected is None:
525
+ app.add_notice(
526
+ f"Unknown provider: {argument}. Choose {', '.join(spec.id for spec in PROVIDERS)}.",
527
+ "warning",
528
+ )
529
+ return
530
+ initial = selected.id
531
+ app.push_screen(
532
+ ProviderOnboardScreen(app.workspace, initial_provider=initial),
533
+ lambda providers: on_provider_onboard(app, providers),
534
+ )
535
+
536
+
537
+ def on_provider_onboard(app: Any, providers: tuple[str, ...] | None) -> None:
538
+ if providers is None:
539
+ app.query_one("#prompt").focus()
540
+ return
541
+ current = set(configured_provider_ids())
542
+ if not current:
543
+ if app._agent is None:
544
+ app.add_notice(OFFLINE_HINT, "warning")
545
+ return
546
+ app.run_worker(reload_after_provider(app), exclusive=False)
547
+
548
+
549
+ async def reload_after_provider(app: Any) -> None:
550
+ from coding_agent.tui.commands.manager import reload_project
551
+
552
+ await reload_project(app)
553
+ current = configured_provider_ids()
554
+ if not current:
555
+ return
556
+ labels = ", ".join(get_provider(provider_id).label for provider_id in current)
557
+ app.add_update(f"Providers ready · {labels}. Use /model to switch.")
@@ -0,0 +1,18 @@
1
+ """Composer input, prompt editor, and slash menu."""
2
+
3
+ from coding_agent.tui.composer.input import Composer, PromptInput, QueuedPrompt, QueuedTurn
4
+ from coding_agent.tui.composer.slash_menu import SlashMenu
5
+ from coding_agent.tui.composer.voice import (
6
+ GROK_VOICE_FULL_ID,
7
+ GROK_VOICE_MODEL,
8
+ )
9
+
10
+ __all__ = [
11
+ "Composer",
12
+ "GROK_VOICE_FULL_ID",
13
+ "GROK_VOICE_MODEL",
14
+ "PromptInput",
15
+ "QueuedPrompt",
16
+ "QueuedTurn",
17
+ "SlashMenu",
18
+ ]