glaip-sdk 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. glaip_sdk/_version.py +8 -0
  2. glaip_sdk/branding.py +13 -0
  3. glaip_sdk/cli/commands/agents.py +180 -39
  4. glaip_sdk/cli/commands/mcps.py +44 -18
  5. glaip_sdk/cli/commands/models.py +11 -5
  6. glaip_sdk/cli/commands/tools.py +35 -16
  7. glaip_sdk/cli/commands/transcripts.py +8 -0
  8. glaip_sdk/cli/constants.py +38 -0
  9. glaip_sdk/cli/context.py +8 -0
  10. glaip_sdk/cli/display.py +34 -19
  11. glaip_sdk/cli/main.py +14 -7
  12. glaip_sdk/cli/masking.py +8 -33
  13. glaip_sdk/cli/pager.py +9 -10
  14. glaip_sdk/cli/slash/agent_session.py +57 -20
  15. glaip_sdk/cli/slash/prompt.py +8 -0
  16. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  17. glaip_sdk/cli/slash/session.py +341 -46
  18. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  19. glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
  20. glaip_sdk/cli/transcript/viewer.py +232 -32
  21. glaip_sdk/cli/update_notifier.py +2 -2
  22. glaip_sdk/cli/utils.py +266 -35
  23. glaip_sdk/cli/validators.py +5 -6
  24. glaip_sdk/client/__init__.py +2 -1
  25. glaip_sdk/client/_agent_payloads.py +30 -0
  26. glaip_sdk/client/agent_runs.py +147 -0
  27. glaip_sdk/client/agents.py +186 -22
  28. glaip_sdk/client/main.py +23 -6
  29. glaip_sdk/client/mcps.py +2 -4
  30. glaip_sdk/client/run_rendering.py +66 -0
  31. glaip_sdk/client/tools.py +2 -3
  32. glaip_sdk/config/constants.py +11 -0
  33. glaip_sdk/models/__init__.py +56 -0
  34. glaip_sdk/models/agent_runs.py +117 -0
  35. glaip_sdk/rich_components.py +58 -2
  36. glaip_sdk/utils/client_utils.py +13 -0
  37. glaip_sdk/utils/export.py +143 -0
  38. glaip_sdk/utils/import_export.py +6 -9
  39. glaip_sdk/utils/rendering/__init__.py +122 -1
  40. glaip_sdk/utils/rendering/renderer/base.py +3 -7
  41. glaip_sdk/utils/rendering/renderer/debug.py +0 -1
  42. glaip_sdk/utils/rendering/renderer/stream.py +4 -12
  43. glaip_sdk/utils/rendering/steps.py +1 -0
  44. glaip_sdk/utils/resource_refs.py +26 -15
  45. glaip_sdk/utils/serialization.py +16 -0
  46. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/METADATA +24 -2
  47. glaip_sdk-0.3.0.dist-info/RECORD +94 -0
  48. glaip_sdk-0.2.1.dist-info/RECORD +0 -86
  49. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/WHEEL +0 -0
  50. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/entry_points.txt +0 -0
@@ -4,7 +4,6 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
- # pylint: disable=duplicate-code
8
7
  import json
9
8
  import re
10
9
  from pathlib import Path
@@ -40,7 +39,10 @@ from glaip_sdk.cli.resolution import resolve_resource_reference
40
39
  from glaip_sdk.cli.rich_helpers import markup_text, print_markup
