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.
Files changed (107) hide show
  1. soothe_cli/__init__.py +5 -0
  2. soothe_cli/cli/__init__.py +1 -0
  3. soothe_cli/cli/commands/__init__.py +1 -0
  4. soothe_cli/cli/commands/autopilot_cmd.py +410 -0
  5. soothe_cli/cli/commands/config_cmd.py +277 -0
  6. soothe_cli/cli/commands/run_cmd.py +87 -0
  7. soothe_cli/cli/commands/status_cmd.py +121 -0
  8. soothe_cli/cli/commands/subagent_names.py +17 -0
  9. soothe_cli/cli/commands/thread_cmd.py +657 -0
  10. soothe_cli/cli/execution/__init__.py +6 -0
  11. soothe_cli/cli/execution/daemon.py +194 -0
  12. soothe_cli/cli/execution/headless.py +99 -0
  13. soothe_cli/cli/execution/launcher.py +31 -0
  14. soothe_cli/cli/main.py +509 -0
  15. soothe_cli/cli/renderer.py +444 -0
  16. soothe_cli/cli/stream/__init__.py +17 -0
  17. soothe_cli/cli/stream/context.py +138 -0
  18. soothe_cli/cli/stream/display_line.py +83 -0
  19. soothe_cli/cli/stream/formatter.py +412 -0
  20. soothe_cli/cli/stream/pipeline.py +521 -0
  21. soothe_cli/cli/utils.py +46 -0
  22. soothe_cli/config/__init__.py +5 -0
  23. soothe_cli/config/cli_config.py +155 -0
  24. soothe_cli/plan/__init__.py +5 -0
  25. soothe_cli/plan/rich_tree.py +54 -0
  26. soothe_cli/shared/__init__.py +107 -0
  27. soothe_cli/shared/command_router.py +246 -0
  28. soothe_cli/shared/config_loader.py +68 -0
  29. soothe_cli/shared/display_policy.py +413 -0
  30. soothe_cli/shared/essential_events.py +68 -0
  31. soothe_cli/shared/event_processor.py +823 -0
  32. soothe_cli/shared/message_processing.py +393 -0
  33. soothe_cli/shared/presentation_engine.py +173 -0
  34. soothe_cli/shared/processor_state.py +80 -0
  35. soothe_cli/shared/renderer_protocol.py +158 -0
  36. soothe_cli/shared/rendering.py +43 -0
  37. soothe_cli/shared/slash_commands.py +354 -0
  38. soothe_cli/shared/subagent_routing.py +63 -0
  39. soothe_cli/shared/suppression_state.py +188 -0
  40. soothe_cli/shared/tool_formatters/__init__.py +27 -0
  41. soothe_cli/shared/tool_formatters/base.py +109 -0
  42. soothe_cli/shared/tool_formatters/execution.py +297 -0
  43. soothe_cli/shared/tool_formatters/fallback.py +128 -0
  44. soothe_cli/shared/tool_formatters/file_ops.py +299 -0
  45. soothe_cli/shared/tool_formatters/goal_formatter.py +331 -0
  46. soothe_cli/shared/tool_formatters/media.py +291 -0
  47. soothe_cli/shared/tool_formatters/structured.py +202 -0
  48. soothe_cli/shared/tool_formatters/web.py +143 -0
  49. soothe_cli/shared/tool_output_formatter.py +227 -0
  50. soothe_cli/shared/tui_trace_log.py +40 -0
  51. soothe_cli/tui/__init__.py +5 -0
  52. soothe_cli/tui/_ask_user_types.py +50 -0
  53. soothe_cli/tui/_cli_context.py +27 -0
  54. soothe_cli/tui/_env_vars.py +56 -0
  55. soothe_cli/tui/_session_stats.py +114 -0
  56. soothe_cli/tui/_version.py +21 -0
  57. soothe_cli/tui/app.py +4992 -0
  58. soothe_cli/tui/app.tcss +302 -0
  59. soothe_cli/tui/command_registry.py +310 -0
  60. soothe_cli/tui/config.py +2381 -0
  61. soothe_cli/tui/daemon_session.py +233 -0
  62. soothe_cli/tui/file_ops.py +409 -0
  63. soothe_cli/tui/formatting.py +28 -0
  64. soothe_cli/tui/hooks.py +23 -0
  65. soothe_cli/tui/input.py +782 -0
  66. soothe_cli/tui/media_utils.py +471 -0
  67. soothe_cli/tui/model_config.py +518 -0
  68. soothe_cli/tui/output.py +69 -0
  69. soothe_cli/tui/project_utils.py +188 -0
  70. soothe_cli/tui/sessions.py +1248 -0
  71. soothe_cli/tui/skills/__init__.py +5 -0
  72. soothe_cli/tui/skills/invocation.py +74 -0
  73. soothe_cli/tui/skills/load.py +93 -0
  74. soothe_cli/tui/textual_adapter.py +1430 -0
  75. soothe_cli/tui/theme.py +838 -0
  76. soothe_cli/tui/tool_display.py +297 -0
  77. soothe_cli/tui/unicode_security.py +502 -0
  78. soothe_cli/tui/update_check.py +447 -0
  79. soothe_cli/tui/widgets/__init__.py +9 -0
  80. soothe_cli/tui/widgets/_links.py +63 -0
  81. soothe_cli/tui/widgets/approval.py +430 -0
  82. soothe_cli/tui/widgets/ask_user.py +392 -0
  83. soothe_cli/tui/widgets/autocomplete.py +666 -0
  84. soothe_cli/tui/widgets/autopilot_dashboard.py +308 -0
  85. soothe_cli/tui/widgets/autopilot_screen.py +64 -0
  86. soothe_cli/tui/widgets/chat_input.py +1834 -0
  87. soothe_cli/tui/widgets/clipboard.py +128 -0
  88. soothe_cli/tui/widgets/diff.py +240 -0
  89. soothe_cli/tui/widgets/editor.py +140 -0
  90. soothe_cli/tui/widgets/history.py +221 -0
  91. soothe_cli/tui/widgets/loading.py +194 -0
  92. soothe_cli/tui/widgets/mcp_viewer.py +352 -0
  93. soothe_cli/tui/widgets/message_store.py +693 -0
  94. soothe_cli/tui/widgets/messages.py +1720 -0
  95. soothe_cli/tui/widgets/model_selector.py +988 -0
  96. soothe_cli/tui/widgets/notification_settings.py +155 -0
  97. soothe_cli/tui/widgets/status.py +403 -0
  98. soothe_cli/tui/widgets/theme_selector.py +158 -0
  99. soothe_cli/tui/widgets/thread_selector.py +1865 -0
  100. soothe_cli/tui/widgets/tool_renderers.py +148 -0
  101. soothe_cli/tui/widgets/tool_widgets.py +254 -0
  102. soothe_cli/tui/widgets/tools.py +165 -0
  103. soothe_cli/tui/widgets/welcome.py +330 -0
  104. soothe_cli-0.1.0.dist-info/METADATA +100 -0
  105. soothe_cli-0.1.0.dist-info/RECORD +107 -0
  106. soothe_cli-0.1.0.dist-info/WHEEL +4 -0
  107. soothe_cli-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,412 @@
