glaip-sdk 0.0.7__py3-none-any.whl → 0.6.5b6__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 (161) hide show
  1. glaip_sdk/__init__.py +6 -3
  2. glaip_sdk/_version.py +12 -5
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1126 -0
  5. glaip_sdk/branding.py +79 -15
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +699 -0
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents.py +503 -183
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +774 -137
  14. glaip_sdk/cli/commands/mcps.py +1124 -181
  15. glaip_sdk/cli/commands/models.py +25 -10
  16. glaip_sdk/cli/commands/tools.py +144 -92
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +61 -0
  19. glaip_sdk/cli/config.py +95 -0
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +150 -0
  22. glaip_sdk/cli/core/__init__.py +79 -0
  23. glaip_sdk/cli/core/context.py +124 -0
  24. glaip_sdk/cli/core/output.py +846 -0
  25. glaip_sdk/cli/core/prompting.py +649 -0
  26. glaip_sdk/cli/core/rendering.py +187 -0
  27. glaip_sdk/cli/display.py +143 -53
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +24 -18
  30. glaip_sdk/cli/main.py +420 -145
  31. glaip_sdk/cli/masking.py +136 -0
  32. glaip_sdk/cli/mcp_validators.py +287 -0
  33. glaip_sdk/cli/pager.py +266 -0
  34. glaip_sdk/cli/parsers/__init__.py +7 -0
  35. glaip_sdk/cli/parsers/json_input.py +177 -0
  36. glaip_sdk/cli/resolution.py +28 -21
  37. glaip_sdk/cli/rich_helpers.py +27 -0
  38. glaip_sdk/cli/slash/__init__.py +15 -0
  39. glaip_sdk/cli/slash/accounts_controller.py +500 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +282 -0
  42. glaip_sdk/cli/slash/prompt.py +245 -0
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +1679 -0
  45. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  46. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  47. glaip_sdk/cli/slash/tui/accounts_app.py +872 -0
  48. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  49. glaip_sdk/cli/slash/tui/loading.py +58 -0
  50. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  51. glaip_sdk/cli/transcript/__init__.py +31 -0
  52. glaip_sdk/cli/transcript/cache.py +536 -0
  53. glaip_sdk/cli/transcript/capture.py +329 -0
  54. glaip_sdk/cli/transcript/export.py +38 -0
  55. glaip_sdk/cli/transcript/history.py +815 -0
  56. glaip_sdk/cli/transcript/launcher.py +77 -0
  57. glaip_sdk/cli/transcript/viewer.py +372 -0
  58. glaip_sdk/cli/update_notifier.py +290 -0
  59. glaip_sdk/cli/utils.py +247 -1238
  60. glaip_sdk/cli/validators.py +16 -18
  61. glaip_sdk/client/__init__.py +2 -1
  62. glaip_sdk/client/_agent_payloads.py +520 -0
  63. glaip_sdk/client/agent_runs.py +147 -0
  64. glaip_sdk/client/agents.py +940 -574
  65. glaip_sdk/client/base.py +163 -48
  66. glaip_sdk/client/main.py +35 -12
  67. glaip_sdk/client/mcps.py +126 -18
  68. glaip_sdk/client/run_rendering.py +415 -0
  69. glaip_sdk/client/shared.py +21 -0
  70. glaip_sdk/client/tools.py +195 -37
  71. glaip_sdk/client/validators.py +20 -48
  72. glaip_sdk/config/constants.py +15 -5
  73. glaip_sdk/exceptions.py +16 -9
  74. glaip_sdk/icons.py +25 -0
  75. glaip_sdk/mcps/__init__.py +21 -0
  76. glaip_sdk/mcps/base.py +345 -0
  77. glaip_sdk/models/__init__.py +90 -0
  78. glaip_sdk/models/agent.py +47 -0
  79. glaip_sdk/models/agent_runs.py +116 -0
  80. glaip_sdk/models/common.py +42 -0
  81. glaip_sdk/models/mcp.py +33 -0
  82. glaip_sdk/models/tool.py +33 -0
  83. glaip_sdk/payload_schemas/__init__.py +7 -0
  84. glaip_sdk/payload_schemas/agent.py +85 -0
  85. glaip_sdk/registry/__init__.py +55 -0
  86. glaip_sdk/registry/agent.py +164 -0
  87. glaip_sdk/registry/base.py +139 -0
  88. glaip_sdk/registry/mcp.py +253 -0
  89. glaip_sdk/registry/tool.py +231 -0
  90. glaip_sdk/rich_components.py +98 -2
  91. glaip_sdk/runner/__init__.py +59 -0
  92. glaip_sdk/runner/base.py +84 -0
  93. glaip_sdk/runner/deps.py +115 -0
  94. glaip_sdk/runner/langgraph.py +597 -0
  95. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  96. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  97. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +158 -0
  98. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  99. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  100. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  101. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +177 -0
  102. glaip_sdk/tools/__init__.py +22 -0
  103. glaip_sdk/tools/base.py +435 -0
  104. glaip_sdk/utils/__init__.py +59 -13
  105. glaip_sdk/utils/a2a/__init__.py +34 -0
  106. glaip_sdk/utils/a2a/event_processor.py +188 -0
  107. glaip_sdk/utils/agent_config.py +53 -40
  108. glaip_sdk/utils/bundler.py +267 -0
  109. glaip_sdk/utils/client.py +111 -0
  110. glaip_sdk/utils/client_utils.py +58 -26
  111. glaip_sdk/utils/datetime_helpers.py +58 -0
  112. glaip_sdk/utils/discovery.py +78 -0
  113. glaip_sdk/utils/display.py +65 -32
  114. glaip_sdk/utils/export.py +143 -0
  115. glaip_sdk/utils/general.py +1 -36
  116. glaip_sdk/utils/import_export.py +20 -25
  117. glaip_sdk/utils/import_resolver.py +492 -0
  118. glaip_sdk/utils/instructions.py +101 -0
  119. glaip_sdk/utils/rendering/__init__.py +115 -1
  120. glaip_sdk/utils/rendering/formatting.py +85 -43
  121. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  122. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +51 -19
  123. glaip_sdk/utils/rendering/layout/progress.py +202 -0
  124. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  125. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  126. glaip_sdk/utils/rendering/models.py +39 -7
  127. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  128. glaip_sdk/utils/rendering/renderer/base.py +672 -759
  129. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  130. glaip_sdk/utils/rendering/renderer/debug.py +75 -22
  131. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  132. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  133. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  134. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  135. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  136. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  137. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  138. glaip_sdk/utils/rendering/state.py +204 -0
  139. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  140. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  141. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  142. glaip_sdk/utils/rendering/steps/format.py +176 -0
  143. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  144. glaip_sdk/utils/rendering/timing.py +36 -0
  145. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  146. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  147. glaip_sdk/utils/resource_refs.py +29 -26
  148. glaip_sdk/utils/runtime_config.py +422 -0
  149. glaip_sdk/utils/serialization.py +184 -51
  150. glaip_sdk/utils/sync.py +142 -0
  151. glaip_sdk/utils/tool_detection.py +33 -0
  152. glaip_sdk/utils/validation.py +21 -30
  153. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +58 -12
  154. glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
  155. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
  156. glaip_sdk/models.py +0 -250
  157. glaip_sdk/utils/rendering/renderer/progress.py +0 -118
  158. glaip_sdk/utils/rendering/steps.py +0 -232
  159. glaip_sdk/utils/rich_utils.py +0 -29
  160. glaip_sdk-0.0.7.dist-info/RECORD +0 -55
  161. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,282 @@
