soothe-cli 0.7.3__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 (132) hide show
  1. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/PKG-INFO +1 -1
  2. soothe_cli-0.7.4/src/soothe_cli/cli/commands/config_cmd.py +103 -0
  3. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/loop_cmd.py +13 -0
  4. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/run_cmd.py +2 -2
  5. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/main.py +2 -0
  6. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/headless/processor.py +1 -1
  7. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/parse/tool_call_resolution.py +12 -2
  8. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/presentation/engine.py +1 -1
  9. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/presentation/renderer_protocol.py +1 -1
  10. soothe_cli-0.7.4/src/soothe_cli/runtime/presentation/subagent_task_display.py +112 -0
  11. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/state/step_router.py +1 -1
  12. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/transport/session.py +64 -0
  13. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_app.py +7 -3
  14. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_execution.py +40 -19
  15. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_history.py +103 -31
  16. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_messages_mixin.py +83 -50
  17. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_module_init.py +3 -0
  18. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_startup.py +0 -7
  19. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_ui.py +19 -2
  20. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/app.tcss +3 -5
  21. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/command_registry.py +6 -0
  22. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/commands/command_router.py +1 -1
  23. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/config.py +4 -23
  24. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/textual_adapter.py +327 -118
  25. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/theme.py +23 -89
  26. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/tips.py +1 -1
  27. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/file_change_preview.py +24 -8
  28. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/loading.py +7 -0
  29. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/message_store.py +10 -1
  30. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/_helpers.py +74 -8
  31. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/app.py +16 -7
  32. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/assistant.py +73 -15
  33. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/clarification.py +26 -9
  34. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +34 -12
  35. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +3 -22
  36. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/cognition_step.py +13 -39
  37. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -2
  38. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +7 -3
  39. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/error.py +9 -10
  40. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/skill.py +7 -17
  41. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/welcome.py +44 -77
  42. soothe_cli-0.7.3/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -89
  43. soothe_cli-0.7.3/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -65
  44. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/.gitignore +0 -0
  45. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/README.md +0 -0
  46. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/pyproject.toml +0 -0
  47. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/__init__.py +0 -0
  48. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/__init__.py +0 -0
  49. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/__init__.py +0 -0
  50. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  51. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
  52. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  53. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/__init__.py +0 -0
  54. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/daemon.py +0 -0
  55. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  56. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/headless.py +0 -0
  57. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  58. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/cli/execution/launcher.py +0 -0
  59. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/config/__init__.py +0 -0
  60. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/config/cli_config.py +0 -0
  61. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/config/loader.py +0 -0
  62. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/config/logging_setup.py +0 -0
  63. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/__init__.py +0 -0
  64. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  65. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  66. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  67. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  68. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  69. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  70. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  71. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  72. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  73. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  74. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  75. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/state/transcript.py +0 -0
  76. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  77. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  78. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  79. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  80. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  81. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/runtime/wire/messages.py +0 -0
  82. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/__init__.py +0 -0
  83. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/_cli_context.py +0 -0
  84. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/_env_vars.py +0 -0
  85. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/_version.py +0 -0
  86. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/__init__.py +0 -0
  87. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_commands.py +0 -0
  88. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/app/_model.py +0 -0
  89. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/binding.py +0 -0
  90. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/commands/__init__.py +0 -0
  91. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
  92. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
  93. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/file_change_notify.py +0 -0
  94. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  95. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/hooks.py +0 -0
  96. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/input.py +0 -0
  97. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/markdown_theme.py +0 -0
  98. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/media_utils.py +0 -0
  99. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/model_config.py +0 -0
  100. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/path_utils.py +0 -0
  101. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/preview_limits.py +0 -0
  102. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/project_utils.py +0 -0
  103. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/sessions.py +0 -0
  104. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/shell_color.py +0 -0
  105. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/skills/__init__.py +0 -0
  106. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/skills/invocation.py +0 -0
  107. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/skills/load.py +0 -0
  108. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/spinner_labels.py +0 -0
  109. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/tool_display.py +0 -0
  110. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/unicode_security.py +0 -0
  111. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/update_check.py +0 -0
  112. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  113. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/_links.py +0 -0
  114. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  115. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  116. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  117. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  118. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
  119. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/diff.py +0 -0
  120. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/editor.py +0 -0
  121. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
  122. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/history.py +0 -0
  123. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  124. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  125. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
  126. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  127. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  128. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  129. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  130. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +0 -0
  131. {soothe_cli-0.7.3 → soothe_cli-0.7.4}/src/soothe_cli/tui/widgets/status.py +0 -0
  132. {soothe_cli-0.7.3 → 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.3
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
@@ -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)
@@ -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)
@@ -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
+ ]
@@ -445,7 +445,7 @@ class StepTaskRouter:
445
445
  ) -> bool:
