soothe-cli 0.5.0__tar.gz → 0.5.2__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 (136) hide show
  1. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/PKG-INFO +1 -1
  2. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/execution/daemon.py +1 -1
  3. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/display_line.py +2 -5
  4. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/formatter.py +3 -2
  5. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/pipeline.py +1 -1
  6. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/core/presentation_engine.py +3 -1
  7. soothe_cli-0.5.2/src/soothe_cli/shared/duration_format.py +42 -0
  8. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/web.py +50 -1
  9. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_app.py +16 -96
  10. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_commands.py +27 -30
  11. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_execution.py +13 -50
  12. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_history.py +146 -93
  13. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_messages_mixin.py +2 -2
  14. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_model.py +27 -79
  15. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_module_init.py +7 -47
  16. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_startup.py +14 -228
  17. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/app.tcss +10 -1
  18. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/config.py +10 -0
  19. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/daemon_session.py +170 -6
  20. soothe_cli-0.5.2/src/soothe_cli/tui/formatting.py +14 -0
  21. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/preview_limits.py +5 -0
  22. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/_adapter.py +0 -7
  23. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/_turn.py +53 -102
  24. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +56 -50
  25. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/theme.py +157 -8
  26. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/message_store.py +13 -0
  27. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/messages.py +444 -105
  28. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/welcome.py +17 -68
  29. soothe_cli-0.5.0/src/soothe_cli/tui/formatting.py +0 -28
  30. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/.gitignore +0 -0
  31. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/README.md +0 -0
  32. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/pyproject.toml +0 -0
  33. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/__init__.py +0 -0
  34. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/__init__.py +0 -0
  35. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/commands/__init__.py +0 -0
  36. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  37. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  38. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  39. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/execution/__init__.py +0 -0
  40. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/execution/headless.py +0 -0
  41. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  42. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/execution/launcher.py +0 -0
  43. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/main.py +0 -0
  44. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/__init__.py +0 -0
  45. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/context.py +0 -0
  46. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/task_scope.py +0 -0
  47. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/config/__init__.py +0 -0
  48. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/config/cli_config.py +0 -0
  49. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/plan/__init__.py +0 -0
  50. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/plan/rich_tree.py +0 -0
  51. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/__init__.py +0 -0
  52. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/commands/__init__.py +0 -0
  53. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/commands/command_router.py +0 -0
  54. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
  55. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
  56. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/config_loader.py +0 -0
  57. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/core/__init__.py +0 -0
  58. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/core/event_processor.py +0 -0
  59. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/core/processor_state.py +0 -0
  60. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
  61. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/__init__.py +0 -0
  62. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/display_policy.py +0 -0
  63. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/essential_events.py +0 -0
  64. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
  65. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
  66. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
  67. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/rendering/__init__.py +0 -0
  68. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
  69. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
  70. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/__init__.py +0 -0
  71. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/_utils.py +0 -0
  72. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/message_processing.py +0 -0
  73. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/rendering.py +0 -0
  74. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
  75. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
  76. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
  77. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
  78. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
  79. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
  80. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
  81. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
  82. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
  83. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
  84. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
  85. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
  86. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
  87. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
  88. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/__init__.py +0 -0
  89. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  90. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/_cli_context.py +0 -0
  91. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/_env_vars.py +0 -0
  92. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/_session_stats.py +0 -0
  93. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/_version.py +0 -0
  94. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/__init__.py +0 -0
  95. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_ui.py +0 -0
  96. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/command_registry.py +0 -0
  97. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/file_ops.py +0 -0
  98. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/hooks.py +0 -0
  99. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/input.py +0 -0
  100. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/media_utils.py +0 -0
  101. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/message_display_filter.py +0 -0
  102. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/model_config.py +0 -0
  103. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/output.py +0 -0
  104. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/project_utils.py +0 -0
  105. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/sessions.py +0 -0
  106. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/skills/__init__.py +0 -0
  107. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/skills/invocation.py +0 -0
  108. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/skills/load.py +0 -0
  109. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/__init__.py +0 -0
  110. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +0 -0
  111. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/_stream_messages.py +0 -0
  112. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/tool_display.py +0 -0
  113. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/unicode_security.py +0 -0
  114. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/update_check.py +0 -0
  115. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  116. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/_links.py +0 -0
  117. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/approval.py +0 -0
  118. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  119. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  120. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  121. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  122. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  123. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  124. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/diff.py +0 -0
  125. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/editor.py +0 -0
  126. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/history.py +0 -0
  127. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/loading.py +0 -0
  128. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  129. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  130. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  131. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  132. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/status.py +0 -0
  133. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  134. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  135. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  136. {soothe_cli-0.5.0 → soothe_cli-0.5.2}/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.5.0
