soothe-cli 0.6.9__tar.gz → 0.6.11__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 (114) hide show
  1. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/PKG-INFO +1 -1
  2. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/commands/status_cmd.py +52 -67
  3. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/parse/tool_call_resolution.py +34 -2
  4. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/policy/display_policy.py +0 -9
  5. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/state/step_router.py +48 -9
  6. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/textual_adapter.py +65 -32
  7. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/tool_display.py +17 -3
  8. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/loading.py +39 -78
  9. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/messages.py +43 -1
  10. soothe_cli-0.6.9/src/soothe_cli/runtime/presentation/async_renderer_protocol.py +0 -168
  11. soothe_cli-0.6.9/src/soothe_cli/runtime/task_scope.py +0 -30
  12. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/.gitignore +0 -0
  13. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/README.md +0 -0
  14. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/pyproject.toml +0 -0
  15. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/__init__.py +0 -0
  16. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/__init__.py +0 -0
  17. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/commands/__init__.py +0 -0
  18. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  19. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  20. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  21. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/execution/__init__.py +0 -0
  22. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/execution/daemon.py +0 -0
  23. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  24. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/execution/headless.py +0 -0
  25. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  26. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/execution/launcher.py +0 -0
  27. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/cli/main.py +0 -0
  28. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/config/__init__.py +0 -0
  29. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/config/cli_config.py +0 -0
  30. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/config/loader.py +0 -0
  31. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/config/logging_setup.py +0 -0
  32. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/__init__.py +0 -0
  33. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/headless/processor.py +0 -0
  34. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  35. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/parse/_utils.py +0 -0
  36. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  37. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
  38. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  39. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  40. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  41. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  42. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  43. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
  44. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  45. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  46. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  47. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  48. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  49. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/state/transcript.py +0 -0
  50. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/transport/session.py +0 -0
  51. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  52. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  53. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  54. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  55. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  56. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/runtime/wire/messages.py +0 -0
  57. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/__init__.py +0 -0
  58. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/_cli_context.py +0 -0
  59. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/_env_vars.py +0 -0
  60. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/_version.py +0 -0
  61. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/__init__.py +0 -0
  62. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/_app.py +0 -0
  63. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/_commands.py +0 -0
  64. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/_execution.py +0 -0
  65. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/_history.py +0 -0
  66. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
  67. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/_model.py +0 -0
  68. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/_module_init.py +0 -0
  69. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/_startup.py +0 -0
  70. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/_ui.py +0 -0
  71. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/app/app.tcss +0 -0
  72. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/binding.py +0 -0
  73. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/command_registry.py +0 -0
  74. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/commands/__init__.py +0 -0
  75. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/commands/command_router.py +0 -0
  76. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
  77. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
  78. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/config.py +0 -0
  79. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/file_change_notify.py +0 -0
  80. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  81. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/hooks.py +0 -0
  82. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/input.py +0 -0
  83. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/media_utils.py +0 -0
  84. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/model_config.py +0 -0
  85. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/path_utils.py +0 -0
  86. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/preview_limits.py +0 -0
  87. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/project_utils.py +0 -0
  88. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/sessions.py +0 -0
  89. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/skills/__init__.py +0 -0
  90. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/skills/invocation.py +0 -0
  91. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/skills/load.py +0 -0
  92. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/theme.py +0 -0
  93. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/tips.py +0 -0
  94. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/unicode_security.py +0 -0
  95. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/update_check.py +0 -0
  96. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  97. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/_links.py +0 -0
  98. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  99. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  100. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  101. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  102. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  103. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/diff.py +0 -0
  104. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/editor.py +0 -0
  105. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  106. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/history.py +0 -0
  107. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  108. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  109. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  110. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  111. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  112. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/status.py +0 -0
  113. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  114. {soothe_cli-0.6.9 → soothe_cli-0.6.11}/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.9
