soothe-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- soothe_cli/__init__.py +5 -0
- soothe_cli/cli/__init__.py +1 -0
- soothe_cli/cli/commands/__init__.py +1 -0
- soothe_cli/cli/commands/autopilot_cmd.py +410 -0
- soothe_cli/cli/commands/config_cmd.py +277 -0
- soothe_cli/cli/commands/run_cmd.py +87 -0
- soothe_cli/cli/commands/status_cmd.py +121 -0
- soothe_cli/cli/commands/subagent_names.py +17 -0
- soothe_cli/cli/commands/thread_cmd.py +657 -0
- soothe_cli/cli/execution/__init__.py +6 -0
- soothe_cli/cli/execution/daemon.py +194 -0
- soothe_cli/cli/execution/headless.py +99 -0
- soothe_cli/cli/execution/launcher.py +31 -0
- soothe_cli/cli/main.py +509 -0
- soothe_cli/cli/renderer.py +444 -0
- soothe_cli/cli/stream/__init__.py +17 -0
- soothe_cli/cli/stream/context.py +138 -0
- soothe_cli/cli/stream/display_line.py +83 -0
- soothe_cli/cli/stream/formatter.py +412 -0
- soothe_cli/cli/stream/pipeline.py +521 -0
- soothe_cli/cli/utils.py +46 -0
- soothe_cli/config/__init__.py +5 -0
- soothe_cli/config/cli_config.py +155 -0
- soothe_cli/plan/__init__.py +5 -0
- soothe_cli/plan/rich_tree.py +54 -0
- soothe_cli/shared/__init__.py +107 -0
- soothe_cli/shared/command_router.py +246 -0
- soothe_cli/shared/config_loader.py +68 -0
- soothe_cli/shared/display_policy.py +413 -0
- soothe_cli/shared/essential_events.py +68 -0
- soothe_cli/shared/event_processor.py +823 -0
- soothe_cli/shared/message_processing.py +393 -0
- soothe_cli/shared/presentation_engine.py +173 -0
- soothe_cli/shared/processor_state.py +80 -0
- soothe_cli/shared/renderer_protocol.py +158 -0
- soothe_cli/shared/rendering.py +43 -0
- soothe_cli/shared/slash_commands.py +354 -0
- soothe_cli/shared/subagent_routing.py +63 -0
- soothe_cli/shared/suppression_state.py +188 -0
- soothe_cli/shared/tool_formatters/__init__.py +27 -0
- soothe_cli/shared/tool_formatters/base.py +109 -0
- soothe_cli/shared/tool_formatters/execution.py +297 -0
- soothe_cli/shared/tool_formatters/fallback.py +128 -0
- soothe_cli/shared/tool_formatters/file_ops.py +299 -0
- soothe_cli/shared/tool_formatters/goal_formatter.py +331 -0
- soothe_cli/shared/tool_formatters/media.py +291 -0
- soothe_cli/shared/tool_formatters/structured.py +202 -0
- soothe_cli/shared/tool_formatters/web.py +143 -0
- soothe_cli/shared/tool_output_formatter.py +227 -0
- soothe_cli/shared/tui_trace_log.py +40 -0
- soothe_cli/tui/__init__.py +5 -0
- soothe_cli/tui/_ask_user_types.py +50 -0
- soothe_cli/tui/_cli_context.py +27 -0
- soothe_cli/tui/_env_vars.py +56 -0
- soothe_cli/tui/_session_stats.py +114 -0
- soothe_cli/tui/_version.py +21 -0
- soothe_cli/tui/app.py +4992 -0
- soothe_cli/tui/app.tcss +302 -0
- soothe_cli/tui/command_registry.py +310 -0
- soothe_cli/tui/config.py +2381 -0
- soothe_cli/tui/daemon_session.py +233 -0
- soothe_cli/tui/file_ops.py +409 -0
- soothe_cli/tui/formatting.py +28 -0
- soothe_cli/tui/hooks.py +23 -0
- soothe_cli/tui/input.py +782 -0
- soothe_cli/tui/media_utils.py +471 -0
- soothe_cli/tui/model_config.py +518 -0
- soothe_cli/tui/output.py +69 -0
- soothe_cli/tui/project_utils.py +188 -0
- soothe_cli/tui/sessions.py +1248 -0
- soothe_cli/tui/skills/__init__.py +5 -0
- soothe_cli/tui/skills/invocation.py +74 -0
- soothe_cli/tui/skills/load.py +93 -0
- soothe_cli/tui/textual_adapter.py +1430 -0
- soothe_cli/tui/theme.py +838 -0
- soothe_cli/tui/tool_display.py +297 -0
- soothe_cli/tui/unicode_security.py +502 -0
- soothe_cli/tui/update_check.py +447 -0
- soothe_cli/tui/widgets/__init__.py +9 -0
- soothe_cli/tui/widgets/_links.py +63 -0
- soothe_cli/tui/widgets/approval.py +430 -0
- soothe_cli/tui/widgets/ask_user.py +392 -0
- soothe_cli/tui/widgets/autocomplete.py +666 -0
- soothe_cli/tui/widgets/autopilot_dashboard.py +308 -0
- soothe_cli/tui/widgets/autopilot_screen.py +64 -0
- soothe_cli/tui/widgets/chat_input.py +1834 -0
- soothe_cli/tui/widgets/clipboard.py +128 -0
- soothe_cli/tui/widgets/diff.py +240 -0
- soothe_cli/tui/widgets/editor.py +140 -0
- soothe_cli/tui/widgets/history.py +221 -0
- soothe_cli/tui/widgets/loading.py +194 -0
- soothe_cli/tui/widgets/mcp_viewer.py +352 -0
- soothe_cli/tui/widgets/message_store.py +693 -0
- soothe_cli/tui/widgets/messages.py +1720 -0
- soothe_cli/tui/widgets/model_selector.py +988 -0
- soothe_cli/tui/widgets/notification_settings.py +155 -0
- soothe_cli/tui/widgets/status.py +403 -0
- soothe_cli/tui/widgets/theme_selector.py +158 -0
- soothe_cli/tui/widgets/thread_selector.py +1865 -0
- soothe_cli/tui/widgets/tool_renderers.py +148 -0
- soothe_cli/tui/widgets/tool_widgets.py +254 -0
- soothe_cli/tui/widgets/tools.py +165 -0
- soothe_cli/tui/widgets/welcome.py +330 -0
- soothe_cli-0.1.0.dist-info/METADATA +100 -0
- soothe_cli-0.1.0.dist-info/RECORD +107 -0
- soothe_cli-0.1.0.dist-info/WHEEL +4 -0
- soothe_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""Stream display pipeline for CLI progress output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from soothe_sdk.protocol import preview_first
|
|
10
|
+
from soothe_sdk.verbosity import VerbosityTier
|
|
11
|
+
|
|
12
|
+
from soothe_cli.cli.stream.context import PipelineContext
|
|
13
|
+
from soothe_cli.cli.stream.display_line import DisplayLine
|
|
14
|
+
from soothe_cli.cli.stream.formatter import (
|
|
15
|
+
format_goal_done,
|
|
16
|
+
format_goal_header,
|
|
17
|
+
format_judgement,
|
|
18
|
+
format_reasoning,
|
|
19
|
+
format_step_done,
|
|
20
|
+
format_step_header,
|
|
21
|
+
format_subagent_done,
|
|
22
|
+
format_subagent_milestone,
|
|
23
|
+
format_tool_call,
|
|
24
|
+
)
|
|
25
|
+
from soothe_cli.shared.display_policy import VerbosityLevel, normalize_verbosity
|
|
26
|
+
from soothe_cli.shared.essential_events import (
|
|
27
|
+
LOOP_REASON_EVENT_TYPE,
|
|
28
|
+
is_goal_start_event_type,
|
|
29
|
+
is_step_complete_event_type,
|
|
30
|
+
is_step_start_event_type,
|
|
31
|
+
)
|
|
32
|
+
from soothe_cli.shared.presentation_engine import PresentationEngine
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# Batch step events for parallel execution
|
|
37
|
+
BATCH_STEP_STARTED = "soothe.cognition.plan.batch.started"
|
|
38
|
+
BATCH_STEP_COMPLETED = "soothe.cognition.plan.batch.completed"
|
|
39
|
+
|
|
40
|
+
GOAL_COMPLETE_EVENTS = {
|
|
41
|
+
"soothe.cognition.agent_loop.completed",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Verbosity tier mapping
|
|
45
|
+
_VERBOSITY_TO_TIER = {
|
|
46
|
+
"quiet": VerbosityTier.QUIET,
|
|
47
|
+
"normal": VerbosityTier.NORMAL,
|
|
48
|
+
"detailed": VerbosityTier.DETAILED,
|
|
49
|
+
"debug": VerbosityTier.DEBUG,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class StreamDisplayPipeline:
|
|
54
|
+
"""Pipeline for processing events into CLI display lines.
|
|
55
|
+
|
|
56
|
+
Processes events with integrated verbosity filtering and context tracking.
|
|
57
|
+
Emits structured DisplayLine objects for rendering.
|
|
58
|
+
|
|
59
|
+
Usage:
|
|
60
|
+
pipeline = StreamDisplayPipeline(verbosity="normal")
|
|
61
|
+
for event in events:
|
|
62
|
+
lines = pipeline.process(event)
|
|
63
|
+
renderer.write_lines(lines)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
verbosity: VerbosityLevel = "normal",
|
|
69
|
+
*,
|
|
70
|
+
presentation_engine: PresentationEngine | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Initialize the pipeline.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
verbosity: Verbosity level for filtering.
|
|
76
|
+
presentation_engine: Shared engine (defaults to a new instance).
|
|
77
|
+
"""
|
|
78
|
+
self._verbosity = normalize_verbosity(verbosity)
|
|
79
|
+
self._verbosity_tier = _VERBOSITY_TO_TIER.get(self._verbosity, VerbosityTier.NORMAL)
|
|
80
|
+
self._context = PipelineContext()
|
|
81
|
+
self._presentation = presentation_engine or PresentationEngine()
|
|
82
|
+
self._current_namespace: tuple[str, ...] = () # Track current namespace
|
|
83
|
+
|
|
84
|
+
def process(self, event: dict[str, Any]) -> list[DisplayLine]:
|
|
85
|
+
"""Process an event into display lines.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
event: Event dictionary with 'type' key.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of DisplayLine objects to render.
|
|
92
|
+
"""
|
|
93
|
+
event_type = event.get("type", "")
|
|
94
|
+
if not event_type:
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
# Extract namespace from event
|
|
98
|
+
self._current_namespace = tuple(event.get("namespace", []))
|
|
99
|
+
|
|
100
|
+
# Classify and filter
|
|
101
|
+
tier = self._classify_event(event_type)
|
|
102
|
+
if tier > self._verbosity_tier:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
# Dispatch to handlers
|
|
106
|
+
return self._dispatch_event(event_type, event)
|
|
107
|
+
|
|
108
|
+
def _classify_event(self, event_type: str) -> VerbosityTier:
|
|
109
|
+
"""Classify event type to verbosity tier.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
event_type: Event type string.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
VerbosityTier for the event.
|
|
116
|
+
"""
|
|
117
|
+
from soothe_sdk.verbosity import classify_event_to_tier
|
|
118
|
+
|
|
119
|
+
# Goal events - NORMAL
|
|
120
|
+
if is_goal_start_event_type(event_type):
|
|
121
|
+
return VerbosityTier.NORMAL
|
|
122
|
+
|
|
123
|
+
# Step start events - NORMAL (user-visible step descriptions)
|
|
124
|
+
if is_step_start_event_type(event_type):
|
|
125
|
+
return VerbosityTier.NORMAL
|
|
126
|
+
|
|
127
|
+
# Goal completion - QUIET (always visible)
|
|
128
|
+
if event_type in GOAL_COMPLETE_EVENTS:
|
|
129
|
+
return VerbosityTier.QUIET
|
|
130
|
+
|
|
131
|
+
# soothe.* events: defer to SDK domain-based classification (RFC-0020)
|
|
132
|
+
# Step completion, tool events, subagent events all use domain defaults
|
|
133
|
+
if event_type.startswith("soothe."):
|
|
134
|
+
return classify_event_to_tier(event_type)
|
|
135
|
+
|
|
136
|
+
# Non-soothe events (from deepagents subagents)
|
|
137
|
+
if ".subagent." in event_type:
|
|
138
|
+
return VerbosityTier.NORMAL
|
|
139
|
+
|
|
140
|
+
# Default to DETAILED (hidden at normal)
|
|
141
|
+
return VerbosityTier.DETAILED
|
|
142
|
+
|
|
143
|
+
def _dispatch_event(self, event_type: str, event: dict[str, Any]) -> list[DisplayLine]:
|
|
144
|
+
"""Dispatch event to appropriate handler.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
event_type: Event type string.
|
|
148
|
+
event: Event dictionary.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of DisplayLine objects.
|
|
152
|
+
"""
|
|
153
|
+
if is_goal_start_event_type(event_type):
|
|
154
|
+
return self._on_goal_started(event)
|
|
155
|
+
|
|
156
|
+
if is_step_start_event_type(event_type):
|
|
157
|
+
return self._on_step_started(event)
|
|
158
|
+
|
|
159
|
+
if ".subagent." in event_type and ".dispatched" in event_type:
|
|
160
|
+
return self._on_subagent_dispatched(event)
|
|
161
|
+
|
|
162
|
+
if ".subagent." in event_type and ".judgement" in event_type:
|
|
163
|
+
return self._on_subagent_judgement(event)
|
|
164
|
+
|
|
165
|
+
if ".subagent." in event_type and ".step" in event_type:
|
|
166
|
+
return self._on_subagent_step(event)
|
|
167
|
+
|
|
168
|
+
if ".subagent." in event_type and ".completed" in event_type:
|
|
169
|
+
return self._on_subagent_completed(event)
|
|
170
|
+
|
|
171
|
+
if is_step_complete_event_type(event_type):
|
|
172
|
+
return self._on_step_completed(event)
|
|
173
|
+
|
|
174
|
+
if event_type in GOAL_COMPLETE_EVENTS:
|
|
175
|
+
return self._on_goal_completed(event)
|
|
176
|
+
|
|
177
|
+
if event_type == LOOP_REASON_EVENT_TYPE:
|
|
178
|
+
return self._on_loop_agent_reason(event)
|
|
179
|
+
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
def _on_goal_started(self, event: dict[str, Any]) -> list[DisplayLine]:
|
|
183
|
+
"""Handle goal start event.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
event: Event dictionary.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Display lines for goal header.
|
|
190
|
+
"""
|
|
191
|
+
goal = event.get("goal", event.get("goal_description", ""))
|
|
192
|
+
if not goal:
|
|
193
|
+
return []
|
|
194
|
+
|
|
195
|
+
# Reset context for new goal
|
|
196
|
+
self._context.reset_goal()
|
|
197
|
+
self._context.current_goal = goal
|
|
198
|
+
self._context.goal_start_time = time.time()
|
|
199
|
+
|
|
200
|
+
# Get steps count if available
|
|
201
|
+
steps = event.get("steps", [])
|
|
202
|
+
self._context.steps_total = len(steps) if steps else 0
|
|
203
|
+
|
|
204
|
+
return [
|
|
205
|
+
format_goal_header(
|
|
206
|
+
goal,
|
|
207
|
+
namespace=self._current_namespace,
|
|
208
|
+
verbosity_tier=self._verbosity_tier,
|
|
209
|
+
)
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
def _on_step_started(self, event: dict[str, Any]) -> list[DisplayLine]:
|
|
213
|
+
"""Handle step start event.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
event: Event dictionary.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Display lines for step header.
|
|
220
|
+
"""
|
|
221
|
+
step_id = event.get("step_id", event.get("id", ""))
|
|
222
|
+
description = event.get("description", event.get("step_description", ""))
|
|
223
|
+
|
|
224
|
+
if not description:
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
# Track step by ID for parallel execution
|
|
228
|
+
if step_id and step_id not in self._context._active_step_ids:
|
|
229
|
+
self._context._active_step_ids.append(step_id)
|
|
230
|
+
if step_id:
|
|
231
|
+
self._context.step_descriptions[step_id] = description
|
|
232
|
+
|
|
233
|
+
# Reset step context for this specific step
|
|
234
|
+
self._context.current_step_id = step_id
|
|
235
|
+
self._context.current_step_description = description
|
|
236
|
+
self._context.step_start_time = time.time()
|
|
237
|
+
self._context.step_header_emitted = True
|
|
238
|
+
|
|
239
|
+
return [
|
|
240
|
+
format_step_header(
|
|
241
|
+
description,
|
|
242
|
+
namespace=self._current_namespace,
|
|
243
|
+
verbosity_tier=self._verbosity_tier,
|
|
244
|
+
)
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
def _on_subagent_dispatched(self, event: dict[str, Any]) -> list[DisplayLine]:
|
|
248
|
+
"""Handle subagent dispatched event.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
event: Event dictionary.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Display lines (none for dispatch, just tracking).
|
|
255
|
+
"""
|
|
256
|
+
# Extract name from event type: soothe.subagent.<name>.dispatched
|
|
257
|
+
event_type = event.get("type", "")
|
|
258
|
+
parts = event_type.split(".")
|
|
259
|
+
name = ""
|
|
260
|
+
# Pattern: soothe.subagent.<name>.dispatched -> parts[0]=soothe, parts[1]=subagent, parts[2]=name
|
|
261
|
+
# Need at least 3 parts for valid subagent event type
|
|
262
|
+
if len(parts) >= 3 and parts[1] == "subagent": # noqa: PLR2004
|
|
263
|
+
name = parts[2]
|
|
264
|
+
name = name or event.get("name", event.get("subagent_name", ""))
|
|
265
|
+
self._context.subagent_name = name
|
|
266
|
+
self._context.subagent_milestones.clear()
|
|
267
|
+
|
|
268
|
+
# Emit tool call for subagent dispatch
|
|
269
|
+
query = event.get("query", event.get("task", event.get("topic", "")))
|
|
270
|
+
args_summary = f'"{preview_first(query, 40)}"' if query else ""
|
|
271
|
+
return [
|
|
272
|
+
format_tool_call(
|
|
273
|
+
f"{name}_subagent",
|
|
274
|
+
args_summary,
|
|
275
|
+
namespace=self._current_namespace,
|
|
276
|
+
verbosity_tier=self._verbosity_tier,
|
|
277
|
+
)
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
def _on_subagent_judgement(self, event: dict[str, Any]) -> list[DisplayLine]:
|
|
281
|
+
"""Handle subagent judgement event.
|
|
282
|
+
|
|
283
|
+
IG-089: Shows meaningful LLM decision reasoning without raw intermediate data.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
event: Event dictionary.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Display lines for judgement.
|
|
290
|
+
"""
|
|
291
|
+
judgement = event.get("judgement", "")
|
|
292
|
+
action = event.get("action", "")
|
|
293
|
+
|
|
294
|
+
if not judgement:
|
|
295
|
+
return []
|
|
296
|
+
|
|
297
|
+
return [
|
|
298
|
+
format_judgement(
|
|
299
|
+
judgement,
|
|
300
|
+
action,
|
|
301
|
+
namespace=self._current_namespace,
|
|
302
|
+
verbosity_tier=self._verbosity_tier,
|
|
303
|
+
)
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
def _on_subagent_step(self, event: dict[str, Any]) -> list[DisplayLine]:
|
|
307
|
+
"""Handle subagent step event (compact hybrid).
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
event: Event dictionary.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Display lines for milestone (if significant).
|
|
314
|
+
"""
|
|
315
|
+
# Only show query/analyze type steps
|
|
316
|
+
step_type = event.get("step_type", event.get("type", ""))
|
|
317
|
+
if step_type not in ("query", "analyze", "search", "fetch"):
|
|
318
|
+
return []
|
|
319
|
+
|
|
320
|
+
brief = event.get("brief", event.get("summary", ""))
|
|
321
|
+
if not brief:
|
|
322
|
+
action = event.get("action", "")
|
|
323
|
+
target = event.get("target", "")
|
|
324
|
+
brief = f"{action}: {target}" if action and target else action or target
|
|
325
|
+
|
|
326
|
+
if not brief:
|
|
327
|
+
return []
|
|
328
|
+
|
|
329
|
+
return [
|
|
330
|
+
format_subagent_milestone(
|
|
331
|
+
preview_first(brief, 60),
|
|
332
|
+
namespace=self._current_namespace,
|
|
333
|
+
verbosity_tier=self._verbosity_tier,
|
|
334
|
+
)
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
def _on_subagent_completed(self, event: dict[str, Any]) -> list[DisplayLine]:
|
|
338
|
+
"""Handle subagent completed event.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
event: Event dictionary.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Display lines for completion.
|
|
345
|
+
"""
|
|
346
|
+
# Handle various summary fields from different subagent events
|
|
347
|
+
summary = event.get("summary", event.get("result", "done"))
|
|
348
|
+
if not summary:
|
|
349
|
+
# For events like ResearchCompletedEvent that have answer_length
|
|
350
|
+
answer_len = event.get("answer_length", 0)
|
|
351
|
+
result_count = event.get("result_count", 0)
|
|
352
|
+
if answer_len:
|
|
353
|
+
summary = f"{answer_len} chars"
|
|
354
|
+
elif result_count:
|
|
355
|
+
summary = f"{result_count} results"
|
|
356
|
+
else:
|
|
357
|
+
summary = "done"
|
|
358
|
+
|
|
359
|
+
duration_s = event.get("duration_s", event.get("duration_seconds", 0))
|
|
360
|
+
|
|
361
|
+
if duration_s == 0:
|
|
362
|
+
duration_ms = event.get("duration_ms", 0)
|
|
363
|
+
duration_s = duration_ms / 1000 if duration_ms else 0
|
|
364
|
+
|
|
365
|
+
return [
|
|
366
|
+
format_subagent_done(
|
|
367
|
+
preview_first(summary, 50),
|
|
368
|
+
duration_s,
|
|
369
|
+
namespace=self._current_namespace,
|
|
370
|
+
verbosity_tier=self._verbosity_tier,
|
|
371
|
+
)
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
def _on_step_completed(self, event: dict[str, Any]) -> list[DisplayLine]:
|
|
375
|
+
"""Handle step completed event.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
event: Event dictionary.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Display lines for step completion.
|
|
382
|
+
"""
|
|
383
|
+
step_id = event.get("step_id", "")
|
|
384
|
+
duration_s = event.get("duration_s", event.get("duration_seconds", 0))
|
|
385
|
+
if duration_s == 0:
|
|
386
|
+
duration_ms = event.get("duration_ms", 0)
|
|
387
|
+
duration_s = duration_ms / 1000 if duration_ms else 0
|
|
388
|
+
|
|
389
|
+
# Use tracked start time if available
|
|
390
|
+
if duration_s == 0 and self._context.step_start_time:
|
|
391
|
+
duration_s = time.time() - self._context.step_start_time
|
|
392
|
+
|
|
393
|
+
# Resolve description robustly for parallel/async step completions
|
|
394
|
+
description = (
|
|
395
|
+
self._context.step_descriptions.get(step_id, "")
|
|
396
|
+
or self._context.current_step_description
|
|
397
|
+
or event.get("description", "")
|
|
398
|
+
or "Completed action"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Get tool call count from event
|
|
402
|
+
tool_call_count = event.get("tool_call_count", 0)
|
|
403
|
+
|
|
404
|
+
# Mark step complete (updates _active_step_ids and steps_completed)
|
|
405
|
+
if step_id:
|
|
406
|
+
self._context.complete_step(step_id)
|
|
407
|
+
self._context.step_descriptions.pop(step_id, None)
|
|
408
|
+
|
|
409
|
+
# Reset current step context (but not _active_step_ids)
|
|
410
|
+
self._context.current_step_id = None
|
|
411
|
+
self._context.current_step_description = None
|
|
412
|
+
self._context.step_start_time = None
|
|
413
|
+
|
|
414
|
+
return [
|
|
415
|
+
format_step_done(
|
|
416
|
+
description,
|
|
417
|
+
duration_s,
|
|
418
|
+
tool_call_count=tool_call_count,
|
|
419
|
+
namespace=self._current_namespace,
|
|
420
|
+
verbosity_tier=self._verbosity_tier,
|
|
421
|
+
)
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
def _on_goal_completed(self, event: dict[str, Any]) -> list[DisplayLine]:
|
|
425
|
+
"""Handle goal completed event.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
event: Event dictionary.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Display lines for goal completion.
|
|
432
|
+
"""
|
|
433
|
+
goal = self._context.current_goal or event.get("goal", "")
|
|
434
|
+
steps = self._context.steps_completed or event.get("total_steps", 0)
|
|
435
|
+
|
|
436
|
+
total_s = event.get("total_duration_s", 0)
|
|
437
|
+
if total_s == 0 and self._context.goal_start_time:
|
|
438
|
+
total_s = time.time() - self._context.goal_start_time
|
|
439
|
+
|
|
440
|
+
# Reset goal context
|
|
441
|
+
self._context.reset_goal()
|
|
442
|
+
|
|
443
|
+
return [
|
|
444
|
+
format_goal_done(
|
|
445
|
+
goal,
|
|
446
|
+
steps,
|
|
447
|
+
total_s,
|
|
448
|
+
namespace=self._current_namespace,
|
|
449
|
+
verbosity_tier=self._verbosity_tier,
|
|
450
|
+
)
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
def _on_loop_agent_reason(self, event: dict[str, Any]) -> list[DisplayLine]:
|
|
454
|
+
"""Handle AgentLoop Reason progress with prominent reasoning display (IG-152)."""
|
|
455
|
+
status = event.get("status", "")
|
|
456
|
+
|
|
457
|
+
# Extract action text (IG-152: full text, no truncation in schema or display)
|
|
458
|
+
action_text = event.get("next_action", "").strip() or self._derive_action_from_status(
|
|
459
|
+
status
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if not action_text:
|
|
463
|
+
return []
|
|
464
|
+
|
|
465
|
+
# Polish: Capitalize first letter if not already
|
|
466
|
+
if action_text and action_text[0].islower():
|
|
467
|
+
action_text = action_text[0].upper() + action_text[1:]
|
|
468
|
+
|
|
469
|
+
# IG-152: Show full action text to user (no truncation)
|
|
470
|
+
# Word boundary respect happens at schema level (preview_first in planner)
|
|
471
|
+
# CLI display should show complete reasoning chain for transparency
|
|
472
|
+
|
|
473
|
+
# Deduplicate repeated actions
|
|
474
|
+
if not self._presentation.should_emit_action(action_text=action_text):
|
|
475
|
+
return []
|
|
476
|
+
|
|
477
|
+
# Determine action type
|
|
478
|
+
action = "complete" if status == "done" else "continue"
|
|
479
|
+
|
|
480
|
+
lines = [
|
|
481
|
+
format_judgement(
|
|
482
|
+
action_text,
|
|
483
|
+
action,
|
|
484
|
+
namespace=self._current_namespace,
|
|
485
|
+
verbosity_tier=self._verbosity_tier,
|
|
486
|
+
)
|
|
487
|
+
]
|
|
488
|
+
|
|
489
|
+
# Add reasoning line if present (IG-XXX: Show internal technical analysis)
|
|
490
|
+
reasoning = event.get("reasoning", "").strip()
|
|
491
|
+
if reasoning:
|
|
492
|
+
lines.append(
|
|
493
|
+
format_reasoning(
|
|
494
|
+
reasoning,
|
|
495
|
+
namespace=self._current_namespace,
|
|
496
|
+
verbosity_tier=self._verbosity_tier,
|
|
497
|
+
)
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
return lines
|
|
501
|
+
|
|
502
|
+
def _derive_action_from_status(self, status: str) -> str:
|
|
503
|
+
"""Fallback action text when metadata missing.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
status: Reason event status field.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Human-readable action description, or empty string if no valid status.
|
|
510
|
+
"""
|
|
511
|
+
if status == "done":
|
|
512
|
+
return "Completing final analysis"
|
|
513
|
+
if status == "replan":
|
|
514
|
+
return "Trying alternative approach"
|
|
515
|
+
if status == "working":
|
|
516
|
+
return "Processing next step"
|
|
517
|
+
# No fallback for missing/empty status - better to skip than emit noise
|
|
518
|
+
return ""
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
__all__ = ["StreamDisplayPipeline"]
|
soothe_cli/cli/utils.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""CLI rendering utilities.
|
|
2
|
+
|
|
3
|
+
This module provides helper functions for creating plain-text output
|
|
4
|
+
to stderr, following the same visual patterns as TUI but without Rich widgets.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_tool_block(
|
|
11
|
+
name: str,
|
|
12
|
+
args_summary: str,
|
|
13
|
+
output: str | None = None,
|
|
14
|
+
status: str = "running", # noqa: ARG001
|
|
15
|
+
) -> str:
|
|
16
|
+
"""Create a tool block with dot prefix and optional output.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
name: Tool name to display.
|
|
20
|
+
args_summary: Summary of tool arguments.
|
|
21
|
+
output: Optional tool output to show with tree connector.
|
|
22
|
+
status: Tool status - 'running', 'success', or 'error'.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Plain text formatted as:
|
|
26
|
+
⚙ ToolName(args_summary)
|
|
27
|
+
└ output
|
|
28
|
+
"""
|
|
29
|
+
# Use gear icon for tools (matches TUI pattern)
|
|
30
|
+
result = f"⚙ {name}({args_summary})"
|
|
31
|
+
|
|
32
|
+
if output is not None:
|
|
33
|
+
# Add output with tree connector
|
|
34
|
+
lines = output.split("\n")
|
|
35
|
+
for i, line in enumerate(lines):
|
|
36
|
+
if i == 0:
|
|
37
|
+
result += f"\n └ {line}"
|
|
38
|
+
else:
|
|
39
|
+
result += f"\n {line}"
|
|
40
|
+
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"make_tool_block",
|
|
46
|
+
]
|