soothe-cli 0.4.7__tar.gz → 0.4.9__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 (137) hide show
  1. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/PKG-INFO +3 -3
  2. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/README.md +1 -1
  3. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/pyproject.toml +1 -1
  4. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/loop_cmd.py +63 -55
  5. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/daemon.py +0 -2
  6. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/config/cli_config.py +0 -13
  7. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/event_processor.py +2 -8
  8. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/message_processing.py +1 -1
  9. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +47 -20
  10. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/_session_stats.py +6 -3
  11. soothe_cli-0.4.9/src/soothe_cli/tui/app/__init__.py +19 -0
  12. soothe_cli-0.4.9/src/soothe_cli/tui/app/_app.py +453 -0
  13. soothe_cli-0.4.9/src/soothe_cli/tui/app/_commands.py +454 -0
  14. soothe_cli-0.4.9/src/soothe_cli/tui/app/_execution.py +978 -0
  15. soothe_cli-0.4.9/src/soothe_cli/tui/app/_history.py +860 -0
  16. soothe_cli-0.4.9/src/soothe_cli/tui/app/_messages_mixin.py +827 -0
  17. soothe_cli-0.4.9/src/soothe_cli/tui/app/_model.py +731 -0
  18. soothe_cli-0.4.9/src/soothe_cli/tui/app/_module_init.py +507 -0
  19. soothe_cli-0.4.9/src/soothe_cli/tui/app/_startup.py +987 -0
  20. soothe_cli-0.4.9/src/soothe_cli/tui/app/_ui.py +606 -0
  21. {soothe_cli-0.4.7/src/soothe_cli/tui → soothe_cli-0.4.9/src/soothe_cli/tui/app}/app.tcss +15 -5
  22. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/model_config.py +3 -3
  23. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/preview_limits.py +1 -1
  24. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/__init__.py +29 -0
  25. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_adapter.py +266 -0
  26. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +285 -0
  27. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_stream_messages.py +256 -0
  28. soothe_cli-0.4.7/src/soothe_cli/tui/textual_adapter.py → soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_turn.py +514 -1093
  29. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +379 -0
  30. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/tool_display.py +96 -0
  31. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/_links.py +6 -1
  32. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/clipboard.py +67 -33
  33. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/loading.py +51 -18
  34. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/message_store.py +31 -1
  35. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/messages.py +1115 -123
  36. soothe_cli-0.4.7/src/soothe_cli/tui/app.py +0 -5632
  37. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/.gitignore +0 -0
  38. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/__init__.py +0 -0
  39. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/__init__.py +0 -0
  40. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/__init__.py +0 -0
  41. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  42. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  43. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/thread_cmd.py +0 -0
  44. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/__init__.py +0 -0
  45. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/headless.py +0 -0
  46. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  47. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/launcher.py +0 -0
  48. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/main.py +0 -0
  49. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/__init__.py +0 -0
  50. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/context.py +0 -0
  51. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/display_line.py +0 -0
  52. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/formatter.py +0 -0
  53. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/pipeline.py +0 -0
  54. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/task_scope.py +0 -0
  55. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/config/__init__.py +0 -0
  56. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/plan/__init__.py +0 -0
  57. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/plan/rich_tree.py +0 -0
  58. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/__init__.py +0 -0
  59. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/__init__.py +0 -0
  60. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/command_router.py +0 -0
  61. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
  62. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
  63. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/config_loader.py +0 -0
  64. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/__init__.py +0 -0
  65. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/presentation_engine.py +0 -0
  66. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/processor_state.py +0 -0
  67. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
  68. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/__init__.py +0 -0
  69. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/display_policy.py +0 -0
  70. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/essential_events.py +0 -0
  71. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
  72. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
  73. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
  74. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/rendering/__init__.py +0 -0
  75. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
  76. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
  77. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/__init__.py +0 -0
  78. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/_utils.py +0 -0
  79. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/rendering.py +0 -0
  80. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
  81. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
  82. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
  83. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
  84. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
  85. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
  86. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
  87. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
  88. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
  89. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
  90. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
  91. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
  92. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
  93. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
  94. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/__init__.py +0 -0
  95. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  96. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/_cli_context.py +0 -0
  97. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/_env_vars.py +0 -0
  98. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/_version.py +0 -0
  99. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/command_registry.py +0 -0
  100. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/config.py +0 -0
  101. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/daemon_session.py +0 -0
  102. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/file_ops.py +0 -0
  103. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/formatting.py +0 -0
  104. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/hooks.py +0 -0
  105. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/input.py +0 -0
  106. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/media_utils.py +0 -0
  107. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/message_display_filter.py +0 -0
  108. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/output.py +0 -0
  109. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/project_utils.py +0 -0
  110. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/sessions.py +0 -0
  111. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/skills/__init__.py +0 -0
  112. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/skills/invocation.py +0 -0
  113. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/skills/load.py +0 -0
  114. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/theme.py +0 -0
  115. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/unicode_security.py +0 -0
  116. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/update_check.py +0 -0
  117. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  118. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/approval.py +0 -0
  119. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  120. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  121. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  122. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  123. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  124. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/diff.py +0 -0
  125. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/editor.py +0 -0
  126. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/history.py +0 -0
  127. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  128. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  129. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  130. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  131. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/status.py +0 -0
  132. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  133. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
  134. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  135. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  136. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/tools.py +0 -0
  137. {soothe_cli-0.4.7 → soothe_cli-0.4.9}/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.4.7
