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
soothe_cli/tui/app.tcss
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/* Deep Agents CLI Textual Stylesheet */
|
|
2
|
+
|
|
3
|
+
/* Define layers for z-ordering */
|
|
4
|
+
Screen {
|
|
5
|
+
layout: vertical;
|
|
6
|
+
layers: base autocomplete;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* Thin scrollbars app-wide */
|
|
10
|
+
* {
|
|
11
|
+
scrollbar-size-vertical: 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Main content goes on base layer by default */
|
|
15
|
+
|
|
16
|
+
/* Chat area - main scrollable messages area */
|
|
17
|
+
#chat {
|
|
18
|
+
height: 1fr;
|
|
19
|
+
padding: 1 2;
|
|
20
|
+
background: $background;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Welcome banner */
|
|
24
|
+
#welcome-banner {
|
|
25
|
+
height: auto;
|
|
26
|
+
margin-bottom: 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Messages area — uses undocumented "stream" layout (Textual ≥5.2.0) for
|
|
30
|
+
O(1) append performance via incremental placement caching. */
|
|
31
|
+
#messages {
|
|
32
|
+
layout: stream;
|
|
33
|
+
height: auto;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Bottom app container - holds ChatInput (now inside scroll) */
|
|
37
|
+
#bottom-app-container {
|
|
38
|
+
height: auto;
|
|
39
|
+
margin-top: 1;
|
|
40
|
+
padding: 0 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Sticky thinking row directly above input box */
|
|
44
|
+
#thinking-status {
|
|
45
|
+
height: auto;
|
|
46
|
+
min-height: 1;
|
|
47
|
+
margin-bottom: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Input area */
|
|
51
|
+
#input-area {
|
|
52
|
+
height: auto;
|
|
53
|
+
min-height: 3;
|
|
54
|
+
max-height: 25;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Approval Menu - inline in messages area */
|
|
58
|
+
.approval-menu {
|
|
59
|
+
height: auto;
|
|
60
|
+
margin: 1 0;
|
|
61
|
+
padding: 0 1;
|
|
62
|
+
background: $surface;
|
|
63
|
+
border: solid $warning;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Placeholder shown while the user is actively typing (approval deferred) */
|
|
67
|
+
.approval-placeholder {
|
|
68
|
+
height: auto;
|
|
69
|
+
margin: 1 0;
|
|
70
|
+
padding: 0 1;
|
|
71
|
+
border: solid $panel;
|
|
72
|
+
color: $text-muted;
|
|
73
|
+
text-style: italic;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.approval-menu .approval-title {
|
|
77
|
+
text-style: bold;
|
|
78
|
+
color: $warning;
|
|
79
|
+
margin-bottom: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.approval-menu .approval-info {
|
|
83
|
+
height: auto;
|
|
84
|
+
color: $text-muted;
|
|
85
|
+
margin-bottom: 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.approval-menu .approval-option {
|
|
89
|
+
height: 1;
|
|
90
|
+
padding: 0 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.approval-menu .approval-option-selected {
|
|
94
|
+
background: $primary;
|
|
95
|
+
text-style: bold;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.approval-menu .approval-help {
|
|
99
|
+
color: $text-muted;
|
|
100
|
+
text-style: italic;
|
|
101
|
+
margin-top: 0;
|
|
102
|
+
margin-bottom: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Status bar */
|
|
106
|
+
#status-bar {
|
|
107
|
+
height: 1;
|
|
108
|
+
dock: bottom;
|
|
109
|
+
margin-bottom: 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Tool approval widgets */
|
|
113
|
+
.tool-approval-widget {
|
|
114
|
+
height: auto;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.approval-description {
|
|
118
|
+
color: $text-muted;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Diff styling — used by EditFileApprovalWidget (fg + bg + padding) */
|
|
122
|
+
.diff-removed {
|
|
123
|
+
height: auto;
|
|
124
|
+
color: $text-error;
|
|
125
|
+
background: $error-muted;
|
|
126
|
+
padding: 0 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.diff-added {
|
|
130
|
+
height: auto;
|
|
131
|
+
color: $text-success;
|
|
132
|
+
background: $success-muted;
|
|
133
|
+
padding: 0 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.diff-context {
|
|
137
|
+
height: auto;
|
|
138
|
+
color: $text-muted;
|
|
139
|
+
padding: 0 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Diff line backgrounds — used by compose_diff_lines (bg only, fg via Content) */
|
|
143
|
+
.diff-line-removed {
|
|
144
|
+
background: $error-muted;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.diff-line-added {
|
|
148
|
+
background: $success-muted;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Mode-specific borders for UserMessage */
|
|
152
|
+
UserMessage.-mode-shell {
|
|
153
|
+
border-left: wide $mode-bash;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
UserMessage.-mode-command {
|
|
157
|
+
border-left: wide $mode-command;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ASCII border overrides */
|
|
161
|
+
UserMessage.-ascii {
|
|
162
|
+
border-left: ascii $primary;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
UserMessage.-ascii.-mode-shell {
|
|
166
|
+
border-left: ascii $mode-bash;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
UserMessage.-ascii.-mode-command {
|
|
170
|
+
border-left: ascii $mode-command;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
QueuedUserMessage.-ascii {
|
|
174
|
+
border-left: ascii $panel;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
ToolCallMessage.-ascii {
|
|
178
|
+
border-left: ascii $panel;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
ToolCallMessage.-ascii:hover {
|
|
182
|
+
border-left: ascii $secondary;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Approval command — warning color for shell commands */
|
|
186
|
+
.approval-menu .approval-command {
|
|
187
|
+
height: auto;
|
|
188
|
+
margin: 0 0 1 0;
|
|
189
|
+
padding: 0 1;
|
|
190
|
+
color: $warning;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Separator line between tool details and options */
|
|
194
|
+
.approval-menu .approval-separator {
|
|
195
|
+
height: 1;
|
|
196
|
+
color: $warning;
|
|
197
|
+
margin: 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Scrollable tool info area in approval menu */
|
|
201
|
+
.approval-menu .tool-info-scroll {
|
|
202
|
+
height: auto;
|
|
203
|
+
max-height: 10;
|
|
204
|
+
margin-top: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Inner container for tool info - allows content to expand for scrolling */
|
|
208
|
+
.approval-menu .tool-info-container {
|
|
209
|
+
height: auto;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Options container with background */
|
|
213
|
+
.approval-menu .approval-options-container {
|
|
214
|
+
height: auto;
|
|
215
|
+
background: $surface-darken-1;
|
|
216
|
+
padding: 0 1;
|
|
217
|
+
margin-top: 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Ask user widget */
|
|
221
|
+
.ask-user-menu {
|
|
222
|
+
height: auto;
|
|
223
|
+
margin: 1 0;
|
|
224
|
+
padding: 0 1;
|
|
225
|
+
background: $surface;
|
|
226
|
+
border: solid $success;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.ask-user-menu .ask-user-title {
|
|
230
|
+
text-style: bold;
|
|
231
|
+
color: $success;
|
|
232
|
+
margin-bottom: 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.ask-user-menu .ask-user-help {
|
|
236
|
+
color: $text-muted;
|
|
237
|
+
text-style: italic;
|
|
238
|
+
margin-top: 0;
|
|
239
|
+
margin-bottom: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.ask-user-menu .ask-user-questions {
|
|
243
|
+
height: auto;
|
|
244
|
+
padding: 0 1;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.ask-user-menu .ask-user-question {
|
|
248
|
+
height: auto;
|
|
249
|
+
margin-bottom: 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.ask-user-menu .ask-user-question-active {
|
|
253
|
+
border-left: thick $success;
|
|
254
|
+
padding-left: 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.ask-user-menu .ask-user-question-inactive {
|
|
258
|
+
opacity: 0.5;
|
|
259
|
+
padding-left: 2;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.ask-user-menu .ask-user-question-text {
|
|
263
|
+
margin: 0 0 0 2;
|
|
264
|
+
padding: 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.ask-user-menu .ask-user-choice {
|
|
268
|
+
height: 1;
|
|
269
|
+
padding: 0 1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.ask-user-menu .ask-user-text-input {
|
|
273
|
+
margin: 1 1 0 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.ask-user-menu .ask-user-other-input {
|
|
277
|
+
margin: 1 1 0 1;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* ChatTextArea: override TextArea's built-in $surface background */
|
|
281
|
+
ChatTextArea {
|
|
282
|
+
background: transparent;
|
|
283
|
+
|
|
284
|
+
& .text-area--cursor-line {
|
|
285
|
+
background: transparent;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
& .text-area--cursor-gutter {
|
|
289
|
+
background: transparent;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* Completion popup styling (used by ChatInput) - appears BELOW input */
|
|
294
|
+
#completion-popup {
|
|
295
|
+
height: auto;
|
|
296
|
+
max-height: 12;
|
|
297
|
+
width: 100%;
|
|
298
|
+
margin-left: 3;
|
|
299
|
+
margin-top: 0;
|
|
300
|
+
padding: 0;
|
|
301
|
+
color: $text;
|
|
302
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Unified slash-command registry.
|
|
2
|
+
|
|
3
|
+
Every slash command is declared once as a `SlashCommand` entry in `COMMANDS`.
|
|
4
|
+
Bypass-tier frozensets and autocomplete tuples are derived automatically — no
|
|
5
|
+
other file should hard-code command metadata.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import StrEnum
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from soothe_cli.tui.skills.load import ExtendedSkillMetadata
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BypassTier(StrEnum):
|
|
19
|
+
"""Classification that controls whether a command can skip the message queue."""
|
|
20
|
+
|
|
21
|
+
ALWAYS = "always"
|
|
22
|
+
"""Execute regardless of any busy state, including mid-thread-switch."""
|
|
23
|
+
|
|
24
|
+
CONNECTING = "connecting"
|
|
25
|
+
"""Bypass only during initial server connection, not during agent/shell."""
|
|
26
|
+
|
|
27
|
+
IMMEDIATE_UI = "immediate_ui"
|
|
28
|
+
"""Open modal UI immediately; real work deferred via `_defer_action` callback."""
|
|
29
|
+
|
|
30
|
+
SIDE_EFFECT_FREE = "side_effect_free"
|
|
31
|
+
"""Execute the side effect immediately; defer chat output until idle."""
|
|
32
|
+
|
|
33
|
+
QUEUED = "queued"
|
|
34
|
+
"""Must wait in the queue when the app is busy."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
38
|
+
class SlashCommand:
|
|
39
|
+
"""A single slash-command definition."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
"""Canonical command name (e.g. `/quit`)."""
|
|
43
|
+
|
|
44
|
+
description: str
|
|
45
|
+
"""Short user-facing description."""
|
|
46
|
+
|
|
47
|
+
bypass_tier: BypassTier
|
|
48
|
+
"""Queue-bypass classification."""
|
|
49
|
+
|
|
50
|
+
hidden_keywords: str = ""
|
|
51
|
+
"""Space-separated terms for fuzzy matching (never displayed)."""
|
|
52
|
+
|
|
53
|
+
aliases: tuple[str, ...] = ()
|
|
54
|
+
"""Alternative names (e.g. `("/q",)` for `/quit`)."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
COMMANDS: tuple[SlashCommand, ...] = (
|
|
58
|
+
SlashCommand(
|
|
59
|
+
name="/autopilot",
|
|
60
|
+
description="Open autopilot dashboard",
|
|
61
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
62
|
+
hidden_keywords="goals autonomous",
|
|
63
|
+
),
|
|
64
|
+
SlashCommand(
|
|
65
|
+
name="/clear",
|
|
66
|
+
description="Clear chat and start new thread",
|
|
67
|
+
bypass_tier=BypassTier.QUEUED,
|
|
68
|
+
hidden_keywords="reset",
|
|
69
|
+
),
|
|
70
|
+
SlashCommand(
|
|
71
|
+
name="/editor",
|
|
72
|
+
description="Open prompt in external editor ($EDITOR)",
|
|
73
|
+
bypass_tier=BypassTier.QUEUED,
|
|
74
|
+
),
|
|
75
|
+
SlashCommand(
|
|
76
|
+
name="/mcp",
|
|
77
|
+
description="Show active MCP servers and tools",
|
|
78
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
79
|
+
hidden_keywords="servers",
|
|
80
|
+
),
|
|
81
|
+
SlashCommand(
|
|
82
|
+
name="/model",
|
|
83
|
+
description="Switch or configure model (--model-params, --default)",
|
|
84
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
85
|
+
),
|
|
86
|
+
SlashCommand(
|
|
87
|
+
name="/notifications",
|
|
88
|
+
description="Configure startup warning preferences",
|
|
89
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
90
|
+
hidden_keywords="warnings alerts suppress",
|
|
91
|
+
),
|
|
92
|
+
SlashCommand( # Static alias; not auto-generated from skill discovery
|
|
93
|
+
name="/remember",
|
|
94
|
+
description="Update memory and skills from conversation",
|
|
95
|
+
bypass_tier=BypassTier.QUEUED,
|
|
96
|
+
),
|
|
97
|
+
SlashCommand(
|
|
98
|
+
name="/threads",
|
|
99
|
+
description="Browse and resume previous threads",
|
|
100
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
101
|
+
hidden_keywords="continue history sessions",
|
|
102
|
+
),
|
|
103
|
+
SlashCommand(
|
|
104
|
+
name="/trace",
|
|
105
|
+
description="Open current thread in LangSmith",
|
|
106
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
107
|
+
),
|
|
108
|
+
SlashCommand(
|
|
109
|
+
name="/tokens",
|
|
110
|
+
description="Token usage",
|
|
111
|
+
bypass_tier=BypassTier.QUEUED,
|
|
112
|
+
hidden_keywords="cost",
|
|
113
|
+
),
|
|
114
|
+
SlashCommand(
|
|
115
|
+
name="/reload",
|
|
116
|
+
description="Reload config from environment variables and .env",
|
|
117
|
+
bypass_tier=BypassTier.QUEUED,
|
|
118
|
+
hidden_keywords="refresh",
|
|
119
|
+
),
|
|
120
|
+
SlashCommand(
|
|
121
|
+
name="/theme",
|
|
122
|
+
description="Switch color theme",
|
|
123
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
124
|
+
hidden_keywords="dark light color appearance",
|
|
125
|
+
),
|
|
126
|
+
SlashCommand(
|
|
127
|
+
name="/update",
|
|
128
|
+
description="Check for and install updates",
|
|
129
|
+
bypass_tier=BypassTier.QUEUED,
|
|
130
|
+
hidden_keywords="upgrade",
|
|
131
|
+
),
|
|
132
|
+
SlashCommand(
|
|
133
|
+
name="/auto-update",
|
|
134
|
+
description="Toggle automatic updates on or off",
|
|
135
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
136
|
+
),
|
|
137
|
+
SlashCommand(
|
|
138
|
+
name="/changelog",
|
|
139
|
+
description="Open changelog in browser",
|
|
140
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
141
|
+
),
|
|
142
|
+
SlashCommand(
|
|
143
|
+
name="/version",
|
|
144
|
+
description="Show version",
|
|
145
|
+
bypass_tier=BypassTier.CONNECTING,
|
|
146
|
+
),
|
|
147
|
+
SlashCommand(
|
|
148
|
+
name="/feedback",
|
|
149
|
+
description="Submit a bug report or feature request",
|
|
150
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
151
|
+
),
|
|
152
|
+
SlashCommand(
|
|
153
|
+
name="/docs",
|
|
154
|
+
description="Open documentation in browser",
|
|
155
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
156
|
+
),
|
|
157
|
+
SlashCommand(
|
|
158
|
+
name="/help",
|
|
159
|
+
description="Show help",
|
|
160
|
+
bypass_tier=BypassTier.QUEUED,
|
|
161
|
+
),
|
|
162
|
+
SlashCommand(
|
|
163
|
+
name="/quit",
|
|
164
|
+
description="Exit app",
|
|
165
|
+
bypass_tier=BypassTier.ALWAYS,
|
|
166
|
+
hidden_keywords="close leave",
|
|
167
|
+
aliases=("/q",),
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
"""All slash commands."""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Derived bypass-tier frozensets
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _build_bypass_set(tier: BypassTier) -> frozenset[str]:
|
|
179
|
+
"""Build a frozenset of command names (including aliases) for a tier.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
tier: The bypass tier to collect.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Frozenset of all names and aliases that belong to `tier`.
|
|
186
|
+
"""
|
|
187
|
+
names: set[str] = set()
|
|
188
|
+
for cmd in COMMANDS:
|
|
189
|
+
if cmd.bypass_tier == tier:
|
|
190
|
+
names.add(cmd.name)
|
|
191
|
+
names.update(cmd.aliases)
|
|
192
|
+
return frozenset(names)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
ALWAYS_IMMEDIATE: frozenset[str] = _build_bypass_set(BypassTier.ALWAYS)
|
|
196
|
+
"""Commands that execute regardless of any busy state."""
|
|
197
|
+
|
|
198
|
+
BYPASS_WHEN_CONNECTING: frozenset[str] = _build_bypass_set(BypassTier.CONNECTING)
|
|
199
|
+
"""Commands that bypass only during initial server connection."""
|
|
200
|
+
|
|
201
|
+
IMMEDIATE_UI: frozenset[str] = _build_bypass_set(BypassTier.IMMEDIATE_UI)
|
|
202
|
+
"""Commands that open modal UI immediately, deferring real work."""
|
|
203
|
+
|
|
204
|
+
SIDE_EFFECT_FREE: frozenset[str] = _build_bypass_set(BypassTier.SIDE_EFFECT_FREE)
|
|
205
|
+
"""Commands whose side effect fires immediately; chat output deferred until idle."""
|
|
206
|
+
|
|
207
|
+
QUEUE_BOUND: frozenset[str] = _build_bypass_set(BypassTier.QUEUED)
|
|
208
|
+
"""Commands that must wait in the queue when the app is busy."""
|
|
209
|
+
|
|
210
|
+
HIDDEN_DEBUG: frozenset[str] = frozenset({"/debug-error"})
|
|
211
|
+
"""Hidden debug commands not exposed in autocomplete or help."""
|
|
212
|
+
|
|
213
|
+
ALL_CLASSIFIED: frozenset[str] = (
|
|
214
|
+
ALWAYS_IMMEDIATE
|
|
215
|
+
| BYPASS_WHEN_CONNECTING
|
|
216
|
+
| IMMEDIATE_UI
|
|
217
|
+
| SIDE_EFFECT_FREE
|
|
218
|
+
| QUEUE_BOUND
|
|
219
|
+
| HIDDEN_DEBUG
|
|
220
|
+
)
|
|
221
|
+
"""Union of all tiers plus hidden debug commands — used by drift tests."""
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# Autocomplete tuples
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
SLASH_COMMANDS: list[tuple[str, str, str]] = [
|
|
229
|
+
(cmd.name, cmd.description, cmd.hidden_keywords) for cmd in COMMANDS
|
|
230
|
+
]
|
|
231
|
+
"""`(name, description, hidden_keywords)` tuples for `SlashCommandController`."""
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def parse_skill_command(command: str) -> tuple[str, str]:
|
|
235
|
+
"""Extract skill name and args from a `/skill:<name>` command.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
command: The full command string (e.g., `/skill:web-research find X`).
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Tuple of `(skill_name, args)`.
|
|
242
|
+
|
|
243
|
+
The skill name is normalized to lowercase. Both are empty strings
|
|
244
|
+
when the command has no skill name after the prefix.
|
|
245
|
+
"""
|
|
246
|
+
after_prefix = command[len("/skill:") :].strip()
|
|
247
|
+
parts = after_prefix.split(maxsplit=1)
|
|
248
|
+
if not parts or not parts[0]:
|
|
249
|
+
return "", ""
|
|
250
|
+
skill_name = parts[0].lower()
|
|
251
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
252
|
+
return skill_name, args
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
_STATIC_SKILL_ALIASES: frozenset[str] = frozenset({"remember"})
|
|
256
|
+
"""Built-in skill names that have a dedicated top-level slash command.
|
|
257
|
+
|
|
258
|
+
Only list skills whose `/skill:<name>` form is redundant because a `/<name>`
|
|
259
|
+
convenience alias exists in `COMMANDS`. Do **not** add every command name
|
|
260
|
+
here — that would silently suppress unrelated user skills that happen to share a
|
|
261
|
+
name with a slash command (e.g., a user skill called `model` should still
|
|
262
|
+
appear as `/skill:model`).
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def build_skill_commands_from_wire(
|
|
267
|
+
rows: list[dict[str, Any]],
|
|
268
|
+
) -> list[tuple[str, str, str]]:
|
|
269
|
+
"""Build autocomplete tuples from daemon ``skills_list_response`` rows.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
rows: Wire-safe dicts with at least ``name`` and optional ``description``.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Sorted list of ``(name, description, hidden_keywords)`` tuples.
|
|
276
|
+
"""
|
|
277
|
+
tuples: list[tuple[str, str, str]] = []
|
|
278
|
+
for row in rows:
|
|
279
|
+
name = str(row.get("name", "")).strip().lower()
|
|
280
|
+
if not name or name in _STATIC_SKILL_ALIASES:
|
|
281
|
+
continue
|
|
282
|
+
desc = str(row.get("description", "")).strip()
|
|
283
|
+
tuples.append((f"/skill:{name}", desc, name))
|
|
284
|
+
tuples.sort(key=lambda t: t[0].lower())
|
|
285
|
+
return tuples
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def build_skill_commands(
|
|
289
|
+
skills: list[ExtendedSkillMetadata],
|
|
290
|
+
) -> list[tuple[str, str, str]]:
|
|
291
|
+
"""Build autocomplete tuples for discovered skills.
|
|
292
|
+
|
|
293
|
+
Each skill becomes a `/skill:<name>` entry with its description
|
|
294
|
+
and the skill name as a hidden keyword for fuzzy matching.
|
|
295
|
+
|
|
296
|
+
Skills that already have a dedicated slash command in `COMMANDS`
|
|
297
|
+
(e.g., `remember` → `/remember`) are excluded to avoid duplicate
|
|
298
|
+
autocomplete entries.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
skills: List of discovered skill metadata.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
List of `(name, description, hidden_keywords)` tuples.
|
|
305
|
+
"""
|
|
306
|
+
return [
|
|
307
|
+
(f"/skill:{skill['name']}", skill["description"], skill["name"])
|
|
308
|
+
for skill in skills
|
|
309
|
+
if skill["name"] not in _STATIC_SKILL_ALIASES
|
|
310
|
+
]
|