1
+ """Agent-specific interaction loop for the command palette.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from contextlib import contextmanager
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ import click
13
+
14
+ from glaip_sdk.branding import ERROR_STYLE, HINT_PREFIX_STYLE
15
+ from glaip_sdk.cli.commands.agents import get as agents_get_command
16
+ from glaip_sdk.cli.commands.agents import run as agents_run_command
17
+ from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
18
+ from glaip_sdk.cli.hints import format_command_hint
19
+ from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
20
+ from glaip_sdk.cli.utils import bind_slash_session_context
21
+
22
+ if TYPE_CHECKING: # pragma: no cover - type checking only
23
+ from glaip_sdk.cli.slash.session import SlashSession
24
+
25
+
26
+ class AgentRunSession:
27
+ """Per-agent execution context for the command palette."""
28
+
29
+ def __init__(self, session: SlashSession, agent: Any) -> None:
30
+ """Initialize the agent run session.
31
+
32
+ Args:
33
+ session: The slash session context
34
+ agent: The agent to interact with
35
+ """
36
+ self.session = session
37
+ self.agent = agent
38
+ self.console = session.console
39
+ self._agent_id = str(getattr(agent, "id", ""))
40
+ self._agent_name = getattr(agent, "name", "") or self._agent_id
41
+ self._prompt_placeholder: str = "Chat with this agent here; use / for shortcuts. Alt+Enter inserts a newline."
42
+ self._contextual_completion_help: dict[str, str] = {
43
+ "details": "Show this agent's configuration (+ expands prompt).",
44
+ "help": "Display this context-aware menu.",
45
+ "runs": "✨ NEW · Browse remote run history for this agent.",
46
+ "exit": "Return to the command palette.",
47
+ "q": "Return to the command palette.",
48
+ }
49
+ self._instruction_preview_limit = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
50
+
51
+ def run(self) -> None:
52
+ """Run the interactive agent session loop."""
53
+ self.session.set_contextual_commands(self._contextual_completion_help, include_global=False)
54
+ previous_agent = getattr(self.session, "_current_agent", None)
55
+ self.session._current_agent = self.agent
56
+ clear_ready = getattr(self.session, "clear_agent_transcript_ready", None)
57
+ if callable(clear_ready):
58
+ clear_ready(self._agent_id)
59
+ try:
60
+ self._display_agent_info()
61
+ self._run_agent_loop()
62
+ finally:
63
+ self.session.set_contextual_commands(None)
64
+ self.session._current_agent = previous_agent
65
+
66
+ def _display_agent_info(self) -> None:
67
+ """Display agent information and summary."""
68
+ self.session._render_header(self.agent, focus_agent=True)
69
+
70
+ def _run_agent_loop(self) -> None:
71
+ """Run the main agent interaction loop."""
72
+ while True:
73
+ raw = self._get_user_input()
74
+ if raw is None:
75
+ return
76
+
77
+ raw = raw.strip()
78
+ if not raw:
79
+ continue
80
+
81
+ if raw.startswith("/"):
82
+ if not self._handle_slash_command(raw, self._agent_id):
83
+ return
84
+ continue
85
+
86
+ self._run_agent(self._agent_id, raw)
87
+
88
+ def _get_user_input(self) -> str | None:
89
+ """Get user input with proper error handling."""
90
+ try:
91
+
92
+ def _prompt_message() -> Any:
93
+ """Get formatted prompt message for agent session."""
94
+ prompt_prefix = f"{self._agent_name} ({self._agent_id}) "
95
+
96
+ # Use FormattedText if prompt_toolkit is available, otherwise use simple string
97
+ if _HAS_PROMPT_TOOLKIT and FormattedText is not None:
98
+ segments = [
99
+ ("class:prompt", prompt_prefix),
100
+ ("class:prompt", "\n› "),
101
+ ]
102
+ return FormattedText(segments)
103
+
104
+ return f"{prompt_prefix}\n› "
105
+
106
+ raw = self.session._prompt(
107
+ _prompt_message,
108
+ placeholder=self._prompt_placeholder,
109
+ )
110
+ if self._prompt_placeholder:
111
+ # Show the guidance once, then fall back to a clean prompt.
112
+ self._prompt_placeholder = ""
113
+ return raw
114
+ except EOFError:
115
+ self.console.print("\nExiting agent context.")
116
+ return None
117
+ except KeyboardInterrupt:
118
+ self.console.print("")
119
+ return ""
120
+
121
+ def _handle_slash_command(self, raw: str, agent_id: str) -> bool:
122
+ """Handle slash commands in agent context. Returns False if should exit."""
123
+ # Handle simple commands first
124
+ if raw == "/":
125
+ return self._handle_help_command()
126
+
127
+ if raw in {"/exit", "/back", "/q"}:
128
+ return self._handle_exit_command()
129
+
130
+ if raw == "/details":
131
+ return self._handle_details_command(agent_id)
132
+
133
+ if raw in {"/help", "/?"}:
134
+ return self._handle_help_command()
135
+
136
+ # Handle other commands through the main session
137
+ return self._handle_other_command(raw)
138
+
139
+ def _handle_help_command(self) -> bool:
140
+ """Handle help command."""
141
+ self.session._cmd_help([], True)
142
+ return True
143
+
144
+ def _handle_exit_command(self) -> bool:
145
+ """Handle exit command."""
146
+ self.console.print("[dim]Returning to the main prompt.[/dim]")
147
+ return False
148
+
149
+ def _handle_details_command(self, agent_id: str) -> bool:
150
+ """Handle details command."""
151
+ self._show_details(agent_id)
152
+ return True
153
+
154
+ def _handle_other_command(self, raw: str) -> bool:
155
+ """Handle other commands through the main session."""
156
+ self.session.handle_command(raw, invoked_from_agent=True)
157
+ return not self.session._should_exit
158
+
159
+ def _show_details(self, agent_id: str, *, enable_prompt: bool = True) -> None:
160
+ """Render the agent's configuration export inside the command palette."""
161
+ try:
162
+ self.session.ctx.invoke(
163
+ agents_get_command,
164
+ agent_ref=agent_id,
165
+ instruction_preview=self._instruction_preview_limit,
166
+ )
167
+ if enable_prompt:
168
+ self._prompt_instruction_view_toggle(agent_id)
169
+ self.console.print(
170
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] Continue the conversation in this prompt, or use "
171
+ f"{format_command_hint('/help') or '/help'} for shortcuts."
172
+ )
173
+ except click.ClickException as exc:
174
+ self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
175
+
176
+ def _prompt_instruction_view_toggle(self, agent_id: str) -> None:
177
+ """Offer a prompt to expand or collapse the instruction preview after details."""
178
+ if not getattr(self.console, "is_terminal", False):
179
+ return
180
+
181
+ while True:
182
+ mode = "expanded" if self._instruction_preview_limit == 0 else "trimmed"
183
+ self.console.print(f"[dim]Instruction view is {mode}. Press Ctrl+T to toggle, Enter to continue.[/dim]")
184
+ try:
185
+ ch = click.getchar()
186
+ except (EOFError, KeyboardInterrupt): # pragma: no cover - defensive guard
187
+ return
188
+
189
+ if not self._handle_instruction_toggle_input(agent_id, ch):
190
+ break
191
+
192
+ if self._instruction_preview_limit == 0:
193
+ self._instruction_preview_limit = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
194
+ self.console.print("")
195
+
196
+ def _handle_instruction_toggle_input(self, agent_id: str, ch: str) -> bool:
197
+ """Process a single toggle keypress; return False when the loop should exit."""
198
+ if ch in {"\r", "\n"}:
199
+ return False
200
+
201
+ lowered = ch.lower()
202
+ if lowered == "t" or ch == "\x14": # support literal 't' or Ctrl+T
203
+ self._instruction_preview_limit = (
204
+ DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT if self._instruction_preview_limit == 0 else 0
205
+ )
206
+ self._show_details(agent_id, enable_prompt=False)
207
+ return True
208
+
209
+ # Ignore other keys and continue prompting.
210
+ return True
211
+
212
+ def _after_agent_run(self) -> None:
213
+ """Handle transcript viewer behaviour after a successful run."""
214
+ payload, manifest = self.session._get_last_transcript()
215
+ if not self._transcript_matches(payload, manifest):
216
+ return
217
+ run_id = str(manifest.get("run_id") or "")
218
+ mark_ready = getattr(self.session, "mark_agent_transcript_ready", None)
219
+ if callable(mark_ready):
220
+ mark_ready(self._agent_id, run_id)
221
+ if self._open_transcript_viewer():
222
+ return
223
+ self.console.print("[dim]Transcript viewer is unavailable in this environment.[/dim]")
224
+
225
+ def _transcript_matches(self, payload: Any, manifest: Any) -> bool:
226
+ """Return True when the latest transcript belongs to this agent."""
227
+ if not payload or not isinstance(manifest, dict):
228
+ return False
229
+ if not manifest.get("run_id"):
230
+ return False
231
+ return manifest.get("agent_id") == self._agent_id
232
+
233
+ def _open_transcript_viewer(self) -> bool:
234
+ """Launch the transcript viewer when terminal support is available."""
235
+ if not getattr(self.console, "is_terminal", False):
236
+ return False
237
+ try:
238
+ current_agent = getattr(self.session, "_current_agent", None)
239
+ self.session.open_transcript_viewer(announce=True)
240
+ if getattr(self.session.console, "is_terminal", False):
241
+ try:
242
+ self.session.console.clear()
243
+ except Exception: # pragma: no cover - defensive cleanup
244
+ pass
245
+ if current_agent is not None: # pragma: no cover - UI refresh best effort
246
+ try:
247
+ self.session._render_header(current_agent, focus_agent=True)
248
+ except Exception: # pragma: no cover - defensive cleanup
249
+ pass
250
+ return True
251
+ except Exception: # pragma: no cover - defensive cleanup
252
+ return False
253
+
254
+ @contextmanager
255
+ def _bind_session_context(self) -> Any:
256
+ """Temporarily attach this slash session to the Click context."""
257
+ with bind_slash_session_context(self.session.ctx, self.session):
258
+ yield
259
+
260
+ def _run_agent(self, agent_id: str, message: str) -> None:
261
+ """Execute the agents run command for the active agent."""
262
+ if not message:
263
+ return
264
+
265
+ try:
266
+ self.session.notify_agent_run_started()
267
+ with self._bind_session_context():
268
+ self.session.ctx.invoke(
269
+ agents_run_command,
270
+ agent_ref=agent_id,
271
+ input_text=message,
272
+ verbose=False,
273
+ )
274
+ self.session.last_run_input = message
275
+ self._after_agent_run()
276
+ except click.ClickException as exc:
277
+ self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
278
+ finally:
279
+ try:
280
+ self.session.notify_agent_run_finished()
281
+ except Exception:
282
+ pass
@@ -0,0 +1,245 @@
1
+ """prompt_toolkit integration helpers for the slash session.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Iterable
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ _HAS_PROMPT_TOOLKIT = False
13
+
14
+ try: # pragma: no cover - optional dependency
15
+ from prompt_toolkit import PromptSession
16
+ from prompt_toolkit.completion import Completer, Completion
17
+ from prompt_toolkit.formatted_text import FormattedText, to_formatted_text
18
+ from prompt_toolkit.key_binding import KeyBindings
19
+ from prompt_toolkit.patch_stdout import patch_stdout
20
+ from prompt_toolkit.styles import Style
21
+
22
+ _HAS_PROMPT_TOOLKIT = True
23
+ except Exception: # pragma: no cover - optional dependency
24
+ PromptSession = None # type: ignore[assignment]
25
+ Completer = None # type: ignore[assignment]
26
+ Completion = None # type: ignore[assignment]
27
+ FormattedText = None # type: ignore[assignment]
28
+ to_formatted_text = None # type: ignore[assignment]
29
+ KeyBindings = None # type: ignore[assignment]
30
+ Style = None # type: ignore[assignment]
31
+ patch_stdout = None # type: ignore[assignment]
32
+
33
+ if TYPE_CHECKING: # pragma: no cover - typing only
34
+ from glaip_sdk.cli.slash.session import SlashSession
35
+
36
+
37
+ if _HAS_PROMPT_TOOLKIT:
38
+
39
+ class SlashCompleter(Completer):
40
+ """Provide slash command completions inside the prompt."""
41
+
42
+ def __init__(self, session: SlashSession) -> None:
43
+ """Initialize the slash completer.
44
+
45
+ Args:
46
+ session: The slash session context
47
+ """
48
+ self._session = session
49
+
50
+ def get_completions(
51
+ self,
52
+ document: Any,
53
+ _complete_event: Any, # type: ignore[no-any-return]
54
+ ) -> Iterable[Completion]:
55
+ """Get completions for slash commands.
56
+
57
+ Args:
58
+ document: The document being edited
59
+ _complete_event: The completion event
60
+
61
+ Yields:
62
+ Completion objects for matching commands
63
+ """
64
+ if Completion is None:
65
+ return
66
+
67
+ text = document.text_before_cursor or ""
68
+ if not text.startswith("/") or " " in text:
69
+ return
70
+
71
+ yield from _iter_command_completions(self._session, text)
72
+ yield from _iter_contextual_completions(self._session, text)
73
+
74
+ else: # pragma: no cover - fallback when prompt_toolkit is missing
75
+
76
+ class SlashCompleter: # type: ignore[too-many-ancestors]
77
+ """Fallback slash completer when prompt_toolkit is not available."""
78
+
79
+ def __init__(self, session: SlashSession) -> None:
80
+ """Initialize the fallback slash completer.
81
+
82
+ Args:
83
+ session: The slash session context
84
+ """
85
+ self._session = session
86
+
87
+
88
+ def setup_prompt_toolkit(
89
+ session: SlashSession,
90
+ *,
91
+ interactive: bool,
92
+ ) -> tuple[Any | None, Any | None]:
93
+ """Configure prompt_toolkit session and style for interactive mode."""
94
+ if not (interactive and _HAS_PROMPT_TOOLKIT):
95
+ return None, None
96
+
97
+ if PromptSession is None or Style is None:
98
+ return None, None
99
+
100
+ bindings = _create_key_bindings(session)
101
+
102
+ prompt_session = PromptSession(
103
+ completer=SlashCompleter(session),
104
+ complete_while_typing=True,
105
+ key_bindings=bindings,
106
+ )
107
+ prompt_style = Style.from_dict(
108
+ {
109
+ "prompt": "bg:#0f172a #facc15 bold",
110
+ "": "bg:#0f172a #e2e8f0",
111
+ "placeholder": "bg:#0f172a #94a3b8 italic",
112
+ }
113
+ )
114
+
115
+ return prompt_session, prompt_style
116
+
117
+
118
+ def _create_key_bindings(_session: SlashSession) -> Any:
119
+ """Create prompt_toolkit key bindings for the command palette."""
120
+ if KeyBindings is None:
121
+ return None
122
+
123
+ bindings = KeyBindings()
124
+
125
+ def _refresh_completions(buffer: Any) -> None: # type: ignore[no-any-return]
126
+ """Refresh completions when slash command is typed.
127
+
128
+ Args:
129
+ buffer: Prompt buffer instance.
130
+ """
131
+ text = buffer.document.text_before_cursor or ""
132
+ if text.startswith("/") and " " not in text:
133
+ buffer.start_completion(select_first=False)
134
+ elif buffer.complete_state is not None:
135
+ buffer.cancel_completion()
136
+
137
+ @bindings.add("/") # type: ignore[misc]
138
+ def _handle_slash_key(event: Any) -> None: # vulture: ignore
139
+ """Handle '/' key press - insert slash and trigger completion."""
140
+ buffer = event.app.current_buffer
141
+ buffer.insert_text("/")
142
+ _refresh_completions(buffer)
143
+
144
+ @bindings.add("backspace") # type: ignore[misc]
145
+ def _handle_backspace_key(event: Any) -> None: # vulture: ignore
146
+ """Handle backspace key - delete character and refresh completions."""
147
+ buffer = event.app.current_buffer
148
+ if buffer.document.cursor_position > 0:
149
+ buffer.delete_before_cursor()
150
+ _refresh_completions(buffer)
151
+
152
+ @bindings.add("c-h") # type: ignore[misc]
153
+ def _handle_ctrl_h_key(event: Any) -> None: # vulture: ignore
154
+ """Handle Ctrl+H key - same as backspace."""
155
+ _handle_backspace_key(event) # Reuse backspace handler
156
+
157
+ @bindings.add("escape", "enter") # type: ignore[misc]
158
+ def _handle_alt_enter_key(event: Any) -> None: # vulture: ignore
159
+ """Handle Alt+Enter key - insert line break and cancel completion."""
160
+ buffer = event.app.current_buffer
161
+ buffer.insert_text("\n")
162
+ if buffer.complete_state is not None:
163
+ buffer.cancel_completion()
164
+
165
+ return bindings
166
+
167
+
168
+ def _iter_command_completions(
169
+ session: SlashSession, text: str
170
+ ) -> Iterable[Completion]: # pragma: no cover - thin wrapper
171
+ """Yield completions for global slash commands."""
172
+ prefix = text[1:]
173
+ seen: set[str] = set()
174
+
175
+ # Early return for contextual commands scenario
176
+ if not _should_include_commands(session):
177
+ return []
178
+
179
+ commands = sorted(session._unique_commands.values(), key=lambda c: c.name)
180
+ agent_context = bool(getattr(session, "_current_agent", None))
181
+
182
+ for cmd in commands:
183
+ if getattr(cmd, "agent_only", False) and not agent_context:
184
+ continue
185
+ yield from _generate_command_completions(cmd, prefix, text, seen)
186
+
187
+
188
+ def _should_include_commands(session: SlashSession) -> bool:
189
+ """Check if commands should be included in completions."""
190
+ return not (session.get_contextual_commands() and not session.should_include_global_commands())
191
+
192
+
193
+ def _generate_command_completions(cmd: Any, prefix: str, text: str, seen: set[str]) -> Iterable[Completion]:
194
+ """Generate completion items for a single command."""
195
+ for alias in (cmd.name, *cmd.aliases):
196
+ if alias in seen or alias.startswith("?"):
197
+ continue
198
+
199
+ if prefix and not alias.startswith(prefix):
200
+ continue
201
+
202
+ seen.add(alias)
203
+ label = f"/{alias}"
204
+ yield Completion(
205
+ text=label,
206
+ start_position=-len(text),
207
+ display=label,
208
+ display_meta=cmd.help,
209
+ )
210
+
211
+
212
+ def _iter_contextual_completions(
213
+ session: SlashSession, text: str
214
+ ) -> Iterable[Completion]: # pragma: no cover - thin wrapper
215
+ """Yield completions for context-specific slash commands."""
216
+ prefix = text[1:]
217
+ seen: set[str] = set()
218
+
219
+ contextual_commands = sorted(session.get_contextual_commands().items(), key=lambda item: item[0])
220
+
221
+ for alias, help_text in contextual_commands:
222
+ if alias in seen:
223
+ continue
224
+ if prefix and not alias.startswith(prefix):
225
+ continue
226
+ seen.add(alias)
227
+ label = f"/{alias}"
228
+ yield Completion(
229
+ text=label,
230
+ start_position=-len(text),
231
+ display=label,
232
+ display_meta=help_text,
233
+ )
234
+
235
+
236
+ __all__ = [
237
+ "SlashCompleter",
238
+ "setup_prompt_toolkit",
239
+ "FormattedText",
240
+ "to_formatted_text",
241
+ "patch_stdout",
242
+ "PromptSession",
243
+ "Style",
244
+ "_HAS_PROMPT_TOOLKIT",
245
+ ]