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.
Files changed (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. 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
+ ]