glaip-sdk 0.0.9__py3-none-any.whl → 0.0.11__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.
@@ -5,6 +5,7 @@ Authors:
5
5
  """
6
6
 
7
7
  import json
8
+ import sys
8
9
  from pathlib import Path
9
10
  from typing import Any
10
11
 
@@ -21,9 +22,6 @@ from glaip_sdk.cli.display import (
21
22
  handle_json_output,
22
23
  handle_rich_output,
23
24
  )
24
- from glaip_sdk.cli.io import (
25
- export_resource_to_file_with_validation as export_resource_to_file,
26
- )
27
25
  from glaip_sdk.cli.io import (
28
26
  fetch_raw_resource_details,
29
27
  )
@@ -40,20 +38,41 @@ from glaip_sdk.cli.utils import (
40
38
  )
41
39
  from glaip_sdk.rich_components import AIPPanel
42
40
  from glaip_sdk.utils import format_datetime
41
+ from glaip_sdk.utils.serialization import (
42
+ build_mcp_export_payload,
43
+ write_resource_export,
44
+ )
43
45
 
44
46
  console = Console()
45
47
 
46
48
 
47
49
  @click.group(name="mcps", no_args_is_help=True)
48
50
  def mcps_group() -> None:
49
- """MCP management operations."""
51
+ """MCP management operations.
52
+
53
+ Provides commands for creating, listing, updating, deleting, and managing
54
+ Model Context Protocol (MCP) configurations.
55
+ """
50
56
  pass
51
57
 
52
58
 
53
59
  def _resolve_mcp(
54
60
  ctx: Any, client: Any, ref: str, select: int | None = None
55
61
  ) -> Any | None:
56
- """Resolve MCP reference (ID or name) with ambiguity handling."""
62
+ """Resolve MCP reference (ID or name) with ambiguity handling.
63
+
64
+ Args:
65
+ ctx: Click context object
66
+ client: API client instance
67
+ ref: MCP reference (ID or name)
68
+ select: Index to select when multiple matches found
69
+
70
+ Returns:
71
+ MCP object if found, None otherwise
72
+
73
+ Raises:
74
+ ClickException: If MCP not found or selection invalid
75
+ """
57
76
  return resolve_resource_reference(
58
77
  ctx,
59
78
  client,
@@ -70,7 +89,14 @@ def _resolve_mcp(
70
89
  @output_flags()
71
90
  @click.pass_context
72
91
  def list_mcps(ctx: Any) -> None:
73
- """List all MCPs."""
92
+ """List all MCPs in a formatted table.
93
+
94
+ Args:
95
+ ctx: Click context containing output format preferences
96
+
97
+ Raises:
98
+ ClickException: If API request fails
99
+ """
74
100
  try:
75
101
  client = get_client(ctx)
76
102
  with spinner_context(
@@ -117,7 +143,18 @@ def list_mcps(ctx: Any) -> None:
117
143
  def create(
118
144
  ctx: Any, name: str, transport: str, description: str | None, config: str | None
119
145
  ) -> None:
120
- """Create a new MCP."""
146
+ """Create a new MCP with specified configuration.
147
+
148
+ Args:
149
+ ctx: Click context containing output format preferences
150
+ name: MCP name (required)
151
+ transport: MCP transport protocol (required)
152
+ description: Optional MCP description
153
+ config: JSON configuration string for MCP settings
154
+
155
+ Raises:
156
+ ClickException: If JSON parsing fails or API request fails
157
+ """
121
158
  try:
122
159
  client = get_client(ctx)
123
160
 
@@ -163,22 +200,191 @@ def create(
163
200
  raise click.ClickException(str(e))
164
201
 
165
202
 
203
+ def _handle_mcp_export(
204
+ ctx: Any,
205
+ client: Any,
206
+ mcp: Any,
207
+ export_path: Path,
208
+ no_auth_prompt: bool,
209
+ auth_placeholder: str,
210
+ ) -> None:
211
+ """Handle MCP export to file with format detection and auth handling.
212
+
213
+ Args:
214
+ ctx: Click context for spinner management
215
+ client: API client for fetching MCP details
216
+ mcp: MCP object to export
217
+ export_path: Target file path (format detected from extension)
218
+ no_auth_prompt: Skip interactive secret prompts if True
219
+ auth_placeholder: Placeholder text for missing secrets
220
+
221
+ Note:
222
+ Supports JSON (.json) and YAML (.yaml/.yml) export formats.
223
+ In interactive mode, prompts for secret values.
224
+ In non-interactive mode, uses placeholder values.
225
+ """
226
+ # Auto-detect format from file extension
227
+ detected_format = detect_export_format(export_path)
228
+
229
+ # Always export comprehensive data - re-fetch with full details
230
+ try:
231
+ with spinner_context(
232
+ ctx,
233
+ "[bold blue]Fetching complete MCP details…[/bold blue]",
234
+ console_override=console,
235
+ ):
236
+ mcp = client.mcps.get_mcp_by_id(mcp.id)
237
+ except Exception as e:
238
+ console.print(
239
+ Text(f"[yellow]⚠️ Could not fetch full MCP details: {e}[/yellow]")
240
+ )
241
+ console.print(Text("[yellow]⚠️ Proceeding with available data[/yellow]"))
242
+
243
+ # Determine if we should prompt for secrets
244
+ prompt_for_secrets = not no_auth_prompt and sys.stdin.isatty()
245
+
246
+ # Warn user if non-interactive mode forces placeholder usage
247
+ if not no_auth_prompt and not sys.stdin.isatty():
248
+ console.print(
249
+ Text(
250
+ "[yellow]⚠️ Non-interactive mode detected. "
251
+ "Using placeholder values for secrets.[/yellow]"
252
+ )
253
+ )
254
+
255
+ # Build and write export payload
256
+ if prompt_for_secrets:
257
+ # Interactive mode: no spinner during prompts
258
+ export_payload = build_mcp_export_payload(
259
+ mcp,
260
+ prompt_for_secrets=prompt_for_secrets,
261
+ placeholder=auth_placeholder,
262
+ console=console,
263
+ )
264
+ with spinner_context(
265
+ ctx,
266
+ "[bold blue]Writing export file…[/bold blue]",
267
+ console_override=console,
268
+ ):
269
+ write_resource_export(export_path, export_payload, detected_format)
270
+ else:
271
+ # Non-interactive mode: spinner for entire export process
272
+ with spinner_context(
273
+ ctx,
274
+ "[bold blue]Exporting MCP configuration…[/bold blue]",
275
+ console_override=console,
276
+ ):
277
+ export_payload = build_mcp_export_payload(
278
+ mcp,
279
+ prompt_for_secrets=prompt_for_secrets,
280
+ placeholder=auth_placeholder,
281
+ console=console,
282
+ )
283
+ write_resource_export(export_path, export_payload, detected_format)
284
+
285
+ console.print(
286
+ Text(
287
+ f"[green]✅ Complete MCP configuration exported to: "
288
+ f"{export_path} (format: {detected_format})[/green]"
289
+ )
290
+ )
291
+
292
+
293
+ def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
294
+ """Display MCP details using raw API data or fallback to Pydantic model.
295
+
296
+ Args:
297
+ ctx: Click context containing output format preferences
298
+ client: API client for fetching raw MCP data
299
+ mcp: MCP object to display details for
300
+
301
+ Note:
302
+ Attempts to fetch raw API data first to preserve all fields.
303
+ Falls back to Pydantic model data if raw data unavailable.
304
+ Formats datetime fields for better readability.
305
+ """
306
+ # Try to fetch raw API data first to preserve ALL fields
307
+ with spinner_context(
308
+ ctx,
309
+ "[bold blue]Fetching detailed MCP data…[/bold blue]",
310
+ console_override=console,
311
+ ):
312
+ raw_mcp_data = fetch_raw_resource_details(client, mcp, "mcps")
313
+
314
+ if raw_mcp_data:
315
+ # Use raw API data - this preserves ALL fields
316
+ formatted_data = raw_mcp_data.copy()
317
+ if "created_at" in formatted_data:
318
+ formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
319
+ if "updated_at" in formatted_data:
320
+ formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
321
+
322
+ output_result(
323
+ ctx,
324
+ formatted_data,
325
+ title="MCP Details",
326
+ panel_title=f"🔌 {raw_mcp_data.get('name', 'Unknown')}",
327
+ )
328
+ else:
329
+ # Fall back to Pydantic model data
330
+ console.print("[yellow]Falling back to Pydantic model data[/yellow]")
331
+ result_data = {
332
+ "id": str(getattr(mcp, "id", "N/A")),
333
+ "name": getattr(mcp, "name", "N/A"),
334
+ "type": getattr(mcp, "type", "N/A"),
335
+ "config": getattr(mcp, "config", "N/A"),
336
+ "status": getattr(mcp, "status", "N/A"),
337
+ "connection_status": getattr(mcp, "connection_status", "N/A"),
338
+ }
339
+ output_result(
340
+ ctx, result_data, title="MCP Details", panel_title=f"🔌 {mcp.name}"
341
+ )
342
+
343
+
166
344
  @mcps_group.command()
167
345
  @click.argument("mcp_ref")
168
346
  @click.option(
169
347
  "--export",
170
348
  type=click.Path(dir_okay=False, writable=True),
171
- help="Export complete MCP configuration to file (format auto-detected from .json/.yaml extension)",
349
+ help="Export complete MCP configuration to file "
350
+ "(format auto-detected from .json/.yaml extension)",
351
+ )
352
+ @click.option(
353
+ "--no-auth-prompt",
354
+ is_flag=True,
355
+ help="Skip interactive secret prompts and use placeholder values.",
356
+ )
357
+ @click.option(
358
+ "--auth-placeholder",
359
+ default="<INSERT VALUE>",
360
+ show_default=True,
361
+ help="Placeholder text used when secrets are unavailable.",
172
362
  )
173
363
  @output_flags()
174
364
  @click.pass_context
175
- def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
176
- """Get MCP details.
365
+ def get(
366
+ ctx: Any,
367
+ mcp_ref: str,
368
+ export: str | None,
369
+ no_auth_prompt: bool,
370
+ auth_placeholder: str,
371
+ ) -> None:
372
+ """Get MCP details and optionally export configuration to file.
373
+
374
+ Args:
375
+ ctx: Click context containing output format preferences
376
+ mcp_ref: MCP reference (ID or name)
377
+ export: Optional file path to export MCP configuration
378
+ no_auth_prompt: Skip interactive secret prompts if True
379
+ auth_placeholder: Placeholder text for missing secrets
380
+
381
+ Raises:
382
+ ClickException: If MCP not found or export fails
177
383
 
178
384
  Examples:
179
385
  aip mcps get my-mcp
180
- aip mcps get my-mcp --export mcp.json # Exports complete configuration as JSON
181
- aip mcps get my-mcp --export mcp.yaml # Exports complete configuration as YAML
386
+ aip mcps get my-mcp --export mcp.json # Export as JSON
387
+ aip mcps get my-mcp --export mcp.yaml # Export as YAML
182
388
  """
