soothe-cli 0.7.1__tar.gz → 0.7.3__tar.gz

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 (131) hide show
  1. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/.gitignore +9 -3
  2. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/PKG-INFO +1 -1
  3. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/autopilot_cmd.py +1 -31
  4. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/cron_cmd.py +13 -4
  5. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/status_cmd.py +26 -55
  6. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/daemon.py +2 -2
  7. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/main.py +27 -0
  8. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/config/cli_config.py +3 -0
  9. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/essential_events.py +3 -1
  10. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/session_stats.py +89 -44
  11. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/transport/session.py +0 -8
  12. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/turn/pipeline.py +99 -4
  13. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/turn/prepare.py +16 -1
  14. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/_env_vars.py +28 -0
  15. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_app.py +15 -3
  16. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_execution.py +11 -32
  17. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_messages_mixin.py +120 -43
  18. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_model.py +15 -10
  19. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_startup.py +99 -41
  20. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_ui.py +42 -5
  21. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/app.tcss +13 -1
  22. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/binding.py +0 -2
  23. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/command_registry.py +2 -8
  24. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/slash_commands.py +34 -14
  25. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/subagent_routing.py +3 -8
  26. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/file_change_notify.py +23 -1
  27. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/file_change_renderers.py +41 -0
  28. soothe_cli-0.7.3/src/soothe_cli/tui/markdown_theme.py +358 -0
  29. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/model_config.py +8 -10
  30. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/preview_limits.py +4 -4
  31. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/sessions.py +8 -0
  32. soothe_cli-0.7.3/src/soothe_cli/tui/shell_color.py +48 -0
  33. soothe_cli-0.7.3/src/soothe_cli/tui/spinner_labels.py +55 -0
  34. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/textual_adapter.py +426 -114
  35. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +3 -8
  36. soothe_cli-0.7.3/src/soothe_cli/tui/widgets/diff.py +228 -0
  37. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/file_change_preview.py +111 -85
  38. soothe_cli-0.7.3/src/soothe_cli/tui/widgets/help_screen.py +210 -0
  39. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/loading.py +65 -17
  40. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/loop_selector.py +22 -131
  41. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -2
  42. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -42
  43. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/assistant.py +11 -4
  44. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/clarification.py +6 -14
  45. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +126 -24
  46. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +3 -10
  47. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_step.py +5 -2
  48. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +7 -0
  49. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/skill.py +2 -4
  50. soothe_cli-0.7.3/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +150 -0
  51. soothe_cli-0.7.1/src/soothe_cli/tui/widgets/diff.py +0 -143
  52. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/README.md +0 -0
  53. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/pyproject.toml +0 -0
  54. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/__init__.py +0 -0
  55. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/__init__.py +0 -0
  56. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/__init__.py +0 -0
  57. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  58. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  59. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/__init__.py +0 -0
  60. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  61. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/headless.py +0 -0
  62. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  63. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/launcher.py +0 -0
  64. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/config/__init__.py +0 -0
  65. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/config/loader.py +0 -0
  66. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/config/logging_setup.py +0 -0
  67. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/__init__.py +0 -0
  68. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/headless/processor.py +0 -0
  69. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  70. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  71. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  72. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  73. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  74. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  75. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  76. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  77. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
  78. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  79. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  80. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  81. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/step_router.py +0 -0
  82. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  83. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/transcript.py +0 -0
  84. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  85. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  86. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  87. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/messages.py +0 -0
  88. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/__init__.py +0 -0
  89. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/_cli_context.py +0 -0
  90. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/_version.py +0 -0
  91. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/__init__.py +0 -0
  92. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_commands.py +0 -0
  93. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_history.py +0 -0
  94. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_module_init.py +0 -0
  95. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/__init__.py +0 -0
  96. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/command_router.py +0 -0
  97. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/config.py +0 -0
  98. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/hooks.py +0 -0
  99. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/input.py +0 -0
  100. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/media_utils.py +0 -0
  101. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/path_utils.py +0 -0
  102. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/project_utils.py +0 -0
  103. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/__init__.py +0 -0
  104. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/invocation.py +0 -0
  105. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/load.py +0 -0
  106. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/theme.py +0 -0
  107. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/tips.py +0 -0
  108. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/tool_display.py +0 -0
  109. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/unicode_security.py +0 -0
  110. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/update_check.py +0 -0
  111. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  112. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/_links.py +0 -0
  113. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  114. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  115. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  116. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  117. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
  118. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/editor.py +0 -0
  119. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/history.py +0 -0
  120. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  121. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  122. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
  123. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
  124. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  125. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
  126. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  127. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  128. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  129. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/status.py +0 -0
  130. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  131. {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/welcome.py +0 -0
@@ -191,6 +191,15 @@ cython_debug/
191
191
  # PyPI configuration file
192
192
  .pypirc
193
193
 
194
+ # Soothe local runtime data
195
+ .soothe/
196
+
197
+ # Tool backup files (created by file editing tools)
198
+ .backups/
199
+
200
+ # Large tool result cache
201
+ large_tool_results/
202
+
194
203
  # Cursor
195
204
  # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
196
205
  # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
@@ -206,7 +215,6 @@ __marimo__/
206
215
  .python-version
207
216
  thirdparty/
208
217
  .cursor/
209
- large_tool_results/
210
218
  plot_*
211
219
  .claude/
212
220
  .DS_Store
@@ -216,5 +224,3 @@ manifest.json
216
224
  _bmad
217
225
  __MACOSX
218
226
  .qoder
219
- .soothe
220
- .backups/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.7.1
3
+ Version: 0.7.3
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/mirasoth/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -55,7 +55,7 @@ def run(
55
55
  max_iterations: int | None = typer.Option(
56
56
  None,
57
57
  "--max-iterations",
58
- help="Ignored — use daemon config agent.autonomous.max_iterations.",
58
+ help="Ignored — use daemon config agent.autopilot.max_iterations.",
59
59
  ),
60
60
  workspace: str | None = typer.Option(
61
61
  None,
@@ -333,36 +333,6 @@ def cancel_goal(
333
333
  typer.echo(f"Cancel result: {result.get('status', result)}")
334
334
 
335
335
 
336
- @app.command("approve")
337
- def approve_goal(
338
- goal_id: str = typer.Argument(
339
- ..., help="Confirmation ID to approve (use 'inbox' to list pending)."
340
- ),
341
- ) -> None:
342
- """Approve a MUST-confirmation goal."""
343
- client = _require_daemon_ws()
344
- try:
345
- result = client.autopilot_approve(goal_id)
346
- except RuntimeError as exc:
347
- typer.echo(str(exc), err=True)
348
- raise typer.Exit(1) from exc
349
- typer.echo(f"Confirmation approved: {result.get('goal_id', goal_id)}")
350
-
351
-
352
- @app.command("reject")
353
- def reject_goal(
354
- goal_id: str = typer.Argument(..., help="Confirmation ID to reject."),
355
- ) -> None:
356
- """Reject a proposed goal."""
357
- client = _require_daemon_ws()
358
- try:
359
- result = client.autopilot_reject(goal_id)
360
- except RuntimeError as exc:
361
- typer.echo(str(exc), err=True)
362
- raise typer.Exit(1) from exc
363
- typer.echo(f"Confirmation rejected: {result.get('goal_id', goal_id)}")
364
-
365
-
366
336
  @app.command("resume")
367
337
  def resume_goal(
368
338
  goal_id: str = typer.Argument(..., help="Goal ID to resume."),
@@ -85,11 +85,16 @@ def add_job(
85
85
  try:
86
86
  result = client.cron_add(text, priority=priority)
87
87
  except RuntimeError as exc:
88
- typer.echo(f"Error: {exc}", err=True)
88
+ message = str(exc)
89
+ if "Autopilot is disabled" in message:
90
+ console.print(f"[yellow]{message}[/yellow]")
91
+ else:
92
+ typer.echo(f"Error: {message}", err=True)
89
93
  sys.exit(1)
90
94
 
91
95
  job = result.get("job") or {}
92
96
  job_id = job.get("id", "?")
97
+ is_duplicate = bool(result.get("duplicate"))
93
98
  console.print(
94
99
  Panel(
95
100
  "\n".join(
@@ -101,10 +106,14 @@ def add_job(
101
106
  f"[bold]Status:[/bold] [green]{job.get('status', 'pending')}[/green]",
102
107
  ]
103
108
  ),
104
- title="Scheduled Job Created",
105
- border_style="green",
109
+ title="Scheduled Job Already Exists" if is_duplicate else "Scheduled Job Created",
110
+ border_style="yellow" if is_duplicate else "green",
106
111
  )
107
112
  )
113
+ if is_duplicate:
114
+ console.print(
115
+ "[yellow]An identical active job is already scheduled; no new job was created.[/yellow]"
116
+ )
108
117
 
109
118
 
110
119
  @app.command("list")
@@ -128,7 +137,7 @@ def list_jobs(
128
137
  """
129
138
  client = _require_cron_client()
130
139
  try:
131
- result = client.cron_list_jobs(status=status)
140
+ result = client.cron_list(status=status)
132
141
  except RuntimeError as exc:
133
142
  typer.echo(f"Error: {exc}", err=True)
134
143
  sys.exit(1)
@@ -50,47 +50,6 @@ async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
50
50
  await client.close()
51
51
 
52
52
 
53
- async def _fetch_ready_state(ws_url: str, timeout: float = 5.0) -> dict[str, Any] | None:
54
- """Fetch daemon readiness state via WebSocket connection_init/ack handshake.
55
-
56
- Sends ``connection_init`` and waits for ``connection_ack`` which carries
57
- the daemon's ``readiness_state``.
58
-
59
- Args:
60
- ws_url: WebSocket URL.
61
- timeout: Timeout for handshake.
62
-
63
- Returns:
64
- ``connection_ack`` result dict (containing ``readiness_state``) or None.
65
- """
66
- import websockets
67
-
68
- init_msg = json.dumps(
69
- {
70
- "proto": "1",
71
- "type": "connection_init",
72
- "params": {
73
- "client_version": "0.5.0",
74
- "accept_proto": ["1"],
75
- "capabilities": ["streaming"],
76
- },
77
- }
78
- )
79
-
80
- try:
81
- async with asyncio.timeout(timeout):
82
- async with websockets.connect(ws_url) as ws:
83
- await ws.send(init_msg)
84
- for _ in range(3):
85
- msg = await ws.recv()
86
- data = json.loads(msg)
87
- if data.get("type") == "connection_ack":
88
- return data.get("result", data)
89
- except Exception:
90
- pass
91
- return None
92
-
93
-
94
53
  def _render_unified_status_table(
95
54
  config: Any,
96
55
  ws_url: str,
@@ -156,10 +115,6 @@ def daemon_status(
156
115
  bool,
157
116
  typer.Option("--json", help="Output as JSON."),
158
117
  ] = False,
159
- verbose: Annotated[
160
- bool,
161
- typer.Option("--verbose", "-v", help="Show detailed status."),
162
- ] = False,
163
118
  ) -> None:
164
119
  """Check daemon status from client side.
165
120
 
@@ -168,7 +123,6 @@ def daemon_status(
168
123
  Examples:
169
124
  soothe status daemon
170
125
  soothe status daemon --json
171
- soothe status daemon -v
172
126
  """
173
127
  config = load_config()
174
128
  ws_url = websocket_url_from_config(config)
@@ -217,11 +171,6 @@ def daemon_status(
217
171
  )
218
172
  sys.exit(1)
219
173
 
220
- # Get readiness state for verbose mode
221
- ready_state = None
222
- if verbose:
223
- ready_state = asyncio.run(_fetch_ready_state(ws_url, timeout=5.0))
224
-
225
174
  if json_output:
226
175
  output = {
227
176
  "status": "running",
@@ -232,10 +181,10 @@ def daemon_status(
232
181
  "daemon_pid": status.get("daemon_pid"),
233
182
  "daemon_version": status.get("daemon_version"),
234
183
  "core_version": status.get("core_version"),
184
+ "readiness_state": status.get("readiness_state", "unknown"),
235
185
  }
236
- if ready_state:
237
- output["readiness_state"] = ready_state.get("state", "unknown")
238
- output["readiness_message"] = ready_state.get("message")
186
+ if status.get("readiness_message"):
187
+ output["readiness_message"] = status.get("readiness_message")
239
188
  console.print_json(json.dumps(output))
240
189
  return
241
190
 
@@ -246,6 +195,15 @@ def daemon_status(
246
195
  daemon_pid = status.get("daemon_pid")
247
196
  daemon_version = status.get("daemon_version")
248
197
  core_version = status.get("core_version")
198
+ # Use readiness_state from daemon_status RPC (already includes state + message)
199
+ readiness_state_from_status = (
200
+ {
201
+ "state": status.get("readiness_state", "unknown"),
202
+ "message": status.get("readiness_message"),
203
+ }
204
+ if status.get("readiness_state")
205
+ else None
206
+ )
249
207
 
250
208
  table = _render_unified_status_table(
251
209
  config,
@@ -254,7 +212,7 @@ def daemon_status(
254
212
  port_live,
255
213
  active_threads,
256
214
  daemon_pid,
257
- ready_state,
215
+ readiness_state_from_status,
258
216
  daemon_live=True,
259
217
  daemon_version=daemon_version,
260
218
  core_version=core_version,
@@ -349,6 +307,9 @@ def status_main(
349
307
  output["daemon"]["daemon_pid"] = status.get("daemon_pid")
350
308
  output["daemon"]["daemon_version"] = status.get("daemon_version")
351
309
  output["daemon"]["core_version"] = status.get("core_version")
310
+ output["daemon"]["readiness_state"] = status.get("readiness_state", "unknown")
311
+ if status.get("readiness_message"):
312
+ output["daemon"]["readiness_message"] = status.get("readiness_message")
352
313
  console.print_json(json.dumps(output))
353
314
  return
354
315
 
@@ -378,6 +339,15 @@ def status_main(
378
339
  daemon_pid = status.get("daemon_pid")
379
340
  daemon_version = status.get("daemon_version")
380
341
  core_version = status.get("core_version")
342
+ # Use readiness_state from daemon_status RPC (already includes state + message)
343
+ readiness_state_from_status = (
344
+ {
345
+ "state": status.get("readiness_state", "unknown"),
346
+ "message": status.get("readiness_message"),
347
+ }
348
+ if status.get("readiness_state")
349
+ else None
350
+ )
381
351
 
382
352
  table = _render_unified_status_table(
383
353
  config,
@@ -386,6 +356,7 @@ def status_main(
386
356
  port_live,
387
357
  active_threads,
388
358
  daemon_pid,
359
+ readiness_state_from_status,
389
360
  daemon_live=True,
390
361
  daemon_version=daemon_version,
391
362
  core_version=core_version,
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import asyncio
10
10
  import logging
11
- import os
12
11
  import signal
13
12
  from typing import Any
14
13
 
@@ -28,6 +27,7 @@ from soothe_cli.cli.execution.daemon_errors import (
28
27
  from soothe_cli.cli.execution.headless_renderer import HeadlessCliRenderer
29
28
  from soothe_cli.runtime import EventProcessor
30
29
  from soothe_cli.runtime.presentation.engine import PresentationEngine
30
+ from soothe_cli.tui._env_vars import resolve_cli_loop_workspace
31
31
  from soothe_cli.tui.commands.subagent_routing import parse_subagent_from_input
32
32
 
33
33
  logger = logging.getLogger(__name__)
@@ -151,7 +151,7 @@ async def _run_headless_session_once(
151
151
 
152
152
  try:
153
153
  await connect_websocket_with_retries(client)
154
- cli_ws = os.environ.get("SOOTHE_CLI_WORKSPACE", "").strip() or os.getcwd()
154
+ cli_ws = resolve_cli_loop_workspace()
155
155
  # IG-441: three first-class modes (batch / adaptive / streaming).
156
156
  # Default is ``adaptive`` for headless runs as well — it gives smooth
157
157
  # progress on long synthesis while keeping wire traffic bounded.
@@ -15,6 +15,12 @@ from soothe_sdk.client.config import SOOTHE_HOME # noqa: E402
15
15
 
16
16
  from soothe_cli.config.cli_config import CLIConfig # noqa: E402
17
17
  from soothe_cli.config.loader import set_runtime_config # noqa: E402
18
+ from soothe_cli.tui.markdown_theme import ( # noqa: E402
19
+ DEFAULT_MARKDOWN_THEME,
20
+ REGISTRY,
21
+ load_markdown_theme_preference,
22
+ markdown_theme_help,
23
+ )
18
24
 
19
25
  app = typer.Typer(
20
26
  name="soothe",
@@ -101,6 +107,16 @@ def main(
101
107
  help="Render assistant messages as Markdown in the TUI.",
102
108
  ),
103
109
  ] = True,
110
+ markdown_theme: Annotated[
111
+ str | None,
112
+ typer.Option(
113
+ "--markdown-theme",
114
+ help=(
115
+ "Markdown appearance preset for TUI cards "
116
+ f"({markdown_theme_help()}). Default: {DEFAULT_MARKDOWN_THEME}."
117
+ ),
118
+ ),
119
+ ] = None,
104
120
  soothe_home: Annotated[
105
121
  str | None,
106
122
  typer.Option("--soothe-home", help="Soothe home directory (default: ~/.soothe)."),
@@ -168,11 +184,22 @@ def main(
168
184
  if mode is not None and mode not in ("manual", "auto"):
169
185
  typer.echo(f"Invalid --mode {mode!r}; expected 'manual' or 'auto'.", err=True)
170
186
  raise typer.Exit(code=2)
187
+ if markdown_theme is not None and markdown_theme not in REGISTRY:
188
+ typer.echo(
189
+ f"Invalid --markdown-theme {markdown_theme!r}; "
190
+ f"expected one of: {markdown_theme_help()}.",
191
+ err=True,
192
+ )
193
+ raise typer.Exit(code=2)
194
+ resolved_markdown_theme = (
195
+ markdown_theme if markdown_theme is not None else load_markdown_theme_preference()
196
+ )
171
197
  cli_cfg = CLIConfig(
172
198
  daemon_host=daemon_host,
173
199
  daemon_port=daemon_port,
174
200
  logging_level=log_level,
175
201
  render_markdown=render_markdown,
202
+ markdown_theme=resolved_markdown_theme,
176
203
  output_streaming_enabled=streaming,
177
204
  output_streaming_mode=streaming_mode,
178
205
  clarification_mode=mode,
@@ -29,6 +29,9 @@ class CLIConfig:
29
29
  render_markdown: bool = True
30
30
  """Render assistant messages as Markdown in TUI (default True)."""
31
31
 
32
+ markdown_theme: str = "match-app"
33
+ """Markdown appearance preset (``match-app``, ``langchain``, ``standard``, …)."""
34
+
32
35
  # Output streaming overrides (RFC-614)
33
36
  output_streaming_enabled: bool | None = None
34
37
  """Override daemon streaming enabled setting."""
@@ -24,12 +24,13 @@ STEP_COMPLETE_EVENT_TYPES: Final[frozenset[str]] = frozenset(
24
24
  )
25
25
 
26
26
  LOOP_REASON_EVENT_TYPE: Final[str] = "soothe.cognition.strange_loop.reasoned"
27
+ INTENT_CLASSIFIED_EVENT_TYPE: Final[str] = "soothe.cognition.intent.classified"
27
28
 
28
29
  ESSENTIAL_PROGRESS_EVENT_TYPES: Final[frozenset[str]] = frozenset(
29
30
  set(GOAL_START_EVENT_TYPES)
30
31
  | set(STEP_START_EVENT_TYPES)
31
32
  | set(STEP_COMPLETE_EVENT_TYPES)
32
- | {LOOP_REASON_EVENT_TYPE}
33
+ | {LOOP_REASON_EVENT_TYPE, INTENT_CLASSIFIED_EVENT_TYPE}
33
34
  )
34
35
 
35
36
 
@@ -56,6 +57,7 @@ def is_step_complete_event_type(event_type: str) -> bool:
56
57
  __all__ = [
57
58
  "ESSENTIAL_PROGRESS_EVENT_TYPES",
58
59
  "GOAL_START_EVENT_TYPES",
60
+ "INTENT_CLASSIFIED_EVENT_TYPE",
59
61
  "LOOP_REASON_EVENT_TYPE",
60
62
  "STEP_COMPLETE_EVENT_TYPES",
61
63
  "STEP_START_EVENT_TYPES",
@@ -8,14 +8,16 @@ config, no widget imports) so that `app.py` can import `SessionStats` and
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import json
11
12
  import time
12
13
  from dataclasses import dataclass, field
14
+ from typing import Any
13
15
 
14
16
  SpinnerStatus = str | None
15
17
  """Spinner line label, or `None` to hide.
16
18
 
17
- Common values include ``Thinking``, ``Offloading``, ``Writing`` (assistant streaming),
18
- ``Tools`` (tool execution), and ``Synthesizing`` (goal-completion stream).
19
+ Common single-word values include ``Thinking``, ``Planning``, ``Executing``,
20
+ ``Tools``, ``Offloading``, ``Writing``, and ``Synthesizing``.
19
21
  """
20
22
 
21
23
 
@@ -242,48 +244,91 @@ class TurnEventStats:
242
244
  self.latency = TurnLatencyStats()
243
245
  self.latency.merge(other.latency)
244
246
 
245
- def summary_line(self) -> str:
246
- """Return a one-line summary suitable for structured logging.
247
-
248
- Returns:
249
- Compact string like ``847 total (612 msg, 89 upd, 146 custom; …)``.
250
- """
251
- parts = [f"{self.total} total"]
252
- mode_parts = []
253
- if self.messages:
254
- mode_parts.append(f"{self.messages} msg")
255
- if self.updates:
256
- mode_parts.append(f"{self.updates} upd")
257
- if self.custom:
258
- mode_parts.append(f"{self.custom} custom")
259
- if mode_parts:
260
- parts.append(f"({', '.join(mode_parts)})")
261
-
262
- detail_parts = []
263
- if self.tool_calls:
264
- detail_parts.append(f"{self.tool_calls} tools")
265
- if self.tool_results:
266
- detail_parts.append(f"{self.tool_results} results")
267
- if self.text_chunks:
268
- detail_parts.append(f"{self.text_chunks} text")
269
- if self.skipped:
270
- detail_parts.append(f"{self.skipped} skipped")
271
- if self.filtered_early:
272
- detail_parts.append(f"{self.filtered_early} filtered-early")
273
- if self.heartbeats_dropped:
274
- detail_parts.append(f"{self.heartbeats_dropped} hb-drop")
275
- if self.post_idle_drained:
276
- detail_parts.append(f"{self.post_idle_drained} post-idle")
277
- if self.inbound_dropped:
278
- detail_parts.append(f"{self.inbound_dropped} inbound-drop")
279
- if self.latency is not None and self.latency.time_to_first_chunk_ms is not None:
280
- detail_parts.append(f"ttfc={self.latency.time_to_first_chunk_ms:.0f}ms")
281
- if self.latency is not None and self.latency.synthesis_visible_ms is not None:
282
- detail_parts.append(f"synth={self.latency.synthesis_visible_ms:.0f}ms")
283
- if detail_parts:
284
- parts.append("; ".join(detail_parts))
285
-
286
- return " ".join(parts)
247
+ @staticmethod
248
+ def _nonzero_fields(**fields: int) -> dict[str, int]:
249
+ return {key: value for key, value in fields.items() if value}
250
+
251
+ def to_log_dict(self) -> dict[str, Any]:
252
+ """Return structured event counters for ``cli.log`` JSON lines."""
253
+ payload: dict[str, Any] = {"total": self.total}
254
+
255
+ modes = self._nonzero_fields(
256
+ messages=self.messages,
257
+ updates=self.updates,
258
+ custom=self.custom,
259
+ )
260
+ if modes:
261
+ payload["modes"] = modes
262
+
263
+ activity = self._nonzero_fields(
264
+ tool_calls=self.tool_calls,
265
+ tool_results=self.tool_results,
266
+ text_chunks=self.text_chunks,
267
+ )
268
+ if activity:
269
+ payload["activity"] = activity
270
+
271
+ degradation = self._nonzero_fields(
272
+ skipped=self.skipped,
273
+ filtered_early=self.filtered_early,
274
+ heartbeats_dropped=self.heartbeats_dropped,
275
+ post_idle_drained=self.post_idle_drained,
276
+ inbound_dropped=self.inbound_dropped,
277
+ )
278
+ if degradation:
279
+ payload["degradation"] = degradation
280
+
281
+ if self.latency is not None:
282
+ latency: dict[str, int] = {}
283
+ if self.latency.time_to_first_chunk_ms is not None:
284
+ latency["ttfc_ms"] = round(self.latency.time_to_first_chunk_ms)
285
+ if self.latency.synthesis_visible_ms is not None:
286
+ latency["synthesis_ms"] = round(self.latency.synthesis_visible_ms)
287
+ if latency:
288
+ payload["latency"] = latency
289
+
290
+ return payload
291
+
292
+
293
+ def build_goal_completed_log_event(
294
+ ev_stats: TurnEventStats,
295
+ *,
296
+ status: str,
297
+ goal_progress: str,
298
+ total_steps: int,
299
+ elapsed_seconds: float,
300
+ ) -> dict[str, Any]:
301
+ """Build a structured goal-completion record for ``cli.log``."""
302
+ event: dict[str, Any] = {
303
+ "event": "goal_completed",
304
+ "elapsed_s": round(elapsed_seconds, 1),
305
+ "events": ev_stats.to_log_dict(),
306
+ }
307
+ if status.strip():
308
+ event["status"] = status.strip()
309
+ if goal_progress.strip():
310
+ event["progress"] = goal_progress.strip()
311
+ if total_steps > 0:
312
+ event["steps"] = total_steps
313
+ return event
314
+
315
+
316
+ def build_turn_finished_log_event(
317
+ ev_stats: TurnEventStats,
318
+ *,
319
+ wall_seconds: float,
320
+ ) -> dict[str, Any]:
321
+ """Build a structured turn-finished record for ``cli.log``."""
322
+ return {
323
+ "event": "turn_finished",
324
+ "wall_s": round(wall_seconds, 1),
325
+ "events": ev_stats.to_log_dict(),
326
+ }
327
+
328
+
329
+ def format_cli_log_event(payload: dict[str, Any]) -> str:
330
+ """Serialize a structured CLI log event as compact JSON."""
331
+ return json.dumps(payload, separators=(",", ":"), sort_keys=True)
287
332
 
288
333
 
289
334
  def format_token_count(count: int) -> str:
@@ -81,7 +81,6 @@ class TuiDaemonSession:
81
81
  self._client = WebSocketClient(url=ws_url)
82
82
  self._rpc_client = WebSocketClient(url=ws_url)
83
83
  self._loop_id: str | None = None
84
- self._autopilot_mode: str | None = None
85
84
  self._read_lock = asyncio.Lock()
86
85
  self._rpc_lock = asyncio.Lock()
87
86
  self._rpc_connected = False
@@ -98,11 +97,6 @@ class TuiDaemonSession:
98
97
  """Active StrangeLoop id for this WebSocket session."""
99
98
  return self._loop_id
100
99
 
101
- @property
102
- def autopilot_mode(self) -> str | None:
103
- """Active loop Solo/Autopilot mode when known."""
104
- return self._autopilot_mode
105
-
106
100
  async def connect(self, *, resume_loop_id: str | None = None) -> dict[str, Any]:
107
101
  """Connect and bootstrap a daemon loop session."""
108
102
  await connect_websocket_with_retries(self._client)
@@ -123,8 +117,6 @@ class TuiDaemonSession:
123
117
  if status_event.get("type") == "error":
124
118
  raise RuntimeError(str(status_event.get("message", "daemon bootstrap failed")))
125
119
  self._loop_id = status_event.get("loop_id")
126
- mode = status_event.get("autopilot_mode")
127
- self._autopilot_mode = str(mode) if mode in ("solo", "autopilot") else None
128
120
  return status_event
129
121
 
130
122
  def _resolve_stream_delivery_mode(self) -> str: