soothe-cli 0.6.16__tar.gz → 0.7.0__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 (129) hide show
  1. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/PKG-INFO +1 -1
  2. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/loop_cmd.py +59 -47
  3. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/status_cmd.py +26 -9
  4. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/daemon.py +54 -3
  5. soothe_cli-0.7.0/src/soothe_cli/runtime/__init__.py +17 -0
  6. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/headless/processor.py +14 -3
  7. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/parse/message_processing.py +0 -6
  8. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/parse/tool_call_resolution.py +32 -6
  9. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/parse/tool_result.py +0 -2
  10. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/state/step_router.py +68 -8
  11. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/transport/session.py +124 -33
  12. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_app.py +15 -0
  13. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_execution.py +11 -6
  14. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_history.py +0 -2
  15. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_messages_mixin.py +64 -38
  16. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_model.py +5 -11
  17. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_module_init.py +3 -1
  18. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_startup.py +17 -50
  19. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_ui.py +0 -1
  20. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/binding.py +2 -1
  21. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/command_registry.py +6 -6
  22. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/commands/__init__.py +0 -6
  23. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/commands/command_router.py +7 -13
  24. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/commands/slash_commands.py +0 -33
  25. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/commands/subagent_routing.py +7 -1
  26. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/config.py +0 -141
  27. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/media_utils.py +1 -25
  28. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/model_config.py +5 -14
  29. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/sessions.py +24 -1
  30. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/skills/invocation.py +0 -13
  31. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/textual_adapter.py +630 -181
  32. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/theme.py +0 -5
  33. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/unicode_security.py +0 -38
  34. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/chat_input.py +4 -4
  35. soothe_cli-0.7.0/src/soothe_cli/tui/widgets/context_viewer.py +298 -0
  36. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/file_change_preview.py +122 -34
  37. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/loop_selector.py +126 -112
  38. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/__init__.py +2 -0
  39. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/cognition_step.py +131 -116
  40. soothe_cli-0.7.0/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +560 -0
  41. soothe_cli-0.7.0/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +122 -0
  42. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/status.py +2 -2
  43. soothe_cli-0.6.16/src/soothe_cli/runtime/__init__.py +0 -82
  44. soothe_cli-0.6.16/src/soothe_cli/runtime/parse/_utils.py +0 -14
  45. soothe_cli-0.6.16/src/soothe_cli/runtime/parse/tool_message_format.py +0 -17
  46. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -947
  47. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/.gitignore +0 -0
  48. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/README.md +0 -0
  49. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/pyproject.toml +0 -0
  50. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/__init__.py +0 -0
  51. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/__init__.py +0 -0
  52. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/__init__.py +0 -0
  53. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  54. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
  55. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  56. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/__init__.py +0 -0
  57. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  58. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/headless.py +0 -0
  59. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  60. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/launcher.py +0 -0
  61. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/cli/main.py +0 -0
  62. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/config/__init__.py +0 -0
  63. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/config/cli_config.py +0 -0
  64. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/config/loader.py +0 -0
  65. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/config/logging_setup.py +0 -0
  66. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  67. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  68. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  69. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  70. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  71. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  72. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
  73. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  74. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  75. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  76. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  77. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  78. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/state/transcript.py +0 -0
  79. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  80. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  81. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  82. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  83. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  84. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/runtime/wire/messages.py +0 -0
  85. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/__init__.py +0 -0
  86. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/_cli_context.py +0 -0
  87. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/_env_vars.py +0 -0
  88. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/_version.py +0 -0
  89. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/__init__.py +0 -0
  90. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_commands.py +0 -0
  91. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/app.tcss +0 -0
  92. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/file_change_notify.py +0 -0
  93. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  94. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/hooks.py +0 -0
  95. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/input.py +0 -0
  96. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/path_utils.py +0 -0
  97. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/preview_limits.py +0 -0
  98. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/project_utils.py +0 -0
  99. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/skills/__init__.py +0 -0
  100. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/skills/load.py +0 -0
  101. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/tips.py +0 -0
  102. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/tool_display.py +0 -0
  103. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/update_check.py +0 -0
  104. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  105. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/_links.py +0 -0
  106. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  107. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  108. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  109. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  110. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/diff.py +0 -0
  111. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/editor.py +0 -0
  112. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/history.py +0 -0
  113. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/loading.py +0 -0
  114. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  115. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  116. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
  117. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
  118. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
  119. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
  120. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +0 -0
  121. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
  122. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  123. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
  124. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
  125. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  126. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  127. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  128. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  129. {soothe_cli-0.6.16 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/welcome.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.6.16
3
+ Version: 0.7.0
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
@@ -17,7 +17,12 @@ from rich.console import Console
17
17
  from rich.panel import Panel
18
18
  from rich.prompt import Prompt
19
19
  from rich.table import Table
20
- from soothe_sdk.client import WebSocketClient, is_daemon_live, websocket_url_from_config
20
+ from soothe_sdk.client import (
21
+ ProtocolError,
22
+ WebSocketClient,
23
+ is_daemon_live,
24
+ websocket_url_from_config,
25
+ )
21
26
 
22
27
  from soothe_cli.runtime import load_config
23
28
 
@@ -44,37 +49,50 @@ async def _check_daemon(ws_url: str) -> bool:
44
49
 
45
50
  async def _rpc(
46
51
  ws_url: str,
47
- send_fn: str,
48
- send_args: dict[str, Any],
49
- response_type: str,
52
+ method: str,
53
+ params: dict[str, Any] | None = None,
54
+ *,
55
+ mode: str = "request",
50
56
  timeout: float = 30.0,
51
57
  ) -> dict[str, Any]:
52
- """Send an RPC request and wait for a matching response.
58
+ """Send a protocol-1 RPC to the daemon and return the response.
59
+
60
+ Uses the protocol-1 client API (RFC-450): ``request()`` for RPC,
61
+ ``notify()`` for fire-and-forget, ``subscribe()`` for event streams.
62
+ The wrapper preserves the dict-based error contract used by command
63
+ handlers — callers check ``if "error" in response``.
53
64
 
54
65
  Args:
55
66
  ws_url: WebSocket URL.
56
- send_fn: Name of the WebSocketClient method to call.
57
- send_args: Keyword arguments for the send method.
58
- response_type: Expected response message type.
67
+ method: RPC method / notification target / subscription target
68
+ (e.g. ``"loop_get"``, ``"loop_input"``, ``"loop_events"``).
69
+ params: Structured parameters object.
70
+ mode: One of ``"request"`` (blocking RPC), ``"notify"``
71
+ (fire-and-forget), or ``"subscribe"`` (start a stream).
59
72
  timeout: Maximum seconds to wait.
60
73
 
61
74
  Returns:
62
- Response dict from daemon.
75
+ Response dict from daemon, or ``{"error": ...}`` on failure. For
76
+ ``notify`` mode returns ``{}``; for ``subscribe`` mode returns
77
+ ``{"subscription_id": <id>}``.
63
78
  """
64
79
  client = WebSocketClient(url=ws_url)
65
80
  try:
66
81
  await client.connect()
67
- method = getattr(client, send_fn)
68
- await method(**send_args)
69
- async with asyncio.timeout(timeout):
70
- while True:
71
- event = await client.read_event()
72
- if not event:
73
- return {"error": "Connection closed"}
74
- if event.get("type") == response_type:
75
- return event
82
+ if mode == "notify":
83
+ await asyncio.wait_for(client.notify(method, params or {}), timeout=timeout)
84
+ return {}
85
+ if mode == "subscribe":
86
+ sub_id = await asyncio.wait_for(client.subscribe(method, params or {}), timeout=timeout)
87
+ return {"subscription_id": sub_id}
88
+ result = await asyncio.wait_for(client.request(method, params or {}), timeout=timeout)
89
+ return result if isinstance(result, dict) else {"result": result}
76
90
  except TimeoutError:
77
91
  return {"error": "Timed out waiting for daemon response"}
92
+ except ProtocolError as exc:
93
+ return {"error": str(exc)}
94
+ except (ConnectionError, OSError) as exc:
95
+ return {"error": f"Connection error: {exc}"}
78
96
  finally:
79
97
  await client.close()
80
98
 
@@ -91,9 +109,8 @@ def _resolve_continue_loop_id(ws_url: str, loop_id: str | None) -> str:
91
109
  response = asyncio.run(
92
110
  _rpc(
93
111
  ws_url,
94
- "send_loop_list",
95
- {"filter_dict": None, "limit": 20},
96
- "loop_list_response",
112
+ "loop_list",
113
+ {"limit": 20},
97
114
  )
98
115
  )
99
116
  if "error" in response:
@@ -153,9 +170,8 @@ def list_loops(
153
170
  response = asyncio.run(
154
171
  _rpc(
155
172
  ws_url,
156
- "send_loop_list",
157
- {"filter_dict": {"status": status} if status else None, "limit": limit},
158
- "loop_list_response",
173
+ "loop_list",
174
+ {"filter": {"status": status} if status else None, "limit": limit},
159
175
  )
160
176
  )
161
177
 
@@ -211,9 +227,8 @@ def describe_loop(
211
227
  response = asyncio.run(
212
228
  _rpc(
213
229
  ws_url,
214
- "send_loop_get",
230
+ "loop_get",
215
231
  {"loop_id": loop_id, "verbose": verbose},
216
- "loop_get_response",
217
232
  )
218
233
  )
219
234
 
@@ -327,9 +342,8 @@ def visualize_loop_tree(
327
342
  response = asyncio.run(
328
343
  _rpc(
329
344
  ws_url,
330
- "send_loop_tree",
345
+ "loop_tree",
331
346
  {"loop_id": loop_id, "format": format},
332
- "loop_tree_response",
333
347
  )
334
348
  )
335
349
 
@@ -384,9 +398,8 @@ def prune_loop_branches(
384
398
  response = asyncio.run(
385
399
  _rpc(
386
400
  ws_url,
387
- "send_loop_prune",
401
+ "loop_prune",
388
402
  {"loop_id": loop_id, "retention_days": retention_days, "dry_run": dry_run},
389
- "loop_prune_response",
390
403
  )
391
404
  )
392
405
 
@@ -394,10 +407,13 @@ def prune_loop_branches(
394
407
  typer.echo(f"Error: {response['error']}", err=True)
395
408
  sys.exit(1)
396
409
 
397
- result = response.get("result", {})
410
+ # Protocol-1: request() returns the result dict directly (e.g.
411
+ # {"pruned": N, "remaining": N, "dry_run": bool}), not wrapped in {"result": ...}.
412
+ pruned = response.get("pruned", 0)
413
+ remaining = response.get("remaining", 0)
398
414
  console.print("[green]Summary:[/green]")
399
- console.print(f" Branches pruned: {result.get('pruned', 0)}")
400
- console.print(f" Remaining: {result.get('remaining', 0)}")
415
+ console.print(f" Branches pruned: {pruned}")
416
+ console.print(f" Remaining: {remaining}")
401
417
 
402
418
 
403
419
  @loop_app.command("delete")
@@ -424,9 +440,8 @@ def delete_loop(
424
440
  response = asyncio.run(
425
441
  _rpc(
426
442
  ws_url,
427
- "send_loop_get",
443
+ "loop_get",
428
444
  {"loop_id": loop_id, "verbose": False},
429
- "loop_get_response",
430
445
  )
431
446
  )
432
447
 
@@ -456,9 +471,8 @@ def delete_loop(
456
471
  delete_response = asyncio.run(
457
472
  _rpc(
458
473
  ws_url,
459
- "send_loop_delete",
474
+ "loop_delete",
460
475
  {"loop_id": loop_id},
461
- "loop_delete_response",
462
476
  )
463
477
  )
464
478
 
@@ -728,9 +742,9 @@ def detach_loop(
728
742
  response = asyncio.run(
729
743
  _rpc(
730
744
  ws_url,
731
- "send_loop_detach",
745
+ "loop_detach",
732
746
  {"loop_id": loop_id},
733
- "loop_detach_response",
747
+ mode="notify",
734
748
  )
735
749
  )
736
750
 
@@ -766,9 +780,9 @@ def attach_loop(
766
780
  response = asyncio.run(
767
781
  _rpc(
768
782
  ws_url,
769
- "send_loop_subscribe",
783
+ "loop_events",
770
784
  {"loop_id": loop_id},
771
- "loop_subscribe_response",
785
+ mode="subscribe",
772
786
  )
773
787
  )
774
788
 
@@ -782,9 +796,8 @@ def attach_loop(
782
796
  status_response = asyncio.run(
783
797
  _rpc(
784
798
  ws_url,
785
- "send_loop_get",
799
+ "loop_get",
786
800
  {"loop_id": loop_id, "verbose": False},
787
- "loop_get_response",
788
801
  )
789
802
  )
790
803
 
@@ -824,9 +837,8 @@ def new_loop(
824
837
  response = asyncio.run(
825
838
  _rpc(
826
839
  ws_url,
827
- "send_loop_new",
840
+ "loop_new",
828
841
  {},
829
- "loop_new_response",
830
842
  )
831
843
  )
832
844
 
@@ -842,9 +854,9 @@ def new_loop(
842
854
  input_response = asyncio.run(
843
855
  _rpc(
844
856
  ws_url,
845
- "send_loop_input",
857
+ "loop_input",
846
858
  {"loop_id": loop_id, "content": prompt},
847
- "loop_input_response",
859
+ mode="notify",
848
860
  )
849
861
  )
850
862
  if "error" in input_response:
@@ -15,7 +15,12 @@ import typer
15
15
  from rich.console import Console
16
16
  from rich.panel import Panel
17
17
  from rich.table import Table
18
- from soothe_sdk.client import WebSocketClient, is_daemon_live, websocket_url_from_config
18
+ from soothe_sdk.client import (
19
+ WebSocketClient,
20
+ check_daemon_status,
21
+ is_daemon_live,
22
+ websocket_url_from_config,
23
+ )
19
24
 
20
25
  from soothe_cli.config.loader import load_config
21
26
 
@@ -38,8 +43,7 @@ async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
38
43
  client = WebSocketClient(url=ws_url)
39
44
  try:
40
45
  await client.connect()
41
- status = await client.fetch_daemon_status(timeout=timeout)
42
- return status
46
+ return await check_daemon_status(client, timeout=timeout)
43
47
  except Exception as e:
44
48
  return {"error": str(e)}
45
49
  finally:
@@ -47,28 +51,41 @@ async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
47
51
 
48
52
 
49
53
  async def _fetch_ready_state(ws_url: str, timeout: float = 5.0) -> dict[str, Any] | None:
50
- """Fetch daemon readiness state via WebSocket handshake.
54
+ """Fetch daemon readiness state via WebSocket connection_init/ack handshake.
51
55
 
52
- The daemon sends a daemon_ready message on connect with its state.
56
+ Sends ``connection_init`` and waits for ``connection_ack`` which carries
57
+ the daemon's ``readiness_state``.
53
58
 
54
59
  Args:
55
60
  ws_url: WebSocket URL.
56
61
  timeout: Timeout for handshake.
57
62
 
58
63
  Returns:
59
- daemon_ready message dict or None.
64
+ ``connection_ack`` result dict (containing ``readiness_state``) or None.
60
65
  """
61
66
  import websockets
62
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
+
63
80
  try:
64
81
  async with asyncio.timeout(timeout):
65
82
  async with websockets.connect(ws_url) as ws:
66
- # Read initial messages - daemon sends status then daemon_ready
83
+ await ws.send(init_msg)
67
84
  for _ in range(3):
68
85
  msg = await ws.recv()
69
86
  data = json.loads(msg)
70
- if data.get("type") == "daemon_ready":
71
- return data
87
+ if data.get("type") == "connection_ack":
88
+ return data.get("result", data)
72
89
  except Exception:
73
90
  pass
74
91
  return None
@@ -39,9 +39,41 @@ _HEADLESS_WORKER_LOST_RETRIES = 1
39
39
  _CANCEL_SEND_TIMEOUT_S = 3.0
40
40
 
41
41
 
42
+ def _unwrap_next(event: dict[str, Any] | None) -> dict[str, Any] | None:
43
+ """Unwrap a protocol-1 ``next`` envelope to its inner streaming frame.
44
+
45
+ Under protocol-1 (RFC-450 §9.3) the daemon wraps free-form streaming
46
+ frames (``event``/``command_response``/card replay) in a
47
+ ``{proto, type:"next", payload:{namespace, mode, data}}`` envelope. This
48
+ helper returns the inner ``data`` dict (the legacy frame) so the headless
49
+ loop and EventProcessor can branch on the same fields as before.
50
+ ``status``/``error``/``response``/``complete`` are sent raw and pass
51
+ through unchanged.
52
+ """
53
+ if not isinstance(event, dict):
54
+ return event
55
+ if event.get("type") != "next":
56
+ return event
57
+ payload = event.get("payload")
58
+ if not isinstance(payload, dict):
59
+ return event
60
+ data = payload.get("data")
61
+ return data if isinstance(data, dict) else event
62
+
63
+
42
64
  def _is_loop_scoped_event(event: dict[str, Any], *, active_loop_id: str) -> bool:
43
- """Return whether a daemon frame belongs to the active StrangeLoop session."""
65
+ """Return whether a daemon frame belongs to the active StrangeLoop session.
66
+
67
+ Unwraps protocol-1 ``next`` envelopes first, then checks ``loop_id`` on the
68
+ inner streaming frame. Non-scoped types (``response``, ``error``,
69
+ ``complete``, etc.) are always considered in-scope.
70
+ """
44
71
  event_type = event.get("type", "")
72
+ if event_type == "next":
73
+ inner = _unwrap_next(event)
74
+ if isinstance(inner, dict):
75
+ event_type = inner.get("type", "")
76
+ return event_type not in {"status", "event"} or (inner.get("loop_id") == active_loop_id)
45
77
  if event_type not in {"status", "event"}:
46
78
  return True
47
79
  return event.get("loop_id") == active_loop_id
@@ -55,7 +87,10 @@ def _emit_headless_error(message: str) -> None:
55
87
  async def _send_cancel_to_daemon(client: WebSocketClient) -> None:
56
88
  """Send /cancel to the daemon with a short timeout."""
57
89
  try:
58
- await asyncio.wait_for(client.send_command("/cancel"), timeout=_CANCEL_SEND_TIMEOUT_S)
90
+ await asyncio.wait_for(
91
+ client.notify("slash_command", {"cmd": "/cancel"}),
92
+ timeout=_CANCEL_SEND_TIMEOUT_S,
93
+ )
59
94
  except Exception:
60
95
  logger.warning("Failed to send /cancel to daemon", exc_info=True)
61
96
 
@@ -216,8 +251,18 @@ async def _run_headless_session_once(
216
251
  if not _is_loop_scoped_event(event, active_loop_id=active_loop_id):
217
252
  continue
218
253
 
254
+ # Unwrap protocol-1 ``next`` envelopes to the inner streaming frame
255
+ # (RFC-450 §9.3). ``status``/``error`` arrive raw and pass through.
256
+ if event_type == "next":
257
+ inner = _unwrap_next(event)
258
+ if isinstance(inner, dict):
259
+ event = inner
260
+ event_type = event.get("type", "")
261
+
219
262
  if event_type == "error":
220
- raw = str(event.get("message", "unknown"))
263
+ # Protocol-1 error envelope: {type:'error', error:{code, message, data}}
264
+ err_obj = event.get("error") or {}
265
+ raw = str(err_obj.get("message") or event.get("message") or "unknown")
221
266
  _emit_headless_error(friendly_daemon_execution_error(raw))
222
267
  return 1, is_daemon_worker_subprocess_lost(raw)
223
268
 
@@ -245,6 +290,12 @@ async def _run_headless_session_once(
245
290
  break
246
291
  if not _is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
247
292
  continue
293
+ # Unwrap ``next`` envelopes before handing to the
294
+ # processor so it sees the legacy frame shape.
295
+ if nxt.get("type") == "next":
296
+ inner = _unwrap_next(nxt)
297
+ if isinstance(inner, dict):
298
+ nxt = inner
248
299
  processor.process_event(nxt)
249
300
 
250
301
  processor.process_event(event)
@@ -0,0 +1,17 @@
1
+ """Daemon event processing and display state for the CLI (source of truth for TUI).
2
+
3
+ Bridges daemon events/messages to in-memory state. ``soothe_cli.tui`` owns widgets
4
+ and layout only.
5
+ """
6
+
7
+ from soothe_cli.config.loader import load_config
8
+ from soothe_cli.config.logging_setup import setup_logging
9
+ from soothe_cli.runtime.headless.processor import EventProcessor
10
+ from soothe_cli.runtime.headless.processor_state import ProcessorState
11
+
12
+ __all__ = [
13
+ "EventProcessor",
14
+ "ProcessorState",
15
+ "load_config",
16
+ "setup_logging",
17
+ ]
@@ -465,9 +465,20 @@ class EventProcessor:
465
465
  self._handle_custom_event(data, namespace)
466
466
 
467
467
  def _handle_error_event(self, event: dict[str, Any]) -> None:
468
- """Handle error events."""
469
- error = event.get("message", event.get("error", "Unknown error"))
470
- context = event.get("code")
468
+ """Handle error events.
469
+
470
+ Supports both legacy flat errors (``{type:'error', message:...}``) and
471
+ protocol-1 error envelopes (``{type:'error', error:{code, message, data}}``).
472
+ """
473
+ err_obj = event.get("error")
474
+ if isinstance(err_obj, dict):
475
+ # Protocol-1 error envelope.
476
+ error = str(err_obj.get("message", "Unknown error"))
477
+ context = err_obj.get("code")
478
+ else:
479
+ # Legacy flat error or bare string.
480
+ error = str(event.get("message", err_obj or "Unknown error"))
481
+ context = event.get("code")
471
482
  self._renderer.on_error(error, context=context)
472
483
 
473
484
  def _handle_messages(
@@ -16,17 +16,14 @@ from soothe_sdk.display.message_processing import (
16
16
  _resolve_pending_lookup_tool_name,
17
17
  accumulate_tool_call_chunks,
18
18
  coerce_tool_call_args_to_dict,
19
- coerce_tool_call_entry_to_dict,
20
19
  extract_tool_args_dict,
21
20
  extract_tool_brief,
22
21
  finalize_pending_tool_call,
23
22
  ingest_tool_call_stream_state,
24
23
  normalize_tool_calls_list,
25
24
  richest_pending_args_for_lookup,
26
- seed_pending_tool_calls_from_message,
27
25
  tool_calls_have_any_arg_dict,
28
26
  tool_ids_touched_by_stream_message,
29
- tool_lookup_step_id,
30
27
  try_parse_pending_tool_call_args,
31
28
  )
32
29
 
@@ -36,16 +33,13 @@ __all__ = [
36
33
  "_resolve_pending_lookup_tool_name",
37
34
  "accumulate_tool_call_chunks",
38
35
  "coerce_tool_call_args_to_dict",
39
- "coerce_tool_call_entry_to_dict",
40
36
  "extract_tool_args_dict",
41
37
  "extract_tool_brief",
42
38
  "finalize_pending_tool_call",
43
39
  "ingest_tool_call_stream_state",
44
40
  "normalize_tool_calls_list",
45
41
  "richest_pending_args_for_lookup",
46
- "seed_pending_tool_calls_from_message",
47
42
  "tool_calls_have_any_arg_dict",
48
43
  "tool_ids_touched_by_stream_message",
49
- "tool_lookup_step_id",
50
44
  "try_parse_pending_tool_call_args",
51
45
  ]
@@ -17,7 +17,15 @@ from collections.abc import Mapping
17
17
  from dataclasses import dataclass
18
18
  from typing import Any
19
19
 
20
- from soothe_sdk.ux.task_namespace import parse_unified_tool_call_id
20
+ from soothe_sdk.ux.execute_namespace import (
21
+ is_root_execute_namespace_key,
22
+ is_step_level_execute_namespace_key,
23
+ )
24
+ from soothe_sdk.ux.task_namespace import (
25
+ TaskScope,
26
+ parse_unified_tool_call_id,
27
+ row_key_for_subgraph_tool,
28
+ )
21
29
 
22
30
  from soothe_cli.runtime.parse.message_processing import (
23
31
  extract_tool_args_dict,
@@ -40,15 +48,32 @@ def tool_args_meaningful(raw: Any) -> bool:
40
48
 
41
49
 
42
50
  def is_execute_step_namespace(ns_key: tuple[str, ...]) -> bool:
43
- """True for CoreAgent execute root namespace (``execute:{run_id}``), not nested ``tools:`` subgraphs."""
44
- if len(ns_key) != 1:
45
- return False
46
- return str(ns_key[0] or "").startswith("execute:")
51
+ """True for CoreAgent execute root namespace (``execute:{run_id}``), not nested ``/N`` or ``tools:`` subgraphs."""
52
+ return is_root_execute_namespace_key(ns_key)
47
53
 
48
54
 
49
55
  def is_step_card_tool_scope(*, ns_key: tuple[str, ...]) -> bool:
50
56
  """True when tool activity belongs on the step card as main execute-graph tools."""
51
- return ns_key == () or is_execute_step_namespace(ns_key)
57
+ return ns_key == () or is_step_level_execute_namespace_key(ns_key)
58
+
59
+
60
+ def resolve_tool_result_row_key(
61
+ *,
62
+ ns_key: tuple[str, ...],
63
+ tool_call_id: str,
64
+ task_scope: TaskScope | None = None,
65
+ ) -> str:
66
+ """Row key for completing a tool row from a streamed ``ToolMessage``.
67
+
68
+ Execute-graph namespaces (``()`` or ``execute:…``) keep the unified ``s:`` id.
69
+ Subagent ``tools:…`` namespaces remap via :func:`row_key_for_subgraph_tool`.
70
+ """
71
+ sid = str(tool_call_id or "").strip()
72
+ if not sid:
73
+ return ""
74
+ if is_step_card_tool_scope(ns_key=ns_key):
75
+ return sid
76
+ return row_key_for_subgraph_tool(ns_key, sid, task_scope=task_scope)
52
77
 
53
78
 
54
79
  def is_main_step_level_tool_call_id(tool_call_id: str) -> bool:
@@ -546,6 +571,7 @@ __all__ = [
546
571
  "merge_tool_display_args",
547
572
  "resolve_stream_tool_name",
548
573
  "resolve_tool_invocations_for_display",
574
+ "resolve_tool_result_row_key",
549
575
  "should_ingest_tool_for_step_stats",
550
576
  "tool_args_meaningful",
551
577
  ]
@@ -3,13 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from soothe_sdk.display.tool_result import (
6
- ToolResultPayload,
7
6
  extract_tool_result_payload,
8
7
  infer_tool_output_suggests_error,
9
8
  )
10
9
 
11
10
  __all__ = [
12
- "ToolResultPayload",
13
11
  "extract_tool_result_payload",
14
12
  "infer_tool_output_suggests_error",
15
13
  ]