3
+ Version: 0.5.2
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
@@ -26,7 +26,7 @@ from soothe_cli.shared.core.presentation_engine import PresentationEngine
26
26
  logger = logging.getLogger(__name__)
27
27
 
28
28
  _DAEMON_FALLBACK_EXIT_CODE = 42
29
- _SESSION_BOOTSTRAP_TIMEOUT_S = 5.0
29
+ _SESSION_BOOTSTRAP_TIMEOUT_S = 30.0
30
30
  _QUERY_START_TIMEOUT_S = 20.0
31
31
 
32
32
 
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
6
 
7
- _MS_PER_SECOND = 1000
7
+ from soothe_cli.shared.duration_format import format_duration_ms
8
8
 
9
9
 
10
10
  @dataclass
@@ -44,10 +44,7 @@ class DisplayLine:
44
44
  parts.append(f" [{self.status}]")
45
45
 
46
46
  if self.duration_ms is not None:
47
- if self.duration_ms >= _MS_PER_SECOND:
48
- parts.append(f" ({self.duration_ms / _MS_PER_SECOND:.1f}s)")
49
- else:
50
- parts.append(f" ({self.duration_ms}ms)")
47
+ parts.append(f" ({format_duration_ms(self.duration_ms)})")
51
48
 
52
49
  return "".join(parts)
53
50
 
@@ -7,6 +7,7 @@ from soothe_cli.cli.stream.task_scope import (
7
7
  format_task_scope_prefix,
8
8
  format_task_subagent_line,
9
9
  )
10
+ from soothe_cli.shared.duration_format import format_duration_ms
10
11
 
11
12
  # Emoji presentation for step-done success (U+2705 + VS16); distinct from ✓ tool rows.
12
13
  _STEP_DONE_OK_MARK = "\u2705\ufe0f"
@@ -101,7 +102,7 @@ def format_subagent_done(
101
102
  ) -> DisplayLine:
