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,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)
|