soothe-cli 0.1.0__py3-none-any.whl

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 (107) hide show
  1. soothe_cli/__init__.py +5 -0
  2. soothe_cli/cli/__init__.py +1 -0
  3. soothe_cli/cli/commands/__init__.py +1 -0
  4. soothe_cli/cli/commands/autopilot_cmd.py +410 -0
  5. soothe_cli/cli/commands/config_cmd.py +277 -0
  6. soothe_cli/cli/commands/run_cmd.py +87 -0
  7. soothe_cli/cli/commands/status_cmd.py +121 -0
  8. soothe_cli/cli/commands/subagent_names.py +17 -0
  9. soothe_cli/cli/commands/thread_cmd.py +657 -0
  10. soothe_cli/cli/execution/__init__.py +6 -0
  11. soothe_cli/cli/execution/daemon.py +194 -0
  12. soothe_cli/cli/execution/headless.py +99 -0
  13. soothe_cli/cli/execution/launcher.py +31 -0
  14. soothe_cli/cli/main.py +509 -0
  15. soothe_cli/cli/renderer.py +444 -0
  16. soothe_cli/cli/stream/__init__.py +17 -0
  17. soothe_cli/cli/stream/context.py +138 -0
  18. soothe_cli/cli/stream/display_line.py +83 -0
  19. soothe_cli/cli/stream/formatter.py +412 -0
  20. soothe_cli/cli/stream/pipeline.py +521 -0
  21. soothe_cli/cli/utils.py +46 -0
  22. soothe_cli/config/__init__.py +5 -0
  23. soothe_cli/config/cli_config.py +155 -0
  24. soothe_cli/plan/__init__.py +5 -0
  25. soothe_cli/plan/rich_tree.py +54 -0
  26. soothe_cli/shared/__init__.py +107 -0
  27. soothe_cli/shared/command_router.py +246 -0
  28. soothe_cli/shared/config_loader.py +68 -0
  29. soothe_cli/shared/display_policy.py +413 -0
  30. soothe_cli/shared/essential_events.py +68 -0
  31. soothe_cli/shared/event_processor.py +823 -0
  32. soothe_cli/shared/message_processing.py +393 -0
  33. soothe_cli/shared/presentation_engine.py +173 -0
  34. soothe_cli/shared/processor_state.py +80 -0
  35. soothe_cli/shared/renderer_protocol.py +158 -0
  36. soothe_cli/shared/rendering.py +43 -0
  37. soothe_cli/shared/slash_commands.py +354 -0
  38. soothe_cli/shared/subagent_routing.py +63 -0
  39. soothe_cli/shared/suppression_state.py +188 -0
  40. soothe_cli/shared/tool_formatters/__init__.py +27 -0
  41. soothe_cli/shared/tool_formatters/base.py +109 -0
  42. soothe_cli/shared/tool_formatters/execution.py +297 -0
  43. soothe_cli/shared/tool_formatters/fallback.py +128 -0
  44. soothe_cli/shared/tool_formatters/file_ops.py +299 -0
  45. soothe_cli/shared/tool_formatters/goal_formatter.py +331 -0
  46. soothe_cli/shared/tool_formatters/media.py +291 -0
  47. soothe_cli/shared/tool_formatters/structured.py +202 -0
  48. soothe_cli/shared/tool_formatters/web.py +143 -0
  49. soothe_cli/shared/tool_output_formatter.py +227 -0
  50. soothe_cli/shared/tui_trace_log.py +40 -0
  51. soothe_cli/tui/__init__.py +5 -0
  52. soothe_cli/tui/_ask_user_types.py +50 -0
  53. soothe_cli/tui/_cli_context.py +27 -0
  54. soothe_cli/tui/_env_vars.py +56 -0
  55. soothe_cli/tui/_session_stats.py +114 -0
  56. soothe_cli/tui/_version.py +21 -0
  57. soothe_cli/tui/app.py +4992 -0
  58. soothe_cli/tui/app.tcss +302 -0
  59. soothe_cli/tui/command_registry.py +310 -0
  60. soothe_cli/tui/config.py +2381 -0
  61. soothe_cli/tui/daemon_session.py +233 -0
  62. soothe_cli/tui/file_ops.py +409 -0
  63. soothe_cli/tui/formatting.py +28 -0
  64. soothe_cli/tui/hooks.py +23 -0
  65. soothe_cli/tui/input.py +782 -0
  66. soothe_cli/tui/media_utils.py +471 -0
  67. soothe_cli/tui/model_config.py +518 -0
  68. soothe_cli/tui/output.py +69 -0
  69. soothe_cli/tui/project_utils.py +188 -0
  70. soothe_cli/tui/sessions.py +1248 -0
  71. soothe_cli/tui/skills/__init__.py +5 -0
  72. soothe_cli/tui/skills/invocation.py +74 -0
  73. soothe_cli/tui/skills/load.py +93 -0
  74. soothe_cli/tui/textual_adapter.py +1430 -0
  75. soothe_cli/tui/theme.py +838 -0
  76. soothe_cli/tui/tool_display.py +297 -0
  77. soothe_cli/tui/unicode_security.py +502 -0
  78. soothe_cli/tui/update_check.py +447 -0
  79. soothe_cli/tui/widgets/__init__.py +9 -0
  80. soothe_cli/tui/widgets/_links.py +63 -0
  81. soothe_cli/tui/widgets/approval.py +430 -0
  82. soothe_cli/tui/widgets/ask_user.py +392 -0
  83. soothe_cli/tui/widgets/autocomplete.py +666 -0
  84. soothe_cli/tui/widgets/autopilot_dashboard.py +308 -0
  85. soothe_cli/tui/widgets/autopilot_screen.py +64 -0
  86. soothe_cli/tui/widgets/chat_input.py +1834 -0
  87. soothe_cli/tui/widgets/clipboard.py +128 -0
  88. soothe_cli/tui/widgets/diff.py +240 -0
  89. soothe_cli/tui/widgets/editor.py +140 -0
  90. soothe_cli/tui/widgets/history.py +221 -0
  91. soothe_cli/tui/widgets/loading.py +194 -0
  92. soothe_cli/tui/widgets/mcp_viewer.py +352 -0
  93. soothe_cli/tui/widgets/message_store.py +693 -0
  94. soothe_cli/tui/widgets/messages.py +1720 -0
  95. soothe_cli/tui/widgets/model_selector.py +988 -0
  96. soothe_cli/tui/widgets/notification_settings.py +155 -0
  97. soothe_cli/tui/widgets/status.py +403 -0
  98. soothe_cli/tui/widgets/theme_selector.py +158 -0
  99. soothe_cli/tui/widgets/thread_selector.py +1865 -0
  100. soothe_cli/tui/widgets/tool_renderers.py +148 -0
  101. soothe_cli/tui/widgets/tool_widgets.py +254 -0
  102. soothe_cli/tui/widgets/tools.py +165 -0
  103. soothe_cli/tui/widgets/welcome.py +330 -0
  104. soothe_cli-0.1.0.dist-info/METADATA +100 -0
  105. soothe_cli-0.1.0.dist-info/RECORD +107 -0
  106. soothe_cli-0.1.0.dist-info/WHEEL +4 -0
  107. soothe_cli-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,308 @@