446
446
  """Register one subgraph tool row on an already-resolved parent step card."""
447
447
  if _is_task_metadata_subgraph_tool(item):
448
- # Inner explore ``task`` chunks are not user-facing tool stats; ingesting
448
+ # Inner subagent ``task`` chunks are not user-facing tool stats; ingesting
449
449
  # them used to rewrite the main ``{step}:s:task:…`` delegation row args.
450
450
  return True
451
451
  display = str(item.display_key or "").strip()
@@ -552,6 +552,70 @@ class TuiDaemonSession:
552
552
  success=True,
553
553
  )
554
554
 
555
+ async def fetch_loop_history(self, loop_id: str) -> SimpleNamespace:
556
+ """Fetch goal display snapshots plus live card tail (RFC-631).
557
+
558
+ Args:
559
+ loop_id: StrangeLoop id.
560
+
561
+ Returns:
562
+ ``SimpleNamespace`` with ``goals``, ``live_cards``, ``live_goal_index``,
563
+ ``context_tokens``, and ``success``.
564
+ """
565
+ lid = str(loop_id or "").strip()
566
+ if not lid:
567
+ return SimpleNamespace(
568
+ goals=[],
569
+ live_cards=[],
570
+ live_goal_index=None,
571
+ context_tokens=0,
572
+ success=False,
573
+ )
574
+
575
+ async with self._rpc_lock:
576
+ await self._ensure_rpc_connected()
577
+ try:
578
+ resp = await self._rpc_client.request(
579
+ "loop_history_fetch",
580
+ {"loop_id": lid},
581
+ timeout=30.0,
582
+ )
583
+ except Exception:
584
+ logger.warning(
585
+ "loop_history_fetch failed for loop %s",
586
+ lid[:16],
587
+ exc_info=True,
588
+ )
589
+ return SimpleNamespace(
590
+ goals=[],
591
+ live_cards=[],
592
+ live_goal_index=None,
593
+ context_tokens=0,
594
+ success=False,
595
+ )
596
+
597
+ goals_raw = resp.get("goals")
598
+ goals = list(goals_raw) if isinstance(goals_raw, list) else []
599
+ live_raw = resp.get("live_cards")
600
+ live_cards = list(live_raw) if isinstance(live_raw, list) else []
601
+ live_goal_index = resp.get("live_goal_index")
602
+ if live_goal_index is not None and not isinstance(live_goal_index, int):
603
+ live_goal_index = None
604
+ context_tokens_raw = resp.get("context_tokens")
605
+ context_tokens = (
606
+ context_tokens_raw
607
+ if isinstance(context_tokens_raw, int) and context_tokens_raw >= 0
608
+ else 0
609
+ )
610
+ success = bool(resp.get("success", True))
611
+ return SimpleNamespace(
612
+ goals=goals,
613
+ live_cards=live_cards,
614
+ live_goal_index=live_goal_index,
615
+ context_tokens=context_tokens,
616
+ success=success,
617
+ )
618
+
555
619
  async def aget_loop_state(self, loop_id: str) -> Any:
