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,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
|
+
]
|