1
+ """RFC-204: Autopilot TUI Dashboard — read-only monitoring view.
2
+
3
+ Four-panel layout (responsive):
4
+ Wide terminal: Goal DAG (left) | Status + Findings + Controls (right)
5
+ Narrow terminal: Vertical stack (DAG → Status → Findings → Controls)
6
+
7
+ All panels are read-only; control actions are done via CLI commands.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING, Any, ClassVar
16
+
17
+ from soothe_sdk import SOOTHE_HOME
18
+ from soothe_sdk.protocol import preview_first
19
+ from textual.containers import Container, ScrollableContainer
20
+ from textual.reactive import reactive
21
+ from textual.widgets import Static
22
+
23
+ if TYPE_CHECKING:
24
+ from textual.app import ComposeResult
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class GoalDagWidget(Static):
30
+ """Displays the goal DAG as a text tree."""
31
+
32
+ goals: list[dict] = reactive([])
33
+
34
+ DEFAULT_CSS = """
35
+ GoalDagWidget {
36
+ width: 1fr;
37
+ height: 1fr;
38
+ border: solid green;
39
+ padding: 0 1;
40
+ }
41
+ """
42
+
43
+ def render(self) -> str:
44
+ """Render the goal DAG as styled text."""
45
+ if not self.goals:
46
+ return "[dim]No goals loaded[/]"
47
+ status_colors = {
48
+ "pending": "dim",
49
+ "active": "yellow",
50
+ "validated": "blue",
51
+ "completed": "green",
52
+ "failed": "red",
53
+ "suspended": "magenta",
54
+ "blocked": "orange",
55
+ }
56
+ icons = {
57
+ "pending": "○",
58
+ "active": "◉",
59
+ "completed": "✓",
60
+ "failed": "✗",
61
+ "suspended": "⏸",
62
+ "blocked": "⏺",
63
+ }
64
+ lines = ["[bold green]Goal DAG[/]", ""]
65
+ for g in self.goals:
66
+ status = g.get("status", "pending")
67
+ color = status_colors.get(status, "dim")
68
+ icon = icons.get(status, "○")
69
+ deps = ""
70
+ if g.get("depends_on"):
71
+ deps = f" [dim](deps: {', '.join(g['depends_on'][:3])})[/]"
72
+ informs = ""
73
+ if g.get("informs"):
74
+ informs = f" [dim](→ {', '.join(g['informs'][:3])})[/]"
75
+ gid = g.get("id", "?")
76
+ desc = preview_first(g.get("description", ""), 50)
77
+ lines.append(f" [{color}]{icon}[/] [{color}]{gid}[/] {desc}{deps}{informs}")
78
+ return "\n".join(lines)
79
+
80
+
81
+ class StatusWidget(Static):
82
+ """Displays overall autopilot status."""
83
+
84
+ state: str = reactive("idle")
85
+ active_count: int = reactive(0)
86
+ completed_count: int = reactive(0)
87
+ iteration_count: int = reactive(0)
88
+
89
+ DEFAULT_CSS = """
90
+ StatusWidget {
91
+ width: 1fr;
92
+ height: auto;
93
+ border: solid blue;
94
+ padding: 0 1;
95
+ margin-bottom: 1;
96
+ }
97
+ """
98
+
99
+ def render(self) -> str:
100
+ """Render the status panel as styled text."""
101
+ parts = [
102
+ f"[bold blue]Status[/] [{self.state}]",
103
+ f" Active: {self.active_count} | "
104
+ f"Completed: {self.completed_count} | "
105
+ f"Iterations: {self.iteration_count}",
106
+ ]
107
+ return "\n".join(parts)
108
+
109
+
110
+ class FindingsWidget(ScrollableContainer):
111
+ """Displays key findings from completed goals."""
112
+
113
+ findings: list[str] = reactive([])
114
+
115
+ DEFAULT_CSS = """
116
+ FindingsWidget {
117
+ width: 1fr;
118
+ height: 1fr;
119
+ border: solid cyan;
120
+ padding: 0 1;
121
+ }
122
+ """
123
+
124
+ def render(self) -> str:
125
+ """Render the findings panel as styled text."""
126
+ if not self.findings:
127
+ return "[dim]No findings yet[/]"
128
+ lines = ["[bold cyan]Findings[/]", ""]
129
+ for i, f in enumerate(self.findings[-20:], 1):
130
+ lines.append(f" {i}. {preview_first(f, 80)}")
131
+ return "\n".join(lines)
132
+
133
+
134
+ class ControlsWidget(Static):
135
+ """Displays available CLI commands (read-only)."""
136
+
137
+ _COMMANDS: ClassVar[list[tuple[str, str]]] = [
138
+ ("soothe autopilot submit 'task'", "Submit task"),
139
+ ("soothe autopilot status", "Check status"),
140
+ ("soothe autopilot list", "List goals"),
141
+ ("soothe autopilot goal <id>", "Goal details"),
142
+ ("soothe autopilot cancel <id>", "Cancel goal"),
143
+ ("soothe autopilot wake", "Exit dreaming"),
144
+ ("soothe autopilot inbox", "View inbox"),
145
+ ]
146
+
147
+ DEFAULT_CSS = """
148
+ ControlsWidget {
149
+ width: 1fr;
150
+ height: auto;
151
+ border: solid yellow;
152
+ padding: 0 1;
153
+ }
154
+ """
155
+
156
+ def render(self) -> str:
157
+ """Render the controls panel as styled text."""
158
+ lines = ["[bold yellow]Available Commands[/] (use CLI)", ""]
159
+ for cmd, desc in self._COMMANDS:
160
+ lines.append(f" [bold]{cmd}[/] [dim]— {desc}[/]")
161
+ return "\n".join(lines)
162
+
163
+
164
+ class AutopilotDashboard(Container):
165
+ """Top-level container for the autopilot dashboard."""
166
+
167
+ DEFAULT_CSS = """
168
+ AutopilotDashboard {
169
+ layout: horizontal;
170
+ }
171
+ AutopilotDashboard.narrow-layout {
172
+ layout: vertical;
173
+ }
174
+ """
175
+
176
+ def __init__(self, *, is_narrow: bool = False, **kwargs: Any) -> None:
177
+ """Initialize dashboard.
178
+
179
+ Args:
180
+ is_narrow: Whether to use vertical layout.
181
+ **kwargs: Passed to parent.
182
+ """
183
+ super().__init__(**kwargs)
184
+ self._is_narrow = is_narrow
185
+ self.goal_dag = GoalDagWidget()
186
+ self.status = StatusWidget()
187
+ self.findings = FindingsWidget()
188
+ self.controls = ControlsWidget()
189
+
190
+ def compose(self) -> ComposeResult:
191
+ """Build the dashboard layout."""
192
+ if self._is_narrow:
193
+ yield ScrollableContainer(self.goal_dag, classes="panel")
194
+ yield ScrollableContainer(
195
+ self.status,
196
+ self.controls,
197
+ self.findings,
198
+ classes="side-panel",
199
+ )
200
+ else:
201
+ yield ScrollableContainer(self.goal_dag, classes="panel")
202
+ yield ScrollableContainer(
203
+ self.status,
204
+ self.findings,
205
+ self.controls,
206
+ classes="side-panel",
207
+ )
208
+
209
+ def update_goals(self, goals: list[dict]) -> None:
210
+ """Update goal display.
211
+
212
+ Args:
213
+ goals: List of goal info dicts.
214
+ """
215
+ self.goal_dag.goals = goals
216
+ active = sum(1 for g in goals if g.get("status") == "active")
217
+ completed = sum(1 for g in goals if g.get("status") == "completed")
218
+ self.status.active_count = active
219
+ self.status.completed_count = completed
220
+
221
+ def add_finding(self, text: str) -> None:
222
+ """Add a finding to the findings panel.
223
+
224
+ Args:
225
+ text: Finding text to add.
226
+ """
227
+ self.findings.findings = [*self.findings.findings, text]
228
+
229
+
230
+ class AutopilotApp:
231
+ """Manages the autopilot dashboard lifecycle.
232
+
233
+ Integrates with the existing TUI infrastructure by providing
234
+ an alternate screen mode.
235
+ """
236
+
237
+ def __init__(self, soothe_home: Path | None = None) -> None:
238
+ """Initialize autopilot manager.
239
+
240
+ Args:
241
+ soothe_home: Root directory for SOOTHE_HOME.
242
+ """
243
+ from pathlib import Path
244
+
245
+ self._soothe_home = soothe_home or Path(SOOTHE_HOME)
246
+ self._dashboard: AutopilotDashboard | None = None
247
+
248
+ def get_dashboard(self, *, is_narrow: bool = False) -> AutopilotDashboard:
249
+ """Get or create the dashboard instance.
250
+
251
+ Args:
252
+ is_narrow: Whether to use vertical layout.
253
+
254
+ Returns:
255
+ Dashboard widget instance.
256
+ """
257
+ if self._dashboard is None:
258
+ self._dashboard = AutopilotDashboard(is_narrow=is_narrow)
259
+ return self._dashboard
260
+
261
+ def refresh_from_files(self) -> None:
262
+ """Reload goal state from files and update dashboard."""
263
+ if not self._dashboard:
264
+ return
265
+
266
+ goals = self._load_goals()
267
+ self._dashboard.update_goals(goals)
268
+
269
+ def _load_goals(self) -> list[dict]:
270
+ """Parse goals from SOOTHE_HOME/autopilot/ files.
271
+
272
+ Returns:
273
+ List of goal info dicts.
274
+ """
275
+ autopilot_dir = self._soothe_home / "autopilot"
276
+ if not autopilot_dir.exists():
277
+ return []
278
+
279
+ goals = []
280
+
281
+ # Check status.json for runtime state
282
+ state_file = autopilot_dir / "status.json"
283
+ if state_file.exists():
284
+ try:
285
+ data = json.loads(state_file.read_text())
286
+ return data.get("goals", [])
287
+ except (json.JSONDecodeError, OSError):
288
+ pass
289
+
290
+ # Fallback: parse goal files
291
+ from soothe_sdk import parse_autopilot_goals
292
+
293
+ goals.extend(parse_autopilot_goals(autopilot_dir))
294
+ return goals
295
+
296
+
297
+ def _parse_autopilot_files(autopilot_dir: Path) -> list[dict]:
298
+ """Parse goals from GOAL.md/GOALS.md files.
299
+
300
+ Args:
301
+ autopilot_dir: Path to autopilot directory.
302
+
303
+ Returns:
304
+ List of goal info dicts.
305
+ """
306
+ from soothe_sdk import parse_autopilot_goals
307
+
308
+ return parse_autopilot_goals(autopilot_dir)
@@ -0,0 +1,64 @@
1
+ """RFC-204: Autopilot Dashboard Screen for Textual TUI.
2
+
3
+ A full-screen, read-only dashboard that replaces the chat TUI when
4
+ viewing autopilot state. Pushed as a modal screen from the main app
5
+ via the `/status` slash command.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, ClassVar
11
+
12
+ from soothe_sdk import SOOTHE_HOME
13
+ from textual.binding import Binding
14
+ from textual.screen import Screen
15
+
16
+ from soothe_cli.tui.widgets.autopilot_dashboard import AutopilotDashboard
17
+
18
+ if TYPE_CHECKING:
19
+ from textual.app import ComposeResult
20
+
21
+
22
+ class AutopilotScreen(Screen):
23
+ """Full-screen autopilot dashboard.
24
+
25
+ Pushed as a modal screen from the main TUI.
26
+ Press Q or Escape to return to the chat TUI.
27
+ """
28
+
29
+ BINDINGS: ClassVar[list] = [
30
+ Binding("q", "quit", "Close", show=True),
31
+ ]
32
+
33
+ def __init__(self, *, is_narrow: bool = False) -> None:
34
+ """Initialize screen.
35
+
36
+ Args:
37
+ is_narrow: Whether to use vertical layout.
38
+ """
39
+ super().__init__()
40
+ self._is_narrow = is_narrow
41
+ self._dashboard = AutopilotDashboard(is_narrow=is_narrow)
42
+
43
+ def compose(self) -> ComposeResult:
44
+ """Build the screen with the dashboard."""
45
+ yield self._dashboard
46
+
47
+ def on_show(self) -> None:
48
+ """Refresh goals when screen is shown."""
49
+ self._dashboard.update_goals(self._load_goals())
50
+
51
+ def _load_goals(self) -> list[dict]:
52
+ """Parse goals from SOOTHE_HOME/autopilot/ files.
53
+
54
+ Returns:
55
+ List of goal info dicts.
56
+ """
57
+ from pathlib import Path
58
+
59
+ from soothe_cli.tui.widgets.autopilot_dashboard import _parse_autopilot_files
60
+
61
+ autopilot_dir = Path(SOOTHE_HOME) / "autopilot"
62
+ if not autopilot_dir.exists():
63
+ return []
64
+ return _parse_autopilot_files(autopilot_dir)