3
+ Version: 0.6.11
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
@@ -74,39 +74,41 @@ async def _fetch_ready_state(ws_url: str, timeout: float = 5.0) -> dict[str, Any
74
74
  return None
75
75
 
76
76
 
77
- def _render_connection_table(config: Any, ws_url: str) -> Table:
78
- """Render connection settings table."""
79
- table = Table(title="Connection Settings")
80
- table.add_column("Setting", style="cyan")
81
- table.add_column("Value", style="green")
82
-
83
- table.add_row("WebSocket URL", ws_url)
84
- table.add_row("Daemon Host", config.daemon_host)
85
- table.add_row("Daemon Port", str(config.daemon_port))
86
- table.add_row("Soothe Home", str(config.soothe_home))
87
-
88
- return table
89
-
90
-
91
- def _render_daemon_table(
77
+ def _render_unified_status_table(
78
+ config: Any,
92
79
  ws_url: str,
93
- running: bool,
94
- port_live: bool,
95
- active_threads: int,
96
- daemon_pid: int | None,
80
+ running: bool | None = None,
81
+ port_live: bool | None = None,
82
+ active_threads: int | None = None,
83
+ daemon_pid: int | None = None,
97
84
  ready_state: dict[str, Any] | None = None,
85
+ daemon_live: bool = True,
98
86
  ) -> Table:
99
- """Render daemon status table."""
100
- table = Table(title="Daemon Status")
87
+ """Render unified status table without duplicated info.
88
+
89
+ Sections:
90
+ - Connection: WebSocket URL, Soothe Home
91
+ - Daemon: Running, Threads, PID (only when daemon is live)
92
+ """
93
+ table = Table(title="Soothe Status")
94
+ table.add_column("Section", style="dim", width=12)
101
95
  table.add_column("Setting", style="cyan")
102
96
  table.add_column("Value", style="green")
103
97
 
104
- table.add_row("WebSocket URL", ws_url)
105
- table.add_row("Running", "[green]Yes[/green]" if running else "[red]No[/red]")
106
- table.add_row("Port Live", "[green]Yes[/green]" if port_live else "[red]No[/red]")
107
- table.add_row("Active Threads", str(active_threads))
98
+ # Connection section
99
+ table.add_row("Connection", "WebSocket URL", ws_url)
100
+ table.add_row("", "Soothe Home", str(config.soothe_home))
101
+
102
+ # Daemon section
103
+ if not daemon_live:
104
+ table.add_row("Daemon", "Status", "[red]Not running[/red]")
105
+ return table
106
+
107
+ table.add_row("Daemon", "Status", "[green]Running[/green]")
108
108
  if daemon_pid:
109
- table.add_row("Daemon PID", str(daemon_pid))
109
+ table.add_row("", "PID", str(daemon_pid))
110
+ if active_threads is not None:
111
+ table.add_row("", "Active Threads", str(active_threads))
110
112
 
111
113
  if ready_state:
112
114
  state = ready_state.get("state", "unknown")
@@ -118,9 +120,9 @@ def _render_daemon_table(
118
120
  "warming": "blue",
119
121
  "stopped": "dim",
120
122
  }.get(state, "white")
121
- table.add_row("Readiness", f"[{state_color}]{state}[/{state_color}]")
123
+ table.add_row("", "Readiness", f"[{state_color}]{state}[/{state_color}]")
122
124
  if ready_state.get("message"):
123
- table.add_row("Message", ready_state["message"])
125
+ table.add_row("", "Message", ready_state["message"])
124
126
 
125
127
  return table
126
128
 
@@ -163,15 +165,9 @@ def daemon_status(
163
165
  )
164
166
  )
165
167
  else:
166
- console.print(
167
- Panel(
168
- f"WebSocket URL: {ws_url}\n"
169
- "Status: [red]Not running[/red]\n"
170
- "Hint: Start with 'soothed start'",
171
- title="Daemon Status",
172
- border_style="red",
173
- )
174
- )
168
+ table = _render_unified_status_table(config, ws_url, daemon_live=False)
169
+ console.print(table)
170
+ console.print("\n[dim]Hint: Start with 'soothed start'[/dim]")
175
171
  sys.exit(1)
176
172
 
177
173
  # Fetch detailed status
@@ -218,14 +214,14 @@ def daemon_status(
218
214
  console.print_json(json.dumps(output))
219
215
  return
220
216
 
221
- # Render daemon status table
217
+ # Render unified daemon status table
222
218
  running = status.get("running", True)
223
219
  port_live = status.get("port_live", True)
224
220
  active_threads = status.get("active_threads", 0)
225
221
  daemon_pid = status.get("daemon_pid")
226
222
 
227
- table = _render_daemon_table(
228
- ws_url, running, port_live, active_threads, daemon_pid, ready_state
223
+ table = _render_unified_status_table(
224
+ config, ws_url, running, port_live, active_threads, daemon_pid, ready_state
229
225
  )
230
226
  console.print(table)
231
227
 
@@ -261,7 +257,12 @@ def connection_status(
261
257
  )
262
258
  return
263
259
 
264
- table = _render_connection_table(config, ws_url)
260
+ # Simple connection table
261
+ table = Table(title="Connection Settings")
262
+ table.add_column("Setting", style="cyan")
263
+ table.add_column("Value", style="green")
264
+ table.add_row("WebSocket URL", ws_url)
265
+ table.add_row("Soothe Home", str(config.soothe_home))
265
266
  console.print(table)
266
267
 
267
268
 
@@ -313,20 +314,11 @@ def status_main(
313
314
  console.print_json(json.dumps(output))
314
315
  return
315
316
 
316
- # Render combined status with tables
317
+ # Render unified status table
317
318
  if not live:
318
- console.print(
319
- Panel(
320
- f"WebSocket URL: {ws_url}\n"
321
- f"Daemon Host: {config.daemon_host}\n"
322
- f"Daemon Port: {config.daemon_port}\n"
323
- f"Soothe Home: {config.soothe_home}\n\n"
324
- "Daemon Status: [red]Not running[/red]\n"
325
- "Hint: Start with 'soothed start'",
326
- title="Soothe Status",
327
- border_style="red",
328
- )
329
- )
319
+ table = _render_unified_status_table(config, ws_url, daemon_live=False)
320
+ console.print(table)
321
+ console.print("\n[dim]Hint: Start with 'soothed start'[/dim]")
330
322
  sys.exit(1)
331
323
 
332
324
  # Fetch detailed daemon status
@@ -335,29 +327,22 @@ def status_main(
335
327
  if "error" in status:
336
328
  console.print(
337
329
  Panel(
338
- f"WebSocket URL: {ws_url}\n"
339
- f"Daemon Host: {config.daemon_host}\n"
340
- f"Daemon Port: {config.daemon_port}\n"
341
- f"Soothe Home: {config.soothe_home}\n\n"
342
- f"Daemon Status: [red]Error[/red]\n"
343
- f"Error: {status['error']}",
344
- title="Soothe Status",
330
+ f"WebSocket URL: {ws_url}\nError: [red]{status['error']}[/red]",
331
+ title="Daemon Status",
345
332
  border_style="red",
346
333
  )
347
334
  )
348
335
  sys.exit(1)
349
336
 
350
- # Render both tables
351
- connection_table = _render_connection_table(config, ws_url)
352
- console.print(connection_table)
353
-
354
337
  running = status.get("running", True)
355
338
  port_live = status.get("port_live", True)
356
339
  active_threads = status.get("active_threads", 0)
357
340
  daemon_pid = status.get("daemon_pid")
358
341
 
359
- daemon_table = _render_daemon_table(ws_url, running, port_live, active_threads, daemon_pid)
360
- console.print(daemon_table)
342
+ table = _render_unified_status_table(
343
+ config, ws_url, running, port_live, active_threads, daemon_pid
344
+ )
345
+ console.print(table)
361
346
 
362
347
 
363
348
  __all__ = [
@@ -39,6 +39,18 @@ def tool_args_meaningful(raw: Any) -> bool:
39
39
  return True
40
40
 
41
41
 
42
+ 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:")
47
+
48
+
49
+ def is_step_card_tool_scope(*, ns_key: tuple[str, ...]) -> bool:
50
+ """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)
52
+
53
+
42
54
  def is_main_step_level_tool_call_id(tool_call_id: str) -> bool:
43
55
  """True for unified main-graph step tools (``{step}:s:{tool}:{n}``), not ``task`` rows."""
44
56
  from soothe_sdk.ux.task_namespace import is_step_level_task_tool_id, parse_unified_tool_call_id
@@ -52,9 +64,23 @@ def is_main_step_level_tool_call_id(tool_call_id: str) -> bool:
52
64
  return not is_step_level_task_tool_id(tcid)
53
65
 
54
66
 
67
+ def is_task_level_subgraph_tool_call_id(tool_call_id: str) -> bool:
68
+ """True for unified subgraph tools (``{step}:t{n}:{tool}:{seq}``), not nested ``task`` rows."""
69
+ from soothe_sdk.ux.task_namespace import (
70
+ is_inner_subgraph_task_tool_id,
71
+ parse_unified_tool_call_id,
72
+ )
73
+
74
+ tcid = str(tool_call_id or "").strip()
75
+ if not tcid or is_inner_subgraph_task_tool_id(tcid):
76
+ return False
77
+ _, type_code, _, _ = parse_unified_tool_call_id(tcid)
78
+ return type_code == "t"
79
+
80
+
55
81
  def should_ingest_tool_for_step_stats(
56
82
  *,
57
- is_main_agent: bool,
83
+ is_step_card_scope: bool,
58
84
  tool_name: str,
59
85
  tool_call_id: str,
60
86
  args_meaningful: bool,
@@ -70,7 +96,10 @@ def should_ingest_tool_for_step_stats(
70
96
  return False
71
97
  if args_meaningful:
72
98
  return True
73
- return is_main_agent and is_main_step_level_tool_call_id(tcid)
99
+ if is_step_card_scope and is_main_step_level_tool_call_id(tcid):
100
+ return True
101
+ # Subgraph explore tools often arrive with ``{"_subgraph_tool": true}`` before real args.
102
+ return not is_step_card_scope and is_task_level_subgraph_tool_call_id(tcid)
74
103
 
75
104
 
76
105
  def _args_from_toolish_block(block: dict[str, Any]) -> dict[str, Any]:
@@ -465,7 +494,10 @@ def build_streaming_args_overlay(
465
494
  __all__ = [
466
495
  "ResolvedToolInvocation",
467
496
  "build_streaming_args_overlay",
497
+ "is_execute_step_namespace",
468
498
  "is_main_step_level_tool_call_id",
499
+ "is_step_card_tool_scope",
500
+ "is_task_level_subgraph_tool_call_id",
469
501
  "is_toolish_display_block",
470
502
  "materialize_ai_blocks_with_resolved_tools",
471
503
  "merge_tool_display_args",
@@ -43,15 +43,6 @@ INTERNAL_EVENT_TYPES: frozenset[str] = frozenset()
43
43
  # and is now filtered automatically by `is_internal_event`.
44
44
  SKIP_EVENT_TYPES: frozenset[str] = frozenset()
45
45
 
46
- PLAN_EVENT_TYPES = frozenset(
47
- {
48
- "soothe.cognition.plan.created",
49
- "soothe.cognition.plan.reflected",
50
- }
51
- )
52
-
53
- MILESTONE_EVENT_TYPES = frozenset()
54
-
55
46
  # =============================================================================
56
47
  # Display Policy Class
57
48
  # =============================================================================
@@ -17,7 +17,7 @@ from typing import Any, TypeAlias
17
17
  from soothe_sdk.ux.task_namespace import (
18
18
  TaskScope,
19
19
  is_inner_subgraph_task_tool_id,
20
- normalize_step_task_tool_call_id,
20
+ normalize_main_task_delegation_id,
21
21
  parse_unified_tool_call_id,
22
22
  prune_bound_pending_namespaces,
23
23
  register_task_spawn_for_step,
@@ -62,6 +62,21 @@ def _subgraph_pending_key(ns_key: tuple[str, ...], lookup_id: str) -> tuple[tupl
62
62
  return (ns_key, str(lookup_id).strip())
63
63
 
64
64
 
65
+ def _is_task_metadata_subgraph_tool(item: PendingSubgraphTool) -> bool:
66
+ """True when a buffered subgraph item is task metadata, not a user-facing tool row."""
67
+ if (item.tool_name or "").strip() == "task":
68
+ return True
69
+ for candidate in (item.lookup_id, item.display_key):
70
+ cid = str(candidate or "").strip()
71
+ if cid and is_inner_subgraph_task_tool_id(cid):
72
+ return True
73
+ args = item.args if isinstance(item.args, dict) else {}
74
+ subagent_type = str(args.get("subagent_type") or "").strip()
75
+ prompt = str(args.get("description") or args.get("prompt") or "").strip()
76
+ # Some providers emit opaque names (e.g. "tool-<id>") for task chunks.
77
+ return bool(subagent_type and prompt)
78
+
79
+
65
80
  @dataclass
66
81
  class StepTaskRouter:
67
82
  """High-performance per-turn router for steps, tools, and task namespaces.
@@ -123,7 +138,15 @@ class StepTaskRouter:
123
138
  if existing is None:
124
139
  self._pending_subgraph_tools[key] = item
125
140
  return
126
- args = item.args if len(item.args) >= len(existing.args) else existing.args
141
+ # Prefer meaningful args over placeholder metadata like {"_subgraph_tool": true}.
142
+ from soothe_cli.runtime.parse.message_processing import extract_tool_args_dict
143
+
144
+ item_meaningful = extract_tool_args_dict(item.args or {})
145
+ existing_meaningful = extract_tool_args_dict(existing.args or {})
146
+ if len(item_meaningful) >= len(existing_meaningful):
147
+ args = item.args
148
+ else:
149
+ args = existing.args
127
150
  raw = item.raw_args if len(item.raw_args) >= len(existing.raw_args) else existing.raw_args
128
151
  self._pending_subgraph_tools[key] = PendingSubgraphTool(
129
152
  ns_key=item.ns_key,
@@ -219,14 +242,12 @@ class StepTaskRouter:
219
242
  if not tcid or is_inner_subgraph_task_tool_id(tcid):
220
243
  return False
221
244
  parsed_sid, type_code, _, _ = parse_unified_tool_call_id(tcid)
222
- if type_code == "t":
223
- return False
224
- sid = parsed_sid if (parsed_sid and type_code == "s") else ""
245
+ sid = parsed_sid if (parsed_sid and type_code in ("s", "t")) else ""
225
246
  if not sid:
226
247
  sid = str(step_id).strip()
227
248
  if not sid:
228
249
  return False
229
- normalized_tcid = normalize_step_task_tool_call_id(sid, tcid)
250
+ normalized_tcid = normalize_main_task_delegation_id(sid, tcid, tool_name="task")
230
251
  spawn_key = (sid, normalized_tcid)
231
252
  if spawn_key in self._spawn_recorded:
232
253
  return False
@@ -375,7 +396,7 @@ class StepTaskRouter:
375
396
  tool_to_step: dict[str, ParentWidget],
376
397
  ) -> bool:
377
398
  """Register one subgraph tool row on an already-resolved parent step card."""
378
- if (item.tool_name or "").strip() == "task":
399
+ if _is_task_metadata_subgraph_tool(item):
379
400
  # Inner explore ``task`` chunks are not user-facing tool stats; ingesting
380
401
  # them used to rewrite the main ``{step}:s:task:…`` delegation row args.
381
402
  return True
@@ -399,13 +420,31 @@ class StepTaskRouter:
399
420
  if has_row(row_id):
400
421
  update = getattr(parent, "update_tool_args", None)
401
422
  if callable(update):
402
- update(row_id, item.args)
423
+ resolved_args = dict(item.args or {})
424
+ # Placeholder args like {"_subgraph_tool": true} are not meaningful.
425
+ # Parse raw_args when resolved_args lacks real invocation kwargs.
426
+ from soothe_cli.runtime.parse.message_processing import extract_tool_args_dict
427
+
428
+ meaningful_args = extract_tool_args_dict(resolved_args)
429
+ if item.raw_args and not meaningful_args:
430
+ parsed = extract_tool_args_dict({"_raw": item.raw_args})
431
+ if parsed:
432
+ resolved_args = parsed
433
+ update(row_id, resolved_args)
403
434
  else:
404
435
  parent_task_id = str(scope[0]).strip()
436
+ resolved_args = dict(item.args or {})
437
+ from soothe_cli.runtime.parse.message_processing import extract_tool_args_dict
438
+
439
+ meaningful_args = extract_tool_args_dict(resolved_args)
440
+ if item.raw_args and not meaningful_args:
441
+ parsed = extract_tool_args_dict({"_raw": item.raw_args})
442
+ if parsed:
443
+ resolved_args = parsed
405
444
  ingest(
406
445
  row_id,
407
446
  item.tool_name,
408
- dict(item.args or {}),
447
+ resolved_args,
409
448
  raw_args=item.raw_args,
410
449
  parent_tool_call_id=parent_task_id or None,
411
450
  )
@@ -50,7 +50,7 @@ from soothe_sdk.ux.stream_tool_wire import STREAM_TOOL_CALL_UPDATE, TOOL_CALL_UP
50
50
  from soothe_sdk.ux.task_namespace import (
51
51
  TaskScope,
52
52
  is_inner_subgraph_task_tool_id,
53
- normalize_step_task_tool_call_id,
53
+ normalize_main_task_delegation_id,
54
54
  parse_unified_tool_call_id,
55
55
  row_key_for_subgraph_tool,
56
56
  )
@@ -62,6 +62,8 @@ from soothe_cli.runtime.parse.message_processing import (
62
62
  )
63
63
  from soothe_cli.runtime.parse.tool_call_resolution import (
64
64
  build_streaming_args_overlay,
65
+ is_execute_step_namespace,
66
+ is_step_card_tool_scope,
65
67
  materialize_ai_blocks_with_resolved_tools,
66
68
  merge_tool_display_args,
67
69
  resolve_stream_tool_name,
@@ -521,7 +523,7 @@ def _ingest_main_task_tool_on_step_card(
521
523
  adapter._tool_display_by_call_id,
522
524
  )
523
525
  return
524
- norm_tcid = normalize_step_task_tool_call_id(sid, tcid)
526
+ norm_tcid = normalize_main_task_delegation_id(sid, tcid, tool_name="task")
525
527
  step_w = _resolve_step_widget_for_tool(
526
528
  adapter,
527
529
  router,
@@ -597,10 +599,18 @@ def _fallback_ingest_subgraph_tool_on_step_card(
597
599
  if step_w is None:
598
600
  return False
599
601
  row_id = lookup if type_code == "t" else (display or lookup)
602
+ resolved_args = dict(args or {})
603
+ # Placeholder args like {"_subgraph_tool": true} are not meaningful.
604
+ # Parse raw_args when resolved_args lacks real invocation kwargs.
605
+ meaningful_args = extract_tool_args_dict(resolved_args)
606
+ if raw_args and not meaningful_args:
607
+ parsed = extract_tool_args_dict({"_raw": raw_args})
608
+ if parsed:
609
+ resolved_args = parsed
600
610
  if step_w.has_tool_call_row(row_id):
601
- step_w.update_tool_args(row_id, args)
611
+ step_w.update_tool_args(row_id, resolved_args)
602
612
  else:
603
- step_w.add_tool_call(row_id, tool_name, args, raw_args=raw_args)
613
+ step_w.add_tool_call(row_id, tool_name, resolved_args, raw_args=raw_args)
604
614
  adapter._tool_to_step[row_id] = step_w
605
615
  adapter._tool_display_by_call_id[row_id] = step_w
606
616
  return True
@@ -858,7 +868,7 @@ async def apply_tool_call_wire_update(
858
868
  if str(data.get("type", "")) != STREAM_TOOL_CALL_UPDATE:
859
869
  return False
860
870
 
861
- if ns_key:
871
+ if ns_key and not is_execute_step_namespace(ns_key):
862
872
  router.on_subgraph_namespace(ns_key)
863
873
 
864
874
  tcid = str(data.get("tool_call_id", "")).strip()
@@ -866,62 +876,79 @@ async def apply_tool_call_wire_update(
866
876
  return True
867
877
 
868
878
  name = str(data.get("name") or "").strip() or "tool"
869
- raw_args = data.get("args")
870
- if not isinstance(raw_args, dict):
871
- raw_args = {}
872
- is_main = ns_key == ()
873
- if not raw_args and not should_ingest_tool_for_step_stats(
874
- is_main_agent=is_main,
879
+ raw_args_field = data.get("args")
880
+ raw_args_stream = ""
881
+ if isinstance(raw_args_field, str):
882
+ raw_args_stream = raw_args_field
883
+ elif raw_args_field is not None:
884
+ try:
885
+ raw_args_stream = json.dumps(raw_args_field, separators=(",", ":"), default=str)
886
+ except (TypeError, ValueError):
887
+ raw_args_stream = str(raw_args_field)
888
+ display_args = extract_tool_args_dict(raw_args_field)
889
+ is_step_scope = is_step_card_tool_scope(ns_key=ns_key)
890
+ if not display_args and not should_ingest_tool_for_step_stats(
891
+ is_step_card_scope=is_step_scope,
875
892
  tool_name=name,
876
893
  tool_call_id=tcid,
877
894
  args_meaningful=False,
878
895
  ):
879
896
  return True
880
897
 
881
- if ui_coalesce is not None and ui_coalesce.note_wire_apply(tcid, raw_args):
898
+ if ui_coalesce is not None and ui_coalesce.note_wire_apply(
899
+ tcid, display_args or raw_args_field
900
+ ):
882
901
  return True
883
902
 
884
903
  overlay = streaming_overlay if streaming_overlay is not None else {}
885
904
  ts = router.resolve_task_scope(ns_key) if ns_key else None
886
905
  merge_id, row_key = (
887
- (tcid, tcid) if is_main else canonical_subgraph_tool_ids(ns_key, tcid, task_scope=ts)
906
+ (tcid, tcid) if is_step_scope else canonical_subgraph_tool_ids(ns_key, tcid, task_scope=ts)
888
907
  )
889
908
 
909
+ overlay_payload = dict(display_args or {})
890
910
  for key in {tcid, merge_id, row_key}:
891
911
  if not key:
892
912
  continue
893
- overlay[key] = dict(raw_args)
913
+ if overlay_payload:
914
+ overlay[key] = dict(overlay_payload)
894
915
  pending_tool_calls_lc[key] = {
895
916
  "name": name,
896
- "args_str": json.dumps(raw_args, separators=(",", ":")),
917
+ "args_str": raw_args_stream
918
+ if raw_args_stream
919
+ else json.dumps(overlay_payload, separators=(",", ":")),
897
920
  "is_complete_json": True,
898
921
  "emitted": False,
899
- "is_main": is_main,
922
+ "is_main": is_step_scope,
900
923
  }
901
924
 
902
- if ns_key:
925
+ if ns_key and not is_execute_step_namespace(ns_key):
903
926
  alias_subgraph_pending_and_overlay(pending_tool_calls_lc, overlay, router, ns_key)
904
927
 
905
928
  display_args = merge_tool_display_args(
906
929
  merge_id or tcid,
907
- block_args=raw_args,
930
+ block_args=display_args,
908
931
  streaming_overlay=overlay,
909
932
  pending_tool_calls_lc=pending_tool_calls_lc,
910
933
  tool_name=name,
911
934
  )
912
935
 
913
- if file_op_tracker is not None and name in FILE_CHANGE_TOOLS and tool_args_meaningful(raw_args):
936
+ if (
937
+ file_op_tracker is not None
938
+ and name in FILE_CHANGE_TOOLS
939
+ and tool_args_meaningful(display_args)
940
+ ):
914
941
  file_tcid = str(merge_id or tcid)
915
- track_file_operation(file_op_tracker, name, raw_args, file_tcid)
942
+ track_file_operation(file_op_tracker, name, display_args, file_tcid)
916
943
  await mount_file_change_preview(
917
944
  adapter,
918
945
  tool_name=name,
919
- args=raw_args,
946
+ args=display_args,
920
947
  tool_call_id=file_tcid,
921
948
  assistant_id=adapter._file_preview_assistant_id,
922
949
  )
923
950
 
924
- if is_main and name == "task":
951
+ if is_step_scope and name == "task":
925
952
  if is_inner_subgraph_task_tool_id(tcid):
926
953
  return True
927
954
  parsed_sid, _, _, _ = parse_unified_tool_call_id(tcid)
@@ -935,7 +962,7 @@ async def apply_tool_call_wire_update(
935
962
  )
936
963
  return True
937
964
 
938
- if is_main:
965
+ if is_step_scope:
939
966
  parsed_sid, _, _, _ = parse_unified_tool_call_id(tcid)
940
967
  bound_step_id = parsed_sid or router.step_id_for_tool(tcid)
941
968
  step_w = _resolve_step_widget_for_tool(
@@ -945,10 +972,13 @@ async def apply_tool_call_wire_update(
945
972
  ns_key=ns_key,
946
973
  )
947
974
  if step_w is not None and name != "task":
975
+ update_payload = dict(display_args or {})
976
+ if not update_payload and raw_args_stream:
977
+ update_payload = {"_raw": raw_args_stream}
948
978
  if step_w.has_tool_call_row(tcid):
949
- step_w.update_tool_args(tcid, display_args)
979
+ step_w.update_tool_args(tcid, update_payload)
950
980
  else:
951
- step_w.add_tool_call(tcid, name, display_args)
981
+ step_w.add_tool_call(tcid, name, update_payload, raw_args=raw_args_stream)
952
982
  adapter._tool_to_step[tcid] = step_w
953
983
  return True
954
984
 
@@ -960,6 +990,7 @@ async def apply_tool_call_wire_update(
960
990
  display_key=display_key,
961
991
  tool_name=name,
962
992
  args=display_args,
993
+ raw_args=raw_args_stream,
963
994
  step_cards=adapter._current_step_messages,
964
995
  tool_to_step=adapter._tool_to_step,
965
996
  tool_display_by_call_id=adapter._tool_display_by_call_id,
@@ -972,6 +1003,7 @@ async def apply_tool_call_wire_update(
972
1003
  display_key=display_key,
973
1004
  tool_name=name,
974
1005
  args=display_args,
1006
+ raw_args=raw_args_stream,
975
1007
  ns_key=ns_key,
976
1008
  )
977
1009
  return True
@@ -1736,6 +1768,7 @@ async def execute_task_textual(
1736
1768
  # namespaces. Assistant *text* from subgraphs is suppressed (avoid duplicate
1737
1769
  # prose with main). Tool stats attach to step cards on the main graph only.
1738
1770
  is_main_agent = ns_key == ()
1771
+ is_step_scope = is_step_card_tool_scope(ns_key=ns_key)
1739
1772
  suppress_subgraph_assistant_text = not is_main_agent
1740
1773
  suppress_main_agent_assistant_text = False
1741
1774
 
@@ -1765,7 +1798,7 @@ async def execute_task_textual(
1765
1798
  message, metadata = data
1766
1799
  message = _normalize_lc_stream_message(message)
1767
1800
 
1768
- if ns_key:
1801
+ if ns_key and not is_execute_step_namespace(ns_key):
1769
1802
  router.on_subgraph_namespace(ns_key)
1770
1803
 
1771
1804
  # Filter out summarization model output, but keep UI feedback.
@@ -2285,7 +2318,7 @@ async def execute_task_textual(
2285
2318
  parsed_args = extract_tool_args_dict(parsed_args)
2286
2319
 
2287
2320
  merge_lookup_id = lookup_id
2288
- if lookup_id and not is_main_agent:
2321
+ if lookup_id and not is_step_scope:
2289
2322
  ts_merge = router.resolve_task_scope(ns_key)
2290
2323
  merge_lookup_id, _rk = canonical_subgraph_tool_ids(
2291
2324
  ns_key, str(lookup_id), task_scope=ts_merge
@@ -2327,7 +2360,7 @@ async def execute_task_textual(
2327
2360
 
2328
2361
  args_meaningful = tool_args_meaningful(parsed_args)
2329
2362
  ingest_for_stats = should_ingest_tool_for_step_stats(
2330
- is_main_agent=is_main_agent,
2363
+ is_step_card_scope=is_step_scope,
2331
2364
  tool_name=str(buffer_name or ""),
2332
2365
  tool_call_id=str(lookup_id or ""),
2333
2366
  args_meaningful=args_meaningful,
@@ -2339,7 +2372,7 @@ async def execute_task_textual(
2339
2372
  if lookup_id and buffer_name and ingest_for_stats:
2340
2373
  if buffer_name in FILE_CHANGE_TOOLS and args_meaningful:
2341
2374
  file_tcid = str(lookup_id)
2342
- if not is_main_agent:
2375
+ if not is_step_scope:
2343
2376
  ts_file = router.resolve_task_scope(ns_key)
2344
2377
  file_tcid, _fk = canonical_subgraph_tool_ids(
2345
2378
  ns_key, file_tcid, task_scope=ts_file
@@ -2359,7 +2392,7 @@ async def execute_task_textual(
2359
2392
  assistant_id=assistant_id,
2360
2393
  )
2361
2394
 
2362
- if is_main_agent and buffer_name == "task":
2395
+ if is_step_scope and buffer_name == "task":
2363
2396
  if not is_inner_subgraph_task_tool_id(str(lookup_id)):
2364
2397
  parsed_step_id, _, _, _ = parse_unified_tool_call_id(
2365
2398
  str(lookup_id)
@@ -2374,7 +2407,7 @@ async def execute_task_textual(
2374
2407
  parsed_args,
2375
2408
  bound_step_id=bound_step_id,
2376
2409
  )
2377
- elif is_main_agent and buffer_name != "task":
2410
+ elif is_step_scope and buffer_name != "task":
2378
2411
  parsed_sid, _, _, _ = parse_unified_tool_call_id(
2379
2412
  str(lookup_id)
2380
2413
  )
@@ -2410,7 +2443,7 @@ async def execute_task_textual(
2410
2443
  parsed_args,
2411
2444
  raw_args=raw_args_stream,
2412
2445
  )
2413
- elif not is_main_agent:
2446
+ elif not is_step_scope:
2414
2447
  ts_disp = router.resolve_task_scope(ns_key)
2415
2448
  _merge_disp, display_key = canonical_subgraph_tool_ids(
2416
2449
  ns_key, str(lookup_id), task_scope=ts_disp