102
103
  """Format a subagent completion line with metrics.
103
104
 
104
- With Task scope: ``⚙ Task(type, \"…\") -> ✓ Completed (Nms)`` using wire task description
105
+ With Task scope: ``⚙ Task(type, \"…\") -> ✓ Completed (human duration)`` using wire task description
105
106
  when provided; falls back to summary text inside quotes.
106
107
  Without scope: legacy ``✓ …`` row with triple markers.
107
108
 
@@ -123,7 +124,7 @@ def format_subagent_done(
123
124
  ms = max(0, int(duration_s * 1000))
124
125
  outcome = "✓ Completed" if task_done_success else "✗ Failed"
125
126
  tail = (answer_summary or "").strip()
126
- base = f"{quoted} -> {outcome} ({ms}ms)"
127
+ base = f"{quoted} -> {outcome} ({format_duration_ms(ms)})"
127
128
  content = f"{base}: {tail}" if tail else base
128
129
  return DisplayLine(
129
130
  level=2,
@@ -159,7 +159,7 @@ class StreamDisplayPipeline:
159
159
  def _task_scope_from_event(self, event: dict[str, Any]) -> tuple[str, str] | None:
160
160
  """Extract IG-334 ``(task_tool_call_id, subagent_type)`` when attached by the renderer."""
161
161
  ts = event.get("task_scope")
162
- if isinstance(ts, tuple) and len(ts) == 2:
162
+ if isinstance(ts, (list, tuple)) and len(ts) == 2:
163
163
  a, b = ts
164
164
  if isinstance(a, str) and isinstance(b, str):
165
165
  return (a, b)
@@ -13,6 +13,8 @@ from dataclasses import dataclass
13
13
  from soothe_sdk.core.verbosity import VerbosityTier, should_show
14
14
  from soothe_sdk.utils import log_preview
15
15
 
16
+ from soothe_cli.shared.duration_format import format_duration_ms
17
+
16
18
 
17
19
  @dataclass
18
20
  class PresentationState:
@@ -152,7 +154,7 @@ class PresentationEngine:
152
154
  icon = "✗" if is_error else "✓"
153
155
  result_line = f"{icon} {summarized}"
154
156
  if duration_ms > 0:
155
- result_line += f" ({duration_ms}ms)"
157
+ result_line += f" ({format_duration_ms(duration_ms)})"
156
158
  return result_line
157
159
 
158
160
  @staticmethod
@@ -0,0 +1,42 @@
1
+ """Human-readable duration strings for CLI and TUI (no Textual imports)."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def format_duration(seconds: float) -> str:
7
+ """Format a duration in seconds into a human-readable string.
8
+
9
+ Args:
10
+ seconds: Duration in seconds.
11
+
12
+ Returns:
13
+ Formatted string like `"5s"`, `"2.3s"`, `"5m 12s"`, or `"1h 23m 4s"`.
14
+ """
15
+ rounded = round(seconds, 1)
16
+ if rounded < 60: # noqa: PLR2004
17
+ if rounded % 1 == 0:
18
+ return f"{int(rounded)}s"
19
+ return f"{rounded:.1f}s"
20
+ minutes, secs = divmod(int(rounded), 60)
21
+ if minutes < 60: # noqa: PLR2004
22
+ return f"{minutes}m {secs}s"
23
+ hours, minutes = divmod(minutes, 60)
24
+ return f"{hours}h {minutes}m {secs}s"
25
+
26
+
27
+ def format_duration_ms(milliseconds: int) -> str:
28
+ """Format a wall-clock duration in milliseconds for status lines and cards.
29
+
30
+ Values under one second stay in milliseconds for precision; longer durations
31
+ reuse :func:`format_duration` (seconds, minutes, hours).
32
+
33
+ Args:
34
+ milliseconds: Elapsed time in milliseconds (negative values are treated as 0).
35
+
36
+ Returns:
37
+ Strings such as ``\"0ms\"``, ``\"240ms\"``, ``\"1.5s\"``, or ``\"2m 15s\"``.
38
+ """
39
+ ms = max(0, int(milliseconds))
40
+ if ms < 1000: # noqa: PLR2004
41
+ return f"{ms}ms"
42
+ return format_duration(ms / 1000.0)
@@ -42,9 +42,13 @@ class WebFormatter(BaseFormatter):
42
42
  # Route to specific formatter (handle both legacy and wizsearch naming)
43
43
  if normalized in ("search_web", "wizsearch_search"):
44
44
  return self._format_search_web(result)
45
- if normalized in ("crawl_web", "wizsearch_crawl"):
45
+ if normalized in ("crawl_web", "wizsearch_crawl", "fetch_url"):
46
46
  return self._format_crawl_web(result)
47
47
 
48
+ # Handle HTTP request tools (IG-339)
49
+ if normalized.startswith("requests_"):
50
+ return self._format_http_request(normalized, result)
51
+
48
52
  msg = f"Unknown web tool: {tool_name}"
49
53
  raise ValueError(msg)
50
54
 
@@ -142,3 +146,48 @@ class WebFormatter(BaseFormatter):
142
146
  detail=detail,
143
147
  metrics={"size_bytes": size_bytes, "words": words, "lines": lines},
144
148
  )
