zjcode 0.0.1__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.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Clipboard utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import pathlib
|
|
9
|
+
from typing import TYPE_CHECKING, Literal
|
|
10
|
+
|
|
11
|
+
from textual.dom import NoScreen
|
|
12
|
+
|
|
13
|
+
from deepagents_code.config import get_glyphs
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
|
|
20
|
+
from textual.app import App
|
|
21
|
+
|
|
22
|
+
_PREVIEW_MAX_LENGTH = 40
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _copy_osc52(text: str) -> None:
|
|
26
|
+
"""Copy text using OSC 52 escape sequence (works over SSH/tmux)."""
|
|
27
|
+
encoded = base64.b64encode(text.encode("utf-8")).decode("ascii")
|
|
28
|
+
osc52_seq = f"\033]52;c;{encoded}\a"
|
|
29
|
+
if os.environ.get("TMUX"):
|
|
30
|
+
osc52_seq = f"\033Ptmux;\033{osc52_seq}\033\\"
|
|
31
|
+
|
|
32
|
+
with pathlib.Path("/dev/tty").open("w", encoding="utf-8") as tty:
|
|
33
|
+
tty.write(osc52_seq)
|
|
34
|
+
tty.flush()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _shorten_preview(texts: list[str]) -> str:
|
|
38
|
+
"""Shorten text for notification preview.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Shortened preview text suitable for notification display.
|
|
42
|
+
"""
|
|
43
|
+
glyphs = get_glyphs()
|
|
44
|
+
dense_text = glyphs.newline.join(texts).replace("\n", glyphs.newline)
|
|
45
|
+
if len(dense_text) > _PREVIEW_MAX_LENGTH:
|
|
46
|
+
return f"{dense_text[: _PREVIEW_MAX_LENGTH - 1]}{glyphs.ellipsis}"
|
|
47
|
+
return dense_text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def copy_text_to_clipboard(app: App, text: str) -> tuple[bool, str | None]:
|
|
51
|
+
"""Copy text to the system clipboard.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
app: The active Textual app, used for the app clipboard backend.
|
|
55
|
+
text: Text to copy.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Tuple of `(success, error_message)`.
|
|
59
|
+
|
|
60
|
+
`success` is `True` when one backend completed without raising.
|
|
61
|
+
`error_message` is `None` on success and the last backend's error
|
|
62
|
+
string when every backend failed, suitable for surfacing to the
|
|
63
|
+
user so they can self-diagnose missing clipboard support.
|
|
64
|
+
"""
|
|
65
|
+
# Backend order: pyperclip first (most reliable when installed), then
|
|
66
|
+
# Textual's app clipboard, then OSC 52 as a last resort for SSH/remote
|
|
67
|
+
# sessions where no local clipboard is reachable.
|
|
68
|
+
copy_methods: list[Callable[[str], object]] = [app.copy_to_clipboard]
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
import pyperclip
|
|
72
|
+
|
|
73
|
+
copy_methods.insert(0, pyperclip.copy)
|
|
74
|
+
except ImportError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
copy_methods.append(_copy_osc52)
|
|
78
|
+
|
|
79
|
+
last_error: str | None = None
|
|
80
|
+
for copy_fn in copy_methods:
|
|
81
|
+
try:
|
|
82
|
+
copy_fn(text)
|
|
83
|
+
# ValueError covers UnicodeEncodeError from the `text.encode("utf-8")`
|
|
84
|
+
# done by both `_copy_osc52` and `App.copy_to_clipboard` on draft text
|
|
85
|
+
# containing lone surrogates, so a bad code point degrades to the next
|
|
86
|
+
# backend instead of escaping the (success, error) contract and crashing
|
|
87
|
+
# the caller.
|
|
88
|
+
except (OSError, RuntimeError, TypeError, ValueError) as e:
|
|
89
|
+
last_error = str(e) or type(e).__name__
|
|
90
|
+
logger.debug(
|
|
91
|
+
"Clipboard copy method %s failed: %s",
|
|
92
|
+
getattr(copy_fn, "__name__", repr(copy_fn)),
|
|
93
|
+
e,
|
|
94
|
+
exc_info=True,
|
|
95
|
+
)
|
|
96
|
+
continue
|
|
97
|
+
else:
|
|
98
|
+
return True, None
|
|
99
|
+
|
|
100
|
+
return False, last_error
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def copy_text_with_feedback(
|
|
104
|
+
app: App,
|
|
105
|
+
text: str,
|
|
106
|
+
*,
|
|
107
|
+
failure_noun: Literal["input", "selection"],
|
|
108
|
+
success_message: str | None = None,
|
|
109
|
+
) -> bool:
|
|
110
|
+
"""Copy text to the clipboard and surface the outcome as a toast.
|
|
111
|
+
|
|
112
|
+
Centralizes the copy-then-notify pattern shared by the Ctrl+C and
|
|
113
|
+
`[ COPY ]` paths so the success/failure messaging stays consistent.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
app: The active Textual app, used for the clipboard backend and toasts.
|
|
117
|
+
text: Text to copy.
|
|
118
|
+
failure_noun: Noun used in the warning toast (e.g. `"input"`,
|
|
119
|
+
`"selection"`).
|
|
120
|
+
success_message: Toast shown on success. When `None`, success is silent
|
|
121
|
+
(used by the selection-copy path, which has no visible draft to
|
|
122
|
+
confirm).
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
`True` when the copy succeeded.
|
|
126
|
+
"""
|
|
127
|
+
success, error = copy_text_to_clipboard(app, text)
|
|
128
|
+
if success:
|
|
129
|
+
if success_message is not None:
|
|
130
|
+
app.notify(success_message, timeout=3, markup=False)
|
|
131
|
+
else:
|
|
132
|
+
# markup=False so the dynamic error string is never parsed as markup.
|
|
133
|
+
app.notify(
|
|
134
|
+
f"Failed to copy {failure_noun}: {error}"
|
|
135
|
+
if error
|
|
136
|
+
else f"Failed to copy {failure_noun} - no clipboard method available",
|
|
137
|
+
severity="warning",
|
|
138
|
+
timeout=3,
|
|
139
|
+
markup=False,
|
|
140
|
+
)
|
|
141
|
+
return success
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def copy_selection_to_clipboard(app: App) -> None:
|
|
145
|
+
"""Copy selected text from app widgets to clipboard.
|
|
146
|
+
|
|
147
|
+
This queries all widgets for their text_selection and copies
|
|
148
|
+
any selected text to the system clipboard.
|
|
149
|
+
"""
|
|
150
|
+
selected_texts = []
|
|
151
|
+
|
|
152
|
+
for widget in app.query("*"):
|
|
153
|
+
# Skip detached widgets before touching `text_selection` — the
|
|
154
|
+
# property reads `widget.screen.selections`, which raises `NoScreen`
|
|
155
|
+
# for transient toasts mid-mount. `hasattr` is not a safe precheck
|
|
156
|
+
# because it would itself invoke the property and propagate
|
|
157
|
+
# `NoScreen`. The try/except handles lifecycle races between the
|
|
158
|
+
# `is_attached` check and the property read.
|
|
159
|
+
if not getattr(widget, "is_attached", False):
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
selection = widget.text_selection
|
|
164
|
+
except (NoScreen, AttributeError) as e:
|
|
165
|
+
logger.debug(
|
|
166
|
+
"Skipping widget %s during selection copy: %s",
|
|
167
|
+
type(widget).__name__,
|
|
168
|
+
e,
|
|
169
|
+
)
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
if not selection:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
result = widget.get_selection(selection)
|
|
177
|
+
except (AttributeError, TypeError, ValueError, IndexError) as e:
|
|
178
|
+
logger.debug(
|
|
179
|
+
"Failed to get selection from widget %s: %s",
|
|
180
|
+
type(widget).__name__,
|
|
181
|
+
e,
|
|
182
|
+
exc_info=True,
|
|
183
|
+
)
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
if not result:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
selected_text, _ = result
|
|
190
|
+
if selected_text.strip():
|
|
191
|
+
selected_texts.append(selected_text)
|
|
192
|
+
|
|
193
|
+
if not selected_texts:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
combined_text = "\n".join(selected_texts)
|
|
197
|
+
|
|
198
|
+
success, _ = copy_text_to_clipboard(app, combined_text)
|
|
199
|
+
if success:
|
|
200
|
+
# Use markup=False to prevent copied text from being parsed as Rich markup
|
|
201
|
+
app.notify(
|
|
202
|
+
f'"{_shorten_preview(selected_texts)}" copied',
|
|
203
|
+
severity="information",
|
|
204
|
+
timeout=2,
|
|
205
|
+
markup=False,
|
|
206
|
+
)
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
# If all methods fail, still notify but warn. markup=False guards against
|
|
210
|
+
# this string ever growing dynamic content (e.g., the backend error reason)
|
|
211
|
+
# that could contain bracket characters.
|
|
212
|
+
app.notify(
|
|
213
|
+
"Failed to copy - no clipboard method available",
|
|
214
|
+
severity="warning",
|
|
215
|
+
timeout=3,
|
|
216
|
+
markup=False,
|
|
217
|
+
)
|
|
@@ -0,0 +1,450 @@
|
|
|
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 entries 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, NamedTuple
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from deepagents_code.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
|
+
argument_hint: str = ""
|
|
54
|
+
"""Placeholder text for autocomplete when the command accepts args."""
|
|
55
|
+
|
|
56
|
+
aliases: tuple[str, ...] = ()
|
|
57
|
+
"""Alternative names (e.g. `("/q",)` for `/quit`)."""
|
|
58
|
+
|
|
59
|
+
def to_entry(self) -> CommandEntry:
|
|
60
|
+
"""Project this command into a `CommandEntry` for autocomplete.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A `CommandEntry` carrying only the fields the autocomplete
|
|
64
|
+
layer needs.
|
|
65
|
+
"""
|
|
66
|
+
return CommandEntry(
|
|
67
|
+
name=self.name,
|
|
68
|
+
description=self.description,
|
|
69
|
+
hidden_keywords=self.hidden_keywords,
|
|
70
|
+
argument_hint=self.argument_hint,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
COMMANDS: tuple[SlashCommand, ...] = (
|
|
75
|
+
SlashCommand(
|
|
76
|
+
name="/agents",
|
|
77
|
+
description="Browse and switch between available agents",
|
|
78
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
79
|
+
hidden_keywords="switch profile persona",
|
|
80
|
+
),
|
|
81
|
+
SlashCommand(
|
|
82
|
+
name="/add-provider",
|
|
83
|
+
description="Register a custom model provider in config.toml",
|
|
84
|
+
bypass_tier=BypassTier.QUEUED,
|
|
85
|
+
hidden_keywords="custom provider gateway endpoint openai-compat",
|
|
86
|
+
argument_hint='<provider-id> "<display-name>" <base-url> "<models>" '
|
|
87
|
+
"[class-path] [api-key-env] [max-input-tokens]",
|
|
88
|
+
),
|
|
89
|
+
SlashCommand(
|
|
90
|
+
name="/auth",
|
|
91
|
+
description="Connect and manage provider and service credentials",
|
|
92
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
93
|
+
hidden_keywords=(
|
|
94
|
+
"key keys credential credentials login token api tracing langsmith"
|
|
95
|
+
),
|
|
96
|
+
aliases=("/connect",),
|
|
97
|
+
),
|
|
98
|
+
SlashCommand(
|
|
99
|
+
name="/clear",
|
|
100
|
+
description="Clear the chat and start a new thread",
|
|
101
|
+
bypass_tier=BypassTier.QUEUED,
|
|
102
|
+
hidden_keywords="reset",
|
|
103
|
+
),
|
|
104
|
+
SlashCommand(
|
|
105
|
+
name="/copy",
|
|
106
|
+
description="Copy the latest assistant message to clipboard",
|
|
107
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
108
|
+
),
|
|
109
|
+
SlashCommand(
|
|
110
|
+
name="/force-clear",
|
|
111
|
+
description="Stop active work, clear the chat, and start a new thread",
|
|
112
|
+
bypass_tier=BypassTier.ALWAYS,
|
|
113
|
+
hidden_keywords="reset interrupt",
|
|
114
|
+
),
|
|
115
|
+
SlashCommand(
|
|
116
|
+
name="/goal",
|
|
117
|
+
description="Set and manage a persistent objective with acceptance criteria",
|
|
118
|
+
bypass_tier=BypassTier.QUEUED,
|
|
119
|
+
hidden_keywords=(
|
|
120
|
+
"objective criteria acceptance amend pause resume rubric grader grading "
|
|
121
|
+
"model iterations"
|
|
122
|
+
),
|
|
123
|
+
argument_hint=(
|
|
124
|
+
"[<objective>|amend <feedback>|pause|resume|show|clear|model|"
|
|
125
|
+
"max-iterations]"
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
SlashCommand(
|
|
129
|
+
name="/editor",
|
|
130
|
+
description="Open prompt in an external editor ($EDITOR)",
|
|
131
|
+
bypass_tier=BypassTier.QUEUED,
|
|
132
|
+
),
|
|
133
|
+
SlashCommand(
|
|
134
|
+
name="/effort",
|
|
135
|
+
description="Set reasoning effort for the current model",
|
|
136
|
+
bypass_tier=BypassTier.QUEUED,
|
|
137
|
+
hidden_keywords="reasoning thinking level",
|
|
138
|
+
argument_hint="[none|low|medium|high|xhigh|max|clear]",
|
|
139
|
+
),
|
|
140
|
+
SlashCommand(
|
|
141
|
+
name="/mcp",
|
|
142
|
+
description="Manage MCP servers and authentication",
|
|
143
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
144
|
+
hidden_keywords="servers oauth authenticate reconnect disable enable",
|
|
145
|
+
argument_hint="[login <server> | reconnect]",
|
|
146
|
+
),
|
|
147
|
+
SlashCommand(
|
|
148
|
+
name="/model",
|
|
149
|
+
description="Switch models or edit model settings",
|
|
150
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
151
|
+
argument_hint=(
|
|
152
|
+
"[<provider>:<model>|--model-params JSON|--default <model>|--clear]"
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
SlashCommand(
|
|
156
|
+
name="/notifications",
|
|
157
|
+
description="Configure startup warnings",
|
|
158
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
159
|
+
hidden_keywords="warnings alerts suppress",
|
|
160
|
+
),
|
|
161
|
+
SlashCommand(
|
|
162
|
+
name="/offload",
|
|
163
|
+
description="Summarize and offload older messages to free context",
|
|
164
|
+
bypass_tier=BypassTier.QUEUED,
|
|
165
|
+
hidden_keywords="compact",
|
|
166
|
+
aliases=("/compact",),
|
|
167
|
+
),
|
|
168
|
+
SlashCommand( # Static alias; not auto-generated from skill discovery
|
|
169
|
+
name="/remember",
|
|
170
|
+
description="Save useful context to memory or skills",
|
|
171
|
+
bypass_tier=BypassTier.QUEUED,
|
|
172
|
+
argument_hint="[context]",
|
|
173
|
+
),
|
|
174
|
+
SlashCommand( # Static alias; not auto-generated from skill discovery
|
|
175
|
+
name="/skill-creator",
|
|
176
|
+
description="Create or refine agent skills",
|
|
177
|
+
bypass_tier=BypassTier.QUEUED,
|
|
178
|
+
argument_hint="[task]",
|
|
179
|
+
),
|
|
180
|
+
SlashCommand(
|
|
181
|
+
name="/threads",
|
|
182
|
+
description="Browse and resume past threads",
|
|
183
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
184
|
+
hidden_keywords="continue history sessions resume back previous",
|
|
185
|
+
argument_hint="[-r [ID]]",
|
|
186
|
+
),
|
|
187
|
+
SlashCommand(
|
|
188
|
+
name="/trace",
|
|
189
|
+
description="Open this thread in LangSmith",
|
|
190
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
191
|
+
),
|
|
192
|
+
SlashCommand(
|
|
193
|
+
name="/tokens",
|
|
194
|
+
description="Show token usage",
|
|
195
|
+
bypass_tier=BypassTier.QUEUED,
|
|
196
|
+
hidden_keywords="cost",
|
|
197
|
+
),
|
|
198
|
+
SlashCommand(
|
|
199
|
+
name="/tools",
|
|
200
|
+
description="List the tools available to the agent",
|
|
201
|
+
bypass_tier=BypassTier.QUEUED,
|
|
202
|
+
hidden_keywords="mcp functions capabilities builtin built-in",
|
|
203
|
+
),
|
|
204
|
+
SlashCommand(
|
|
205
|
+
name="/reload",
|
|
206
|
+
description="Reload environment and config",
|
|
207
|
+
bypass_tier=BypassTier.QUEUED,
|
|
208
|
+
hidden_keywords="refresh",
|
|
209
|
+
),
|
|
210
|
+
SlashCommand(
|
|
211
|
+
name="/rubric",
|
|
212
|
+
description="Set explicit acceptance criteria for rubric grading",
|
|
213
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
214
|
+
hidden_keywords="criteria acceptance grader grading evaluation iterations",
|
|
215
|
+
argument_hint="[set|next|file|show|clear|model|max-iterations]",
|
|
216
|
+
aliases=("/criteria",),
|
|
217
|
+
),
|
|
218
|
+
SlashCommand(
|
|
219
|
+
name="/restart",
|
|
220
|
+
description="Restart the agent server",
|
|
221
|
+
bypass_tier=BypassTier.ALWAYS,
|
|
222
|
+
hidden_keywords="respawn server",
|
|
223
|
+
),
|
|
224
|
+
SlashCommand(
|
|
225
|
+
name="/theme",
|
|
226
|
+
description="Change color theme",
|
|
227
|
+
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
228
|
+
hidden_keywords="dark light color appearance",
|
|
229
|
+
),
|
|
230
|
+
SlashCommand(
|
|
231
|
+
name="/scrollbar",
|
|
232
|
+
description="Show or hide the chat scrollbar",
|
|
233
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
234
|
+
hidden_keywords="scroll scroller bar vertical",
|
|
235
|
+
),
|
|
236
|
+
SlashCommand(
|
|
237
|
+
name="/timestamps",
|
|
238
|
+
description="Show or hide message timestamps",
|
|
239
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
240
|
+
hidden_keywords="time footer footers date dates",
|
|
241
|
+
),
|
|
242
|
+
SlashCommand(
|
|
243
|
+
name="/update",
|
|
244
|
+
description="Check for and install updates",
|
|
245
|
+
bypass_tier=BypassTier.QUEUED,
|
|
246
|
+
hidden_keywords="upgrade dependencies deps refresh",
|
|
247
|
+
argument_hint="[--deps] [--prerelease]",
|
|
248
|
+
),
|
|
249
|
+
SlashCommand(
|
|
250
|
+
name="/install",
|
|
251
|
+
description="Install an optional integration",
|
|
252
|
+
bypass_tier=BypassTier.QUEUED,
|
|
253
|
+
hidden_keywords="extra extras add provider sandbox dependency",
|
|
254
|
+
argument_hint="<extra> [--force]",
|
|
255
|
+
),
|
|
256
|
+
SlashCommand(
|
|
257
|
+
name="/auto-update",
|
|
258
|
+
description="Turn automatic updates on or off",
|
|
259
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
260
|
+
),
|
|
261
|
+
SlashCommand(
|
|
262
|
+
name="/changelog",
|
|
263
|
+
description="Open the changelog in a browser",
|
|
264
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
265
|
+
),
|
|
266
|
+
SlashCommand(
|
|
267
|
+
name="/version",
|
|
268
|
+
description="Show version information",
|
|
269
|
+
bypass_tier=BypassTier.CONNECTING,
|
|
270
|
+
aliases=("/about",),
|
|
271
|
+
),
|
|
272
|
+
SlashCommand(
|
|
273
|
+
name="/feedback",
|
|
274
|
+
description="Send feedback or report an issue",
|
|
275
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
276
|
+
),
|
|
277
|
+
SlashCommand(
|
|
278
|
+
name="/docs",
|
|
279
|
+
description="Open the docs",
|
|
280
|
+
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
281
|
+
),
|
|
282
|
+
SlashCommand(
|
|
283
|
+
name="/help",
|
|
284
|
+
description="Show help and available commands",
|
|
285
|
+
bypass_tier=BypassTier.QUEUED,
|
|
286
|
+
),
|
|
287
|
+
SlashCommand(
|
|
288
|
+
name="/quit",
|
|
289
|
+
description="Exit app",
|
|
290
|
+
bypass_tier=BypassTier.ALWAYS,
|
|
291
|
+
hidden_keywords="close leave",
|
|
292
|
+
aliases=("/q",),
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
"""All slash commands."""
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
# Derived bypass-tier frozensets
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _build_bypass_set(tier: BypassTier) -> frozenset[str]:
|
|
304
|
+
"""Build a frozenset of command names (including aliases) for a tier.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
tier: The bypass tier to collect.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Frozenset of all names and aliases that belong to `tier`.
|
|
311
|
+
"""
|
|
312
|
+
names: set[str] = set()
|
|
313
|
+
for cmd in COMMANDS:
|
|
314
|
+
if cmd.bypass_tier == tier:
|
|
315
|
+
names.add(cmd.name)
|
|
316
|
+
names.update(cmd.aliases)
|
|
317
|
+
return frozenset(names)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
ALWAYS_IMMEDIATE: frozenset[str] = _build_bypass_set(BypassTier.ALWAYS)
|
|
321
|
+
"""Commands that execute regardless of any busy state."""
|
|
322
|
+
|
|
323
|
+
BYPASS_WHEN_CONNECTING: frozenset[str] = _build_bypass_set(BypassTier.CONNECTING)
|
|
324
|
+
"""Commands that bypass only during initial server connection."""
|
|
325
|
+
|
|
326
|
+
IMMEDIATE_UI: frozenset[str] = _build_bypass_set(BypassTier.IMMEDIATE_UI)
|
|
327
|
+
"""Commands that open modal UI immediately, deferring real work."""
|
|
328
|
+
|
|
329
|
+
SIDE_EFFECT_FREE: frozenset[str] = _build_bypass_set(BypassTier.SIDE_EFFECT_FREE)
|
|
330
|
+
"""Commands whose side effect fires immediately; chat output deferred until idle."""
|
|
331
|
+
|
|
332
|
+
QUEUE_BOUND: frozenset[str] = _build_bypass_set(BypassTier.QUEUED)
|
|
333
|
+
"""Commands that must wait in the queue when the app is busy."""
|
|
334
|
+
|
|
335
|
+
HIDDEN_COMMANDS: frozenset[str] = frozenset({"/debug", "/debug-error"})
|
|
336
|
+
"""Power-user commands kept out of autocomplete and help."""
|
|
337
|
+
|
|
338
|
+
STARTUP_RECOVERY_COMMANDS: frozenset[str] = frozenset(
|
|
339
|
+
{"/install", "/reload", "/update"}
|
|
340
|
+
)
|
|
341
|
+
"""`QUEUED`-tier commands that must still run when startup has failed.
|
|
342
|
+
|
|
343
|
+
When the configured model can't be built (e.g. its provider package is
|
|
344
|
+
missing) the server never starts and the app holds a `_server_startup_error`
|
|
345
|
+
state that parks queued messages. These are the recovery escape hatches for
|
|
346
|
+
that state — install the missing package, reload config/env, or upgrade the
|
|
347
|
+
tool — so they must bypass the queue rather than sit behind the very failure
|
|
348
|
+
they repair. `/model` and `/auth` already escape via `IMMEDIATE_UI` (which
|
|
349
|
+
opens a modal and defers the real work); the commands here instead perform
|
|
350
|
+
their repair work directly, so they stay `QUEUED` and rely on this exemption.
|
|
351
|
+
The bypass itself is gated in `_can_bypass_queue`. Every entry is also
|
|
352
|
+
`QUEUE_BOUND` — the recovery exemption is orthogonal to the normal queue.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
ALL_CLASSIFIED: frozenset[str] = (
|
|
356
|
+
ALWAYS_IMMEDIATE
|
|
357
|
+
| BYPASS_WHEN_CONNECTING
|
|
358
|
+
| IMMEDIATE_UI
|
|
359
|
+
| SIDE_EFFECT_FREE
|
|
360
|
+
| QUEUE_BOUND
|
|
361
|
+
| HIDDEN_COMMANDS
|
|
362
|
+
)
|
|
363
|
+
"""Union of all tiers plus hidden commands — used by drift tests."""
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ---------------------------------------------------------------------------
|
|
367
|
+
# Autocomplete entries
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class CommandEntry(NamedTuple):
|
|
372
|
+
"""A single autocomplete entry for the slash-command controller."""
|
|
373
|
+
|
|
374
|
+
name: str
|
|
375
|
+
"""Canonical command name (e.g. `/quit`)."""
|
|
376
|
+
|
|
377
|
+
description: str
|
|
378
|
+
"""Short user-facing description."""
|
|
379
|
+
|
|
380
|
+
hidden_keywords: str
|
|
381
|
+
"""Space-separated terms for fuzzy matching (never displayed)."""
|
|
382
|
+
|
|
383
|
+
argument_hint: str
|
|
384
|
+
"""Placeholder text shown when the command accepts arguments (e.g. `[context]`)."""
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
SLASH_COMMANDS: list[CommandEntry] = [cmd.to_entry() for cmd in COMMANDS]
|
|
388
|
+
"""Autocomplete entries derived from `COMMANDS` for `SlashCommandController`."""
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def parse_skill_command(command: str) -> tuple[str, str]:
|
|
392
|
+
"""Extract skill name and args from a `/skill:<name>` command.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
command: The full command string (e.g., `/skill:web-research find X`).
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Tuple of `(skill_name, args)`.
|
|
399
|
+
|
|
400
|
+
The skill name is normalized to lowercase. Both are empty strings
|
|
401
|
+
when the command has no skill name after the prefix.
|
|
402
|
+
"""
|
|
403
|
+
after_prefix = command[len("/skill:") :].strip()
|
|
404
|
+
parts = after_prefix.split(maxsplit=1)
|
|
405
|
+
if not parts or not parts[0]:
|
|
406
|
+
return "", ""
|
|
407
|
+
skill_name = parts[0].lower()
|
|
408
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
409
|
+
return skill_name, args
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
_STATIC_SKILL_ALIASES: frozenset[str] = frozenset({"remember", "skill-creator"})
|
|
413
|
+
"""Built-in skill names that have a dedicated top-level slash command.
|
|
414
|
+
|
|
415
|
+
Only list skills whose `/skill:<name>` form is redundant because a `/<name>`
|
|
416
|
+
convenience alias exists in `COMMANDS`. Do **not** add every command name
|
|
417
|
+
here — that would silently suppress unrelated user skills that happen to share a
|
|
418
|
+
name with a slash command (e.g., a user skill called `model` should still
|
|
419
|
+
appear as `/skill:model`).
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def build_skill_commands(
|
|
424
|
+
skills: list[ExtendedSkillMetadata],
|
|
425
|
+
) -> list[CommandEntry]:
|
|
426
|
+
"""Build autocomplete entries for discovered skills.
|
|
427
|
+
|
|
428
|
+
Each skill becomes a `/skill:<name>` entry with its description
|
|
429
|
+
and the skill name as a hidden keyword for fuzzy matching.
|
|
430
|
+
|
|
431
|
+
Skills that already have a dedicated slash command in `COMMANDS`
|
|
432
|
+
(e.g., `remember` → `/remember`) are excluded to avoid duplicate
|
|
433
|
+
autocomplete entries.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
skills: List of discovered skill metadata.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
List of `CommandEntry` instances.
|
|
440
|
+
"""
|
|
441
|
+
return [
|
|
442
|
+
CommandEntry(
|
|
443
|
+
name=f"/skill:{skill['name']}",
|
|
444
|
+
description=skill["description"],
|
|
445
|
+
hidden_keywords=skill["name"],
|
|
446
|
+
argument_hint="",
|
|
447
|
+
)
|
|
448
|
+
for skill in skills
|
|
449
|
+
if skill["name"] not in _STATIC_SKILL_ALIASES
|
|
450
|
+
]
|