3
+ Version: 0.4.9
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
@@ -22,7 +22,7 @@ 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
24
  Requires-Dist: soothe-sdk<1.0.0,>=0.4.0
25
- Requires-Dist: textual>=0.40.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
@@ -25,7 +25,7 @@ classifiers = [
25
25
  dependencies = [
26
26
  "soothe-sdk>=0.4.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
@@ -83,6 +83,55 @@ async def _rpc(
83
83
  await client.close()
84
84
 
85
85
 
86
+ def _resolve_continue_loop_id(ws_url: str, loop_id: str | None) -> str:
87
+ """Resolve target loop ID for `loop continue`.
88
+
89
+ If `loop_id` is omitted, chooses the most recent loop, preferring active
90
+ statuses such as `running` and `detached`.
91
+ """
92
+ if loop_id:
93
+ return loop_id
94
+
95
+ response = asyncio.run(
96
+ _rpc(
97
+ ws_url,
98
+ "send_loop_list",
99
+ {"filter_dict": None, "limit": 20},
100
+ "loop_list_response",
101
+ )
102
+ )
103
+ if "error" in response:
104
+ typer.echo(f"Error: {response['error']}", err=True)
105
+ sys.exit(1)
106
+
107
+ loops = response.get("loops", [])
108
+ if not loops:
109
+ typer.echo(
110
+ "Error: No loops found. Start one first with `soothe loop new`.",
111
+ err=True,
112
+ )
113
+ sys.exit(1)
114
+
115
+ preferred_statuses = {"running", "detached"}
116
+ selected = next(
117
+ (loop for loop in loops if loop.get("status") in preferred_statuses),
118
+ loops[0],
119
+ )
120
+ selected_loop_id = str(selected.get("loop_id", "")).strip()
121
+ if not selected_loop_id:
122
+ typer.echo(
123
+ "Error: Unable to resolve loop ID from loop list response.",
124
+ err=True,
125
+ )
126
+ sys.exit(1)
127
+
128
+ console.print(
129
+ "[info]No LOOP_ID provided; using most recent loop: "
130
+ f"{selected_loop_id} ({selected.get('status', 'unknown')})[/info]"
131
+ )
132
+ return selected_loop_id
133
+
134
+
86
135
  @loop_app.command("list")
87
136
  def list_loops(
88
137
  status: Annotated[
@@ -632,7 +681,7 @@ def render_dot_tree(tree: dict[str, Any]) -> None:
632
681
 
633
682
  @loop_app.command("continue")
634
683
  def continue_loop(
635
- loop_id: Annotated[str, typer.Argument(help="Loop identifier to continue")],
684
+ loop_id: Annotated[str | None, typer.Argument(help="Loop identifier to continue")] = None,
636
685
  prompt: Annotated[
637
686
  str | None,
638
687
  typer.Option("--prompt", "-p", help="Optional prompt to send after continuing."),
@@ -643,70 +692,29 @@ def continue_loop(
643
692
  Replaces: soothe thread continue <thread_id>
644
693
 
645
694
  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
695
+ - Resolve target loop (explicit `LOOP_ID` or most-recent loop)
696
+ - Launch TUI on that loop
697
+ - Optionally submit initial prompt in the resumed session
650
698
 
651
699
  Example:
700
+ soothe loop continue
652
701
  soothe loop continue loop_abc123
653
702
  soothe loop continue loop_abc123 --prompt "translate to chinese"
654
703
  """
655
704
  config = load_config()
656
705
  ws_url = websocket_url_from_config(config)
657
706
  _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
- )
707
+ resolved_loop_id = _resolve_continue_loop_id(ws_url, loop_id)
708
+ from soothe_cli.cli.commands.run_cmd import run_impl
709
+
710
+ run_impl(
711
+ prompt=prompt,
712
+ thread_id=resolved_loop_id,
713
+ no_tui=False,
714
+ autonomous=False,
715
+ max_iterations=None,
667
716
  )
668
717
 
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
718
 
711
719
  @loop_app.command("detach")
712
720
  def detach_loop(
@@ -47,7 +47,6 @@ async def run_headless_via_daemon(
47
47
 
48
48
  ws_url = websocket_url_from_config(cfg)
49
49
  client = WebSocketClient(url=ws_url)
50
- final_output_mode = getattr(cfg, "final_output_mode", "streaming")
51
50
 
52
51
  try:
53
52
  await connect_websocket_with_retries(client)
@@ -85,7 +84,6 @@ async def run_headless_via_daemon(
85
84
  renderer = HeadlessCliRenderer()
86
85
  processor = EventProcessor(
87
86
  renderer,
88
- final_output_mode=final_output_mode,
89
87
  presentation_engine=presentation,
90
88
  headless_output=True,
91
89
  )
@@ -27,8 +27,6 @@ class CLIConfig:
27
27
  # logging_level: DEBUG/INFO/… for ~/.soothe/logs/soothe-cli.log; None = default INFO.
28
28
  logging_level: str | None = None
29
29
 
30
- final_output_mode: str = "streaming"
31
-
32
30
  # Output streaming overrides (RFC-614)
33
31
  output_streaming_enabled: bool | None = None
34
32
  """Override daemon streaming enabled setting."""
@@ -107,26 +105,15 @@ class CLIConfig:
107
105
  transports = daemon_section.get("transports", {})
108
106
  websocket = transports.get("websocket", {})
109
107
  websocket_legacy = data.get("websocket", {})
110
- ui_section = data.get("ui", {})
111
108
 
112
109
  raw_level = data.get("logging_level")
113
110
  if raw_level is not None and not isinstance(raw_level, str):
114
111
  raw_level = None
115
112
 
116
- raw_final_output_mode = data.get("final_output_mode")
117
- if raw_final_output_mode is None and isinstance(ui_section, dict):
118
- raw_final_output_mode = ui_section.get("final_output_mode")
119
- if not isinstance(raw_final_output_mode, str):
120
- raw_final_output_mode = "streaming"
121
- final_output_mode = raw_final_output_mode.strip().lower()
122
- if final_output_mode not in {"streaming", "batch"}:
123
- final_output_mode = "streaming"
124
-
125
113
  return cls(
126
114
  daemon_host=websocket.get("host", websocket_legacy.get("host", "127.0.0.1")),
127
115
  daemon_port=websocket.get("port", websocket_legacy.get("port", 8765)),
128
116
  logging_level=raw_level,
129
- final_output_mode=final_output_mode,
130
117
  soothe_home=Path(data.get("home", str(Path.home() / ".soothe"))),
131
118
  )
132
119
 
@@ -82,7 +82,6 @@ class EventProcessor:
82
82
  self,
83
83
  renderer: RendererProtocol,
84
84
  *,
85
- final_output_mode: str = "streaming",
86
85
  presentation_engine: PresentationEngine | None = None,
87
86
  tui_debug: bool = False,
88
87
  headless_output: bool = False,
@@ -98,9 +97,6 @@ class EventProcessor:
98
97
  """
99
98
  self._renderer = renderer
100
99
  self._headless_output = headless_output
101
- self._final_output_mode = (
102
- final_output_mode if final_output_mode in {"streaming", "batch"} else "streaming"
103
- )
104
100
  self._tui_debug = tui_debug
105
101
 
106
102
  rebind = getattr(renderer, "_rebind_presentation", None)
@@ -1103,12 +1099,10 @@ class EventProcessor:
1103
1099
  Dict with enabled, mode, and synthesis_streaming fields.
1104
1100
  """
1105
1101
  # Use defaults - streaming is enabled by default per RFC-614
1106
- # final_output_mode controls batch/streaming display mode
1102
+ # Always use streaming mode
1107
1103
  config = {
1108
1104
  "enabled": True,
1109
- "mode": self._final_output_mode
1110
- if self._final_output_mode in {"streaming", "batch"}
1111
- else "streaming",
1105
+ "mode": "streaming",
1112
1106
  "synthesis_streaming": True,
1113
1107
  }
1114
1108
 
@@ -392,7 +392,7 @@ def format_tool_call_args(tool_name: str, tool_call: dict[str, Any]) -> str:
392
392
  def _is_path_arg_name(key: str) -> bool:
393
393
  return _PATH_ARG_PATTERN.match(key) is not None
394
394
 
395
- max_value_length = 40 # Max length for displayed values
395
+ max_value_length = 50 # Max length for displayed values
396
396
 
397
397
  def _display_path_value(raw: str) -> str:
398
398
  out = convert_and_abbreviate_path(raw)
@@ -13,8 +13,8 @@ from soothe_cli.shared.tools.tool_output_formatter import ToolBrief
13
13
  class FileOpsFormatter(BaseFormatter):
14
14
  """Formatter for file operation tools.
15
15
 
16
- Handles: read_file, write_file, delete_file, list_files, search_files, glob,
17
- grep, ls, file_info
16
+ Handles: read_file, write_file, edit_file, delete_file, list_files, search_files,
17
+ glob, grep, ls, file_info, edit_file_lines, insert_lines, delete_lines, apply_diff
18
18
 
19
19
  Provides semantic summaries with size, line count, and item count metrics.
20
20
  """
@@ -46,6 +46,26 @@ class FileOpsFormatter(BaseFormatter):
46
46
  return self._format_read_file(result)
47
47
  if normalized == "write_file":
48
48
  return self._format_write_file(result)
49
+ if normalized == "edit_file":
50
+ return self._format_file_mutation(
51
+ result, success_summary="Edited file", failure_summary="Edit failed"
52
+ )
53
+ if normalized == "edit_file_lines":
54
+ return self._format_file_mutation(
55
+ result, success_summary="Updated file", failure_summary="Line edit failed"
56
+ )
57
+ if normalized == "insert_lines":
58
+ return self._format_file_mutation(
59
+ result, success_summary="Inserted lines", failure_summary="Insert failed"
60
+ )
61
+ if normalized == "delete_lines":
62
+ return self._format_file_mutation(
63
+ result, success_summary="Deleted lines", failure_summary="Delete lines failed"
64
+ )
65
+ if normalized == "apply_diff":
66
+ return self._format_file_mutation(
67
+ result, success_summary="Applied patch", failure_summary="Patch failed"
68
+ )
49
69
  if normalized == "delete_file":
50
70
  return self._format_delete_file(result)
51
71
  if normalized in ("list_files", "ls"):
@@ -143,6 +163,28 @@ class FileOpsFormatter(BaseFormatter):
143
163
  metrics={"size_bytes": size_bytes, "lines": lines},
144
164
  )
145
165
 
166
+ def _format_file_mutation(
167
+ self,
168
+ result: str,
169
+ *,
170
+ success_summary: str,
171
+ failure_summary: str,
172
+ ) -> ToolBrief:
173
+ """Format success/error for tools that mutate file content (write, edit, patch, …)."""
174
+ if text_looks_like_error(result):
175
+ return ToolBrief(
176
+ icon="✗",
177
+ summary=failure_summary,
178
+ detail=self._truncate_text(result, 80),
179
+ metrics={"error": True},
180
+ )
181
+ return ToolBrief(
182
+ icon="✓",
183
+ summary=success_summary,
184
+ detail=None,
185
+ metrics={},
186
+ )
187
+
146
188
  def _format_write_file(self, result: str) -> ToolBrief:
147
189
  """Format write_file result.
148
190
 
@@ -157,25 +199,10 @@ class FileOpsFormatter(BaseFormatter):
157
199
  Example:
158
200
  >>> brief = formatter._format_write_file("Successfully wrote to file")
159
201
  >>> brief.summary
160
- 'Wrote 0 B'
202
+ 'Wrote file'
161
203
  """
162
- # Check for error
163
- if text_looks_like_error(result):
164
- return ToolBrief(
165
- icon="✗",
166
- summary="Write failed",
167
- detail=self._truncate_text(result, 80),
168
- metrics={"error": True},
169
- )
170
-
171
- # Try to extract size from result (if available)
172
- # Common patterns: "Wrote X bytes", "Successfully wrote X"
173
- # For now, show simple success
174
- return ToolBrief(
175
- icon="✓",
176
- summary="Wrote file",
177
- detail=None,
178
- metrics={},
204
+ return self._format_file_mutation(
205
+ result, success_summary="Wrote file", failure_summary="Write failed"
179
206
  )
180
207
 
181
208
  def _format_delete_file(self, result: str) -> ToolBrief:
@@ -9,10 +9,13 @@ config, no widget imports) so that `app.py` can import `SessionStats` and
9
9
  from __future__ import annotations
10
10
 
11
11
  from dataclasses import dataclass, field
12
- from typing import Literal
13
12
 
14
- SpinnerStatus = Literal["Thinking", "Offloading"] | None
15
- """Valid spinner display states, or `None` to hide."""
13
+ SpinnerStatus = str | None
14
+ """Spinner line label, or `None` to hide.
15
+
16
+ Common values include ``Thinking``, ``Offloading``, ``Writing`` (assistant streaming),
17
+ ``Tools`` (tool execution), and ``Synthesizing`` (goal-completion stream).
18
+ """
16
19
 
17
20
 
18
21
  @dataclass
@@ -0,0 +1,19 @@
1
+ """Public API for the app sub-package."""
2
+
3
+ from soothe_cli.tui.app._app import SootheApp
4
+ from soothe_cli.tui.app._module_init import (
5
+ AppResult,
6
+ TextualSessionState,
7
+ run_textual_app,
8
+ run_textual_tui,
9
+ save_theme_preference,
10
+ )
11
+
12
+ __all__ = [
13
+ "SootheApp",
14
+ "AppResult",
15
+ "TextualSessionState",
16
+ "run_textual_app",
17
+ "run_textual_tui",
18
+ "save_theme_preference",
19
+ ]