41
40
  from glaip_sdk.cli.utils import (
42
41
  coerce_to_row,
42
+ format_datetime_fields,
43
43
  get_client,
44
+ handle_best_effort_check,
45
+ handle_resource_export,
44
46
  output_list,
45
47
  output_result,
46
48
  spinner_context,
@@ -57,16 +59,32 @@ def tools_group() -> None:
57
59
  pass
58
60
 
59
61
 
60
- # pylint: disable=duplicate-code
61
62
  def _resolve_tool(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None:
62
- """Resolve tool reference (ID or name) with ambiguity handling."""
63
+ """Resolve a tool by ID or name, handling ambiguous matches interactively.
64
+
65
+ This function provides tool-specific resolution logic. It uses
66
+ resolve_resource_reference to find tools by UUID or name, with interactive
67
+ selection when multiple matches are found.
68
+
69
+ Args:
70
+ ctx: Click context for CLI operations.
71
+ client: API client instance.
72
+ ref: Tool reference (UUID string or name).
73
+ select: Pre-selected index for non-interactive mode (1-based).
74
+
75
+ Returns:
76
+ Tool object if found, None otherwise.
77
+ """
78
+ # Configure tool-specific resolution with standard fuzzy matching
79
+ get_by_id = client.get_tool
80
+ find_by_name = client.find_tools
63
81
  return resolve_resource_reference(
64
82
  ctx,
65
83
  client,
66
84
  ref,
67
85
  "tool",
68
- client.get_tool,
69
- client.find_tools,
86
+ get_by_id,
87
+ find_by_name,
70
88
  "Tool",
71
89
  select=select,
72
90
  )
@@ -100,19 +118,16 @@ def _validate_name_match(provided: str | None, internal: str) -> str:
100
118
 
101
119
  def _check_duplicate_name(client: Any, tool_name: str) -> None:
102
120
  """Raise if a tool with the same name already exists."""
103
- try:
121
+
122
+ def _check_duplicate() -> None:
104
123
  existing = client.find_tools(name=tool_name)
105
124
  if existing:
106
125
  raise click.ClickException(
107
126
  f"A tool named '{tool_name}' already exists. "
108
127
  "Please change your plugin's 'name' to a unique value, then re-run."
109
128
  )
110
- except click.ClickException:
111
- # Re-raise ClickException (intended error)
112
- raise
113
- except Exception:
114
- # Non-fatal: best-effort duplicate check for other errors
115
- pass
129
+
130
+ handle_best_effort_check(_check_duplicate)
116
131
 
117
132
 
118
133
  def _parse_tags(tags: str | None) -> list[str]:
@@ -211,6 +226,14 @@ def list_tools(ctx: Any, tool_type: str | None) -> None:
211
226
 
212
227
  # Transform function for safe dictionary access
213
228
  def transform_tool(tool: Any) -> dict[str, Any]:
229
+ """Transform a tool object to a display row dictionary.
230
+
231
+ Args:
232
+ tool: Tool object to transform.
233
+
234
+ Returns:
235
+ Dictionary with id, name, and framework fields.
236
+ """
214
237
  row = coerce_to_row(tool, ["id", "name", "framework"])
215
238
  # Ensure id is always a string
216
239
  row["id"] = str(row["id"])
@@ -341,8 +364,6 @@ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None
341
364
 
342
365
  # Handle export option
343
366
  if export:
344
- from glaip_sdk.cli.utils import handle_resource_export
345
-
346
367
  handle_resource_export(
347
368
  ctx,
348
369
  tool,
@@ -363,8 +384,6 @@ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None
363
384
  if raw_tool_data:
364
385
  # Use raw API data - this preserves ALL fields
365
386
  # Format dates for better display (minimal postprocessing)
366
- from glaip_sdk.cli.utils import format_datetime_fields
367
-
368
387
  formatted_data = format_datetime_fields(raw_tool_data)
369
388
 
370
389
  # Display using output_result with raw data
@@ -156,6 +156,14 @@ def _launch_transcript_viewer(
156
156
  viewer_ctx = _build_viewer_context(entry, meta, events)
157
157
 
158
158
  def _export(destination: Path) -> Path:
159
+ """Export cached transcript to destination.
160
+
161
+ Args:
162
+ destination: Path to export transcript to.
163
+
164
+ Returns:
165
+ Path to exported transcript file.
166
+ """
159
167
  return export_cached_transcript(destination=destination, run_id=entry.run_id)
160
168
 
161
169
  run_viewer_session(target_console, viewer_ctx, _export, initial_view=initial_view)
@@ -0,0 +1,38 @@
1
+ """CLI-specific constants for glaip-sdk.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ # Minimum length that forces multiline YAML strings to be rendered using the literal
8
+ # block style. This prevents long prompts and instructions from being inlined.
9
+ LITERAL_STRING_THRESHOLD = 200
10
+
11
+ # Masking configuration
12
+ MASKING_ENABLED = True
13
+ MASK_SENSITIVE_FIELDS = {
14
+ "api_key",
15
+ "apikey",
16
+ "token",
17
+ "access_token",
18
+ "secret",
19
+ "client_secret",
20
+ "password",
21
+ "private_key",
22
+ "bearer",
23
+ }
24
+
25
+ # Table + pager behaviour
26
+ TABLE_SORT_ENABLED = True
27
+ PAGER_MODE = "auto" # valid values: "auto", "on", "off"
28
+ PAGER_WRAP_LINES = False
29
+ PAGER_HEADER_ENABLED = True
30
+
31
+ # Update notification toggle
32
+ UPDATE_CHECK_ENABLED = True
33
+
34
+ # Agent instruction preview defaults
35
+ DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT = 800
36
+
37
+ # Remote runs defaults
38
+ DEFAULT_REMOTE_RUNS_PAGE_LIMIT = 20
glaip_sdk/cli/context.py CHANGED
@@ -106,6 +106,14 @@ def output_flags() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
106
106
  """
107
107
 
108
108
  def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
109
+ """Apply output flags to a click command.
110
+
111
+ Args:
112
+ f: Click command function to decorate.
113
+
114
+ Returns:
115
+ Decorated command function.
116
+ """
109
117
  f = click.option(
110
118
  "--json",
111
119
  "json_mode",
glaip_sdk/cli/display.py CHANGED
@@ -17,7 +17,7 @@ from rich.text import Text
17
17
 
18
18
  from glaip_sdk.branding import ERROR_STYLE, SUCCESS, SUCCESS_STYLE, WARNING_STYLE
19
19
  from glaip_sdk.cli.rich_helpers import markup_text
20
- from glaip_sdk.cli.utils import command_hint, format_command_hint
20
+ from glaip_sdk.cli.utils import command_hint, format_command_hint, in_slash_mode
21
21
  from glaip_sdk.icons import ICON_AGENT, ICON_TOOL
22
22
  from glaip_sdk.rich_components import AIPPanel
23
23
 
@@ -304,6 +304,7 @@ def display_agent_run_suggestions(agent: Any) -> Panel:
304
304
  """Return a panel with post-creation suggestions for an agent."""
305
305
  agent_id = getattr(agent, "id", "")
306
306
  agent_name = getattr(agent, "name", "")
307
+ slash_mode = in_slash_mode()
307
308
  run_hint_id = command_hint(
308
309
  f'agents run {agent_id} "Your message here"',
309
310
  slash_command=None,
@@ -313,27 +314,41 @@ def display_agent_run_suggestions(agent: Any) -> Panel:
313
314
  slash_command=None,
314
315
  )
315
316
 
316
- cli_section = ""
317
- if run_hint_id and run_hint_name:
318
- cli_section = (
319
- "📋 Prefer the CLI instead?\n"
320
- f" {format_command_hint(run_hint_id) or run_hint_id}\n"
321
- f" {format_command_hint(run_hint_name) or run_hint_name}\n\n"
317
+ content_parts: list[str] = ["[bold blue]💡 Next Steps:[/bold blue]\n\n"]
318
+
319
+ if slash_mode:
320
+ slash_shortcuts = "\n".join(
321
+ f" {format_command_hint(command, description) or command}"
322
+ for command, description in (
323
+ ("/details", "Show configuration (toggle preview)"),
324
+ ("/help", "Show command palette menu"),
325
+ ("/exit", "Return to the palette"),
326
+ )
327
+ )
328
+ content_parts.append(
329
+ f"🚀 Start chatting with [bold]{agent_name}[/bold] right here:\n"
330
+ f" Type your message below and press Enter to run it immediately.\n\n"
331
+ f"{ICON_TOOL} Slash shortcuts:\n"
332
+ f"{slash_shortcuts}"
333
+ )
334
+ else:
335
+ cli_hint_lines = [format_command_hint(hint) or hint for hint in (run_hint_id, run_hint_name) if hint]
336
+ if cli_hint_lines:
337
+ joined_hints = "\n".join(f" {hint}" for hint in cli_hint_lines)
338
+ content_parts.append(f"🚀 Run this agent from the CLI:\n{joined_hints}\n\n")
339
+ content_parts.append(
340
+ f"{ICON_TOOL} Available options:\n"
341
+ f" [dim]--chat-history[/dim] Include previous conversation\n"
342
+ f" [dim]--file[/dim] Attach files\n"
343
+ f" [dim]--input[/dim] Alternative input method\n"
344
+ f" [dim]--timeout[/dim] Set execution timeout\n"
345
+ f" [dim]--save[/dim] Save transcript to file\n"
346
+ f" [dim]--verbose[/dim] Show detailed execution\n\n"
347
+ f"💡 [dim]Input text can be positional OR use --input flag (both work!)[/dim]"
322
348
  )
323
349
 
324
350
  return AIPPanel(
325
- f"[bold blue]💡 Next Steps:[/bold blue]\n\n"
326
- f"🚀 Start chatting with [bold]{agent_name}[/bold] right here:\n"
327
- f" Type your message below and press Enter to run it immediately.\n\n"
328
- f"{cli_section}"
329
- f"{ICON_TOOL} Available options:\n"
330
- f" [dim]--chat-history[/dim] Include previous conversation\n"
331
- f" [dim]--file[/dim] Attach files\n"
332
- f" [dim]--input[/dim] Alternative input method\n"
333
- f" [dim]--timeout[/dim] Set execution timeout\n"
334
- f" [dim]--save[/dim] Save transcript to file\n"
335
- f" [dim]--verbose[/dim] Show detailed execution\n\n"
336
- f"💡 [dim]Input text can be positional OR use --input flag (both work!)[/dim]",
351
+ "".join(content_parts),
337
352
  title=f"{ICON_AGENT} Ready to Run Agent",
338
353
  border_style="blue",
339
354
  padding=(0, 1),
glaip_sdk/cli/main.py CHANGED
@@ -34,7 +34,7 @@ from glaip_sdk.cli.commands.mcps import mcps_group
34
34
  from glaip_sdk.cli.commands.models import models_group
35
35
  from glaip_sdk.cli.commands.tools import tools_group
36
36
  from glaip_sdk.cli.commands.transcripts import transcripts_group
37
- from glaip_sdk.cli.commands.update import update_command
37
+ from glaip_sdk.cli.commands.update import _build_upgrade_command, update_command
38
38
  from glaip_sdk.cli.config import load_config
39
39
  from glaip_sdk.cli.transcript import get_transcript_cache_stats
40
40
  from glaip_sdk.cli.update_notifier import maybe_notify_update
@@ -387,6 +387,7 @@ def version() -> None:
387
387
  )
388
388
  def update(check_only: bool, force: bool) -> None:
389
389
  """Update AIP SDK to the latest version from PyPI."""
390
+ slash_mode = in_slash_mode()
390
391
  try:
391
392
  console = Console()
392
393
 
@@ -400,11 +401,15 @@ def update(check_only: bool, force: bool) -> None:
400
401
  )
401
402
  return
402
403
 
404
+ update_hint = ""
405
+ if not slash_mode:
406
+ update_hint = "\n💡 Use --check-only to just check for updates"
407
+
403
408
  console.print(
404
409
  AIPPanel(
405
410
  "[bold blue]🔄 Updating AIP SDK...[/bold blue]\n\n"
406
- "📦 This will update the package from PyPI\n"
407
- "💡 Use --check-only to just check for updates",
411
+ "📦 This will update the package from PyPI"
412
+ f"{update_hint}",
408
413
  title="Update Process",
409
414
  border_style="blue",
410
415
  padding=(0, 1),
@@ -413,8 +418,6 @@ def update(check_only: bool, force: bool) -> None:
413
418
 
414
419
  # Update using pip
415
420
  try:
416
- from glaip_sdk.cli.commands.update import _build_upgrade_command
417
-
418
421
  cmd = list(_build_upgrade_command(include_prerelease=False))
419
422
  # Replace package name with "glaip-sdk" (main.py uses different name)
420
423
  cmd[-1] = "glaip-sdk"
@@ -422,11 +425,15 @@ def update(check_only: bool, force: bool) -> None:
422
425
  cmd.insert(5, "--force-reinstall")
423
426
  subprocess.run(cmd, capture_output=True, text=True, check=True)
424
427
 
428
+ verify_hint = ""
429
+ if not slash_mode:
430
+ verify_hint = "\n💡 Restart your terminal or run 'aip --version' to verify"
431
+
425
432
  console.print(
426
433
  AIPPanel(
427
434
  f"[{SUCCESS_STYLE}]✅ Update successful![/]\n\n"
428
- "🔄 AIP SDK has been updated to the latest version\n"
429
- "💡 Restart your terminal or run 'aip --version' to verify",
435
+ "🔄 AIP SDK has been updated to the latest version"
436
+ f"{verify_hint}",
430
437
  title="🎉 Update Complete",
431
438
  border_style=SUCCESS,
432
439
  padding=(0, 1),
glaip_sdk/cli/masking.py CHANGED
@@ -6,9 +6,10 @@ Authors:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import os
10
9
  from typing import Any
11
10
 
11
+ from glaip_sdk.cli.constants import MASKING_ENABLED, MASK_SENSITIVE_FIELDS
12
+
12
13
  __all__ = [
13
14
  "mask_payload",
14
15
  "mask_rows",
@@ -18,18 +19,6 @@ __all__ = [
18
19
  "_resolve_mask_fields",
19
20
  ]
20
21
 
21
- _DEFAULT_MASK_FIELDS = {
22
- "api_key",
23
- "apikey",
24
- "token",
25
- "access_token",
26
- "secret",
27
- "client_secret",
28
- "password",
29
- "private_key",
30
- "bearer",
31
- }
32
-
33
22
 
34
23
  def _mask_value(raw: Any) -> str:
35
24
  """Return a masked representation of the provided value.
@@ -90,22 +79,10 @@ def _maybe_mask_row(row: dict[str, Any], mask_fields: set[str]) -> dict[str, Any
90
79
 
91
80
 
92
81
  def _resolve_mask_fields() -> set[str]:
93
- """Resolve the set of sensitive fields to mask based on environment.
94
-
95
- Returns:
96
- set[str]: Set of field names to mask. Empty set if masking is disabled
97
- via AIP_MASK_OFF environment variable, custom fields from
98
- AIP_MASK_FIELDS, or default fields if neither is set.
99
- """
100
- if os.getenv("AIP_MASK_OFF", "0") in {"1", "true", "on", "yes"}:
82
+ """Return the configured set of fields that should be masked."""
83
+ if not MASKING_ENABLED:
101
84
  return set()
102
-
103
- env_fields = (os.getenv("AIP_MASK_FIELDS") or "").strip()
104
- if env_fields:
105
- parts = [part.strip().lower() for part in env_fields.split(",") if part.strip()]
106
- return set(parts)
107
-
108
- return set(_DEFAULT_MASK_FIELDS)
85
+ return set(MASK_SENSITIVE_FIELDS)
109
86
 
110
87
 
111
88
  def mask_payload(payload: Any) -> Any:
@@ -115,9 +92,7 @@ def mask_payload(payload: Any) -> Any:
115
92
  payload: Any data structure (dict, list, or primitive) to mask.
116
93
 
117
94
  Returns:
118
- Any: The payload with sensitive fields masked based on environment
119
- configuration. Returns original payload if masking is disabled
120
- or if an error occurs during masking.
95
+ Any: The payload with sensitive fields masked based on configuration.
121
96
  """
122
97
  mask_fields = _resolve_mask_fields()
123
98
  if not mask_fields:
@@ -136,8 +111,8 @@ def mask_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
136
111
 
137
112
  Returns:
138
113
  list[dict[str, Any]]: List of rows with sensitive fields masked based
139
- on environment configuration. Returns original
140
- rows if masking is disabled or if an error occurs.
114
+ on configuration. Returns original rows if
115
+ masking is disabled or if an error occurs.
141
116
  """
142
117
  mask_fields = _resolve_mask_fields()
143
118
  if not mask_fields:
glaip_sdk/cli/pager.py CHANGED
@@ -19,6 +19,8 @@ from typing import Any
19
19
 
20
20
  from rich.console import Console
21
21
 
22
+ from glaip_sdk.cli.constants import PAGER_HEADER_ENABLED, PAGER_MODE, PAGER_WRAP_LINES
23
+
22
24
  __all__ = [
23
25
  "console",
24
26
  "_prepare_pager_env",
@@ -64,8 +66,7 @@ def _prepare_pager_env(clear_on_exit: bool = True) -> None:
64
66
  -R : pass ANSI color escapes
65
67
  -S : chop long lines (horizontal scroll with ←/→)
66
68
  (No -F, no -X) so we open a full-screen pager and clear on exit.
67
- Toggle wrapping with AIP_PAGER_WRAP=1 to drop -S.
68
- Power users can override via AIP_LESS_FLAGS.
69
+ Toggle wrapping via `PAGER_WRAP_LINES` (True drops -S).
69
70
 
70
71
  Args:
71
72
  clear_on_exit: Whether to clear the pager on exit (default: True)
@@ -75,10 +76,9 @@ def _prepare_pager_env(clear_on_exit: bool = True) -> None:
75
76
  """
76
77
  os.environ.pop("LESSSECURE", None)
77
78
  if os.getenv("LESS") is None:
78
- want_wrap = os.getenv("AIP_PAGER_WRAP", "0") == "1"
79
- base = "-R" if want_wrap else "-RS"
79
+ base = "-R" if PAGER_WRAP_LINES else "-RS"
80
80
  default_flags = base if clear_on_exit else (base + "FX")
81
- os.environ["LESS"] = os.getenv("AIP_LESS_FLAGS", default_flags)
81
+ os.environ["LESS"] = default_flags
82
82
 
83
83
 
84
84
  def _render_ansi(renderable: Any) -> str:
@@ -111,8 +111,7 @@ def _pager_header() -> str:
111
111
  Returns:
112
112
  str: Header text containing navigation help, or empty string if disabled
113
113
  """
114
- v = (os.getenv("AIP_PAGER_HEADER", "1") or "1").strip().lower()
115
- if v in {"0", "false", "off"}:
114
+ if not PAGER_HEADER_ENABLED:
116
115
  return ""
117
116
  return "\n".join(
118
117
  [
@@ -254,10 +253,10 @@ def _should_page_output(row_count: int, is_tty: bool) -> bool:
254
253
  bool: True if output should be paginated, False otherwise
255
254
  """
256
255
  active_console = _get_console()
257
- pager_env = (os.getenv("AIP_PAGER", "auto") or "auto").lower()
258
- if pager_env in ("0", "off", "false"):
256
+ pager_mode = (PAGER_MODE or "auto").lower()
257
+ if pager_mode in ("0", "off", "false"):
259
258
  return False
260
- if pager_env in ("1", "on", "true"):
259
+ if pager_mode in ("1", "on", "true"):
261
260
  return is_tty
262
261
  try:
263
262
  term_h = active_console.size.height or 24
@@ -14,8 +14,9 @@ import click
14
14
  from glaip_sdk.branding import ERROR_STYLE, HINT_PREFIX_STYLE
15
15
  from glaip_sdk.cli.commands.agents import get as agents_get_command
16
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
17
18
  from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
18
- from glaip_sdk.cli.utils import format_command_hint
19
+ from glaip_sdk.cli.utils import bind_slash_session_context, format_command_hint
19
20
 
20
21
  if TYPE_CHECKING: # pragma: no cover - type checking only
21
22
  from glaip_sdk.cli.slash.session import SlashSession
@@ -38,11 +39,13 @@ class AgentRunSession:
38
39
  self._agent_name = getattr(agent, "name", "") or self._agent_id
39
40
  self._prompt_placeholder: str = "Chat with this agent here; use / for shortcuts. Alt+Enter inserts a newline."
40
41
  self._contextual_completion_help: dict[str, str] = {
41
- "details": "Show this agent's full configuration.",
42
+ "details": "Show this agent's configuration (+ expands prompt).",
42
43
  "help": "Display this context-aware menu.",
44
+ "runs": "✨ NEW · Browse remote run history for this agent.",
43
45
  "exit": "Return to the command palette.",
44
46
  "q": "Return to the command palette.",
45
47
  }
48
+ self._instruction_preview_limit = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
46
49
 
47
50
  def run(self) -> None:
48
51
  """Run the interactive agent session loop."""
@@ -86,6 +89,7 @@ class AgentRunSession:
86
89
  try:
87
90
 
88
91
  def _prompt_message() -> Any:
92
+ """Get formatted prompt message for agent session."""
89
93
  prompt_prefix = f"{self._agent_name} ({self._agent_id}) "
90
94
 
91
95
  # Use FormattedText if prompt_toolkit is available, otherwise use simple string
@@ -122,7 +126,7 @@ class AgentRunSession:
122
126
  if raw in {"/exit", "/back", "/q"}:
123
127
  return self._handle_exit_command()
124
128
 
125
- if raw in {"/details", "/detail"}:
129
+ if raw == "/details":
126
130
  return self._handle_details_command(agent_id)
127
131
 
128
132
  if raw in {"/help", "/?"}:
@@ -151,16 +155,59 @@ class AgentRunSession:
151
155
  self.session.handle_command(raw, invoked_from_agent=True)
152
156
  return not self.session._should_exit
153
157
 
154
- def _show_details(self, agent_id: str) -> None:
158
+ def _show_details(self, agent_id: str, *, enable_prompt: bool = True) -> None:
159
+ """Render the agent's configuration export inside the command palette."""
155
160
  try:
156
- self.session.ctx.invoke(agents_get_command, agent_ref=agent_id)
157
- self.console.print(
158
- f"[{HINT_PREFIX_STYLE}]Tip:[/] Continue the conversation in this prompt, or use "
159
- f"{format_command_hint('/help') or '/help'} for shortcuts."
161
+ self.session.ctx.invoke(
162
+ agents_get_command,
163
+ agent_ref=agent_id,
164
+ instruction_preview=self._instruction_preview_limit,
160
165
  )
166
+ if enable_prompt:
167
+ self._prompt_instruction_view_toggle(agent_id)
168
+ self.console.print(
169
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] Continue the conversation in this prompt, or use "
170
+ f"{format_command_hint('/help') or '/help'} for shortcuts."
171
+ )
161
172
  except click.ClickException as exc:
162
173
  self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
163
174
 
175
+ def _prompt_instruction_view_toggle(self, agent_id: str) -> None:
176
+ """Offer a prompt to expand or collapse the instruction preview after details."""
177
+ if not getattr(self.console, "is_terminal", False):
178
+ return
179
+
180
+ while True:
181
+ mode = "expanded" if self._instruction_preview_limit == 0 else "trimmed"
182
+ self.console.print(f"[dim]Instruction view is {mode}. Press Ctrl+T to toggle, Enter to continue.[/dim]")
183
+ try:
184
+ ch = click.getchar()
185
+ except (EOFError, KeyboardInterrupt): # pragma: no cover - defensive guard
186
+ return
187
+
188
+ if not self._handle_instruction_toggle_input(agent_id, ch):
189
+ break
190
+
191
+ if self._instruction_preview_limit == 0:
192
+ self._instruction_preview_limit = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
193
+ self.console.print("")
194
+
195
+ def _handle_instruction_toggle_input(self, agent_id: str, ch: str) -> bool:
196
+ """Process a single toggle keypress; return False when the loop should exit."""
197
+ if ch in {"\r", "\n"}:
198
+ return False
199
+
200
+ lowered = ch.lower()
201
+ if lowered == "t" or ch == "\x14": # support literal 't' or Ctrl+T
202
+ self._instruction_preview_limit = (
203
+ DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT if self._instruction_preview_limit == 0 else 0
204
+ )
205
+ self._show_details(agent_id, enable_prompt=False)
206
+ return True
207
+
208
+ # Ignore other keys and continue prompting.
209
+ return True
210
+
164
211
  def _after_agent_run(self) -> None:
165
212
  """Handle transcript viewer behaviour after a successful run."""
166
213
  payload, manifest = self.session._get_last_transcript()
@@ -206,21 +253,11 @@ class AgentRunSession:
206
253
  @contextmanager
207
254
  def _bind_session_context(self) -> Any:
208
255
  """Temporarily attach this slash session to the Click context."""
209
- ctx_obj = getattr(self.session.ctx, "obj", None)
210
- has_context = isinstance(ctx_obj, dict)
211
- previous_session = ctx_obj.get("_slash_session") if has_context else None
212
- if has_context:
213
- ctx_obj["_slash_session"] = self.session
214
- try:
256
+ with bind_slash_session_context(self.session.ctx, self.session):
215
257
  yield
216
- finally:
217
- if has_context:
218
- if previous_session is None:
219
- ctx_obj.pop("_slash_session", None)
220
- else:
221
- ctx_obj["_slash_session"] = previous_session
222
258
 
223
259
  def _run_agent(self, agent_id: str, message: str) -> None:
260
+ """Execute the agents run command for the active agent."""
224
261
  if not message:
225
262
  return
226
263
 
@@ -123,6 +123,11 @@ def _create_key_bindings(_session: SlashSession) -> Any:
123
123
  bindings = KeyBindings()
124
124
 
125
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
+ """
126
131
  text = buffer.document.text_before_cursor or ""
127
132
  if text.startswith("/") and " " not in text:
128
133
  buffer.start_completion(select_first=False)
@@ -172,8 +177,11 @@ def _iter_command_completions(
172
177
  return []
173
178
 
174
179
  commands = sorted(session._unique_commands.values(), key=lambda c: c.name)
180
+ agent_context = bool(getattr(session, "_current_agent", None))
175
181
 
176
182
  for cmd in commands:
183
+ if getattr(cmd, "agent_only", False) and not agent_context:
184
+ continue
177
185
  yield from _generate_command_completions(cmd, prefix, text, seen)
178
186
 
179
187