soothe-cli 0.4.8__tar.gz → 0.5.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 (139) hide show
  1. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/PKG-INFO +4 -4
  2. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/README.md +1 -1
  3. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/pyproject.toml +2 -2
  4. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/autopilot_cmd.py +2 -2
  5. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/loop_cmd.py +80 -87
  6. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/run_cmd.py +7 -5
  7. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/daemon.py +21 -9
  8. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/headless.py +2 -2
  9. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/launcher.py +2 -2
  10. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/main.py +1 -5
  11. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/__init__.py +0 -4
  12. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/command_router.py +33 -14
  13. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/slash_commands.py +15 -66
  14. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/event_processor.py +12 -13
  15. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/presentation_engine.py +1 -1
  16. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/processor_state.py +3 -3
  17. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/tui_trace_log.py +1 -0
  18. soothe_cli-0.5.0/src/soothe_cli/tui/app/__init__.py +19 -0
  19. soothe_cli-0.5.0/src/soothe_cli/tui/app/_app.py +451 -0
  20. soothe_cli-0.5.0/src/soothe_cli/tui/app/_commands.py +446 -0
  21. soothe_cli-0.5.0/src/soothe_cli/tui/app/_execution.py +980 -0
  22. soothe_cli-0.5.0/src/soothe_cli/tui/app/_history.py +855 -0
  23. soothe_cli-0.5.0/src/soothe_cli/tui/app/_messages_mixin.py +810 -0
  24. soothe_cli-0.5.0/src/soothe_cli/tui/app/_model.py +726 -0
  25. soothe_cli-0.5.0/src/soothe_cli/tui/app/_module_init.py +501 -0
  26. soothe_cli-0.5.0/src/soothe_cli/tui/app/_startup.py +973 -0
  27. soothe_cli-0.5.0/src/soothe_cli/tui/app/_ui.py +606 -0
  28. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/command_registry.py +2 -2
  29. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/config.py +4 -4
  30. soothe_cli-0.5.0/src/soothe_cli/tui/daemon_session.py +206 -0
  31. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/model_config.py +20 -88
  32. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/output.py +1 -1
  33. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/preview_limits.py +1 -1
  34. soothe_cli-0.5.0/src/soothe_cli/tui/sessions.py +380 -0
  35. soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/__init__.py +111 -0
  36. soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/_adapter.py +266 -0
  37. soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +285 -0
  38. soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/_stream_messages.py +256 -0
  39. soothe_cli-0.4.8/src/soothe_cli/tui/textual_adapter.py → soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/_turn.py +42 -1115
  40. soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +379 -0
  41. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/_links.py +6 -1
  42. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/approval.py +2 -2
  43. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/clipboard.py +67 -33
  44. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/history.py +1 -1
  45. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/loop_selector.py +4 -5
  46. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/messages.py +129 -39
  47. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/welcome.py +43 -7
  48. soothe_cli-0.4.8/src/soothe_cli/cli/commands/thread_cmd.py +0 -412
  49. soothe_cli-0.4.8/src/soothe_cli/tui/app.py +0 -5636
  50. soothe_cli-0.4.8/src/soothe_cli/tui/daemon_session.py +0 -331
  51. soothe_cli-0.4.8/src/soothe_cli/tui/sessions.py +0 -1359
  52. soothe_cli-0.4.8/src/soothe_cli/tui/widgets/thread_selector.py +0 -1817
  53. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/.gitignore +0 -0
  54. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/__init__.py +0 -0
  55. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/__init__.py +0 -0
  56. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/__init__.py +0 -0
  57. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/__init__.py +0 -0
  58. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  59. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/__init__.py +0 -0
  60. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/context.py +0 -0
  61. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/display_line.py +0 -0
  62. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/formatter.py +0 -0
  63. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/pipeline.py +0 -0
  64. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/task_scope.py +0 -0
  65. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/config/__init__.py +0 -0
  66. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/config/cli_config.py +0 -0
  67. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/plan/__init__.py +0 -0
  68. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/plan/rich_tree.py +0 -0
  69. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/__init__.py +0 -0
  70. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
  71. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/config_loader.py +0 -0
  72. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/__init__.py +0 -0
  73. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
  74. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/__init__.py +0 -0
  75. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/display_policy.py +0 -0
  76. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/essential_events.py +0 -0
  77. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
  78. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
  79. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/rendering/__init__.py +0 -0
  80. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
  81. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
  82. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/__init__.py +0 -0
  83. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/_utils.py +0 -0
  84. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/message_processing.py +0 -0
  85. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/rendering.py +0 -0
  86. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
  87. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
  88. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
  89. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
  90. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
  91. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
  92. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
  93. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
  94. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
  95. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
  96. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
  97. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
  98. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
  99. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
  100. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
  101. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/__init__.py +0 -0
  102. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  103. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/_cli_context.py +0 -0
  104. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/_env_vars.py +0 -0
  105. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/_session_stats.py +0 -0
  106. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/_version.py +0 -0
  107. {soothe_cli-0.4.8/src/soothe_cli/tui → soothe_cli-0.5.0/src/soothe_cli/tui/app}/app.tcss +0 -0
  108. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/file_ops.py +0 -0
  109. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/formatting.py +0 -0
  110. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/hooks.py +0 -0
  111. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/input.py +0 -0
  112. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/media_utils.py +0 -0
  113. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/message_display_filter.py +0 -0
  114. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/project_utils.py +0 -0
  115. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/skills/__init__.py +0 -0
  116. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/skills/invocation.py +0 -0
  117. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/skills/load.py +0 -0
  118. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/theme.py +0 -0
  119. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/tool_display.py +0 -0
  120. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/unicode_security.py +0 -0
  121. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/update_check.py +0 -0
  122. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  123. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  124. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  125. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  126. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  127. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  128. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/diff.py +0 -0
  129. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/editor.py +0 -0
  130. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/loading.py +0 -0
  131. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  132. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  133. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  134. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  135. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/status.py +0 -0
  136. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  137. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  138. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  139. {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.4.8
3
+ Version: 0.5.0
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/OpenSoothe/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -21,8 +21,8 @@ Requires-Python: <4.0,>=3.11
21
21
  Requires-Dist: python-dotenv<2.0.0,>=1.0.0
22
22
  Requires-Dist: pyyaml<7.0.0,>=6.0.0
23
23
  Requires-Dist: rich>=13.0.0
24
- Requires-Dist: soothe-sdk<1.0.0,>=0.4.0
25
- Requires-Dist: textual>=0.40.0
24
+ Requires-Dist: soothe-sdk<1.0.0,>=0.5.0
25
+ Requires-Dist: textual>=8.0.0
26
26
  Requires-Dist: typer<1.0.0,>=0.9.0
27
27
  Requires-Dist: websockets>=12.0
28
28
  Provides-Extra: dev
@@ -71,7 +71,7 @@ This package is the **client** component that communicates with the Soothe daemo
71
71
 
72
72
  - `soothe-sdk>=0.2.0` - WebSocket client, protocol, types
73
73
  - `typer>=0.9.0` - CLI framework
74
- - `textual>=0.40.0` - TUI framework
74
+ - `textual>=8.0.0` - TUI framework
75
75
  - `rich>=13.0.0` - Console output
76
76
 
77
77
  ## Configuration
@@ -36,7 +36,7 @@ This package is the **client** component that communicates with the Soothe daemo
36
36
 
37
37
  - `soothe-sdk>=0.2.0` - WebSocket client, protocol, types
38
38
  - `typer>=0.9.0` - CLI framework
39
- - `textual>=0.40.0` - TUI framework
39
+ - `textual>=8.0.0` - TUI framework
40
40
  - `rich>=13.0.0` - Console output
41
41
 
42
42
  ## Configuration
@@ -23,9 +23,9 @@ classifiers = [
23
23
  "Topic :: Software Development :: Libraries :: Python Modules",
24
24
  ]
25
25
  dependencies = [
26
- "soothe-sdk>=0.4.0,<1.0.0", # WebSocket client, protocol, types
26
+ "soothe-sdk>=0.5.0,<1.0.0", # WebSocket client, protocol, types
27
27
  "typer>=0.9.0,<1.0.0", # CLI framework
28
- "textual>=0.40.0", # TUI framework
28
+ "textual>=8.0.0", # TUI framework
29
29
  "rich>=13.0.0", # Console output
30
30
  "pyyaml>=6.0.0,<7.0.0", # Config loading
31
31
  "python-dotenv>=1.0.0,<2.0.0", # .env loading
@@ -31,11 +31,11 @@ def run(
31
31
 
32
32
  run_impl(
33
33
  prompt=prompt,
34
- config=config,
35
- thread_id=None,
34
+ resume_loop_id=None,
36
35
  no_tui=True,
37
36
  autonomous=True,
38
37
  max_iterations=max_iterations,
38
+ config_path=config,
39
39
  )
40
40
 
41
41
 
@@ -1,13 +1,9 @@
1
- """Loop management CLI commands for managing AgentLoop instances.
2
-
3
- Replaces thread-based commands with loop-based commands.
4
- Users interact with loops (threads are internal implementation detail).
1
+ """Loop management CLI commands for AgentLoop instances.
5
2
 
6
3
  RFC-503: Loop-First User Experience
7
4
  RFC-504: Loop Management CLI Commands
8
5
 
9
- All loop operations communicate exclusively via daemon WebSocket RPC.
10
- The daemon must be running for loop commands to work.
6
+ All loop operations use daemon WebSocket RPC; the daemon must be running.
11
7
  """
12
8
 
13
9
  from __future__ import annotations
@@ -83,6 +79,55 @@ async def _rpc(
83
79
  await client.close()
84
80
 
85
81
 
82
+ def _resolve_continue_loop_id(ws_url: str, loop_id: str | None) -> str:
83
+ """Resolve target loop ID for `loop continue`.
84
+
85
+ If `loop_id` is omitted, chooses the most recent loop, preferring active
86
+ statuses such as `running` and `detached`.
87
+ """
88
+ if loop_id:
89
+ return loop_id
90
+
91
+ response = asyncio.run(
92
+ _rpc(
93
+ ws_url,
94
+ "send_loop_list",
95
+ {"filter_dict": None, "limit": 20},
96
+ "loop_list_response",
97
+ )
98
+ )
99
+ if "error" in response:
100
+ typer.echo(f"Error: {response['error']}", err=True)
101
+ sys.exit(1)
102
+
103
+ loops = response.get("loops", [])
104
+ if not loops:
105
+ typer.echo(
106
+ "Error: No loops found. Start one first with `soothe loop new`.",
107
+ err=True,
108
+ )
109
+ sys.exit(1)
110
+
111
+ preferred_statuses = {"running", "detached"}
112
+ selected = next(
113
+ (loop for loop in loops if loop.get("status") in preferred_statuses),
114
+ loops[0],
115
+ )
116
+ selected_loop_id = str(selected.get("loop_id", "")).strip()
117
+ if not selected_loop_id:
118
+ typer.echo(
119
+ "Error: Unable to resolve loop ID from loop list response.",
120
+ err=True,
121
+ )
122
+ sys.exit(1)
123
+
124
+ console.print(
125
+ "[info]No LOOP_ID provided; using most recent loop: "
126
+ f"{selected_loop_id} ({selected.get('status', 'unknown')})[/info]"
127
+ )
128
+ return selected_loop_id
129
+
130
+
86
131
  @loop_app.command("list")
87
132
  def list_loops(
88
133
  status: Annotated[
@@ -96,8 +141,6 @@ def list_loops(
96
141
  ) -> None:
97
142
  """List all AgentLoop instances.
98
143
 
99
- Replaces: soothe thread list
100
-
101
144
  Examples:
102
145
  soothe loop list
103
146
  soothe loop list --status running
@@ -129,7 +172,7 @@ def list_loops(
129
172
  table = Table(title="AgentLoops")
130
173
  table.add_column("Loop ID", style="cyan")
131
174
  table.add_column("Status", style="green")
132
- table.add_column("Threads", justify="right")
175
+ table.add_column("Contexts", justify="right")
133
176
  table.add_column("Goals", justify="right")
134
177
  table.add_column("Switches", justify="right")
135
178
  table.add_column("Created", style="dim")
@@ -157,8 +200,6 @@ def describe_loop(
157
200
  ) -> None:
158
201
  """Show detailed loop information.
159
202
 
160
- Replaces: soothe thread describe
161
-
162
203
  Example:
163
204
  soothe loop show loop_abc123
164
205
  soothe loop show loop_abc123 --verbose
@@ -196,13 +237,12 @@ def describe_loop(
196
237
  )
197
238
  )
198
239
 
199
- # Thread context (internal, shown for debugging)
200
- # RFC-503: Hide thread IDs, show only thread count
240
+ # Internal checkpoint context counts from loop metadata
201
241
  console.print(
202
242
  Panel(
203
- f"Internal Threads: {len(loop.get('thread_ids', []))}\n"
204
- f"Thread Switches: {loop.get('total_thread_switches', 0)}",
205
- title="Thread Context (Internal)",
243
+ f"Internal contexts: {len(loop.get('thread_ids', []))}\n"
244
+ f"Context switches: {loop.get('total_thread_switches', 0)}",
245
+ title="Checkpoint contexts (internal)",
206
246
  border_style="dim",
207
247
  )
208
248
  )
@@ -211,7 +251,7 @@ def describe_loop(
211
251
  console.print(
212
252
  Panel(
213
253
  f"Goals Completed: {loop.get('total_goals_completed', 0)}\n"
214
- f"Thread Switches: {loop.get('total_thread_switches', 0)}\n"
254
+ f"Context switches: {loop.get('total_thread_switches', 0)}\n"
215
255
  f"Duration: {format_duration(loop.get('total_duration_ms', 0))}\n"
216
256
  f"Tokens Used: {format_tokens(loop.get('total_tokens_used', 0))}",
217
257
  title="Execution Summary",
@@ -370,9 +410,7 @@ def delete_loop(
370
410
  ) -> None:
371
411
  """Delete loop entirely.
372
412
 
373
- Removes loop directory but preserves thread checkpoints.
374
-
375
- Replaces: soothe thread delete
413
+ Removes this loop's run directory and related artifacts.
376
414
 
377
415
  Example:
378
416
  soothe loop delete loop_abc123
@@ -405,7 +443,7 @@ def delete_loop(
405
443
  console.print(
406
444
  f"[warning]Warning: This will permanently delete {loop_id} and all associated data:[/warning]"
407
445
  )
408
- console.print(f" - {len(loop.get('thread_ids', []))} internal thread contexts")
446
+ console.print(f" - {len(loop.get('thread_ids', []))} internal checkpoint contexts")
409
447
  console.print(f" - {loop.get('total_goals_completed', 0)} goal execution records")
410
448
  console.print(" - Working memory spills")
411
449
 
@@ -432,9 +470,7 @@ def delete_loop(
432
470
  console.print(" Removed checkpoint database")
433
471
  console.print(" Removed metadata")
434
472
  console.print(" Removed working memory spills")
435
- console.print(
436
- "[dim] Preserved thread checkpoints (run `soothe thread delete` to remove)[/dim]"
437
- )
473
+ console.print("[dim] LangGraph checkpoints may remain until pruned separately[/dim]")
438
474
 
439
475
 
440
476
  # Helper functions
@@ -520,7 +556,7 @@ def format_anchor_summary(anchors: list[dict[str, Any]]) -> str:
520
556
  line = f" iteration {anchor['iteration']}: [dim]{anchor['checkpoint_id']}[/dim] "
521
557
  line += f"({anchor['anchor_type']})"
522
558
 
523
- # Check for thread switch (RFC-503: show context refreshed without thread ID)
559
+ # Context refresh when loop scope (LangGraph thread_id) changes between anchors
524
560
  if anchor["iteration"] > 0:
525
561
  prev_anchors = [a for a in anchors if a["iteration"] == anchor["iteration"] - 1]
526
562
  if prev_anchors and prev_anchors[0]["thread_id"] != anchor["thread_id"]:
@@ -538,10 +574,10 @@ def render_ascii_tree(tree: dict[str, Any]) -> None:
538
574
  for iteration in main_line:
539
575
  iter_num = iteration["iteration"]
540
576
 
541
- # RFC-503: Hide thread ID, show context refresh notification
577
+ # Iteration marker (IDs omitted in UI)
542
578
  console.print(f" iteration {iter_num}")
543
579
 
544
- # Show context refresh note if thread switch occurred
580
+ # Context refresh when the tree marks a switch
545
581
  if iteration.get("thread_switch"):
546
582
  console.print(" [cyan][context refreshed][/cyan]")
547
583
 
@@ -561,7 +597,7 @@ def render_ascii_tree(tree: dict[str, Any]) -> None:
561
597
  console.print("\n[bold red]Failed Branches:[/bold red]")
562
598
 
563
599
  for branch in branches:
564
- # RFC-503: Hide thread ID in failed branches
600
+ # Branch identity only (no per-anchor checkpoint id in UI)
565
601
  console.print(f" [dim]{branch['branch_id']}[/dim] (iteration {branch['iteration']})")
566
602
  console.print(f" ├─ [dim]{branch['root_checkpoint']}[/dim] [root] ← Rewind point")
567
603
 
@@ -632,7 +668,7 @@ def render_dot_tree(tree: dict[str, Any]) -> None:
632
668
 
633
669
  @loop_app.command("continue")
634
670
  def continue_loop(
635
- loop_id: Annotated[str, typer.Argument(help="Loop identifier to continue")],
671
+ loop_id: Annotated[str | None, typer.Argument(help="Loop identifier to continue")] = None,
636
672
  prompt: Annotated[
637
673
  str | None,
638
674
  typer.Option("--prompt", "-p", help="Optional prompt to send after continuing."),
@@ -640,73 +676,30 @@ def continue_loop(
640
676
  ) -> None:
641
677
  """Continue execution on existing loop.
642
678
 
643
- Replaces: soothe thread continue <thread_id>
644
-
645
679
  Behavior:
646
- - Load loop checkpoint by loop_id
647
- - Attach TUI to loop (subscribe to loop events)
648
- - Execute optional prompt on current thread (internal)
649
- - Display loop status
680
+ - Resolve target loop (explicit `LOOP_ID` or most-recent loop)
681
+ - Launch TUI on that loop
682
+ - Optionally submit initial prompt in the resumed session
650
683
 
651
684
  Example:
685
+ soothe loop continue
652
686
  soothe loop continue loop_abc123
653
687
  soothe loop continue loop_abc123 --prompt "translate to chinese"
654
688
  """
655
689
  config = load_config()
656
690
  ws_url = websocket_url_from_config(config)
657
691
  _require_daemon(ws_url)
658
-
659
- # Subscribe to loop
660
- response = asyncio.run(
661
- _rpc(
662
- ws_url,
663
- "send_loop_subscribe",
664
- {"loop_id": loop_id},
665
- "loop_subscribe_response",
666
- )
692
+ resolved_loop_id = _resolve_continue_loop_id(ws_url, loop_id)
693
+ from soothe_cli.cli.commands.run_cmd import run_impl
694
+
695
+ run_impl(
696
+ prompt=prompt,
697
+ resume_loop_id=resolved_loop_id,
698
+ no_tui=False,
699
+ autonomous=False,
700
+ max_iterations=None,
667
701
  )
668
702
 
669
- if "error" in response:
670
- typer.echo(f"Error: {response['error']}", err=True)
671
- sys.exit(1)
672
-
673
- console.print(f"[success]Attached to loop {loop_id}[/success]")
674
-
675
- # Show loop status
676
- status_response = asyncio.run(
677
- _rpc(
678
- ws_url,
679
- "send_loop_get",
680
- {"loop_id": loop_id, "verbose": False},
681
- "loop_get_response",
682
- )
683
- )
684
-
685
- loop = status_response.get("loop", {})
686
- console.print(
687
- Panel(
688
- f"Status: {loop.get('status', 'unknown')}\n"
689
- f"Goals: {loop.get('total_goals_completed', 0)} completed\n"
690
- f"Internal Threads: {len(loop.get('thread_ids', []))}",
691
- title=f"Loop: {loop_id}",
692
- )
693
- )
694
-
695
- # Execute prompt if provided
696
- if prompt:
697
- input_response = asyncio.run(
698
- _rpc(
699
- ws_url,
700
- "send_loop_input",
701
- {"loop_id": loop_id, "content": prompt},
702
- "loop_input_response",
703
- )
704
- )
705
- if "error" in input_response:
706
- typer.echo(f"Error: {input_response['error']}", err=True)
707
- sys.exit(1)
708
- console.print("[info]Prompt sent to loop[/info]")
709
-
710
703
 
711
704
  @loop_app.command("detach")
712
705
  def detach_loop(
@@ -716,7 +709,7 @@ def detach_loop(
716
709
 
717
710
  Behavior:
718
711
  - Unsubscribe client from loop events
719
- - Loop continues executing (all threads continue)
712
+ - Loop keeps running on the daemon
720
713
  - Loop checkpoint saved at detachment point
721
714
  - Client can reattach later with 'soothe loop attach'
722
715
 
@@ -799,7 +792,7 @@ def attach_loop(
799
792
  Panel(
800
793
  f"Status: {loop.get('status', 'unknown')}\n"
801
794
  f"Goals: {loop.get('total_goals_completed', 0)} completed\n"
802
- f"Internal Threads: {len(loop.get('thread_ids', []))}",
795
+ f"Internal contexts: {len(loop.get('thread_ids', []))}",
803
796
  title=f"Loop: {loop_id} (Reattached)",
804
797
  )
805
798
  )
@@ -17,18 +17,20 @@ logger = logging.getLogger(__name__)
17
17
 
18
18
  def run_impl(
19
19
  prompt: str | None,
20
- thread_id: str | None,
20
+ resume_loop_id: str | None,
21
21
  no_tui: bool, # noqa: FBT001
22
22
  autonomous: bool, # noqa: FBT001
23
23
  max_iterations: int | None,
24
24
  streaming_enabled: bool | None = None,
25
25
  streaming_mode: str | None = None,
26
+ *,
27
+ config_path: str | None = None,
26
28
  ) -> None:
27
29
  """Core implementation for running Soothe agent.
28
30
 
29
31
  Args:
30
32
  prompt: Optional prompt for headless mode
31
- thread_id: Thread ID to resume
33
+ resume_loop_id: Existing loop id to attach to (optional)
32
34
  no_tui: Force headless mode
33
35
  autonomous: Enable autonomous iteration mode
34
36
  max_iterations: Max iterations for autonomous mode
@@ -38,7 +40,7 @@ def run_impl(
38
40
  startup_start = time.perf_counter()
39
41
 
40
42
  try:
41
- cfg = load_config()
43
+ cfg = load_config(config_path)
42
44
  log_level = resolve_cli_log_level(logging_level=cfg.logging_level)
43
45
  log_file = Path(SOOTHE_HOME) / "logs" / "soothe-cli.log"
44
46
  setup_logging(log_level, log_file=log_file)
@@ -65,13 +67,13 @@ def run_impl(
65
67
  run_headless(
66
68
  cfg,
67
69
  prompt or "",
68
- thread_id=thread_id,
70
+ resume_loop_id=resume_loop_id,
69
71
  autonomous=autonomous,
70
72
  max_iterations=max_iterations,
71
73
  )
72
74
  else:
73
75
  # TUI mode (with optional initial prompt)
74
- run_tui(cfg, thread_id=thread_id, initial_prompt=prompt)
76
+ run_tui(cfg, resume_loop_id=resume_loop_id, initial_prompt=prompt)
75
77
 
76
78
  run_elapsed_s = time.perf_counter() - run_start
77
79
  typer.echo(f"Total running time: {run_elapsed_s:.2f}s", err=True)
@@ -13,7 +13,7 @@ from typing import Any
13
13
 
14
14
  import typer
15
15
  from soothe_sdk.client import (
16
- bootstrap_thread_session,
16
+ bootstrap_loop_session,
17
17
  connect_websocket_with_retries,
18
18
  websocket_url_from_config,
19
19
  )
@@ -30,11 +30,19 @@ _SESSION_BOOTSTRAP_TIMEOUT_S = 5.0
30
30
  _QUERY_START_TIMEOUT_S = 20.0
31
31
 
32
32
 
33
+ def _is_loop_scoped_event(event: dict[str, Any], *, active_loop_id: str) -> bool:
34
+ """Return whether a daemon frame belongs to the active AgentLoop session."""
35
+ event_type = event.get("type", "")
36
+ if event_type not in {"status", "event"}:
37
+ return True
38
+ return event.get("loop_id") == active_loop_id
39
+
40
+
33
41
  async def run_headless_via_daemon(
34
42
  cfg: Any,
35
43
  prompt: str,
36
44
  *,
37
- thread_id: str | None = None,
45
+ resume_loop_id: str | None = None,
38
46
  autonomous: bool = False,
39
47
  max_iterations: int | None = None,
40
48
  ) -> int:
@@ -51,27 +59,27 @@ async def run_headless_via_daemon(
51
59
  try:
52
60
  await connect_websocket_with_retries(client)
53
61
  cli_ws = os.environ.get("SOOTHE_CLI_WORKSPACE", "").strip() or os.getcwd()
54
- status_event = await bootstrap_thread_session(
62
+ status_event = await bootstrap_loop_session(
55
63
  client,
56
- resume_thread_id=thread_id,
64
+ resume_loop_id=resume_loop_id,
57
65
  verbosity="normal",
58
66
  workspace=cli_ws,
59
- thread_status_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
60
- subscription_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
67
+ subscribe_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
61
68
  )
62
69
  if status_event.get("type") == "error":
63
70
  typer.echo(f"Daemon error: {status_event.get('message', 'unknown')}", err=True)
64
71
  return 1
65
72
 
66
- actual_thread_id = status_event.get("thread_id")
67
- if not actual_thread_id:
68
- typer.echo("Error: No thread_id in status message", err=True)
73
+ active_loop_id = status_event.get("loop_id")
74
+ if not active_loop_id:
75
+ typer.echo("Error: No loop_id after session bootstrap", err=True)
69
76
  return 1
70
77
 
71
78
  subagent_name, cleaned_prompt = parse_subagent_from_input(prompt)
72
79
 
73
80
  await asyncio.wait_for(
74
81
  client.send_input(
82
+ active_loop_id,
75
83
  cleaned_prompt if subagent_name else prompt,
76
84
  autonomous=autonomous,
77
85
  max_iterations=max_iterations,
@@ -104,6 +112,8 @@ async def run_headless_via_daemon(
104
112
  break
105
113
 
106
114
  event_type = event.get("type", "")
115
+ if not _is_loop_scoped_event(event, active_loop_id=active_loop_id):
116
+ continue
107
117
 
108
118
  if event_type == "error":
109
119
  typer.echo(f"Daemon error: {event.get('message', 'unknown')}", err=True)
@@ -132,6 +142,8 @@ async def run_headless_via_daemon(
132
142
  break
133
143
  if not nxt:
134
144
  break
145
+ if not _is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
146
+ continue
135
147
  processor.process_event(nxt)
136
148
 
137
149
  processor.process_event(event)
@@ -22,7 +22,7 @@ def run_headless(
22
22
  cfg: CLIConfig,
23
23
  prompt: str,
24
24
  *,
25
- thread_id: str | None = None,
25
+ resume_loop_id: str | None = None,
26
26
  autonomous: bool = False,
27
27
  max_iterations: int | None = None,
28
28
  ) -> None:
@@ -77,7 +77,7 @@ def run_headless(
77
77
  return await run_headless_via_daemon(
78
78
  cfg,
79
79
  prompt,
80
- thread_id=thread_id,
80
+ resume_loop_id=resume_loop_id,
81
81
  autonomous=autonomous,
82
82
  max_iterations=max_iterations,
83
83
  )
@@ -10,7 +10,7 @@ from soothe_cli.config import CLIConfig
10
10
  def run_tui(
11
11
  cfg: CLIConfig,
12
12
  *,
13
- thread_id: str | None = None,
13
+ resume_loop_id: str | None = None,
14
14
  initial_prompt: str | None = None,
15
15
  ) -> None:
16
16
  """Launch the Textual TUI (with daemon auto-start)."""
@@ -19,7 +19,7 @@ def run_tui(
19
19
 
20
20
  run_textual_tui(
21
21
  config=cfg,
22
- thread_id=thread_id,
22
+ resume_loop_id=resume_loop_id,
23
23
  initial_prompt=initial_prompt,
24
24
  )
25
25
  except ImportError:
@@ -106,7 +106,7 @@ def main(
106
106
 
107
107
  run_impl(
108
108
  prompt=prompt,
109
- thread_id=None,
109
+ resume_loop_id=None,
110
110
  no_tui=no_tui,
111
111
  autonomous=False,
112
112
  max_iterations=None,
@@ -118,15 +118,11 @@ def main(
118
118
  # ---------------------------------------------------------------------------
119
119
  # Sub-command groups (nested Typer apps)
120
120
  # ---------------------------------------------------------------------------
121
- # Thread: read-only diagnostics per RFC-503 (Loop-First UX). Lifecycle
122
- # management lives under `soothe loop <subcommand>`.
123
121
 
124
122
  from soothe_cli.cli.commands.autopilot_cmd import app as _autopilot_app # noqa: E402
125
123
  from soothe_cli.cli.commands.loop_cmd import loop_app as _loop_app # noqa: E402
126
- from soothe_cli.cli.commands.thread_cmd import thread_app as _thread_app # noqa: E402
127
124
 
128
125
  for _sub_app, _name in (
129
- (_thread_app, "thread"),
130
126
  (_loop_app, "loop"),
131
127
  (_autopilot_app, "autopilot"),
132
128
  ):
@@ -21,8 +21,6 @@ from soothe_sdk.utils import setup_logging
21
21
  # Import from commands subdirectory
22
22
  from soothe_cli.shared.commands.slash_commands import (
23
23
  KEYBOARD_SHORTCUTS,
24
- SLASH_COMMANDS,
25
- parse_autonomous_command,
26
24
  show_commands,
27
25
  show_config,
28
26
  show_history,
@@ -108,8 +106,6 @@ __all__ = [
108
106
  "update_name_map_from_tool_calls",
109
107
  # Slash commands (IG-176)
110
108
  "KEYBOARD_SHORTCUTS",
111
- "SLASH_COMMANDS",
112
- "parse_autonomous_command",
113
109
  "show_commands",
114
110
  "show_config",
115
111
  "show_history",