149
+
150
+ def _format_http_request(self, tool_name: str, result: Any) -> ToolBrief: # noqa: ANN401
151
+ r"""Format HTTP request tool result (requests_get, requests_post, etc.).
152
+
153
+ Shows HTTP method and response status/size.
154
+
155
+ Args:
156
+ tool_name: Name of the HTTP tool (requests_get, requests_post, etc.).
157
+ result: Tool result (string with response content or error).
158
+
159
+ Returns:
160
+ ToolBrief with HTTP request summary.
161
+
162
+ Example:
163
+ >>> brief = formatter._format_http_request("requests_get", '{"data": 123}')
164
+ >>> brief.summary
165
+ 'GET 12 B'
166
+ """
167
+ # Extract method from tool name
168
+ method = tool_name.replace("requests_", "").upper()
169
+
170
+ # Check for error
171
+ if text_looks_like_error(result):
172
+ return ToolBrief(
173
+ icon="✗",
174
+ summary=f"{method} failed",
175
+ detail=self._truncate_text(result, 80),
176
+ metrics={"error": True},
177
+ )
178
+
179
+ # Calculate size
180
+ if isinstance(result, str):
181
+ size_bytes = len(result.encode("utf-8"))
182
+ else:
183
+ size_bytes = len(str(result).encode("utf-8"))
184
+
185
+ size_str = self._format_size(size_bytes)
186
+ summary = f"{method} {size_str}"
187
+
188
+ return ToolBrief(
189
+ icon="✓",
190
+ summary=summary,
191
+ detail=None,
192
+ metrics={"method": method, "size_bytes": size_bytes},
193
+ )
@@ -10,7 +10,6 @@ from pathlib import Path
10
10
  from typing import TYPE_CHECKING, Any, ClassVar
11
11
 
12
12
  if TYPE_CHECKING:
13
- from langgraph.pregel import Pregel
14
13
  from textual.app import ComposeResult
15
14
  from textual.worker import Worker
16
15
 
