soothe-cli 0.4.8__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.8 → soothe_cli-0.4.9}/PKG-INFO +3 -3
  2. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/README.md +1 -1
  3. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/pyproject.toml +1 -1
  4. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/loop_cmd.py +63 -55
  5. soothe_cli-0.4.9/src/soothe_cli/tui/app/__init__.py +19 -0
  6. soothe_cli-0.4.9/src/soothe_cli/tui/app/_app.py +453 -0
  7. soothe_cli-0.4.9/src/soothe_cli/tui/app/_commands.py +454 -0
  8. soothe_cli-0.4.9/src/soothe_cli/tui/app/_execution.py +978 -0
  9. soothe_cli-0.4.9/src/soothe_cli/tui/app/_history.py +860 -0
  10. soothe_cli-0.4.9/src/soothe_cli/tui/app/_messages_mixin.py +827 -0
  11. soothe_cli-0.4.9/src/soothe_cli/tui/app/_model.py +731 -0
  12. soothe_cli-0.4.9/src/soothe_cli/tui/app/_module_init.py +507 -0
  13. soothe_cli-0.4.9/src/soothe_cli/tui/app/_startup.py +987 -0
  14. soothe_cli-0.4.9/src/soothe_cli/tui/app/_ui.py +606 -0
  15. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/model_config.py +3 -3
  16. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/preview_limits.py +1 -1
  17. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/__init__.py +29 -0
  18. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_adapter.py +266 -0
  19. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +285 -0
  20. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_stream_messages.py +256 -0
  21. soothe_cli-0.4.8/src/soothe_cli/tui/textual_adapter.py → soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_turn.py +41 -1114
  22. soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +379 -0
  23. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/_links.py +6 -1
  24. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/clipboard.py +67 -33
  25. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/messages.py +129 -39
  26. soothe_cli-0.4.8/src/soothe_cli/tui/app.py +0 -5636
  27. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/.gitignore +0 -0
  28. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/__init__.py +0 -0
  29. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/__init__.py +0 -0
  30. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/__init__.py +0 -0
  31. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  32. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  33. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/thread_cmd.py +0 -0
  34. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/__init__.py +0 -0
  35. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/daemon.py +0 -0
  36. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/headless.py +0 -0
  37. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  38. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/launcher.py +0 -0
  39. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/main.py +0 -0
  40. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/__init__.py +0 -0
  41. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/context.py +0 -0
  42. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/display_line.py +0 -0
  43. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/formatter.py +0 -0
  44. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/pipeline.py +0 -0
  45. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/task_scope.py +0 -0
  46. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/config/__init__.py +0 -0
  47. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/config/cli_config.py +0 -0
  48. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/plan/__init__.py +0 -0
  49. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/plan/rich_tree.py +0 -0
  50. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/__init__.py +0 -0
  51. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/__init__.py +0 -0
  52. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/command_router.py +0 -0
  53. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
  54. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
  55. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/config_loader.py +0 -0
  56. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/__init__.py +0 -0
  57. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/event_processor.py +0 -0
  58. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/presentation_engine.py +0 -0
  59. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/processor_state.py +0 -0
  60. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
  61. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/__init__.py +0 -0
  62. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/display_policy.py +0 -0
  63. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/essential_events.py +0 -0
  64. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
  65. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
  66. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
  67. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/rendering/__init__.py +0 -0
  68. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
  69. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
  70. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/__init__.py +0 -0
  71. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/_utils.py +0 -0
  72. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/message_processing.py +0 -0
  73. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/rendering.py +0 -0
  74. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
  75. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
  76. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
  77. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
  78. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
  79. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
  80. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
  81. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
  82. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
  83. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
  84. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
  85. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
  86. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
  87. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
  88. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
  89. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/__init__.py +0 -0
  90. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  91. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/_cli_context.py +0 -0
  92. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/_env_vars.py +0 -0
  93. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/_session_stats.py +0 -0
  94. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/_version.py +0 -0
  95. {soothe_cli-0.4.8/src/soothe_cli/tui → soothe_cli-0.4.9/src/soothe_cli/tui/app}/app.tcss +0 -0
  96. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/command_registry.py +0 -0
  97. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/config.py +0 -0
  98. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/daemon_session.py +0 -0
  99. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/file_ops.py +0 -0
  100. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/formatting.py +0 -0
  101. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/hooks.py +0 -0
  102. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/input.py +0 -0
  103. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/media_utils.py +0 -0
  104. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/message_display_filter.py +0 -0
  105. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/output.py +0 -0
  106. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/project_utils.py +0 -0
  107. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/sessions.py +0 -0
  108. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/skills/__init__.py +0 -0
  109. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/skills/invocation.py +0 -0
  110. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/skills/load.py +0 -0
  111. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/theme.py +0 -0
  112. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/tool_display.py +0 -0
  113. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/unicode_security.py +0 -0
  114. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/update_check.py +0 -0
  115. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  116. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/approval.py +0 -0
  117. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  118. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  119. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  120. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  121. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  122. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/diff.py +0 -0
  123. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/editor.py +0 -0
  124. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/history.py +0 -0
  125. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/loading.py +0 -0
  126. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  127. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  128. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  129. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  130. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  131. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/status.py +0 -0
  132. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  133. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
  134. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  135. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  136. {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/tools.py +0 -0
  137. {soothe_cli-0.4.8 → 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.8
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(
@@ -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
+ ]
@@ -0,0 +1,453 @@
1
+ """SootheApp: main Textual application class, composed from mixin modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from collections import deque
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any, ClassVar
11
+
12
+ if TYPE_CHECKING:
13
+ from langgraph.pregel import Pregel
14
+ from textual.app import ComposeResult
15
+ from textual.worker import Worker
16
+
17
+ from soothe_cli.tui.skills.load import ExtendedSkillMetadata
18
+ from soothe_cli.tui.textual_adapter import TextualUIAdapter
19
+ from soothe_cli.tui.widgets.approval import ApprovalMenu
20
+ from soothe_cli.tui.widgets.ask_user import AskUserMenu
21
+
22
+ from textual.app import App
23
+ from textual.binding import Binding, BindingType
24
+ from textual.containers import Container, Vertical, VerticalScroll
25
+ from textual.message import Message
26
+ from textual.widgets import Static
27
+
28
+ from soothe_cli.tui import theme
29
+ from soothe_cli.tui._session_stats import SessionStats
30
+ from soothe_cli.tui.app._commands import _CommandsMixin
31
+ from soothe_cli.tui.app._execution import _ExecutionMixin
32
+ from soothe_cli.tui.app._history import _HistoryMixin
33
+ from soothe_cli.tui.app._messages_mixin import _MessagesMixin
34
+ from soothe_cli.tui.app._model import _ModelMixin
35
+ from soothe_cli.tui.app._module_init import (
36
+ DeferredAction,
37
+ QueuedMessage,
38
+ TextualSessionState,
39
+ _load_theme_preference,
40
+ )
41
+ from soothe_cli.tui.app._startup import _StartupMixin
42
+ from soothe_cli.tui.app._ui import _UIMixin
43
+ from soothe_cli.tui.widgets.chat_input import ChatInput
44
+ from soothe_cli.tui.widgets.loading import LoadingWidget
45
+ from soothe_cli.tui.widgets.message_store import MessageStore
46
+ from soothe_cli.tui.widgets.messages import (
47
+ QueuedUserMessage,
48
+ )
49
+ from soothe_cli.tui.widgets.status import StatusBar
50
+ from soothe_cli.tui.widgets.welcome import WelcomeBanner
51
+
52
+ logger = logging.getLogger(__name__)
53
+ _monotonic = time.monotonic
54
+
55
+ InputMode = (
56
+ "normal" # Literal type alias — actual value used in _module_init; here for isinstance guards
57
+ )
58
+
59
+
60
+ class SootheApp(
61
+ App,
62
+ _StartupMixin,
63
+ _HistoryMixin,
64
+ _CommandsMixin,
65
+ _ModelMixin,
66
+ _ExecutionMixin,
67
+ _UIMixin,
68
+ _MessagesMixin,
69
+ ):
70
+ """Main Textual application for Soothe.
71
+
72
+ SOOTHE: Migrated from Soothe, now connects to Soothe daemon backend.
73
+ """
74
+
75
+ TITLE = "Soothe" # SOOTHE: Changed title
76
+ """Textual application title."""
77
+
78
+ CSS_PATH = "app.tcss"
79
+ """Path to the Textual CSS stylesheet for the app layout."""
80
+
81
+ ENABLE_COMMAND_PALETTE = False
82
+ """Disable Textual's built-in command palette in favor of the custom slash
83
+ command system."""
84
+
85
+ SCROLL_SENSITIVITY_Y = 1.0
86
+ """Vertical scroll speed (reduced from Textual default for finer control)."""
87
+
88
+ BINDINGS: ClassVar[list[BindingType]] = [
89
+ Binding("escape", "interrupt", "Interrupt", show=False, priority=True),
90
+ Binding(
91
+ "ctrl+c",
92
+ "quit_or_interrupt",
93
+ "Quit/Interrupt",
94
+ show=False,
95
+ priority=True,
96
+ ),
97
+ Binding("ctrl+d", "quit_app", "Quit", show=False, priority=True),
98
+ Binding("ctrl+t", "toggle_auto_approve", "Toggle Auto-Approve", show=False),
99
+ Binding(
100
+ "shift+tab",
101
+ "toggle_auto_approve",
102
+ "Toggle Auto-Approve",
103
+ show=False,
104
+ priority=True,
105
+ ),
106
+ Binding(
107
+ "ctrl+o",
108
+ "toggle_tool_output",
109
+ "Toggle Tool Output",
110
+ show=False,
111
+ priority=True,
112
+ ),
113
+ Binding(
114
+ "ctrl+x",
115
+ "open_editor",
116
+ "Open Editor",
117
+ show=False,
118
+ priority=True,
119
+ ),
120
+ # Approval menu keys (handled at App level for reliability)
121
+ Binding("up", "approval_up", "Up", show=False),
122
+ Binding("k", "approval_up", "Up", show=False),
123
+ Binding("down", "approval_down", "Down", show=False),
124
+ Binding("j", "approval_down", "Down", show=False),
125
+ Binding("enter", "approval_select", "Select", show=False),
126
+ Binding("y", "approval_yes", "Yes", show=False),
127
+ Binding("1", "approval_yes", "Yes", show=False),
128
+ Binding("2", "approval_auto", "Auto", show=False),
129
+ Binding("a", "approval_auto", "Auto", show=False),
130
+ Binding("3", "approval_no", "No", show=False),
131
+ Binding("n", "approval_no", "No", show=False),
132
+ ]
133
+ """App-level keybindings for interrupt, quit, toggles, and approval menu
134
+ navigation."""
135
+
136
+ class ServerReady(Message):
137
+ """Posted by the background server-startup worker on success."""
138
+
139
+ def __init__( # noqa: D107
140
+ self,
141
+ agent: Any, # noqa: ANN401
142
+ server_proc: Any, # noqa: ANN401
143
+ mcp_server_info: list[Any] | None,
144
+ ) -> None:
145
+ super().__init__()
146
+ self.agent = agent
147
+ self.server_proc = server_proc
148
+ self.mcp_server_info = mcp_server_info
149
+
150
+ class ServerStartFailed(Message):
151
+ """Posted by the background server-startup worker on failure."""
152
+
153
+ def __init__(self, error: Exception) -> None: # noqa: D107
154
+ super().__init__()
155
+ self.error = error
156
+
157
+ class DaemonReady(Message):
158
+ """Posted by the background daemon-connect worker on success."""
159
+
160
+ def __init__(self, session: Any, status_event: dict[str, Any]) -> None: # noqa: D107, ANN401
161
+ super().__init__()
162
+ self.session = session
163
+ self.status_event = status_event
164
+
165
+ def __init__(
166
+ self,
167
+ *,
168
+ agent: Pregel | None = None,
169
+ assistant_id: str | None = None,
170
+ auto_approve: bool = False,
171
+ cwd: str | Path | None = None,
172
+ thread_id: str | None = None,
173
+ resume_thread: str | None = None,
174
+ initial_prompt: str | None = None,
175
+ initial_skill: str | None = None,
176
+ mcp_server_info: list[dict[str, Any]] | None = None,
177
+ profile_override: dict[str, Any] | None = None,
178
+ server_proc: Any | None = None,
179
+ server_kwargs: dict[str, Any] | None = None,
180
+ mcp_preload_kwargs: dict[str, Any] | None = None,
181
+ model_kwargs: dict[str, Any] | None = None,
182
+ daemon_config: Any | None = None,
183
+ **kwargs: Any,
184
+ ) -> None:
185
+ """Initialize the Deep Agents application.
186
+
187
+ Args:
188
+ agent: Pre-configured LangGraph agent, or `None` when server
189
+ startup is deferred via `server_kwargs`.
190
+ assistant_id: Agent identifier for memory storage
191
+ auto_approve: Whether to start with auto-approve enabled
192
+ cwd: Current working directory to display
193
+ thread_id: Thread ID for the session.
194
+
195
+ `None` when `resume_thread` is provided (resolved asynchronously).
196
+ resume_thread: Raw resume intent from `-r` flag.
197
+
198
+ `'__MOST_RECENT__'` for bare `-r`, a thread ID string for
199
+ `-r <id>`, or `None` for new sessions.
200
+
201
+ Resolved via `_resolve_resume_thread`
202
+ during `_start_server_background`.
203
+
204
+ Requires `server_kwargs` to be set; ignored otherwise.
205
+ initial_prompt: Optional prompt to auto-submit when session starts
206
+ initial_skill: Optional skill name to invoke when session starts.
207
+ mcp_server_info: MCP server metadata for the `/mcp` viewer.
208
+ profile_override: Extra profile fields from `--profile-override`,
209
+ retained so later profile-aware behavior stays consistent with
210
+ the CLI override, including model selection details and
211
+ on-demand `create_model()` calls.
212
+ server_proc: LangGraph server process for the interactive session.
213
+ server_kwargs: When provided, server startup is deferred.
214
+
215
+ The app shows a "Connecting..." state and starts the server in
216
+ the background using these kwargs
217
+ for `start_server_and_get_agent`.
218
+ mcp_preload_kwargs: Kwargs for `_preload_session_mcp_server_info`,
219
+ run concurrently with server startup when `server_kwargs` is set.
220
+ model_kwargs: Kwargs for deferred `create_model()`.
221
+
222
+ When provided, model creation runs in a background worker after
223
+ first paint instead of blocking startup.
224
+ **kwargs: Additional arguments passed to parent
225
+ """
226
+ super().__init__(**kwargs)
227
+
228
+ self._register_custom_themes()
229
+
230
+ # Apply saved theme preference (or default)
231
+ self.theme = _load_theme_preference()
232
+
233
+ self._agent = agent
234
+
235
+ self._assistant_id = assistant_id
236
+
237
+ self._auto_approve = auto_approve
238
+
239
+ self._cwd = str(cwd) if cwd else str(Path.cwd())
240
+
241
+ self._lc_loop_id = thread_id
242
+ """LangChain loop identifier (thread_id in langgraph internals).
243
+
244
+ Named `_lc_loop_id` to reflect RFC-503 loop-first UX while avoiding
245
+ collision with Textual's `App._thread_id`.
246
+ """
247
+
248
+ self._resume_thread_intent = resume_thread
249
+
250
+ self._initial_prompt = initial_prompt
251
+
252
+ self._initial_skill = (
253
+ initial_skill.strip().lower() if initial_skill and initial_skill.strip() else None
254
+ )
255
+
256
+ self._mcp_server_info = mcp_server_info
257
+
258
+ self._profile_override = profile_override
259
+
260
+ self._server_proc = server_proc
261
+
262
+ self._server_kwargs = server_kwargs
263
+
264
+ self._mcp_preload_kwargs = mcp_preload_kwargs
265
+
266
+ self._model_kwargs = model_kwargs
267
+
268
+ self._daemon_config = daemon_config
269
+
270
+ self._daemon_session: Any | None = None
271
+
272
+ self._daemon_skills_wire: list[dict[str, Any]] = []
273
+ """Cached ``skills_list_response`` rows when the TUI uses ``TuiDaemonSession``."""
274
+
275
+ self._connecting = server_kwargs is not None or daemon_config is not None
276
+ # Extract sandbox type from server kwargs for trace metadata.
277
+ # ServerConfig.__post_init__ normalizes "none" → None, but server_kwargs carries
278
+ # the raw argparse value, so guard against both.
279
+
280
+ raw = (server_kwargs or {}).get("sandbox_type")
281
+
282
+ self._sandbox_type: str | None = raw if raw and raw != "none" else None
283
+
284
+ self._model_override: str | None = None
285
+
286
+ self._model_params_override: dict[str, Any] | None = None
287
+
288
+ self._mcp_tool_count = sum(len(s.tools) for s in (mcp_server_info or []))
289
+
290
+ self._status_bar: StatusBar | None = None
291
+
292
+ self._chat_input: ChatInput | None = None
293
+
294
+ self._quit_pending = False
295
+
296
+ self._session_state: TextualSessionState | None = None
297
+
298
+ self._ui_adapter: TextualUIAdapter | None = None
299
+
300
+ self._pending_approval_widget: ApprovalMenu | None = None
301
+
302
+ self._pending_ask_user_widget: AskUserMenu | None = None
303
+ # Agent task tracking for interruption
304
+
305
+ self._agent_worker: Worker[None] | None = None
306
+
307
+ self._agent_running = False
308
+
309
+ self._server_startup_error: str | None = None
310
+ """Set when the background server fails to start; persists for the
311
+ session lifetime (server failure is terminal).
312
+
313
+ Shown in place of the generic 'Agent not configured' message.
314
+ """
315
+
316
+ self._shell_process: asyncio.subprocess.Process | None = None
317
+ """Shell command process tracking for interruption (! commands)."""
318
+
319
+ self._shell_worker: Worker[None] | None = None
320
+
321
+ self._shell_running = False
322
+
323
+ self._loading_widget: LoadingWidget | None = None
324
+
325
+ self._context_tokens: int = 0
326
+ """Local cache of the last total-context token count.
327
+
328
+ Source of truth is `_context_tokens` in graph state; this is a sync
329
+ copy for the status bar.
330
+ """
331
+
332
+ self._tokens_approximate: bool = False
333
+ """Whether the cached token count is stale (interrupted generation)."""
334
+
335
+ self._last_typed_at: float | None = None
336
+ """Typing-aware approval deferral state."""
337
+
338
+ self._approval_placeholder: Static | None = None
339
+
340
+ self._update_available: tuple[bool, str | None] = (False, None)
341
+ """Update availability state — set by _check_for_updates, read on exit."""
342
+
343
+ self._session_stats: SessionStats = SessionStats()
344
+ """Cumulative usage stats across all turns in this session."""
345
+
346
+ self._inflight_turn_stats: SessionStats | None = None
347
+ """Stats for the currently executing turn.
348
+
349
+ Held here so `exit()` can merge them synchronously before the event loop
350
+ tears down (e.g. `Ctrl+D` during a pending tool call).
351
+ """
352
+
353
+ self._inflight_turn_start: float = 0.0
354
+ """Monotonic timestamp when the current turn started."""
355
+
356
+ self._pending_messages: deque[QueuedMessage] = deque()
357
+ """User message queue for sequential processing."""
358
+
359
+ self._queued_widgets: deque[QueuedUserMessage] = deque()
360
+
361
+ self._processing_pending = False
362
+
363
+ self._thread_switching = False
364
+
365
+ self._model_switching = False
366
+ self._detaching = False
367
+
368
+ self._deferred_actions: list[DeferredAction] = []
369
+ """Deferred actions executed after the current busy state resolves."""
370
+
371
+ self._message_store = MessageStore()
372
+ """Message virtualization store."""
373
+
374
+ self._hydrate_scheduled = False
375
+ """Whether a hydrate task has been queued via `call_later`."""
376
+
377
+ self._hydrate_in_progress = False
378
+ """Whether `_hydrate_messages_above` is currently running."""
379
+
380
+ self._last_hydration_check_mono: float = 0.0
381
+ """Monotonic timestamp of the last scroll-triggered hydration check."""
382
+
383
+ self._startup_task: asyncio.Task[None] | None = None
384
+ """Startup task reference (set in on_mount)."""
385
+
386
+ self._discovered_skills: list[ExtendedSkillMetadata] = []
387
+ """Cached skill metadata from daemon RPC (populated by startup
388
+ discovery worker, refreshed on `/reload`).
389
+ """
390
+
391
+ # Lazily imported here to avoid pulling image dependencies into
392
+ # argument parsing paths.
393
+ from soothe_cli.tui.input import MediaTracker
394
+
395
+ self._image_tracker = MediaTracker()
396
+
397
+ def _remote_agent(self) -> Any: # noqa: ANN401
398
+ """Return the agent if it appears to be a remote agent, or `None`.
399
+
400
+ Returns `None` when no agent is configured or the agent is a local graph.
401
+ """
402
+ # RemoteAgent module doesn't exist in this package; always return None.
403
+ # When the SDK provides a RemoteAgent class, this can be re-implemented.
404
+ return None
405
+
406
+ def _runtime_backend_ready(self) -> bool:
407
+ """Return whether the app has a usable execution backend."""
408
+ return self._daemon_session is not None or self._agent is not None
409
+
410
+ def get_theme_variable_defaults(self) -> dict[str, str]:
411
+ """Return custom CSS variable defaults for the current theme.
412
+
413
+ Most styling uses Textual's built-in variables (`$primary`,
414
+ `$text-muted`, `$error-muted`, etc.). This override injects the
415
+ app-specific variables (`$mode-bash`, `$mode-command`, `$skill`,
416
+ `$skill-hover`, `$tool`, `$tool-hover`, `$cognition`, `$cognition-hover`)
417
+ that have no Textual equivalent.
418
+
419
+ Returns:
420
+ Dict of CSS variable names to hex color values.
421
+ """
422
+ colors = theme.get_theme_colors(self)
423
+ return theme.get_css_variable_defaults(colors=colors)
424
+
425
+ def compose(self) -> ComposeResult:
426
+ """Compose the application layout.
427
+
428
+ Yields:
429
+ UI components for the main chat area and status bar.
430
+ """
431
+ # Main chat area with scrollable messages
432
+ # VerticalScroll tracks user scroll intent for better auto-scroll behavior
433
+ with VerticalScroll(id="chat"):
434
+ with Vertical(id="chat-body"):
435
+ yield WelcomeBanner(
436
+ thread_id=self._lc_loop_id,
437
+ mcp_tool_count=self._mcp_tool_count,
438
+ connecting=self._connecting,
439
+ resuming=self._resume_thread_intent is not None,
440
+ local_server=self._server_kwargs is not None,
441
+ id="welcome-banner",
442
+ )
443
+ yield Container(id="messages")
444
+ with Container(id="bottom-app-container"):
445
+ yield Container(id="thinking-status")
446
+ yield ChatInput(
447
+ cwd=self._cwd,
448
+ image_tracker=self._image_tracker,
449
+ id="input-area",
450
+ )
451
+
452
+ # Status bar at bottom
453
+ yield StatusBar(cwd=self._cwd, id="status-bar")