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.
- soothe_cli/__init__.py +5 -0
- soothe_cli/cli/__init__.py +1 -0
- soothe_cli/cli/commands/__init__.py +1 -0
- soothe_cli/cli/commands/autopilot_cmd.py +410 -0
- soothe_cli/cli/commands/config_cmd.py +277 -0
- soothe_cli/cli/commands/run_cmd.py +87 -0
- soothe_cli/cli/commands/status_cmd.py +121 -0
- soothe_cli/cli/commands/subagent_names.py +17 -0
- soothe_cli/cli/commands/thread_cmd.py +657 -0
- soothe_cli/cli/execution/__init__.py +6 -0
- soothe_cli/cli/execution/daemon.py +194 -0
- soothe_cli/cli/execution/headless.py +99 -0
- soothe_cli/cli/execution/launcher.py +31 -0
- soothe_cli/cli/main.py +509 -0
- soothe_cli/cli/renderer.py +444 -0
- soothe_cli/cli/stream/__init__.py +17 -0
- soothe_cli/cli/stream/context.py +138 -0
- soothe_cli/cli/stream/display_line.py +83 -0
- soothe_cli/cli/stream/formatter.py +412 -0
- soothe_cli/cli/stream/pipeline.py +521 -0
- soothe_cli/cli/utils.py +46 -0
- soothe_cli/config/__init__.py +5 -0
- soothe_cli/config/cli_config.py +155 -0
- soothe_cli/plan/__init__.py +5 -0
- soothe_cli/plan/rich_tree.py +54 -0
- soothe_cli/shared/__init__.py +107 -0
- soothe_cli/shared/command_router.py +246 -0
- soothe_cli/shared/config_loader.py +68 -0
- soothe_cli/shared/display_policy.py +413 -0
- soothe_cli/shared/essential_events.py +68 -0
- soothe_cli/shared/event_processor.py +823 -0
- soothe_cli/shared/message_processing.py +393 -0
- soothe_cli/shared/presentation_engine.py +173 -0
- soothe_cli/shared/processor_state.py +80 -0
- soothe_cli/shared/renderer_protocol.py +158 -0
- soothe_cli/shared/rendering.py +43 -0
- soothe_cli/shared/slash_commands.py +354 -0
- soothe_cli/shared/subagent_routing.py +63 -0
- soothe_cli/shared/suppression_state.py +188 -0
- soothe_cli/shared/tool_formatters/__init__.py +27 -0
- soothe_cli/shared/tool_formatters/base.py +109 -0
- soothe_cli/shared/tool_formatters/execution.py +297 -0
- soothe_cli/shared/tool_formatters/fallback.py +128 -0
- soothe_cli/shared/tool_formatters/file_ops.py +299 -0
- soothe_cli/shared/tool_formatters/goal_formatter.py +331 -0
- soothe_cli/shared/tool_formatters/media.py +291 -0
- soothe_cli/shared/tool_formatters/structured.py +202 -0
- soothe_cli/shared/tool_formatters/web.py +143 -0
- soothe_cli/shared/tool_output_formatter.py +227 -0
- soothe_cli/shared/tui_trace_log.py +40 -0
- soothe_cli/tui/__init__.py +5 -0
- soothe_cli/tui/_ask_user_types.py +50 -0
- soothe_cli/tui/_cli_context.py +27 -0
- soothe_cli/tui/_env_vars.py +56 -0
- soothe_cli/tui/_session_stats.py +114 -0
- soothe_cli/tui/_version.py +21 -0
- soothe_cli/tui/app.py +4992 -0
- soothe_cli/tui/app.tcss +302 -0
- soothe_cli/tui/command_registry.py +310 -0
- soothe_cli/tui/config.py +2381 -0
- soothe_cli/tui/daemon_session.py +233 -0
- soothe_cli/tui/file_ops.py +409 -0
- soothe_cli/tui/formatting.py +28 -0
- soothe_cli/tui/hooks.py +23 -0
- soothe_cli/tui/input.py +782 -0
- soothe_cli/tui/media_utils.py +471 -0
- soothe_cli/tui/model_config.py +518 -0
- soothe_cli/tui/output.py +69 -0
- soothe_cli/tui/project_utils.py +188 -0
- soothe_cli/tui/sessions.py +1248 -0
- soothe_cli/tui/skills/__init__.py +5 -0
- soothe_cli/tui/skills/invocation.py +74 -0
- soothe_cli/tui/skills/load.py +93 -0
- soothe_cli/tui/textual_adapter.py +1430 -0
- soothe_cli/tui/theme.py +838 -0
- soothe_cli/tui/tool_display.py +297 -0
- soothe_cli/tui/unicode_security.py +502 -0
- soothe_cli/tui/update_check.py +447 -0
- soothe_cli/tui/widgets/__init__.py +9 -0
- soothe_cli/tui/widgets/_links.py +63 -0
- soothe_cli/tui/widgets/approval.py +430 -0
- soothe_cli/tui/widgets/ask_user.py +392 -0
- soothe_cli/tui/widgets/autocomplete.py +666 -0
- soothe_cli/tui/widgets/autopilot_dashboard.py +308 -0
- soothe_cli/tui/widgets/autopilot_screen.py +64 -0
- soothe_cli/tui/widgets/chat_input.py +1834 -0
- soothe_cli/tui/widgets/clipboard.py +128 -0
- soothe_cli/tui/widgets/diff.py +240 -0
- soothe_cli/tui/widgets/editor.py +140 -0
- soothe_cli/tui/widgets/history.py +221 -0
- soothe_cli/tui/widgets/loading.py +194 -0
- soothe_cli/tui/widgets/mcp_viewer.py +352 -0
- soothe_cli/tui/widgets/message_store.py +693 -0
- soothe_cli/tui/widgets/messages.py +1720 -0
- soothe_cli/tui/widgets/model_selector.py +988 -0
- soothe_cli/tui/widgets/notification_settings.py +155 -0
- soothe_cli/tui/widgets/status.py +403 -0
- soothe_cli/tui/widgets/theme_selector.py +158 -0
- soothe_cli/tui/widgets/thread_selector.py +1865 -0
- soothe_cli/tui/widgets/tool_renderers.py +148 -0
- soothe_cli/tui/widgets/tool_widgets.py +254 -0
- soothe_cli/tui/widgets/tools.py +165 -0
- soothe_cli/tui/widgets/welcome.py +330 -0
- soothe_cli-0.1.0.dist-info/METADATA +100 -0
- soothe_cli-0.1.0.dist-info/RECORD +107 -0
- soothe_cli-0.1.0.dist-info/WHEEL +4 -0
- 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)
|