@@ -133,22 +132,8 @@ class SootheApp(
133
132
  """App-level keybindings for interrupt, quit, toggles, and approval menu
134
133
  navigation."""
135
134
 
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
135
  class ServerStartFailed(Message):
151
- """Posted by the background server-startup worker on failure."""
136
+ """Posted when daemon bootstrap or background connection fails."""
152
137
 
153
138
  def __init__(self, error: Exception) -> None: # noqa: D107
154
139
  super().__init__()
@@ -165,63 +150,30 @@ class SootheApp(
165
150
  def __init__(
166
151
  self,
167
152
  *,
168
- agent: Pregel | None = None,
153
+ daemon_config: Any,
169
154
  assistant_id: str | None = None,
170
155
  auto_approve: bool = False,
171
156
  cwd: str | Path | None = None,
172
157
  resume_loop_id: str | None = None,
173
- resume_loop_intent: str | None = None,
174
158
  initial_prompt: str | None = None,
175
159
  initial_skill: str | None = None,
176
160
  mcp_server_info: list[dict[str, Any]] | None = None,
177
161
  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
162
  **kwargs: Any,
184
163
  ) -> None:
185
- """Initialize the Deep Agents application.
164
+ """Initialize the Textual application (daemon-backed execution only).
186
165
 
187
166
  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
- resume_loop_id: Initial AgentLoop id (daemon-backed).
194
-
195
- `None` when `resume_loop_intent` is provided (resolved asynchronously).
196
- resume_loop_intent: Raw resume intent from `-r` flag.
197
-
198
- `'__MOST_RECENT__'` for bare `-r`, a loop id for
199
- `-r <id>`, or `None` for new sessions.
200
-
201
- Resolved via `_resolve_resume_loop_intent`
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
167
+ daemon_config: Loaded Soothe configuration (WebSocket URL, etc.).
168
+ assistant_id: Agent identifier for memory storage.
169
+ auto_approve: Whether to start with auto-approve enabled.
170
+ cwd: Current working directory to display.
171
+ resume_loop_id: Initial AgentLoop id when attaching to an existing loop.
172
+ initial_prompt: Optional prompt to auto-submit when session starts.
206
173
  initial_skill: Optional skill name to invoke when session starts.
207
174
  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
175
+ profile_override: Extra profile fields from ``--profile-override``.
176
+ **kwargs: Additional arguments passed to the Textual ``App``.
225
177
  """
226
178
  super().__init__(**kwargs)
227
179
 
@@ -230,8 +182,6 @@ class SootheApp(
230
182
  # Apply saved theme preference (or default)
231
183
  self.theme = _load_theme_preference()
232
184
 
233
- self._agent = agent
234
-
235
185
  self._assistant_id = assistant_id
236
186
 
237
187
  self._auto_approve = auto_approve
@@ -242,8 +192,6 @@ class SootheApp(
242
192
  # Named `_lc_loop_id` to avoid colliding with Textual's App._thread_id.
243
193
  self._lc_loop_id = resume_loop_id
244
194
 
245
- self._resume_loop_intent = resume_loop_intent
246
-
247
195
  self._initial_prompt = initial_prompt
248
196
 
249
197
  self._initial_skill = (
@@ -254,14 +202,6 @@ class SootheApp(
254
202
 
255
203
  self._profile_override = profile_override
256
204
 
257
- self._server_proc = server_proc
258
-
259
- self._server_kwargs = server_kwargs
260
-
261
- self._mcp_preload_kwargs = mcp_preload_kwargs
262
-
263
- self._model_kwargs = model_kwargs
264
-
265
205
  self._daemon_config = daemon_config
266
206
 
267
207
  self._daemon_session: Any | None = None
@@ -269,14 +209,9 @@ class SootheApp(
269
209
  self._daemon_skills_wire: list[dict[str, Any]] = []
270
210
  """Cached ``skills_list_response`` rows when the TUI uses ``TuiDaemonSession``."""
271
211
 
272
- self._connecting = server_kwargs is not None or daemon_config is not None
273
- # Extract sandbox type from server kwargs for trace metadata.
274
- # ServerConfig.__post_init__ normalizes "none" → None, but server_kwargs carries
275
- # the raw argparse value, so guard against both.
212
+ self._connecting = True
276
213
 
277
- raw = (server_kwargs or {}).get("sandbox_type")
278
-
279
- self._sandbox_type: str | None = raw if raw and raw != "none" else None
214
+ self._sandbox_type: str | None = None
280
215
 
281
216
  self._model_override: str | None = None
282
217
 
@@ -304,11 +239,7 @@ class SootheApp(
304
239
  self._agent_running = False
305
240
 
306
241
  self._server_startup_error: str | None = None
307
- """Set when the background server fails to start; persists for the
308
- session lifetime (server failure is terminal).
309
-
310
- Shown in place of the generic 'Agent not configured' message.
311
- """
242
+ """Set when daemon bootstrap fails; persists for the session lifetime."""
312
243
 
313
244
  self._shell_process: asyncio.subprocess.Process | None = None
314
245
  """Shell command process tracking for interruption (! commands)."""
@@ -391,18 +322,9 @@ class SootheApp(
391
322
 
392
323
  self._image_tracker = MediaTracker()
393
324
 
394
- def _remote_agent(self) -> Any: # noqa: ANN401
395
- """Return the agent if it appears to be a remote agent, or `None`.
396
-
397
- Returns `None` when no agent is configured or the agent is a local graph.
398
- """
399
- # RemoteAgent module doesn't exist in this package; always return None.
400
- # When the SDK provides a RemoteAgent class, this can be re-implemented.
401
- return None
402
-
403
325
  def _runtime_backend_ready(self) -> bool:
404
- """Return whether the app has a usable execution backend."""
405
- return self._daemon_session is not None or self._agent is not None
326
+ """Return whether the app has a connected daemon session."""
327
+ return self._daemon_session is not None
406
328
 
407
329
  def get_theme_variable_defaults(self) -> dict[str, str]:
408
330
  """Return custom CSS variable defaults for the current theme.
@@ -434,8 +356,6 @@ class SootheApp(
434
356
  mcp_tool_count=self._mcp_tool_count,
435
357
  workspace_path=self._cwd,
436
358
  connecting=self._connecting,
437
- resuming=self._resume_loop_intent is not None,
438
- local_server=self._server_kwargs is not None,
439
359
  id="welcome-banner",
440
360
  )
441
361
  yield Container(id="messages")
@@ -9,7 +9,6 @@ from contextlib import suppress
9
9
  from typing import TYPE_CHECKING, Any
10
10
 
11
11
  if TYPE_CHECKING:
12
- from langchain_core.runnables import RunnableConfig
13
12
  from textual.content import Content
14
13
 
15
14
  from textual.app import ScreenStackError
@@ -191,24 +190,25 @@ class _CommandsMixin:
191
190
  self._update_tokens(0)
192
191
  # Clear status message (e.g., "Interrupted" from previous session)
193
192
  self._update_status("")
194
- # New AgentLoop (daemon) or new local loop id
195
193
  if self._session_state:
196
- if self._daemon_session is not None:
194
+ if self._daemon_session is None:
195
+ await self._mount_message(
196
+ AppMessage("Not connected to the daemon; cannot start a new loop.")
197
+ )
198
+ else:
197
199
  status_event = await self._daemon_session.new_loop()
198
200
  new_loop_id = (
199
201
  str(status_event.get("loop_id", "")) or self._session_state.reset_loop()
200
202
  )
201
203
  self._session_state.loop_id = new_loop_id
202
204
  self._lc_loop_id = new_loop_id
203
- else:
204
- new_loop_id = self._session_state.reset_loop()
205
- try:
206
- banner = self.query_one("#welcome-banner", WelcomeBanner)
207
- banner.update_loop_id(new_loop_id)
208
- except NoMatches:
209
- pass
210
- self._clear_loop_model_override()
211
- await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
205
+ try:
206
+ banner = self.query_one("#welcome-banner", WelcomeBanner)
207
+ banner.update_loop_id(new_loop_id)
208
+ except NoMatches:
209
+ pass
210
+ self._clear_loop_model_override()
211
+ await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
212
212
  elif cmd == "/editor":
213
213
  await self.action_open_editor()
214
214
  elif cmd == "/loops":
@@ -367,13 +367,6 @@ class _CommandsMixin:
367
367
  report += "\nTheme registry reload failed. Check config.yml for errors."
368
368
  await self._mount_message(AppMessage(report))
369
369
 
370
- # Re-discover skills so autocomplete reflects any new/removed skills
371
- if self._daemon_config is None:
372
- self.run_worker(
373
- self._discover_skills(),
374
- exclusive=True,
375
- group="startup-skill-discovery",
376
- )
377
370
  if self._daemon_session is not None:
378
371
  self.run_worker(
379
372
  self._refresh_daemon_skills_catalog(),
@@ -424,22 +417,26 @@ class _CommandsMixin:
424
417
  Returns:
425
418
  Token count as an integer, or `None` if state is unavailable.
426
419
  """
427
- if not self._agent:
420
+ if not self._lc_loop_id:
428
421
  return None
429
422
  try:
430
- from langchain_core.messages.utils import (
431
- count_tokens_approximately,
432
- )
423
+ from langchain_core.messages import messages_from_dict
424
+ from langchain_core.messages.utils import count_tokens_approximately
433
425
 
434
- config: RunnableConfig = {
435
- "configurable": {"thread_id": self._lc_loop_id},
436
- }
437
- state = await self._agent.aget_state(config)
438
- if not state or not state.values:
426
+ if self._daemon_session is None:
439
427
  return None
440
- messages = state.values.get("messages", [])
441
- if not messages:
428
+ snap = await self._daemon_session.aget_loop_state(self._lc_loop_id)
429
+ vals = getattr(snap, "values", None)
430
+ if not isinstance(vals, dict):
442
431
  return None
432
+ raw = vals.get("messages")
433
+ if not isinstance(raw, list) or not raw:
434
+ return None
435
+ if isinstance(raw[0], dict):
436
+ messages = messages_from_dict(raw)
437
+ else:
438
+ messages = raw
439
+
443
440
  return count_tokens_approximately(messages)
444
441
  except Exception: # best-effort for /tokens display
445
442
  logger.debug("Failed to retrieve conversation token count", exc_info=True)
@@ -10,10 +10,7 @@ import sys
10
10
  import time
11
11
  import webbrowser
12
12
  from contextlib import suppress
13
- from typing import TYPE_CHECKING, Any, Literal
14
-
15
- if TYPE_CHECKING:
16
- from langchain_core.runnables import RunnableConfig
13
+ from typing import Any, Literal
17
14
 
18
15
  from textual.app import ScreenStackError
19
16
  from textual.containers import VerticalScroll
@@ -532,24 +529,25 @@ class _ExecutionMixin:
532
529
  self._update_tokens(0)
533
530
  # Clear status message (e.g., "Interrupted" from previous session)
534
531
  self._update_status("")
535
- # New AgentLoop (daemon) or new local loop id
536
532
  if self._session_state:
537
- if self._daemon_session is not None:
533
+ if self._daemon_session is None:
534
+ await self._mount_message(
535
+ AppMessage("Not connected to the daemon; cannot start a new loop.")
536
+ )
537
+ else:
538
538
  status_event = await self._daemon_session.new_loop()
539
539
  new_loop_id = (
540
540
  str(status_event.get("loop_id", "")) or self._session_state.reset_loop()
541
541
  )
542
542
  self._session_state.loop_id = new_loop_id
543
543
  self._lc_loop_id = new_loop_id
544
- else:
545
- new_loop_id = self._session_state.reset_loop()
546
- try:
547
- banner = self.query_one("#welcome-banner", WelcomeBanner)
548
- banner.update_loop_id(new_loop_id)
549
- except NoMatches:
550
- pass
551
- self._clear_loop_model_override()
552
- await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
544
+ try:
545
+ banner = self.query_one("#welcome-banner", WelcomeBanner)
546
+ banner.update_loop_id(new_loop_id)
547
+ except NoMatches:
548
+ pass
549
+ self._clear_loop_model_override()
550
+ await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
553
551
  elif cmd == "/editor":
554
552
  await self.action_open_editor()
555
553
  elif cmd == "/loops":
@@ -708,13 +706,6 @@ class _ExecutionMixin:
708
706
  report += "\nTheme registry reload failed. Check config.yml for errors."
709
707
  await self._mount_message(AppMessage(report))
710
708
 
711
- # Re-discover skills so autocomplete reflects any new/removed skills
712
- if self._daemon_config is None:
713
- self.run_worker(
714
- self._discover_skills(),
715
- exclusive=True,
716
- group="startup-skill-discovery",
717
- )
718
709
  if self._daemon_session is not None:
719
710
  self.run_worker(
720
711
  self._refresh_daemon_skills_catalog(),
@@ -759,33 +750,6 @@ class _ExecutionMixin:
759
750
  AppMessage("Skills require a daemon connection. Connect to a daemon first.")
760
751
  )
761
752
 
762
- async def _get_conversation_token_count(self) -> int | None:
763
- """Return the approximate conversation-only token count.
764
-
765
- Returns:
766
- Token count as an integer, or `None` if state is unavailable.
767
- """
768
- if not self._agent:
769
- return None
770
- try:
771
- from langchain_core.messages.utils import (
772
- count_tokens_approximately,
773
- )
774
-
775
- config: RunnableConfig = {
776
- "configurable": {"thread_id": self._lc_loop_id},
777
- }
778
- state = await self._agent.aget_state(config)
779
- if not state or not state.values:
780
- return None
781
- messages = state.values.get("messages", [])
782
- if not messages:
783
- return None
784
- return count_tokens_approximately(messages)
785
- except Exception: # best-effort for /tokens display
786
- logger.debug("Failed to retrieve conversation token count", exc_info=True)
787
- return None
788
-
789
753
  async def _handle_user_message(self, message: str) -> None:
790
754
  """Handle a user message to send to the agent.
791
755
 
@@ -878,7 +842,6 @@ class _ExecutionMixin:
878
842
  try:
879
843
  await execute_task_textual(
880
844
  user_input=message,
881
- agent=self._agent,
882
845
  daemon_session=self._daemon_session,
883
846
  assistant_id=self._assistant_id,
884
847
  session_state=self._session_state,