183
389
  try:
184
390
  client = get_client(ctx)
@@ -188,83 +394,12 @@ def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
188
394
 
189
395
  # Handle export option
190
396
  if export:
191
- export_path = Path(export)
192
- # Auto-detect format from file extension
193
- detected_format = detect_export_format(export_path)
194
-
195
- # Always export comprehensive data - re-fetch MCP with full details if needed
196
- try:
197
- with spinner_context(
198
- ctx,
199
- "[bold blue]Fetching complete MCP details…[/bold blue]",
200
- console_override=console,
201
- ):
202
- mcp = client.mcps.get_mcp_by_id(mcp.id)
203
- except Exception as e:
204
- console.print(
205
- Text(f"[yellow]⚠️ Could not fetch full MCP details: {e}[/yellow]")
206
- )
207
- console.print(
208
- Text("[yellow]⚠️ Proceeding with available data[/yellow]")
209
- )
210
-
211
- with spinner_context(
212
- ctx,
213
- "[bold blue]Exporting MCP configuration…[/bold blue]",
214
- console_override=console,
215
- ):
216
- export_resource_to_file(mcp, export_path, detected_format)
217
- console.print(
218
- Text(
219
- f"[green]✅ Complete MCP configuration exported to: {export_path} (format: {detected_format})[/green]"
220
- )
397
+ _handle_mcp_export(
398
+ ctx, client, mcp, Path(export), no_auth_prompt, auth_placeholder
221
399
  )
222
400
 
223
- # Try to fetch raw API data first to preserve ALL fields
224
- with spinner_context(
225
- ctx,
226
- "[bold blue]Fetching detailed MCP data…[/bold blue]",
227
- console_override=console,
228
- ):
229
- raw_mcp_data = fetch_raw_resource_details(client, mcp, "mcps")
230
-
231
- if raw_mcp_data:
232
- # Use raw API data - this preserves ALL fields
233
- # Format dates for better display (minimal postprocessing)
234
- formatted_data = raw_mcp_data.copy()
235
- if "created_at" in formatted_data:
236
- formatted_data["created_at"] = format_datetime(
237
- formatted_data["created_at"]
238
- )
239
- if "updated_at" in formatted_data:
240
- formatted_data["updated_at"] = format_datetime(
241
- formatted_data["updated_at"]
242
- )
243
-
244
- # Display using output_result with raw data
245
- output_result(
246
- ctx,
247
- formatted_data,
248
- title="MCP Details",
249
- panel_title=f"🔌 {raw_mcp_data.get('name', 'Unknown')}",
250
- )
251
- else:
252
- # Fall back to original method if raw fetch fails
253
- console.print("[yellow]Falling back to Pydantic model data[/yellow]")
254
-
255
- # Create result data with actual available fields
256
- result_data = {
257
- "id": str(getattr(mcp, "id", "N/A")),
258
- "name": getattr(mcp, "name", "N/A"),
259
- "type": getattr(mcp, "type", "N/A"),
260
- "config": getattr(mcp, "config", "N/A"),
261
- "status": getattr(mcp, "status", "N/A"),
262
- "connection_status": getattr(mcp, "connection_status", "N/A"),
263
- }
264
-
265
- output_result(
266
- ctx, result_data, title="MCP Details", panel_title=f"🔌 {mcp.name}"
267
- )
401
+ # Display MCP details
402
+ _display_mcp_details(ctx, client, mcp)
268
403
 
269
404
  except Exception as e:
270
405
  raise click.ClickException(str(e))
@@ -275,7 +410,15 @@ def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
275
410
  @output_flags()
276
411
  @click.pass_context
277
412
  def list_tools(ctx: Any, mcp_ref: str) -> None:
278
- """List tools from MCP."""
413
+ """List tools available from a specific MCP.
414
+
415
+ Args:
416
+ ctx: Click context containing output format preferences
417
+ mcp_ref: MCP reference (ID or name)
418
+
419
+ Raises:
420
+ ClickException: If MCP not found or tools fetch fails
421
+ """
279
422
  try:
280
423
  client = get_client(ctx)
281
424
 
@@ -325,7 +468,19 @@ def list_tools(ctx: Any, mcp_ref: str) -> None:
325
468
  @output_flags()
326
469
  @click.pass_context
327
470
  def connect(ctx: Any, config_file: str) -> None:
328
- """Connect to MCP using config file."""
471
+ """Test MCP connection using a configuration file.
472
+
473
+ Args:
474
+ ctx: Click context containing output format preferences
475
+ config_file: Path to MCP configuration JSON file
476
+
477
+ Raises:
478
+ ClickException: If config file invalid or connection test fails
479
+
480
+ Note:
481
+ Loads MCP configuration from JSON file and tests connectivity.
482
+ Displays success or failure with connection details.
483
+ """
329
484
  try:
330
485
  client = get_client(ctx)
331
486
 
@@ -337,7 +492,8 @@ def connect(ctx: Any, config_file: str) -> None:
337
492
  if view != "json":
338
493
  console.print(
339
494
  Text(
340
- f"[yellow]Connecting to MCP with config from {config_file}...[/yellow]"
495
+ f"[yellow]Connecting to MCP with config from "
496
+ f"{config_file}...[/yellow]"
341
497
  )
342
498
  )
343
499
 
@@ -379,7 +535,22 @@ def update(
379
535
  description: str | None,
380
536
  config: str | None,
381
537
  ) -> None:
382
- """Update an existing MCP."""
538
+ """Update an existing MCP with new configuration values.
539
+
540
+ Args:
541
+ ctx: Click context containing output format preferences
542
+ mcp_ref: MCP reference (ID or name)
543
+ name: New MCP name (optional)
544
+ description: New description (optional)
545
+ config: New JSON configuration string (optional)
546
+
547
+ Raises:
548
+ ClickException: If MCP not found, JSON invalid, or no fields specified
549
+
550
+ Note:
551
+ At least one field must be specified for update.
552
+ Uses PUT for complete updates or PATCH for partial updates.
553
+ """
383
554
  try:
384
555
  client = get_client(ctx)
385
556
 
@@ -425,7 +596,20 @@ def update(
425
596
  @output_flags()
426
597
  @click.pass_context
427
598
  def delete(ctx: Any, mcp_ref: str, yes: bool) -> None:
428
- """Delete an MCP."""
599
+ """Delete an MCP after confirmation.
600
+
601
+ Args:
602
+ ctx: Click context containing output format preferences
603
+ mcp_ref: MCP reference (ID or name)
604
+ yes: Skip confirmation prompt if True
605
+
606
+ Raises:
607
+ ClickException: If MCP not found or deletion fails
608
+
609
+ Note:
610
+ Requires confirmation unless --yes flag is provided.
611
+ Deletion is permanent and cannot be undone.
612
+ """
429
613
  try:
430
614
  client = get_client(ctx)
431
615
 
glaip_sdk/cli/main.py CHANGED
@@ -24,6 +24,7 @@ from glaip_sdk.cli.commands.configure import (
24
24
  from glaip_sdk.cli.commands.mcps import mcps_group
25
25
  from glaip_sdk.cli.commands.models import models_group
26
26
  from glaip_sdk.cli.commands.tools import tools_group
27
+ from glaip_sdk.cli.update_notifier import maybe_notify_update
27
28
  from glaip_sdk.cli.utils import spinner_context, update_spinner
28
29
  from glaip_sdk.config.constants import (
29
30
  DEFAULT_AGENT_RUN_TIMEOUT,
@@ -82,6 +83,13 @@ def main(
82
83
 
83
84
  ctx.obj["tty"] = not no_tty
84
85
 
86
+ if not ctx.resilient_parsing and ctx.obj["tty"]:
87
+ console = Console()
88
+ maybe_notify_update(
89
+ _SDK_VERSION,
90
+ console=console,
91
+ )
92
+
85
93
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
86
94
  if _should_launch_slash(ctx) and SlashSession is not None:
87
95
  session = SlashSession(ctx)
@@ -12,6 +12,7 @@ import click
12
12
 
13
13
  from glaip_sdk.cli.commands.agents import get as agents_get_command
14
14
  from glaip_sdk.cli.commands.agents import run as agents_run_command
15
+ from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
15
16
 
16
17
  if TYPE_CHECKING: # pragma: no cover - type checking only
17
18
  from .session import SlashSession
@@ -34,6 +35,7 @@ class AgentRunSession:
34
35
  "help": "Display this context-aware menu.",
35
36
  "exit": "Return to the command palette.",
36
37
  "q": "Return to the command palette.",
38
+ "verbose": "Toggle verbose streaming output.",
37
39
  }
38
40
 
39
41
  def run(self) -> None:
@@ -71,8 +73,30 @@ class AgentRunSession:
71
73
  def _get_user_input(self) -> str | None:
72
74
  """Get user input with proper error handling."""
73
75
  try:
76
+
77
+ def _prompt_message() -> Any:
78
+ verbose_enabled = self.session.verbose_enabled
79
+ verbose_tag = "[verbose:on]" if verbose_enabled else "[verbose:off]"
80
+ prompt_prefix = f"{self._agent_name} ({self._agent_id}) "
81
+
82
+ # Use FormattedText if prompt_toolkit is available, otherwise use simple string
83
+ if _HAS_PROMPT_TOOLKIT and FormattedText is not None:
84
+ segments = [
85
+ ("class:prompt", prompt_prefix),
86
+ (
87
+ "class:prompt-verbose-on"
88
+ if verbose_enabled
89
+ else "class:prompt-verbose-off",
90
+ verbose_tag,
91
+ ),
92
+ ("class:prompt", "\n› "),
93
+ ]
94
+ return FormattedText(segments)
95
+
96
+ return f"{prompt_prefix}{verbose_tag}\n› "
97
+
74
98
  raw = self.session._prompt(
75
- f"{self._agent_name} ({self._agent_id})\n› ",
99
+ _prompt_message,
76
100
  placeholder=self._prompt_placeholder,
77
101
  )
78
102
  if self._prompt_placeholder:
@@ -138,9 +162,29 @@ class AgentRunSession:
138
162
  return
139
163
 
140
164
  try:
165
+ ctx = self.session.ctx
166
+ ctx_obj = getattr(ctx, "obj", None)
167
+ previous_session = None
168
+ if isinstance(ctx_obj, dict):
169
+ previous_session = ctx_obj.get("_slash_session")
170
+ ctx_obj["_slash_session"] = self.session
171
+
172
+ self.session.notify_agent_run_started()
141
173
  self.session.ctx.invoke(
142
- agents_run_command, agent_ref=agent_id, input_text=message
174
+ agents_run_command,
175
+ agent_ref=agent_id,
176
+ input_text=message,
177
+ verbose=self.session.verbose_enabled,
143
178
  )
144
179
  self.session.last_run_input = message
145
180
  except click.ClickException as exc:
146
181
  self.console.print(f"[red]{exc}[/red]")
182
+ finally:
183
+ try:
184
+ self.session.notify_agent_run_finished()
185
+ finally:
186
+ if isinstance(ctx_obj, dict):
187
+ if previous_session is None:
188
+ ctx_obj.pop("_slash_session", None)
189
+ else:
190
+ ctx_obj["_slash_session"] = previous_session
@@ -14,20 +14,27 @@ _HAS_PROMPT_TOOLKIT = False
14
14
  try: # pragma: no cover - optional dependency
15
15
  from prompt_toolkit import PromptSession
16
16
  from prompt_toolkit.completion import Completer, Completion
17
- from prompt_toolkit.formatted_text import FormattedText
17
+ from prompt_toolkit.formatted_text import FormattedText, to_formatted_text
18
18
  from prompt_toolkit.key_binding import KeyBindings
19
19
  from prompt_toolkit.patch_stdout import patch_stdout
20
20
  from prompt_toolkit.styles import Style
21
21
 
22
+ try:
23
+ from prompt_toolkit.application import run_in_terminal as ptk_run_in_terminal
24
+ except Exception: # pragma: no cover - compatibility fallback
25
+ ptk_run_in_terminal = None
26
+
22
27
  _HAS_PROMPT_TOOLKIT = True
23
28
  except Exception: # pragma: no cover - optional dependency
24
29
  PromptSession = None # type: ignore[assignment]
25
30
  Completer = None # type: ignore[assignment]
26
31
  Completion = None # type: ignore[assignment]
27
32
  FormattedText = None # type: ignore[assignment]
33
+ to_formatted_text = None # type: ignore[assignment]
28
34
  KeyBindings = None # type: ignore[assignment]
29
35
  Style = None # type: ignore[assignment]
30
36
  patch_stdout = None # type: ignore[assignment]
37
+ ptk_run_in_terminal = None
31
38
 
32
39
  if TYPE_CHECKING: # pragma: no cover - typing only
33
40
  from .session import SlashSession
@@ -76,7 +83,7 @@ def setup_prompt_toolkit(
76
83
  if PromptSession is None or Style is None:
77
84
  return None, None
78
85
 
79
- bindings = _create_key_bindings()
86
+ bindings = _create_key_bindings(session)
80
87
 
81
88
  prompt_session = PromptSession(
82
89
  completer=SlashCompleter(session),
@@ -86,6 +93,8 @@ def setup_prompt_toolkit(
86
93
  prompt_style = Style.from_dict(
87
94
  {
88
95
  "prompt": "bg:#0f172a #facc15 bold",
96
+ "prompt-verbose-on": "bg:#0f172a #34d399 bold",
97
+ "prompt-verbose-off": "bg:#0f172a #f87171 bold",
89
98
  "": "bg:#0f172a #e2e8f0",
90
99
  "placeholder": "bg:#0f172a #94a3b8 italic",
91
100
  }
@@ -94,7 +103,7 @@ def setup_prompt_toolkit(
94
103
  return prompt_session, prompt_style
95
104
 
96
105
 
97
- def _create_key_bindings() -> Any:
106
+ def _create_key_bindings(session: SlashSession) -> Any:
98
107
  """Create prompt_toolkit key bindings for the command palette."""
99
108
 
100
109
  if KeyBindings is None:
@@ -109,29 +118,57 @@ def _create_key_bindings() -> Any:
109
118
  elif buffer.complete_state is not None:
110
119
  buffer.cancel_completion()
111
120
 
112
- @bindings.add("/") # type: ignore[misc]
113
121
  def _trigger_slash_completion(event: Any) -> None: # pragma: no cover - UI
114
122
  buffer = event.app.current_buffer
115
123
  buffer.insert_text("/")
116
124
  _refresh_completions(buffer)
117
125
 
118
- @bindings.add("backspace") # type: ignore[misc]
119
126
  def _handle_backspace(event: Any) -> None: # pragma: no cover - UI
120
127
  buffer = event.app.current_buffer
121
128
  if buffer.document.cursor_position > 0:
122
129
  buffer.delete_before_cursor()
123
130
  _refresh_completions(buffer)
124
131
 
132
+ def _toggle_verbose(event: Any) -> None: # pragma: no cover - UI
133
+ _execute_toggle_verbose(session, event.app)
134
+
135
+ @bindings.add("/") # type: ignore[misc]
136
+ def _add_trigger_slash_completion(event: Any) -> None:
137
+ _trigger_slash_completion(event)
138
+
139
+ @bindings.add("backspace") # type: ignore[misc]
140
+ def _add_handle_backspace(event: Any) -> None:
141
+ _handle_backspace(event)
142
+
125
143
  @bindings.add("c-h") # type: ignore[misc]
126
- def _handle_ctrl_h(event: Any) -> None: # pragma: no cover - UI
127
- buffer = event.app.current_buffer
128
- if buffer.document.cursor_position > 0:
129
- buffer.delete_before_cursor()
130
- _refresh_completions(buffer)
144
+ def _add_handle_ctrl_h(event: Any) -> None:
145
+ _handle_backspace(event) # Reuse backspace handler
146
+
147
+ @bindings.add("c-t") # type: ignore[misc]
148
+ def _add_toggle_verbose(event: Any) -> None:
149
+ _toggle_verbose(event)
131
150
 
132
151
  return bindings
133
152
 
134
153
 
154
+ def _execute_toggle_verbose(
155
+ session: SlashSession, app: Any
156
+ ) -> None: # pragma: no cover - UI
157
+ """Execute verbose toggle with proper terminal handling."""
158
+
159
+ def _announce() -> None:
160
+ session.toggle_verbose(announce=False)
161
+
162
+ run_in_terminal = getattr(app, "run_in_terminal", None)
163
+ if callable(run_in_terminal):
164
+ run_in_terminal(_announce)
165
+ elif ptk_run_in_terminal is not None:
166
+ ptk_run_in_terminal(_announce)
167
+ else:
168
+ _announce()
169
+ app.invalidate()
170
+
171
+
135
172
  def _iter_command_completions(
136
173
  session: SlashSession, text: str
137
174
  ) -> Iterable[Completion]: # pragma: no cover - thin wrapper
@@ -191,6 +228,7 @@ __all__ = [
191
228
  "SlashCompleter",
192
229
  "setup_prompt_toolkit",
193
230
  "FormattedText",
231
+ "to_formatted_text",
194
232
  "patch_stdout",
195
233
  "PromptSession",
196
234
  "Style",