soothe-cli 0.7.2__tar.gz → 0.7.4__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 (133) hide show
  1. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/PKG-INFO +1 -1
  2. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/autopilot_cmd.py +1 -31
  3. soothe_cli-0.7.4/src/soothe_cli/cli/commands/config_cmd.py +103 -0
  4. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/cron_cmd.py +13 -4
  5. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/loop_cmd.py +13 -0
  6. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/run_cmd.py +2 -2
  7. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/status_cmd.py +26 -55
  8. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/daemon.py +2 -2
  9. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/main.py +2 -0
  10. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/headless/processor.py +1 -1
  11. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/parse/tool_call_resolution.py +12 -2
  12. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/presentation/engine.py +1 -1
  13. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/presentation/renderer_protocol.py +1 -1
  14. soothe_cli-0.7.4/src/soothe_cli/runtime/presentation/subagent_task_display.py +112 -0
  15. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/state/session_stats.py +89 -44
  16. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/state/step_router.py +1 -1
  17. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/transport/session.py +64 -0
  18. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/_env_vars.py +28 -0
  19. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_app.py +19 -5
  20. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_execution.py +50 -49
  21. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_history.py +103 -31
  22. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_messages_mixin.py +203 -93
  23. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_model.py +15 -1
  24. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_module_init.py +3 -0
  25. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_startup.py +29 -30
  26. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_ui.py +47 -6
  27. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/app.tcss +16 -6
  28. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/command_registry.py +8 -8
  29. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/commands/command_router.py +1 -1
  30. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/commands/slash_commands.py +37 -10
  31. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/commands/subagent_routing.py +3 -8
  32. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/config.py +4 -23
  33. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/file_change_notify.py +23 -1
  34. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/file_change_renderers.py +41 -0
  35. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/preview_limits.py +4 -4
  36. soothe_cli-0.7.4/src/soothe_cli/tui/shell_color.py +48 -0
  37. soothe_cli-0.7.4/src/soothe_cli/tui/spinner_labels.py +55 -0
  38. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/textual_adapter.py +705 -205
  39. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/theme.py +23 -89
  40. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/tips.py +1 -1
  41. soothe_cli-0.7.4/src/soothe_cli/tui/widgets/diff.py +228 -0
  42. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/file_change_preview.py +133 -91
  43. soothe_cli-0.7.4/src/soothe_cli/tui/widgets/help_screen.py +210 -0
  44. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/loading.py +42 -10
  45. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/message_store.py +10 -1
  46. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/_helpers.py +74 -8
  47. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/app.py +16 -7
  48. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/assistant.py +81 -15
  49. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/clarification.py +32 -23
  50. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +160 -36
  51. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +3 -22
  52. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/cognition_step.py +18 -41
  53. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +5 -0
  54. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +7 -3
  55. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/error.py +9 -10
  56. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/skill.py +7 -17
  57. soothe_cli-0.7.4/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +150 -0
  58. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/welcome.py +44 -77
  59. soothe_cli-0.7.2/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -89
  60. soothe_cli-0.7.2/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -65
  61. soothe_cli-0.7.2/src/soothe_cli/tui/widgets/diff.py +0 -143
  62. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/.gitignore +0 -0
  63. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/README.md +0 -0
  64. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/pyproject.toml +0 -0
  65. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/__init__.py +0 -0
  66. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/__init__.py +0 -0
  67. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/__init__.py +0 -0
  68. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/__init__.py +0 -0
  69. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  70. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/headless.py +0 -0
  71. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  72. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/launcher.py +0 -0
  73. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/config/__init__.py +0 -0
  74. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/config/cli_config.py +0 -0
  75. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/config/loader.py +0 -0
  76. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/config/logging_setup.py +0 -0
  77. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/__init__.py +0 -0
  78. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  79. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  80. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  81. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  82. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  83. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  84. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  85. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  86. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  87. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  88. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/state/transcript.py +0 -0
  89. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  90. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  91. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  92. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  93. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  94. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/runtime/wire/messages.py +0 -0
  95. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/__init__.py +0 -0
  96. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/_cli_context.py +0 -0
  97. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/_version.py +0 -0
  98. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/__init__.py +0 -0
  99. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_commands.py +0 -0
  100. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/binding.py +0 -0
  101. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/commands/__init__.py +0 -0
  102. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/hooks.py +0 -0
  103. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/input.py +0 -0
  104. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/markdown_theme.py +0 -0
  105. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/media_utils.py +0 -0
  106. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/model_config.py +0 -0
  107. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/path_utils.py +0 -0
  108. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/project_utils.py +0 -0
  109. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/sessions.py +0 -0
  110. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/skills/__init__.py +0 -0
  111. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/skills/invocation.py +0 -0
  112. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/skills/load.py +0 -0
  113. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/tool_display.py +0 -0
  114. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/unicode_security.py +0 -0
  115. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/update_check.py +0 -0
  116. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  117. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/_links.py +0 -0
  118. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  119. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  120. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  121. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  122. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
  123. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/editor.py +0 -0
  124. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/history.py +0 -0
  125. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  126. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  127. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
  128. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  129. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  130. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  131. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  132. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/status.py +0 -0
  133. {soothe_cli-0.7.2 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.7.2
3
+ Version: 0.7.4
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."),
@@ -0,0 +1,103 @@
1
+ """Config management CLI commands for the soothe daemon."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import sys
8
+ from typing import Annotated, Any
9
+
10
+ import typer
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from soothe_sdk.client import (
14
+ WebSocketClient,
15
+ request_daemon_config_reload,
16
+ websocket_url_from_config,
17
+ )
18
+
19
+ from soothe_cli.config.loader import load_config
20
+
21
+ console = Console()
22
+
23
+ # Create config command group
24
+ config_app = typer.Typer(help="Manage daemon configuration", no_args_is_help=True)
25
+
26
+
27
+ async def _trigger_config_reload(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
28
+ """Trigger config reload via WebSocket RPC.
29
+
30
+ Args:
31
+ ws_url: WebSocket URL to connect to.
32
+ timeout: Request timeout in seconds.
33
+
34
+ Returns:
35
+ Response dict from daemon, or error dict on failure.
36
+ """
37
+ client = WebSocketClient(url=ws_url)
38
+ try:
39
+ await client.connect()
40
+ return await request_daemon_config_reload(client, timeout=timeout)
41
+ except Exception as e:
42
+ return {"success": False, "error": str(e)}
43
+ finally:
44
+ await client.close()
45
+
46
+
47
+ @config_app.command("reload")
48
+ def config_reload(
49
+ json_output: Annotated[
50
+ bool,
51
+ typer.Option("--json", help="Output as JSON."),
52
+ ] = False,
53
+ ) -> None:
54
+ """Trigger config hot-reload on the running daemon.
55
+
56
+ Sends a config_reload RPC request to the daemon, which triggers immediate
57
+ reload of watched config files (config.yml and daemon.yml). Requires the
58
+ daemon to have hot-reload enabled via daemon.enable_config_reload().
59
+
60
+ Examples:
61
+ soothe config reload
62
+ soothe config reload --json
63
+ """
64
+ config = load_config()
65
+ ws_url = websocket_url_from_config(config)
66
+
67
+ # Trigger reload
68
+ result = asyncio.run(_trigger_config_reload(ws_url, timeout=5.0))
69
+
70
+ if json_output:
71
+ output = {
72
+ "websocket_url": ws_url,
73
+ "success": result.get("success", False),
74
+ }
75
+ if result.get("error"):
76
+ output["error"] = result.get("error")
77
+ if result.get("message"):
78
+ output["message"] = result.get("message")
79
+ console.print_json(json.dumps(output))
80
+ return
81
+
82
+ if result.get("success"):
83
+ console.print(
84
+ Panel(
85
+ f"WebSocket URL: {ws_url}\n"
86
+ f"Status: [green]Reload triggered[/green]\n"
87
+ f"Message: {result.get('message', 'Config reload initiated')}",
88
+ title="Config Reload",
89
+ border_style="green",
90
+ )
91
+ )
92
+ else:
93
+ error_msg = result.get("error", "Unknown error")
94
+ if result.get("message"):
95
+ error_msg = result.get("message")
96
+ console.print(
97
+ Panel(
98
+ f"WebSocket URL: {ws_url}\nStatus: [red]Failed[/red]\nError: {error_msg}",
99
+ title="Config Reload",
100
+ border_style="red",
101
+ )
102
+ )
103
+ sys.exit(1)
@@ -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)
@@ -720,6 +720,18 @@ def continue_loop(
720
720
  )
721
721
 
722
722
 
723
+ @loop_app.command("resume")
724
+ def resume_loop(
725
+ loop_id: Annotated[str | None, typer.Argument(help="Loop identifier to continue")] = None,
726
+ prompt: Annotated[
727
+ str | None,
728
+ typer.Option("--prompt", "-p", help="Optional prompt to send after continuing."),
729
+ ] = None,
730
+ ) -> None:
731
+ """Alias for ``continue`` — resume execution on existing loop."""
732
+ continue_loop(loop_id, prompt)
733
+
734
+
723
735
  @loop_app.command("detach")
724
736
  def detach_loop(
725
737
  loop_id: Annotated[str, typer.Argument(help="Loop identifier to detach")],
@@ -873,6 +885,7 @@ __all__ = [
873
885
  "prune_loop_branches",
874
886
  "delete_loop",
875
887
  "continue_loop",
888
+ "resume_loop",
876
889
  "detach_loop",
877
890
  "attach_loop",
878
891
  "new_loop",
@@ -100,7 +100,7 @@ def run_impl(
100
100
  result = get_last_app_result()
101
101
  if result is not None and result.loop_id:
102
102
  typer.echo(
103
- f"💡 To continue this loop: soothe loop continue {result.loop_id}",
103
+ f"💡 To continue this loop: soothe loop resume {result.loop_id}",
104
104
  err=True,
105
105
  )
106
106
 
@@ -113,7 +113,7 @@ def run_impl(
113
113
  result = get_last_app_result()
114
114
  if result is not None and result.loop_id:
115
115
  typer.echo(
116
- f"💡 To continue this loop: soothe loop continue {result.loop_id}",
116
+ f"💡 To continue this loop: soothe loop resume {result.loop_id}",
117
117
  err=True,
118
118
  )
119
119
  sys.exit(130)
@@ -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.
@@ -228,6 +228,7 @@ def main(
228
228
  # ---------------------------------------------------------------------------
229
229
 
230
230
  from soothe_cli.cli.commands.autopilot_cmd import app as _autopilot_app # noqa: E402
231
+ from soothe_cli.cli.commands.config_cmd import config_app as _config_app # noqa: E402
231
232
  from soothe_cli.cli.commands.cron_cmd import app as _cron_app # noqa: E402
232
233
  from soothe_cli.cli.commands.loop_cmd import loop_app as _loop_app # noqa: E402
233
234
  from soothe_cli.cli.commands.status_cmd import status_app as _status_app # noqa: E402
@@ -237,6 +238,7 @@ for _sub_app, _name in (
237
238
  (_loop_app, "loop"),
238
239
  (_autopilot_app, "autopilot"),
239
240
  (_cron_app, "cron"),
241
+ (_config_app, "config"),
240
242
  ):
241
243
  add_help_alias(_sub_app)
242
244
  app.add_typer(_sub_app, name=_name)
@@ -242,7 +242,7 @@ class EventProcessor:
242
242
  """Headless stdout: only RFC-614 loop-tagged finals (IG-343 / IG-345).
243
243
 
244
244
  Suppresses unphased execute-wave narration on the main graph; ``goal_completion``,
245
- ``quiz``, etc. are routed via ``assistant_output_phase`` before this path.
245
+ ``goal_completion``, ``chitchat``, etc. are routed via ``assistant_output_phase`` before this path.
246
246
 
247
247
  Subgraph streams without RFC-614 phases are skipped in ``_handle_ai_message`` before
248
248
  this runs; loop-tagged subgraph finals use ``_dispatch_loop_tagged_assistant_text``.
@@ -152,7 +152,7 @@ def should_ingest_tool_for_step_stats(
152
152
  """Whether the TUI should register a tool row for step-card stats.
153
153
 
154
154
  Main execute-graph tools are tracked as soon as ``tool_call_id`` and ``tool_name``
155
- are known; display does not wait for streamed args. Subgraph explore tools often
155
+ are known; display does not wait for streamed args. Subgraph tools often
156
156
  arrive with placeholder kwargs before real args.
157
157
  """
158
158
  from soothe_sdk.ux.task_namespace import is_step_level_task_tool_id
@@ -165,7 +165,7 @@ def should_ingest_tool_for_step_stats(
165
165
  return True
166
166
  if is_step_card_scope:
167
167
  return not is_step_level_task_tool_id(tcid)
168
- # Subgraph explore tools often arrive with ``{"_subgraph_tool": true}`` before real args.
168
+ # Subgraph tools often arrive with ``{"_subgraph_tool": true}`` before real args.
169
169
  return is_task_level_subgraph_tool_call_id(tcid)
170
170
 
171
171
 
@@ -456,6 +456,11 @@ def materialize_ai_blocks_with_resolved_tools(
456
456
  append + backfill + merge passes).
457
457
  """
458
458
  tool_only = [b for b in expanded_blocks if isinstance(b, dict) and is_toolish_display_block(b)]
459
+ is_text_only_chunk = (
460
+ bool(expanded_blocks)
461
+ and not tool_only
462
+ and all(isinstance(b, dict) and b.get("type") == "text" for b in expanded_blocks)
463
+ )
459
464
  resolved = resolve_tool_invocations_for_display(
460
465
  message,
461
466
  tool_only,
@@ -491,6 +496,11 @@ def materialize_ai_blocks_with_resolved_tools(
491
496
 
492
497
  for r in resolved:
493
498
  if r.tool_call_id not in seen_tool_ids:
499
+ # Plain text stream deltas must not inherit stale ``tool_calls`` from
500
+ # the LangChain message object — that appended phantom tool blocks and
501
+ # forced a flush+pop between every text token (weather-style trivial).
502
+ if is_text_only_chunk:
503
+ continue
494
504
  out.append(
495
505
  {
496
506
  "type": "tool_call",
@@ -49,7 +49,7 @@ class PresentationEngine:
49
49
 
50
50
  @property
51
51
  def final_answer_locked(self) -> bool:
52
- """True after a custom final or quiz-phase response was emitted for this turn."""
52
+ """True after a custom final or chitchat-phase response was emitted for this turn."""
53
53
  return self._state.final_answer_locked
54
54
 
55
55
  def mark_final_answer_locked(self) -> None:
@@ -141,7 +141,7 @@ class RendererProtocol(Protocol):
141
141
  Catch-all for events not covered by specific callbacks.
142
142
 
143
143
  Args:
144
- event_type: Full event type string (e.g., ``soothe.subagent.explore.started``).
144
+ event_type: Full event type string (e.g., ``soothe.subagent.tacitus.started``).
145
145
  data: Event payload.
146
146
  namespace: Subagent namespace tuple (empty for main agent).
147
147
  task_scope: When subgraph streams are bound to a Task tool call (IG-334).
@@ -0,0 +1,112 @@
1
+ """Format subagent Task-scope assistant blobs for CLI/TUI.
2
+
3
+ Tacitus emits structured JSON in subgraph assistant streams. Clients show
4
+ user-facing summaries only; internal planning JSON is suppressed.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from collections.abc import Iterator
11
+ from typing import Any
12
+
13
+
14
+ def _is_tacitus_internal_json_object(obj: dict[str, Any]) -> bool:
15
+ """True when ``obj`` is Tacitus engine scratch (not a user-facing report)."""
16
+ keys = set(obj.keys())
17
+ if "sub_questions" in keys:
18
+ return True
19
+ if "queries" in keys and "query" not in keys:
20
+ return True
21
+ if "is_sufficient" in keys or "follow_up_queries" in keys or "knowledge_gap" in keys:
22
+ return True
23
+ return False
24
+
25
+
26
+ def _iter_embedded_json_objects(raw: str) -> Iterator[dict[str, Any]]:
27
+ """Yield dict objects embedded anywhere in ``raw`` (prose + concatenated JSON)."""
28
+ dec = json.JSONDecoder()
29
+ i = 0
30
+ s = raw
31
+ n = len(s)
32
+ while i < n:
33
+ brace = s.find("{", i)
34
+ if brace < 0:
35
+ break
36
+ try:
37
+ obj, end = dec.raw_decode(s, brace)
38
+ except json.JSONDecodeError:
39
+ i = brace + 1
40
+ continue
41
+ if isinstance(obj, dict):
42
+ yield obj
43
+ i = end
44
+
45
+
46
+ def _strip_concatenated_json_objects(raw: str, *, predicate) -> str:
47
+ """Remove JSON objects matching ``predicate``; keep surrounding prose."""
48
+ stripped = raw.strip()
49
+ if not stripped:
50
+ return raw
51
+
52
+ dec_objects: list[tuple[int, int, dict[str, Any]]] = []
53
+ dec = json.JSONDecoder()
54
+ i = 0
55
+ n = len(stripped)
56
+ while i < n:
57
+ while i < n and stripped[i].isspace():
58
+ i += 1
59
+ if i >= n:
60
+ break
61
+ try:
62
+ obj, end = dec.raw_decode(stripped, i)
63
+ except json.JSONDecodeError:
64
+ break
65
+ if isinstance(obj, dict) and predicate(obj):
66
+ dec_objects.append((i, end, obj))
67
+ i = end
68
+
69
+ if not dec_objects:
70
+ return raw
71
+
72
+ parts: list[str] = []
73
+ cursor = 0
74
+ for start, end, _obj in dec_objects:
75
+ if start > cursor:
76
+ parts.append(stripped[cursor:start])
77
+ cursor = end
78
+ if cursor < n:
79
+ parts.append(stripped[cursor:])
80
+ return " ".join("".join(parts).split())
81
+
82
+
83
+ def format_subagent_task_assistant_for_display(
84
+ raw: str,
85
+ *,
86
+ subagent_type: str | None = None,
87
+ ) -> str:
88
+ """Return display-safe assistant text for a delegated task namespace.
89
+
90
+ Args:
91
+ raw: Full assistant text from a subgraph namespace.
92
+ subagent_type: Built-in subagent id when known (``tacitus``, …).
93
+
94
+ Returns:
95
+ Scrubbed one-line text, or ``""`` when only internal payloads remain.
96
+ """
97
+ agent = (subagent_type or "").strip().lower()
98
+ text = raw
99
+ if agent == "tacitus":
100
+ stripped = text.strip()
101
+ internal_present = any(
102
+ _is_tacitus_internal_json_object(obj) for obj in _iter_embedded_json_objects(stripped)
103
+ )
104
+ if internal_present:
105
+ return ""
106
+ text = _strip_concatenated_json_objects(text, predicate=_is_tacitus_internal_json_object)
107
+ return (text or "").strip()
108
+
109
+
110
+ __all__ = [
111
+ "format_subagent_task_assistant_for_display",
112
+ ]