556
620
  """Load StrangeLoop state channels from the daemon (``loop_state_get`` RPC).
557
621
 
@@ -300,6 +300,12 @@ class SootheApp(
300
300
  self._message_store = MessageStore()
301
301
  """Message virtualization store."""
302
302
 
303
+ self._loop_history_load_lock = asyncio.Lock()
304
+ """Serialize resume history loads to prevent duplicate widget mounts."""
305
+
306
+ self._loop_history_loaded_for: str | None = None
307
+ """Loop id whose transcript was last painted by ``_load_loop_history``."""
308
+
303
309
  self._hydrate_scheduled = False
304
310
  """Whether a hydrate task has been queued via `call_later`."""
305
311
 
@@ -355,8 +361,7 @@ class SootheApp(
355
361
 
356
362
  Most styling uses Textual's built-in variables (`$primary`,
357
363
  `$text-muted`, `$error-muted`, etc.). This override injects the
358
- app-specific variables (`$mode-bash`, `$mode-command`, `$skill`,
359
- `$skill-hover`, `$tool`, `$tool-hover`, `$cognition`, `$cognition-hover`)
364
+ app-specific variables (`$mode-bash`, `$mode-command`, `$cognition`)
360
365
  that have no Textual equivalent.
361
366
 
362
367
  Returns:
@@ -378,7 +383,6 @@ class SootheApp(
378
383
  yield WelcomeBanner(
379
384
  loop_id=self._lc_loop_id,
380
385
  mcp_tool_count=self._mcp_tool_count,
381
- workspace_path=self._cwd,
382
386
  id="welcome-banner",
383
387
  )
384
388
  yield Container(id="messages")
@@ -494,6 +494,10 @@ class _ExecutionMixin:
494
494
  elif cmd == "/help":
495
495
  await self._show_help_screen()
496
496
 
497
+ elif cmd == "/goals":
498
+ await self._mount_message(UserMessage(command))
499
+ await self._show_goal_history()
500
+
497
501
  elif cmd in {"/changelog", "/docs", "/feedback"}:
498
502
  await self._open_url_command(command, cmd)
499
503
  elif cmd == "/version":
@@ -889,25 +893,42 @@ class _ExecutionMixin:
889
893
  self._inflight_turn_stats = turn_stats
890
894
  self._inflight_turn_start = time.monotonic()
891
895
  try:
892
- await execute_task_textual(
893
- user_input=message,
894
- daemon_session=self._daemon_session,
895
- assistant_id=self._assistant_id,
896
- session_state=self._session_state,
897
- adapter=self._ui_adapter,
898
- image_tracker=self._image_tracker,
899
- sandbox_type=self._sandbox_type,
900
- workspace=self._cwd,
901
- message_kwargs=message_kwargs,
902
- context=CLIContext(
903
- model=self._model_override,
904
- model_params=self._model_params_override or {},
905
- ),
906
- turn_stats=turn_stats,
907
- skip_daemon_send_turn=skip_daemon_send_turn,
908
- clarification_mode=getattr(self, "_clarification_mode", None),
909
- is_shutting_down=lambda: getattr(self, "_exit", False),
910
- )
896
+ for attempt in (1, 2):
897
+ try:
898
+ await execute_task_textual(
899
+ user_input=message,
900
+ daemon_session=self._daemon_session,
901
+ assistant_id=self._assistant_id,
902
+ session_state=self._session_state,
903
+ adapter=self._ui_adapter,
904
+ image_tracker=self._image_tracker,
905
+ sandbox_type=self._sandbox_type,
906
+ workspace=self._cwd,
907
+ message_kwargs=message_kwargs,
908
+ context=CLIContext(
909
+ model=self._model_override,
910
+ model_params=self._model_params_override or {},
911
+ ),
912
+ turn_stats=turn_stats,
913
+ skip_daemon_send_turn=skip_daemon_send_turn,
914
+ clarification_mode=getattr(self, "_clarification_mode", None),
915
+ is_shutting_down=lambda: getattr(self, "_exit", False),
916
+ )
917
+ break
918
+ except Exception as e:
919
+ if (
920
+ attempt == 1
921
+ and self._daemon_session is not None
922
+ and is_daemon_connection_error(e)
923
+ ):
924
+ try:
925
+ await self._daemon_session.ensure_connected()
926
+ logger.info("Retrying turn after daemon reconnect")
927
+ skip_daemon_send_turn = False
928
+ continue
929
+ except (ConnectionError, OSError, TimeoutError):
930
+ pass
931
+ raise
911
932
  except Exception as e: # Resilient tool rendering
912
933
  logger.exception("Agent execution failed")
913
934
  if is_daemon_connection_error(e):