soothe-cli 0.7.7__tar.gz → 0.7.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 (130) hide show
  1. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/PKG-INFO +1 -1
  2. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/policy/display_policy.py +0 -15
  3. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -3
  4. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/app/_app.py +0 -2
  5. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/app/_execution.py +31 -0
  6. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/config.py +0 -45
  7. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/preview_limits.py +2 -5
  8. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/textual_adapter.py +7 -3
  9. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/context_viewer.py +1 -1
  10. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +166 -29
  11. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +27 -1
  12. soothe_cli-0.7.7/src/soothe_cli/runtime/wire/display_text.py +0 -15
  13. soothe_cli-0.7.7/src/soothe_cli/tui/app/_commands.py +0 -50
  14. soothe_cli-0.7.7/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -341
  15. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/.gitignore +0 -0
  16. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/README.md +0 -0
  17. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/pyproject.toml +0 -0
  18. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/__init__.py +0 -0
  19. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/__init__.py +0 -0
  20. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/commands/__init__.py +0 -0
  21. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  22. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
  23. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
  24. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  25. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  26. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  27. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/execution/__init__.py +0 -0
  28. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/execution/daemon.py +0 -0
  29. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  30. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/execution/headless.py +0 -0
  31. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  32. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/execution/launcher.py +0 -0
  33. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/cli/main.py +0 -0
  34. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/config/__init__.py +0 -0
  35. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/config/cli_config.py +0 -0
  36. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/config/loader.py +0 -0
  37. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/config/logging_setup.py +0 -0
  38. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/__init__.py +0 -0
  39. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/headless/processor.py +0 -0
  40. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  41. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  42. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  43. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  44. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  45. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  46. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  47. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  48. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  49. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  50. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
  51. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  52. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  53. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/state/step_router.py +0 -0
  54. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/state/transcript.py +0 -0
  55. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/transport/session.py +0 -0
  56. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  57. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  58. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  59. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  60. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/runtime/wire/messages.py +0 -0
  61. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/__init__.py +0 -0
  62. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/_cli_context.py +0 -0
  63. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/_env_vars.py +0 -0
  64. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/_version.py +0 -0
  65. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/app/__init__.py +0 -0
  66. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/app/_history.py +0 -0
  67. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
  68. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/app/_model.py +0 -0
  69. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/app/_module_init.py +0 -0
  70. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/app/_startup.py +0 -0
  71. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/app/_ui.py +0 -0
  72. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/app/app.tcss +0 -0
  73. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/binding.py +0 -0
  74. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/command_registry.py +0 -0
  75. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/commands/__init__.py +0 -0
  76. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/commands/command_router.py +0 -0
  77. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
  78. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
  79. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/file_change_notify.py +0 -0
  80. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  81. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/hooks.py +0 -0
  82. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/input.py +0 -0
  83. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/markdown_theme.py +0 -0
  84. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/media_utils.py +0 -0
  85. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/model_config.py +0 -0
  86. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/path_utils.py +0 -0
  87. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/project_utils.py +0 -0
  88. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/sessions.py +0 -0
  89. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/shell_color.py +0 -0
  90. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/skills/__init__.py +0 -0
  91. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/skills/invocation.py +0 -0
  92. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/skills/load.py +0 -0
  93. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/spinner_labels.py +0 -0
  94. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/theme.py +0 -0
  95. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/tips.py +0 -0
  96. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/tool_display.py +0 -0
  97. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/unicode_security.py +0 -0
  98. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/update_check.py +0 -0
  99. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  100. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/_links.py +0 -0
  101. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  102. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  103. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  104. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/diff.py +0 -0
  105. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/editor.py +0 -0
  106. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  107. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
  108. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/history.py +0 -0
  109. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/loading.py +0 -0
  110. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  111. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  112. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  113. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
  114. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
  115. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
  116. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
  117. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
  118. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
  119. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/cognition_step.py +0 -0
  120. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
  121. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
  122. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  123. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
  124. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
  125. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  126. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  127. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  128. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/status.py +0 -0
  129. {soothe_cli-0.7.7 → soothe_cli-0.7.9}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  130. {soothe_cli-0.7.7 → soothe_cli-0.7.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.7.7
3
+ Version: 0.7.9
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/mirasoth/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -143,24 +143,9 @@ class DisplayPolicy:
143
143
  return event_type in INTERNAL_EVENT_TYPES or event_type.startswith("soothe.internal.")
144
144
 
145
145
 
146
- # =============================================================================
147
- # Factory Function
148
- # =============================================================================
149
-
150
-
151
- def create_display_policy() -> DisplayPolicy:
152
- """Create the shared display policy instance (single fixed UX tier)."""
153
- return DisplayPolicy()
154
-
155
-
156
- # =============================================================================
157
- # Exports
158
- # =============================================================================
159
-
160
146
  __all__ = [
161
147
  "INTERNAL_EVENT_TYPES",
162
148
  "SKIP_EVENT_TYPES",
163
149
  "DisplayPolicy",
164
150
  "VerbosityTier",
165
- "create_display_policy",
166
151
  ]
@@ -139,6 +139,3 @@ class StreamingTextAccumulator:
139
139
  def clear(self) -> None:
140
140
  """Clear all accumulated state (call after finalizing)."""
141
141
  self.streams.clear()
142
-
143
-
144
- StreamAccumulator = StreamingTextAccumulator
@@ -23,7 +23,6 @@ from textual.message import Message
23
23
 
24
24
  from soothe_cli.runtime.state.session_stats import SessionStats
25
25
  from soothe_cli.tui import theme
26
- from soothe_cli.tui.app._commands import _CommandsMixin
27
26
  from soothe_cli.tui.app._execution import _ExecutionMixin
28
27
  from soothe_cli.tui.app._history import _HistoryMixin
29
28
  from soothe_cli.tui.app._messages_mixin import _MessagesMixin
@@ -55,7 +54,6 @@ class SootheApp(
55
54
  App,
56
55
  _StartupMixin,
57
56
  _HistoryMixin,
58
- _CommandsMixin,
59
57
  _ModelMixin,
60
58
  _ExecutionMixin,
61
59
  _UIMixin,
@@ -450,6 +450,37 @@ class _ExecutionMixin:
450
450
  """
451
451
  return f"{prefix}: {loop_id}"
452
452
 
453
+ async def _get_conversation_token_count(self) -> int | None:
454
+ """Return the approximate conversation-only token count.
455
+
456
+ Returns:
457
+ Token count as an integer, or `None` if state is unavailable.
458
+ """
459
+ if not self._lc_loop_id:
460
+ return None
461
+ try:
462
+ from langchain_core.messages import messages_from_dict
463
+ from langchain_core.messages.utils import count_tokens_approximately
464
+
465
+ if self._daemon_session is None:
466
+ return None
467
+ snap = await self._daemon_session.aget_loop_state(self._lc_loop_id)
468
+ vals = getattr(snap, "values", None)
469
+ if not isinstance(vals, dict):
470
+ return None
471
+ raw = vals.get("messages")
472
+ if not isinstance(raw, list) or not raw:
473
+ return None
474
+ if isinstance(raw[0], dict):
475
+ messages = messages_from_dict(raw)
476
+ else:
477
+ messages = raw
478
+
479
+ return count_tokens_approximately(messages)
480
+ except Exception: # best-effort for /tokens display
481
+ logger.debug("Failed to retrieve conversation token count", exc_info=True)
482
+ return None
483
+
453
484
  async def _handle_command(self, command: str) -> None:
454
485
  """Handle a slash command.
455
486
 
@@ -1242,51 +1242,6 @@ class Settings:
1242
1242
  return None
1243
1243
  return self.project_root / ".agents" / "skills"
1244
1244
 
1245
- @staticmethod
1246
- def get_user_claude_skills_dir() -> Path:
1247
- """Get user-level `~/.claude/skills/` directory (experimental).
1248
-
1249
- Convenience bridge for cross-tool skill sharing with Claude Code.
1250
- This is experimental and may be removed.
1251
-
1252
- Returns:
1253
- Path to `~/.claude/skills/`
1254
- """
1255
- return Path.home() / ".claude" / "skills"
1256
-
1257
- def get_project_claude_skills_dir(self) -> Path | None:
1258
- """Get project-level `.claude/skills/` directory (experimental).
1259
-
1260
- Convenience bridge for cross-tool skill sharing with Claude Code.
1261
- This is experimental and may be removed.
1262
-
1263
- Returns:
1264
- Path to `{project_root}/.claude/skills/`, or `None` if not in a project.
1265
- """
1266
- if not self.project_root:
1267
- return None
1268
- return self.project_root / ".claude" / "skills"
1269
-
1270
- @staticmethod
1271
- def get_built_in_skills_dir() -> Path:
1272
- """Get the directory containing built-in skills that ship with the CLI.
1273
-
1274
- Returns:
1275
- Path to the `built_in_skills/` directory within the package.
1276
- """
1277
- return Path(__file__).parent / "built_in_skills"
1278
-
1279
- def get_extra_skills_dirs(self) -> list[Path]:
1280
- """Get user-configured extra skill directories.
1281
-
1282
- Set via `SOOTHE_EXTRA_SKILLS_DIRS` (colon-separated paths) or
1283
- `[skills].extra_allowed_dirs` in `SOOTHE_HOME/config/config.yml`.
1284
-
1285
- Returns:
1286
- List of extra skill directory paths, or empty list if not configured.
1287
- """
1288
- return self.extra_skills_dirs or []
1289
-
1290
1245
 
1291
1246
  class SessionState:
1292
1247
  """Mutable session state shared across the app, adapter, and agent."""
@@ -48,8 +48,5 @@ APPROVAL_WARNING_TEXT_TRUNCATE_CHARS: Final[int] = 220
48
48
  # --- Unified diff snippets in chat (`file_ops`, DiffMessage) ---
49
49
  APPROVAL_DIFF_MAX_LINES: Final[int] = 15
50
50
 
51
- # --- Autopilot dashboard (`autopilot_dashboard`) ---
52
- AUTOPILOT_GOAL_DESCRIPTION_PREVIEW_CHARS: Final[int] = 50
53
- AUTOPILOT_FINDING_LINE_PREVIEW_CHARS: Final[int] = 80
54
- AUTOPILOT_FINDINGS_VISIBLE_COUNT: Final[int] = 20
55
- AUTOPILOT_GRAPH_EDGE_PREVIEW_COUNT: Final[int] = 3
51
+ # --- Plan quick-view overlay (Ctrl+T goal tree step rows) ---
52
+ PLAN_QUICK_VIEW_STEP_LINE_MAX_CHARS: Final[int] = 76
@@ -148,6 +148,7 @@ from soothe_cli.tui.widgets.messages import (
148
148
  flush_deferred_tools_refreshes,
149
149
  reset_turn_tool_refresh_state,
150
150
  )
151
+ from soothe_cli.tui.widgets.messages.cognition_goal_tree import _normalize_step_dependencies
151
152
 
152
153
  logger = logging.getLogger(__name__)
153
154
 
@@ -1522,8 +1523,9 @@ def _record_plan_step_dag(adapter: TextualUIAdapter, raw_steps: list[Any]) -> No
1522
1523
  order.append(sid)
1523
1524
  in_plan.add(sid)
1524
1525
  raw_deps = raw.get("dependencies")
1525
- if isinstance(raw_deps, list):
1526
- dep_map[sid] = tuple(str(dep).strip() for dep in raw_deps if str(dep).strip())
1526
+ deps = _normalize_step_dependencies(raw_deps)
1527
+ if deps:
1528
+ dep_map[sid] = deps
1527
1529
  adapter._plan_step_order = order
1528
1530
  adapter._plan_step_ids = in_plan
1529
1531
  adapter._plan_step_dependencies = dep_map
@@ -1743,7 +1745,7 @@ def _tui_effective_ai_blocks(
1743
1745
  """Build content blocks for TUI streaming (text + tool calls).
1744
1746
 
1745
1747
  Tool kwargs are merged in
1746
- :func:`soothe_cli.shared.tool_call_resolution.materialize_ai_blocks_with_resolved_tools`.
1748
+ :func:`soothe_cli.runtime.parse.tool_call_resolution.materialize_ai_blocks_with_resolved_tools`.
1747
1749
  """
1748
1750
  from langchain_core.messages import AIMessageChunk
1749
1751
 
@@ -2256,6 +2258,7 @@ async def _try_mount_instant_loop_assistant_phase(
2256
2258
  await current_msg.append_content(text)
2257
2259
  if adapter._sync_message_content and current_msg.id:
2258
2260
  adapter._sync_message_content(current_msg.id, current_msg._content)
2261
+ await current_msg.stop_stream()
2259
2262
  if adapter._set_active_message:
2260
2263
  adapter._set_active_message(None)
2261
2264
  await _maybe_set_thinking_spinner(adapter, clarification_pending=clarification_pending)
@@ -2270,6 +2273,7 @@ async def _try_mount_instant_loop_assistant_phase(
2270
2273
  assistant_message_by_namespace[ns_key] = output_widget
2271
2274
  if repaired:
2272
2275
  await output_widget.append_content(repaired)
2276
+ await output_widget.stop_stream()
2273
2277
  if adapter._sync_message_content and output_widget.id:
2274
2278
  adapter._sync_message_content(output_widget.id, output_widget._content)
2275
2279
  if adapter._set_active_message:
@@ -28,7 +28,7 @@ from soothe_cli.tui.config import get_glyphs, is_ascii_mode
28
28
  logger = logging.getLogger(__name__)
29
29
  _REFRESH_INTERVAL_S = 1.0
30
30
 
31
- # Status color mapping (mirrors autopilot_dashboard.py conventions)
31
+ # Status color mapping for goal/context display
32
32
  STATUS_COLORS: dict[str, str] = {
33
33
  "pending": "dim",
34
34
  "active": "yellow",
@@ -3,6 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ import re
7
+ from time import time
6
8
  from typing import TYPE_CHECKING, Any
7
9
 
8
10
  from textual.containers import Vertical
@@ -12,6 +14,7 @@ from textual.widgets import Static
12
14
  from soothe_cli.runtime.presentation.duration_format import format_duration
13
15
  from soothe_cli.tui import theme
14
16
  from soothe_cli.tui.config import get_glyphs
17
+ from soothe_cli.tui.preview_limits import PLAN_QUICK_VIEW_STEP_LINE_MAX_CHARS
15
18
  from soothe_cli.tui.widgets.messages._helpers import _assemble_card_header
16
19
 
17
20
  if TYPE_CHECKING:
@@ -20,14 +23,36 @@ if TYPE_CHECKING:
20
23
  logger = logging.getLogger(__name__)
21
24
 
22
25
  _MAX_GOAL_HEADER = 100
23
- _MAX_GOAL_STEP_DESC = 200
26
+ _MAX_GOAL_STEP_DESC = 80
27
+ _MAX_STEP_SUMMARY_TAIL = 80
28
+ _MAX_DEPENDENCY_IDS = 3
24
29
 
30
+ _DONE_WITH_TOOLS_RE = re.compile(r"^Done(?:\s*\[\d+\s+tools?\])?$", re.IGNORECASE)
25
31
 
26
- def _plan_quick_view_step_summary(success: bool, summary: str) -> str:
32
+
33
+ def _dependency_suffix(deps: tuple[str, ...]) -> str:
34
+ """Return a compact dependency hint for plan quick-view rows."""
35
+ if not deps:
36
+ return ""
37
+ shown = deps[:_MAX_DEPENDENCY_IDS]
38
+ text = ", ".join(shown)
39
+ if len(deps) > _MAX_DEPENDENCY_IDS:
40
+ text += ", …"
41
+ return f" (→ {text})"
42
+
43
+
44
+ def _plan_quick_view_step_summary(
45
+ success: bool,
46
+ summary: str,
47
+ *,
48
+ tool_call_count: int = 0,
49
+ ) -> str:
27
50
  """Return a Ctrl+T-safe step summary tail (hide recoverable tool errors)."""
28
51
  tail = (summary or "").strip()
29
52
  if not tail or tail in ("Done", "Failed"):
30
53
  return ""
54
+ if success and tool_call_count > 0 and _DONE_WITH_TOOLS_RE.match(tail):
55
+ return ""
31
56
  if success:
32
57
  try:
33
58
  from soothe.foundation.sloop.engine.act_wave_finalize import is_error_tool_result_text
@@ -39,6 +64,12 @@ def _plan_quick_view_step_summary(success: bool, summary: str) -> str:
39
64
  return tail
40
65
 
41
66
 
67
+ def _normalize_step_dependencies(raw_deps: Any) -> tuple[str, ...]:
68
+ if not isinstance(raw_deps, list):
69
+ return ()
70
+ return tuple(str(dep).strip() for dep in raw_deps if str(dep).strip())
71
+
72
+
42
73
  class _StepLineState:
43
74
  """Mutable row state for the goal → steps aggregate."""
44
75
 
@@ -50,6 +81,8 @@ class _StepLineState:
50
81
  "duration_ms",
51
82
  "tool_call_count",
52
83
  "summary",
84
+ "dependencies",
85
+ "started_at",
53
86
  )
54
87
 
55
88
  def __init__(
@@ -62,6 +95,8 @@ class _StepLineState:
62
95
  duration_ms: int = 0,
63
96
  tool_call_count: int = 0,
64
97
  summary: str = "",
98
+ dependencies: tuple[str, ...] = (),
99
+ started_at: float | None = None,
65
100
  ) -> None:
66
101
  self.step_id = step_id
67
102
  self.description = description
@@ -70,6 +105,8 @@ class _StepLineState:
70
105
  self.duration_ms = duration_ms
71
106
  self.tool_call_count = tool_call_count
72
107
  self.summary = summary
108
+ self.dependencies = dependencies
109
+ self.started_at = started_at
73
110
 
74
111
 
75
112
  class CognitionGoalTreeMessage(Vertical):
@@ -138,8 +175,12 @@ class CognitionGoalTreeMessage(Vertical):
138
175
  @staticmethod
139
176
  def _clip(text: str, max_len: int) -> str:
140
177
  t = (text or "").strip().replace("\n", " ")
178
+ if max_len <= 0:
179
+ return "…"
141
180
  if len(t) <= max_len:
142
181
  return t
182
+ if max_len == 1:
183
+ return "…"
143
184
  return t[: max_len - 1].rstrip() + "…"
144
185
 
145
186
  def _goal_tree_status(self) -> str:
@@ -198,45 +239,111 @@ class CognitionGoalTreeMessage(Vertical):
198
239
  g = get_glyphs()
199
240
  return f"{g.output_prefix} "
200
241
 
201
- def _goal_tree_step_line_content(self, st: _StepLineState) -> Content:
202
- """One goal→step row: dim tree gutter, foreground body (parity with ``CognitionStepMessage`` tool rows)."""
242
+ def _step_icon(self, st: _StepLineState) -> str:
203
243
  g = get_glyphs()
244
+ if st.phase == "pending" or st.phase == "queued":
245
+ return g.circle_empty
246
+ if st.phase == "running":
247
+ frames = g.spinner_frames
248
+ return frames[self._spinner_position % len(frames)]
249
+ return g.checkmark if st.success else g.error
250
+
251
+ def _step_stats_suffix(self, st: _StepLineState) -> str:
252
+ """Duration and tool-count suffix for running or completed rows."""
253
+ if st.phase == "running":
254
+ parts: list[str] = []
255
+ if st.started_at is not None:
256
+ elapsed_s = max(0.001, time() - st.started_at)
257
+ parts.append(format_duration(elapsed_s))
258
+ if st.tool_call_count > 0:
259
+ parts.append(f"{st.tool_call_count} tools")
260
+ if not parts:
261
+ return ""
262
+ return " · " + " · ".join(parts)
263
+ if st.phase not in ("done", "error"):
264
+ return ""
265
+ dur_s = max(0.001, st.duration_ms / 1000.0)
266
+ parts = [format_duration(dur_s)]
267
+ if st.tool_call_count > 0:
268
+ parts.append(f"{st.tool_call_count} tools")
269
+ return " · " + " · ".join(parts)
270
+
271
+ def _step_summary_suffix(self, st: _StepLineState) -> str:
272
+ if st.phase not in ("done", "error"):
273
+ return ""
274
+ tail = _plan_quick_view_step_summary(
275
+ st.success,
276
+ st.summary,
277
+ tool_call_count=st.tool_call_count,
278
+ )
279
+ if not tail:
280
+ return ""
281
+ return f" — {self._clip(tail, _MAX_STEP_SUMMARY_TAIL)}"
282
+
283
+ def _fit_step_line(
284
+ self,
285
+ st: _StepLineState,
286
+ *,
287
+ icon: str,
288
+ max_line_width: int | None,
289
+ ) -> str:
290
+ """Build a single-line step row, clipping description to fit ``max_line_width``."""
291
+ step_prefix = f"{st.step_id}: " if st.step_id else ""
292
+ dep_suffix = _dependency_suffix(st.dependencies)
293
+ queued_suffix = " · queued" if st.phase == "queued" else ""
294
+ stats_suffix = self._step_stats_suffix(st)
295
+ summary_suffix = self._step_summary_suffix(st)
296
+ lead = f"{icon} {step_prefix}"
297
+
298
+ def assemble(desc: str, *, include_summary: bool) -> str:
299
+ summary = summary_suffix if include_summary else ""
300
+ return f"{lead}{desc}{dep_suffix}{queued_suffix}{stats_suffix}{summary}"
301
+
302
+ width = max_line_width or PLAN_QUICK_VIEW_STEP_LINE_MAX_CHARS
303
+ desc = self._clip(st.description, _MAX_GOAL_STEP_DESC)
304
+ line = assemble(desc, include_summary=True)
305
+ if len(line) <= width:
306
+ return line
307
+
308
+ line = assemble(desc, include_summary=False)
309
+ if len(line) <= width:
310
+ return line
311
+
312
+ fixed_len = len(f"{lead}{dep_suffix}{queued_suffix}{stats_suffix}")
313
+ budget = width - fixed_len
314
+ clipped_desc = self._clip(st.description, budget)
315
+ return f"{lead}{clipped_desc}{dep_suffix}{queued_suffix}{stats_suffix}"
316
+
317
+ def _goal_tree_step_line_content(
318
+ self,
319
+ st: _StepLineState,
320
+ *,
321
+ max_line_width: int | None = None,
322
+ ) -> Content:
323
+ """One goal→step row: dim tree gutter, foreground body (parity with ``CognitionStepMessage`` tool rows)."""
204
324
  try:
205
325
  colors = theme.get_theme_colors(self)
206
326
  except Exception: # noqa: BLE001
207
327
  colors = theme.DARK_COLORS
208
328
  gutter = self._indent_prefix()
209
- body = self._clip(st.description, _MAX_GOAL_STEP_DESC)
210
- label = f"{st.step_id}: {body}" if st.step_id else body
329
+ icon = self._step_icon(st)
330
+ rest = self._fit_step_line(st, icon=icon, max_line_width=max_line_width)
331
+
211
332
  if st.phase == "pending":
212
- rest = f"{g.circle_empty} {label}"
213
333
  return Content.assemble(
214
334
  Content.styled(gutter, "dim"),
215
335
  Content.styled(rest, "dim"),
216
336
  )
217
337
  if st.phase == "queued":
218
- rest = f"{g.circle_empty} {label} · queued"
219
338
  return Content.assemble(
220
339
  Content.styled(gutter, "dim"),
221
340
  Content.styled(rest, colors.cognition),
222
341
  )
223
342
  if st.phase == "running":
224
- frames = g.spinner_frames
225
- frame = frames[self._spinner_position % len(frames)]
226
- rest = f"{frame} {label}"
227
343
  return Content.assemble(
228
344
  Content.styled(gutter, "dim"),
229
345
  Content.styled(rest, colors.foreground),
230
346
  )
231
- icon = g.checkmark if st.success else g.error
232
- dur_s = max(0.001, st.duration_ms / 1000.0)
233
- dur = format_duration(dur_s)
234
- rest = f"{icon} {label} · {dur}"
235
- if st.tool_call_count > 0:
236
- rest += f" · {st.tool_call_count} tools"
237
- tail = _plan_quick_view_step_summary(st.success, st.summary)
238
- if tail:
239
- rest += f" — {self._clip(tail, 80)}"
240
347
  if st.phase == "error" or (st.phase == "done" and not st.success):
241
348
  return Content.assemble(
242
349
  Content.styled(gutter, "dim"),
@@ -247,13 +354,15 @@ class CognitionGoalTreeMessage(Vertical):
247
354
  Content.styled(rest, colors.foreground),
248
355
  )
249
356
 
250
- def _assemble_steps_content(self) -> Content:
357
+ def _assemble_steps_content(self, *, max_line_width: int | None = None) -> Content:
251
358
  line_contents: list[Content] = []
252
359
  for sid in self._step_order:
253
360
  st = self._steps.get(sid)
254
361
  if st is None:
255
362
  continue
256
- line_contents.append(self._goal_tree_step_line_content(st))
363
+ line_contents.append(
364
+ self._goal_tree_step_line_content(st, max_line_width=max_line_width)
365
+ )
257
366
  if not line_contents:
258
367
  return Content("")
259
368
  parts: list[object] = []
@@ -273,16 +382,29 @@ class CognitionGoalTreeMessage(Vertical):
273
382
  except Exception:
274
383
  pass
275
384
 
276
- def plan_quick_view_content(self) -> Content:
385
+ def plan_quick_view_content(self, *, max_line_width: int | None = None) -> Content:
277
386
  """Full goal tree snapshot for the sticky plan quick-view overlay."""
278
387
  parts: list[object] = [self._goal_header_content()]
279
- steps = self._assemble_steps_content()
388
+ steps = self._assemble_steps_content(max_line_width=max_line_width)
280
389
  if steps.plain.strip():
281
390
  parts.extend([Content("\n"), steps])
282
391
  if self._footer_visible and self._footer_plain:
283
392
  parts.extend([Content("\n"), self._goal_footer_styled_content()])
284
393
  return Content.assemble(*parts)
285
394
 
395
+ def sync_running_live_stats(
396
+ self,
397
+ stats: dict[str, tuple[int, float | None]],
398
+ ) -> None:
399
+ """Update in-flight tool counts and started_at from step cards."""
400
+ for sid, (tool_count, started_at) in stats.items():
401
+ st = self._steps.get(sid)
402
+ if st is None or st.phase != "running":
403
+ continue
404
+ st.tool_call_count = max(0, int(tool_count))
405
+ if started_at is not None:
406
+ st.started_at = started_at
407
+
286
408
  def tick_running_spinner(self) -> None:
287
409
  """Advance the running-row spinner when any step is in flight."""
288
410
  if not any(st.phase == "running" for st in self._steps.values()):
@@ -339,6 +461,8 @@ class CognitionGoalTreeMessage(Vertical):
339
461
  "duration_ms": st.duration_ms,
340
462
  "tool_call_count": st.tool_call_count,
341
463
  "summary": st.summary,
464
+ "dependencies": list(st.dependencies),
465
+ "started_at": st.started_at,
342
466
  }
343
467
  )
344
468
  return {
@@ -366,6 +490,8 @@ class CognitionGoalTreeMessage(Vertical):
366
490
  sid = str(row.get("id", "")).strip()
367
491
  if not sid:
368
492
  continue
493
+ started_raw = row.get("started_at")
494
+ started_at = float(started_raw) if started_raw is not None else None
369
495
  st = _StepLineState(
370
496
  sid,
371
497
  str(row.get("description", "")),
@@ -374,6 +500,8 @@ class CognitionGoalTreeMessage(Vertical):
374
500
  duration_ms=int(row.get("duration_ms", 0)),
375
501
  tool_call_count=int(row.get("tool_call_count", 0)),
376
502
  summary=str(row.get("summary", "")),
503
+ dependencies=_normalize_step_dependencies(row.get("dependencies")),
504
+ started_at=started_at,
377
505
  )
378
506
  self._step_order.append(sid)
379
507
  self._steps[sid] = st
@@ -398,14 +526,23 @@ class CognitionGoalTreeMessage(Vertical):
398
526
  continue
399
527
  planned_ids.add(sid)
400
528
  desc = str(row.get("description", "")).strip() or "(step)"
529
+ deps = _normalize_step_dependencies(row.get("dependencies"))
401
530
  existing = self._steps.get(sid)
402
531
  if existing is None:
403
532
  self._step_order.append(sid)
404
- self._steps[sid] = _StepLineState(sid, desc, phase="pending")
533
+ self._steps[sid] = _StepLineState(
534
+ sid,
535
+ desc,
536
+ phase="pending",
537
+ dependencies=deps,
538
+ )
405
539
  elif existing.phase in ("pending", "queued"):
406
540
  existing.description = desc
541
+ existing.dependencies = deps
407
542
  elif existing.phase == "running" and desc:
408
543
  existing.description = desc
544
+ if deps:
545
+ existing.dependencies = deps
409
546
  for sid in list(self._step_order):
410
547
  st = self._steps.get(sid)
411
548
  if st is not None and st.phase == "pending" and sid not in planned_ids:
@@ -440,12 +577,10 @@ class CognitionGoalTreeMessage(Vertical):
440
577
  st.phase = phase
441
578
  if desc:
442
579
  st.description = desc
580
+ if phase == "running" and st.started_at is None:
581
+ st.started_at = time()
443
582
  self._refresh_steps_display()
444
583
 
445
- def add_step_running(self, step_id: str, description: str) -> None:
446
- """Register a step in running state and refresh the aggregate."""
447
- self.set_step_phase(step_id, "running", description=description)
448
-
449
584
  def complete_step(
450
585
  self,
451
586
  step_id: str,
@@ -468,6 +603,7 @@ class CognitionGoalTreeMessage(Vertical):
468
603
  st.duration_ms = duration_ms
469
604
  st.tool_call_count = tool_call_count
470
605
  st.summary = summary or ""
606
+ st.started_at = None
471
607
  self._refresh_steps_display()
472
608
 
473
609
  def set_loop_finished(
@@ -523,6 +659,7 @@ class CognitionGoalTreeMessage(Vertical):
523
659
  st.success = False
524
660
  st.duration_ms = 0
525
661
  st.summary = msg
662
+ st.started_at = None
526
663
  self._refresh_steps_display()
527
664
  self._footer_plain = self._clip(msg, 120)
528
665
  self._footer_visible = True
@@ -9,6 +9,8 @@ from textual.containers import Vertical, VerticalScroll
9
9
  from textual.content import Content
10
10
  from textual.widgets import Static
11
11
 
12
+ from soothe_cli.tui.config import get_glyphs
13
+ from soothe_cli.tui.preview_limits import PLAN_QUICK_VIEW_STEP_LINE_MAX_CHARS
12
14
  from soothe_cli.tui.widgets.messages._helpers import _RUNNING_SPINNER_INTERVAL_SECONDS
13
15
 
14
16
  if TYPE_CHECKING:
@@ -29,6 +31,17 @@ def get_live_goal_tree(app: Any) -> CognitionGoalTreeMessage | None:
29
31
  return tree
30
32
 
31
33
 
34
+ def _goal_tree_running_live_stats(adapter: Any) -> dict[str, tuple[int, float | None]]:
35
+ """Collect live tool counts and start times from running step cards."""
36
+ stats: dict[str, tuple[int, float | None]] = {}
37
+ for sid, card in getattr(adapter, "_current_step_messages", {}).items():
38
+ if card._status != "running":
39
+ continue
40
+ idx = card._build_row_index()
41
+ stats[sid] = (idx.main_tool_count, card._start_time)
42
+ return stats
43
+
44
+
32
45
  class PlanQuickViewOverlay(Vertical):
33
46
  """Floating quick-view panel of the full plan above the chat prompt.
34
47
 
@@ -143,8 +156,21 @@ class PlanQuickViewOverlay(Vertical):
143
156
  self._content.update(Content.styled("No active plan.", "dim"))
144
157
  return
145
158
  try:
159
+ adapter = getattr(self.app, "_ui_adapter", None)
160
+ if adapter is not None:
161
+ tree.sync_running_live_stats(_goal_tree_running_live_stats(adapter))
146
162
  tree.tick_running_spinner()
147
- self._content.update(tree.plan_quick_view_content())
163
+ max_line_width = self._plan_quick_view_line_width()
164
+ self._content.update(tree.plan_quick_view_content(max_line_width=max_line_width))
148
165
  except Exception: # noqa: BLE001
149
166
  logger.debug("Failed to render plan quick view", exc_info=True)
150
167
  self._content.update(Content.styled("(plan view unavailable)", "dim"))
168
+
169
+ def _plan_quick_view_line_width(self) -> int:
170
+ """Available columns for one plan step row inside the overlay."""
171
+ overlay_padding = 4
172
+ gutter_len = len(get_glyphs().output_prefix) + 1
173
+ width = self.size.width if self.size.width else 0
174
+ if width > overlay_padding + gutter_len:
175
+ return max(PLAN_QUICK_VIEW_STEP_LINE_MAX_CHARS, width - overlay_padding - gutter_len)
176
+ return PLAN_QUICK_VIEW_STEP_LINE_MAX_CHARS
@@ -1,15 +0,0 @@
1
- """Re-export shim for ``soothe_sdk.display.text_extract`` (RFC-413)."""
2
-
3
- from __future__ import annotations
4
-
5
- from soothe_sdk.display.text_extract import (
6
- extract_ai_text_for_display,
7
- extract_user_text_for_display,
8
- normalize_stream_message,
9
- )
10
-
11
- __all__ = [
12
- "extract_ai_text_for_display",
13
- "extract_user_text_for_display",
14
- "normalize_stream_message",
15
- ]