1
+ """Formatter functions for CLI display lines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from soothe_sdk.verbosity import VerbosityTier
6
+
7
+ from soothe_cli.cli.stream.display_line import DisplayLine, indent_for_level
8
+
9
+
10
+ def abbreviate_text(text: str, max_length: int = 50) -> str:
11
+ """Abbreviate text to max_length, preserving start and end.
12
+
13
+ Args:
14
+ text: Text to abbreviate.
15
+ max_length: Maximum length before abbreviation.
16
+
17
+ Returns:
18
+ Abbreviated text with "..." in middle if too long.
19
+
20
+ Examples:
21
+ >>> abbreviate_text("Short text")
22
+ "Short text"
23
+ >>> abbreviate_text(
24
+ ... "Run cloc on src/ and tests/ directories to count Soothe source and test code"
25
+ ... )
26
+ "Run cloc on src/ and ... test code"
27
+ """
28
+ if len(text) <= max_length:
29
+ return text
30
+
31
+ # Find word boundary in first ~25 chars
32
+ first_end = min(25, len(text))
33
+ while first_end > 0 and text[first_end] != " ":
34
+ first_end -= 1
35
+ if first_end == 0:
36
+ first_end = 25 # No space found, use fixed position
37
+
38
+ # Find word boundary in last ~10 chars
39
+ last_start = max(len(text) - 10, 0)
40
+ while last_start < len(text) and text[last_start] != " ":
41
+ last_start += 1
42
+ if last_start == len(text):
43
+ last_start = len(text) - 10 # No space found, use fixed position
44
+
45
+ first_part = text[:first_end].rstrip()
46
+ last_part = text[last_start:].lstrip()
47
+ return f"{first_part} ... {last_part}"
48
+
49
+
50
+ def _derive_source_prefix(
51
+ namespace: tuple[str, ...],
52
+ verbosity_tier: VerbosityTier,
53
+ ) -> str | None:
54
+ """Derive source prefix from namespace for debug mode.
55
+
56
+ Args:
57
+ namespace: Event namespace tuple (empty = main, non-empty = subagent).
58
+ verbosity_tier: Current verbosity tier.
59
+
60
+ Returns:
61
+ Source prefix string if DEBUG level, None otherwise.
62
+
63
+ Examples:
64
+ >>> _derive_source_prefix((), VerbosityTier.DEBUG)
65
+ '[main]'
66
+ >>> _derive_source_prefix(("research",), VerbosityTier.DEBUG)
67
+ '[subagent:research]'
68
+ >>> _derive_source_prefix((), VerbosityTier.NORMAL)
69
+ None
70
+ """
71
+ # Only show prefix at DEBUG verbosity
72
+ if verbosity_tier < VerbosityTier.DEBUG:
73
+ return None
74
+
75
+ if not namespace:
76
+ return "[main]"
77
+ # Format: [subagent:name1:name2]
78
+ return "[subagent:" + ":".join(namespace) + "]"
79
+
80
+
81
+ def format_goal_header(
82
+ goal: str,
83
+ *,
84
+ namespace: tuple[str, ...] = (),
85
+ verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
86
+ ) -> DisplayLine:
87
+ """Format a goal header line.
88
+
89
+ Args:
90
+ goal: Goal description.
91
+ namespace: Event namespace (empty for main, non-empty for subagent).
92
+ verbosity_tier: Current verbosity tier.
93
+
94
+ Returns:
95
+ DisplayLine for goal header.
96
+ """
97
+ # Add inline symbol for goal marker
98
+ content = f"🚩 {goal}"
99
+ return DisplayLine(
100
+ level=1,
101
+ content=content,
102
+ icon="●",
103
+ indent=indent_for_level(1),
104
+ source_prefix=_derive_source_prefix(namespace, verbosity_tier),
105
+ newline_before=True, # Add separator for goal start
106
+ )
107
+
108
+
109
+ def format_step_header(
110
+ description: str,
111
+ *,
112
+ parallel: bool = False,
113
+ namespace: tuple[str, ...] = (),
114
+ verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
115
+ ) -> DisplayLine:
116
+ """Format a step header line with checkbox style.
117
+
118
+ Args:
119
+ description: Step description.
120
+ parallel: Whether step has parallel tools.
121
+ namespace: Event namespace.
122
+ verbosity_tier: Current verbosity tier.
123
+
124
+ Returns:
125
+ DisplayLine for step header with hollow circle icon.
126
+ """
127
+ suffix = " (parallel)" if parallel else ""
128
+ # Add inline symbol for step progression
129
+ content = f"⏩ {description}{suffix}"
130
+ return DisplayLine(
131
+ level=2,
132
+ content=content,
133
+ icon="○", # Hollow circle for in-progress step
134
+ indent=indent_for_level(2),
135
+ source_prefix=_derive_source_prefix(namespace, verbosity_tier),
136
+ newline_before=True, # Add separator for step start
137
+ )
138
+
139
+
140
+ def format_tool_call(
141
+ name: str,
142
+ args_summary: str,
143
+ *,
144
+ running: bool = False,
145
+ namespace: tuple[str, ...] = (),
146
+ verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
147
+ ) -> DisplayLine:
148
+ """Format a tool call line.
149
+
150
+ Args:
151
+ name: Tool name.
152
+ args_summary: Truncated args.
153
+ running: Whether tool is in parallel mode.
154
+ namespace: Event namespace.
155
+ verbosity_tier: Current verbosity tier.
156
+
157
+ Returns:
158
+ DisplayLine for tool call.
159
+ """
160
+ # Add inline symbol for tool execution
161
+ content = f"🔧 {name}({args_summary})"
162
+ return DisplayLine(
163
+ level=2,
164
+ content=content,
165
+ icon="⚙",
166
+ indent=indent_for_level(2),
167
+ status="running" if running else None,
168
+ source_prefix=_derive_source_prefix(namespace, verbosity_tier),
169
+ )
170
+
171
+
172
+ def format_tool_result(
173
+ summary: str,
174
+ duration_ms: int,
175
+ *,
176
+ is_error: bool = False,
177
+ namespace: tuple[str, ...] = (),
178
+ verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
179
+ ) -> DisplayLine:
180
+ """Format a tool result line.
181
+
182
+ Args:
183
+ summary: Result summary.
184
+ duration_ms: Duration in milliseconds.
185
+ is_error: Whether result is an error.
186
+ namespace: Event namespace.
187
+ verbosity_tier: Current verbosity tier.
188
+
189
+ Returns:
190
+ DisplayLine for tool result.
191
+ """
192
+ # Add inline symbol for result status
193
+ inline_symbol = "❌" if is_error else "✨"
194
+ content = f"{inline_symbol} {summary}"
195
+ return DisplayLine(
196
+ level=3,
197
+ content=content,
198
+ icon="✗" if is_error else "✓",
199
+ indent=indent_for_level(3),
200
+ duration_ms=duration_ms,
201
+ source_prefix=_derive_source_prefix(namespace, verbosity_tier),
202
+ )
203
+
204
+
205
+ def format_subagent_milestone(
206
+ brief: str,
207
+ *,
208
+ namespace: tuple[str, ...] = (),
209
+ verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
210
+ ) -> DisplayLine:
211
+ """Format a subagent milestone line.
212
+
213
+ Args:
214
+ brief: Milestone description.
215
+ namespace: Event namespace.
216
+ verbosity_tier: Current verbosity tier.
217
+
218
+ Returns:
219
+ DisplayLine for milestone.
220
+ """
221
+ # Add inline symbol for subagent investigation
222
+ content = f"🕵🏻‍♂️ {brief}"
223
+ return DisplayLine(
224
+ level=3,
225
+ content=content,
226
+ icon="✓",
227
+ indent=indent_for_level(3),
228
+ source_prefix=_derive_source_prefix(namespace, verbosity_tier),
229
+ newline_before=True, # Add separator before subagent milestone
230
+ )
231
+
232
+
233
+ def format_subagent_done(
234
+ summary: str,
235
+ duration_s: float,
236
+ *,
237
+ namespace: tuple[str, ...] = (),
238
+ verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
239
+ ) -> DisplayLine:
240
+ """Format a subagent completion line.
241
+
242
+ Args:
243
+ summary: Completion summary.
244
+ duration_s: Duration in seconds.
245
+ namespace: Event namespace.
246
+ verbosity_tier: Current verbosity tier.
247
+
248
+ Returns:
249
+ DisplayLine for subagent done.
250
+ """
251
+ duration_ms = int(duration_s * 1000)
252
+ # Add inline symbol for subagent investigation complete
253
+ content = f"🕵🏻‍♂️ Done: {summary}"
254
+ return DisplayLine(
255
+ level=3,
256
+ content=content,
257
+ icon="✓",
258
+ indent=indent_for_level(3),
259
+ duration_ms=duration_ms,
260
+ source_prefix=_derive_source_prefix(namespace, verbosity_tier),
261
+ )
262
+
263
+
264
+ def format_reasoning(
265
+ reasoning: str,
266
+ *,
267
+ namespace: tuple[str, ...] = (),
268
+ verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
269
+ ) -> DisplayLine:
270
+ """Format a reasoning line for LLM decision internal analysis.
271
+
272
+ IG-XXX: Shows technical reasoning with "Reasoning:" prefix for clarity.
273
+
274
+ Args:
275
+ reasoning: Internal technical analysis text.
276
+ namespace: Event namespace.
277
+ verbosity_tier: Current verbosity tier.
278
+
279
+ Returns:
280
+ DisplayLine for reasoning.
281
+ """
282
+ # Polish: Add "Reasoning:" prefix to make internal analysis visible
283
+ content = f"💭 Reasoning: {reasoning}"
284
+
285
+ return DisplayLine(
286
+ level=3, # Use level 3 for less prominence (subordinate to next_action)
287
+ content=content,
288
+ icon="•",
289
+ indent=indent_for_level(3),
290
+ source_prefix=_derive_source_prefix(namespace, verbosity_tier),
291
+ newline_before=True, # Add separator for reasoning display
292
+ )
293
+
294
+
295
+ def format_judgement(
296
+ judgement: str,
297
+ action: str,
298
+ *,
299
+ namespace: tuple[str, ...] = (),
300
+ verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
301
+ ) -> DisplayLine:
302
+ """Format a judgement line for LLM decision reasoning.
303
+
304
+ IG-089: Shows meaningful judgement info without raw intermediate data.
305
+ IG-XXX: Prominent reasoning display with "Reason:" prefix for clarity.
306
+
307
+ Args:
308
+ judgement: Human-readable summary of the decision.
309
+ action: Action taken ("continue" or "complete").
310
+ namespace: Event namespace.
311
+ verbosity_tier: Current verbosity tier.
312
+
313
+ Returns:
314
+ DisplayLine for judgement.
315
+ """
316
+ action_icon = "→" if action == "continue" else "✓"
317
+
318
+ # Polish: Add "Reason:" prefix to make LLM reasoning prominent
319
+ content = f"🌀 {judgement}"
320
+
321
+ return DisplayLine(
322
+ level=2, # Use level 2 for more prominence (like step headers)
323
+ content=content,
324
+ icon=action_icon,
325
+ indent=indent_for_level(2),
326
+ source_prefix=_derive_source_prefix(namespace, verbosity_tier),
327
+ newline_before=True, # Add separator before judgement indicator
328
+ )
329
+
330
+
331
+ def format_step_done(
332
+ description: str,
333
+ duration_s: float,
334
+ *,
335
+ tool_call_count: int = 0,
336
+ namespace: tuple[str, ...] = (),
337
+ verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
338
+ ) -> DisplayLine:
339
+ """Format a step completion line with solid checkbox.
340
+
341
+ Args:
342
+ description: Step description (same as header).
343
+ duration_s: Duration in seconds.
344
+ tool_call_count: Number of tool calls made during step execution.
345
+ namespace: Event namespace.
346
+ verbosity_tier: Current verbosity tier.
347
+
348
+ Returns:
349
+ DisplayLine for step done with solid circle icon.
350
+ """
351
+ duration_ms = int(duration_s * 1000)
352
+ # Abbreviate description for cleaner display
353
+ abbreviated = abbreviate_text(description, max_length=50)
354
+ tool_info = f" [{tool_call_count} tools]" if tool_call_count > 0 else ""
355
+ content = f"✅ {abbreviated}{tool_info}"
356
+ return DisplayLine(
357
+ level=2,
358
+ content=content,
359
+ icon="●", # Solid circle for completed step
360
+ indent=indent_for_level(2),
361
+ duration_ms=duration_ms,
362
+ source_prefix=_derive_source_prefix(namespace, verbosity_tier),
363
+ )
364
+
365
+
366
+ def format_goal_done(
367
+ goal: str,
368
+ steps: int,
369
+ total_s: float,
370
+ *,
371
+ namespace: tuple[str, ...] = (),
372
+ verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
373
+ ) -> DisplayLine:
374
+ """Format a goal completion line.
375
+
376
+ Args:
377
+ goal: Goal description.
378
+ steps: Total steps completed.
379
+ total_s: Total duration in seconds.
380
+ namespace: Event namespace.
381
+ verbosity_tier: Current verbosity tier.
382
+
383
+ Returns:
384
+ DisplayLine for goal done.
385
+ """
386
+ duration_ms = int(total_s * 1000)
387
+ # Add inline symbol for goal completion celebration
388
+ content = f"🏆 {goal} (complete, {steps} steps)"
389
+ return DisplayLine(
390
+ level=1,
391
+ content=content,
392
+ icon="●",
393
+ indent=indent_for_level(1),
394
+ duration_ms=duration_ms,
395
+ source_prefix=_derive_source_prefix(namespace, verbosity_tier),
396
+ newline_before=True, # Add separator before final report
397
+ )
398
+
399
+
400
+ __all__ = [
401
+ "abbreviate_text",
402
+ "format_goal_done",
403
+ "format_goal_header",
404
+ "format_judgement",
405
+ "format_reasoning",
406
+ "format_step_done",
407
+ "format_step_header",
408
+ "format_subagent_done",
409
+ "format_subagent_milestone",
410
+ "format_tool_call",
411
+ "format_tool_result",
412
+ ]