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,352 @@
1
+ """Read-only MCP server and tool viewer modal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, ClassVar
6
+
7
+ from textual.binding import Binding, BindingType
8
+ from textual.containers import Vertical, VerticalScroll
9
+ from textual.content import Content
10
+ from textual.events import (
11
+ Click, # noqa: TC002 - needed at runtime for Textual event dispatch
12
+ )
13
+ from textual.screen import ModalScreen
14
+ from textual.widgets import Static
15
+
16
+ if TYPE_CHECKING:
17
+ from textual.app import ComposeResult
18
+
19
+ from soothe_cli.tui.mcp_tools import MCPServerInfo
20
+
21
+ from soothe_cli.tui import theme
22
+ from soothe_cli.tui.config import get_glyphs, is_ascii_mode
23
+
24
+
25
+ class MCPToolItem(Static):
26
+ """A selectable tool item in the MCP viewer."""
27
+
28
+ def __init__(
29
+ self,
30
+ name: str,
31
+ description: str,
32
+ index: int,
33
+ *,
34
+ classes: str = "",
35
+ ) -> None:
36
+ """Initialize a tool item.
37
+
38
+ Args:
39
+ name: Tool name.
40
+ description: Full tool description.
41
+ index: Flat index of this tool in the list.
42
+ classes: CSS classes.
43
+ """
44
+ if description:
45
+ label = Content.from_markup(" $name [dim]$desc[/dim]", name=name, desc=description)
46
+ else:
47
+ label = Content.from_markup(" $name", name=name)
48
+ super().__init__(label, classes=classes)
49
+ self.tool_name = name
50
+ self.tool_description = description
51
+ self.index = index
52
+ self._expanded = False
53
+
54
+ def _format_collapsed(self, name: str, description: str) -> Content:
55
+ """Build the collapsed (single-line) label.
56
+
57
+ Truncates the description with `(...)` if it would overflow
58
+ the widget width.
59
+
60
+ Args:
61
+ name: Tool name.
62
+ description: Tool description.
63
+
64
+ Returns:
65
+ Styled Content label.
66
+ """
67
+ if not description:
68
+ return Content.from_markup(" $name", name=name)
69
+ prefix_len = 2 + len(name) + 1
70
+ avail = self.size.width - prefix_len - 1 if self.size.width else 0
71
+ ellipsis = " (...)"
72
+ if avail > 0 and len(description) > avail:
73
+ cut = max(0, avail - len(ellipsis))
74
+ desc_text = description[:cut] + ellipsis
75
+ else:
76
+ desc_text = description
77
+ return Content.from_markup(" $name [dim]$desc[/dim]", name=name, desc=desc_text)
78
+
79
+ @staticmethod
80
+ def _format_expanded(name: str, description: str) -> Content:
81
+ """Build the expanded (multi-line) label.
82
+
83
+ Args:
84
+ name: Tool name.
85
+ description: Tool description.
86
+
87
+ Returns:
88
+ Styled Content label with full description on next line.
89
+ """
90
+ if description:
91
+ return Content.from_markup(
92
+ " [bold]$name[/bold]\n [dim]$desc[/dim]",
93
+ name=name,
94
+ desc=description,
95
+ )
96
+ return Content.from_markup(" [bold]$name[/bold]", name=name)
97
+
98
+ def toggle_expand(self) -> None:
99
+ """Toggle between collapsed and expanded view."""
100
+ self._expanded = not self._expanded
101
+ if self._expanded:
102
+ label = self._format_expanded(self.tool_name, self.tool_description)
103
+ self.styles.height = "auto"
104
+ else:
105
+ label = self._format_collapsed(self.tool_name, self.tool_description)
106
+ self.styles.height = 1
107
+ self.update(label)
108
+
109
+ def on_mount(self) -> None:
110
+ """Re-render with correct truncation once width is known."""
111
+ if not self._expanded:
112
+ self.update(self._format_collapsed(self.tool_name, self.tool_description))
113
+
114
+ def on_resize(self) -> None:
115
+ """Re-truncate when widget width changes."""
116
+ if not self._expanded:
117
+ self.update(self._format_collapsed(self.tool_name, self.tool_description))
118
+
119
+ def on_click(self, event: Click) -> None:
120
+ """Handle click — select and toggle expand via parent screen.
121
+
122
+ Args:
123
+ event: The click event.
124
+ """
125
+ event.stop()
126
+ screen = self.screen
127
+ if isinstance(screen, MCPViewerScreen):
128
+ screen._move_to(self.index)
129
+ self.toggle_expand()
130
+
131
+
132
+ class MCPViewerScreen(ModalScreen[None]):
133
+ """Modal viewer for active MCP servers and their tools.
134
+
135
+ Displays servers grouped by name with transport type and tool count.
136
+ Navigate with arrow keys, Enter to expand/collapse tool descriptions,
137
+ Escape to close.
138
+ """
139
+
140
+ BINDINGS: ClassVar[list[BindingType]] = [
141
+ Binding("up", "move_up", "Up", show=False, priority=True),
142
+ Binding("k", "move_up", "Up", show=False, priority=True),
143
+ Binding("down", "move_down", "Down", show=False, priority=True),
144
+ Binding("j", "move_down", "Down", show=False, priority=True),
145
+ Binding("enter", "toggle_expand", "Expand", show=False, priority=True),
146
+ Binding("pageup", "page_up", "Page up", show=False, priority=True),
147
+ Binding("pagedown", "page_down", "Page down", show=False, priority=True),
148
+ Binding("escape", "cancel", "Close", show=False, priority=True),
149
+ ]
150
+
151
+ CSS = """
152
+ MCPViewerScreen {
153
+ align: center middle;
154
+ }
155
+
156
+ MCPViewerScreen > Vertical {
157
+ width: 80;
158
+ max-width: 90%;
159
+ height: 80%;
160
+ background: $surface;
161
+ border: solid $primary;
162
+ padding: 1 2;
163
+ }
164
+
165
+ MCPViewerScreen .mcp-viewer-title {
166
+ text-style: bold;
167
+ color: $primary;
168
+ text-align: center;
169
+ margin-bottom: 1;
170
+ }
171
+
172
+ MCPViewerScreen .mcp-list {
173
+ height: 1fr;
174
+ min-height: 5;
175
+ scrollbar-gutter: stable;
176
+ background: $background;
177
+ }
178
+
179
+ MCPViewerScreen .mcp-server-header {
180
+ color: $primary;
181
+ margin-top: 1;
182
+ }
183
+
184
+ MCPViewerScreen .mcp-list > .mcp-server-header:first-child {
185
+ margin-top: 0;
186
+ }
187
+
188
+ MCPViewerScreen .mcp-tool-item {
189
+ height: 1;
190
+ padding: 0 1;
191
+ }
192
+
193
+ MCPViewerScreen .mcp-tool-item:hover {
194
+ background: $surface-lighten-1;
195
+ }
196
+
197
+ MCPViewerScreen .mcp-tool-selected {
198
+ background: $primary;
199
+ text-style: bold;
200
+ }
201
+
202
+ MCPViewerScreen .mcp-tool-selected:hover {
203
+ background: $primary-lighten-1;
204
+ }
205
+
206
+ MCPViewerScreen .mcp-empty {
207
+ color: $text-muted;
208
+ text-style: italic;
209
+ text-align: center;
210
+ margin-top: 2;
211
+ }
212
+
213
+ MCPViewerScreen .mcp-viewer-help {
214
+ height: 1;
215
+ color: $text-muted;
216
+ text-style: italic;
217
+ margin-top: 1;
218
+ text-align: center;
219
+ }
220
+ """
221
+
222
+ def __init__(self, server_info: list[MCPServerInfo]) -> None:
223
+ """Initialize the MCP viewer screen.
224
+
225
+ Args:
226
+ server_info: List of MCP server metadata to display.
227
+ """
228
+ super().__init__()
229
+ self._server_info = server_info
230
+ self._tool_widgets: list[MCPToolItem] = []
231
+ self._selected_index = 0
232
+
233
+ def compose(self) -> ComposeResult:
234
+ """Compose the screen layout.
235
+
236
+ Yields:
237
+ Widgets for the MCP viewer UI.
238
+ """
239
+ glyphs = get_glyphs()
240
+ total_servers = len(self._server_info)
241
+ total_tools = sum(len(s.tools) for s in self._server_info)
242
+
243
+ with Vertical():
244
+ if total_servers:
245
+ server_label = "server" if total_servers == 1 else "servers"
246
+ tool_label = "tool" if total_tools == 1 else "tools"
247
+ title = f"MCP Servers ({total_servers} {server_label}, {total_tools} {tool_label})"
248
+ else:
249
+ title = "MCP Servers"
250
+ yield Static(title, classes="mcp-viewer-title")
251
+
252
+ with VerticalScroll(classes="mcp-list"):
253
+ if not self._server_info:
254
+ yield Static(
255
+ "No MCP servers configured.\nUse `--mcp-config` to load servers.",
256
+ classes="mcp-empty",
257
+ )
258
+ else:
259
+ flat_index = 0
260
+ for server in self._server_info:
261
+ tool_count = len(server.tools)
262
+ t_label = "tool" if tool_count == 1 else "tools"
263
+ yield Static(
264
+ Content.from_markup(
265
+ f"[bold]$name[/bold] [dim]$transport {glyphs.bullet} {tool_count} {t_label}[/dim]",
266
+ name=server.name,
267
+ transport=server.transport,
268
+ ),
269
+ classes="mcp-server-header",
270
+ )
271
+ for tool in server.tools:
272
+ classes = "mcp-tool-item"
273
+ if flat_index == 0:
274
+ classes += " mcp-tool-selected"
275
+ widget = MCPToolItem(
276
+ name=tool.name,
277
+ description=tool.description,
278
+ index=flat_index,
279
+ classes=classes,
280
+ )
281
+ self._tool_widgets.append(widget)
282
+ yield widget
283
+ flat_index += 1
284
+
285
+ help_text = (
286
+ f"{glyphs.arrow_up}/{glyphs.arrow_down} navigate"
287
+ f" {glyphs.bullet} Enter expand/collapse"
288
+ f" {glyphs.bullet} Esc close"
289
+ )
290
+ yield Static(help_text, classes="mcp-viewer-help")
291
+
292
+ async def on_mount(self) -> None:
293
+ """Apply ASCII border fallback if needed."""
294
+ if is_ascii_mode():
295
+ container = self.query_one(Vertical)
296
+ colors = theme.get_theme_colors(self)
297
+ container.styles.border = ("ascii", colors.success)
298
+
299
+ def _move_to(self, index: int) -> None:
300
+ """Move selection to the given index.
301
+
302
+ Args:
303
+ index: Target tool index.
304
+ """
305
+ if not self._tool_widgets:
306
+ return
307
+ old = self._selected_index
308
+ self._selected_index = index
309
+
310
+ if old != index:
311
+ self._tool_widgets[old].remove_class("mcp-tool-selected")
312
+ self._tool_widgets[index].add_class("mcp-tool-selected")
313
+ self._tool_widgets[index].scroll_visible()
314
+
315
+ def _move_selection(self, delta: int) -> None:
316
+ """Move selection by delta positions.
317
+
318
+ Args:
319
+ delta: Number of positions to move.
320
+ """
321
+ if not self._tool_widgets:
322
+ return
323
+ count = len(self._tool_widgets)
324
+ target = (self._selected_index + delta) % count
325
+ self._move_to(target)
326
+
327
+ def action_move_up(self) -> None:
328
+ """Move selection up."""
329
+ self._move_selection(-1)
330
+
331
+ def action_move_down(self) -> None:
332
+ """Move selection down."""
333
+ self._move_selection(1)
334
+
335
+ def action_toggle_expand(self) -> None:
336
+ """Toggle expand/collapse on the selected tool."""
337
+ if self._tool_widgets:
338
+ self._tool_widgets[self._selected_index].toggle_expand()
339
+
340
+ def action_page_up(self) -> None:
341
+ """Scroll up by one page."""
342
+ scroll = self.query_one(".mcp-list", VerticalScroll)
343
+ scroll.scroll_page_up()
344
+
345
+ def action_page_down(self) -> None:
346
+ """Scroll down by one page."""
347
+ scroll = self.query_one(".mcp-list", VerticalScroll)
348
+ scroll.scroll_page_down()
349
+
350
+ def action_cancel(self) -> None:
351
+ """Close the viewer."""
352
+ self.dismiss(None)