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.
- glaip_sdk/branding.py +8 -4
- glaip_sdk/cli/auth.py +414 -0
- glaip_sdk/cli/commands/agents.py +25 -3
- glaip_sdk/cli/commands/mcps.py +276 -92
- glaip_sdk/cli/main.py +8 -0
- glaip_sdk/cli/slash/agent_session.py +46 -2
- glaip_sdk/cli/slash/prompt.py +48 -10
- glaip_sdk/cli/slash/session.py +258 -68
- glaip_sdk/cli/update_notifier.py +107 -0
- glaip_sdk/cli/utils.py +34 -8
- glaip_sdk/config/constants.py +0 -4
- glaip_sdk/exceptions.py +0 -6
- glaip_sdk/models.py +0 -2
- glaip_sdk/utils/import_export.py +3 -7
- glaip_sdk/utils/rendering/renderer/base.py +105 -45
- glaip_sdk/utils/rendering/renderer/config.py +2 -2
- glaip_sdk/utils/serialization.py +93 -2
- {glaip_sdk-0.0.9.dist-info → glaip_sdk-0.0.11.dist-info}/METADATA +2 -1
- {glaip_sdk-0.0.9.dist-info → glaip_sdk-0.0.11.dist-info}/RECORD +21 -19
- {glaip_sdk-0.0.9.dist-info → glaip_sdk-0.0.11.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.9.dist-info → glaip_sdk-0.0.11.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -29,6 +29,7 @@ from .prompt import (
|
|
|
29
29
|
Style,
|
|
30
30
|
patch_stdout,
|
|
31
31
|
setup_prompt_toolkit,
|
|
32
|
+
to_formatted_text,
|
|
32
33
|
)
|
|
33
34
|
|
|
34
35
|
SlashHandler = Callable[["SlashSession", list[str], bool], bool]
|
|
@@ -61,6 +62,8 @@ class SlashSession:
|
|
|
61
62
|
self._interactive = bool(sys.stdin.isatty() and sys.stdout.isatty())
|
|
62
63
|
self._config_cache: dict[str, Any] | None = None
|
|
63
64
|
self._welcome_rendered = False
|
|
65
|
+
self._verbose_enabled = False
|
|
66
|
+
self._active_renderer: Any | None = None
|
|
64
67
|
|
|
65
68
|
self._home_placeholder = "Start with / to browse commands"
|
|
66
69
|
|
|
@@ -73,6 +76,8 @@ class SlashSession:
|
|
|
73
76
|
self._setup_prompt_toolkit()
|
|
74
77
|
self._register_defaults()
|
|
75
78
|
self._branding = AIPBranding.create_from_sdk()
|
|
79
|
+
self._suppress_login_layout = False
|
|
80
|
+
self._default_actions_shown = False
|
|
76
81
|
|
|
77
82
|
# ------------------------------------------------------------------
|
|
78
83
|
# Session orchestration
|
|
@@ -93,7 +98,9 @@ class SlashSession:
|
|
|
93
98
|
if not self._ensure_configuration():
|
|
94
99
|
return
|
|
95
100
|
|
|
96
|
-
self._render_header(initial=
|
|
101
|
+
self._render_header(initial=not self._welcome_rendered)
|
|
102
|
+
if not self._default_actions_shown:
|
|
103
|
+
self._show_default_quick_actions()
|
|
97
104
|
self._render_home_hint()
|
|
98
105
|
self._run_interactive_loop()
|
|
99
106
|
|
|
@@ -151,6 +158,7 @@ class SlashSession:
|
|
|
151
158
|
self.console.print(
|
|
152
159
|
"[yellow]Configuration required.[/] Launching `/login` wizard..."
|
|
153
160
|
)
|
|
161
|
+
self._suppress_login_layout = True
|
|
154
162
|
try:
|
|
155
163
|
self._cmd_login([], False)
|
|
156
164
|
except KeyboardInterrupt:
|
|
@@ -158,6 +166,8 @@ class SlashSession:
|
|
|
158
166
|
"[red]Configuration aborted. Closing the command palette.[/red]"
|
|
159
167
|
)
|
|
160
168
|
return False
|
|
169
|
+
finally:
|
|
170
|
+
self._suppress_login_layout = False
|
|
161
171
|
|
|
162
172
|
return True
|
|
163
173
|
|
|
@@ -209,58 +219,60 @@ class SlashSession:
|
|
|
209
219
|
def _cmd_help(self, _args: list[str], invoked_from_agent: bool) -> bool:
|
|
210
220
|
try:
|
|
211
221
|
if invoked_from_agent:
|
|
212
|
-
|
|
213
|
-
table.add_column("Input", style="cyan", no_wrap=True)
|
|
214
|
-
table.add_column("What happens", style="green")
|
|
215
|
-
table.add_row(
|
|
216
|
-
"<message>", "Run the active agent once with that prompt."
|
|
217
|
-
)
|
|
218
|
-
table.add_row("/details", "Show the full agent export and metadata.")
|
|
219
|
-
table.add_row(
|
|
220
|
-
self.STATUS_COMMAND, "Display connection status without leaving."
|
|
221
|
-
)
|
|
222
|
-
table.add_row("/exit (/back)", "Return to the slash home screen.")
|
|
223
|
-
table.add_row("/help (/?)", "Display this context-aware menu.")
|
|
224
|
-
self.console.print(table)
|
|
225
|
-
if self.last_run_input:
|
|
226
|
-
self.console.print(f"[dim]Last run input:[/] {self.last_run_input}")
|
|
227
|
-
self.console.print(
|
|
228
|
-
"[dim]Global commands (e.g. `/login`, `/status`) remain available inside the agent prompt.[/dim]"
|
|
229
|
-
)
|
|
222
|
+
self._render_agent_help()
|
|
230
223
|
else:
|
|
231
|
-
|
|
232
|
-
table.add_column("Command", style="cyan", no_wrap=True)
|
|
233
|
-
table.add_column("Description", style="green")
|
|
234
|
-
|
|
235
|
-
for cmd in sorted(self._unique_commands.values(), key=lambda c: c.name):
|
|
236
|
-
aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
|
|
237
|
-
verb = f"/{cmd.name}"
|
|
238
|
-
if aliases:
|
|
239
|
-
verb = f"{verb} ({aliases})"
|
|
240
|
-
table.add_row(verb, cmd.help)
|
|
241
|
-
|
|
242
|
-
self.console.print(table)
|
|
243
|
-
self.console.print(
|
|
244
|
-
"[dim]Tip: `{self.AGENTS_COMMAND}` lets you jump into an agent run prompt quickly.[/dim]"
|
|
245
|
-
)
|
|
224
|
+
self._render_global_help()
|
|
246
225
|
except Exception as exc: # pragma: no cover - UI/display errors
|
|
247
226
|
self.console.print(f"[red]Error displaying help: {exc}[/red]")
|
|
248
227
|
return False
|
|
249
228
|
|
|
250
229
|
return True
|
|
251
230
|
|
|
231
|
+
def _render_agent_help(self) -> None:
|
|
232
|
+
table = Table(title="Agent Context")
|
|
233
|
+
table.add_column("Input", style="cyan", no_wrap=True)
|
|
234
|
+
table.add_column("What happens", style="green")
|
|
235
|
+
table.add_row("<message>", "Run the active agent once with that prompt.")
|
|
236
|
+
table.add_row("/details", "Show the full agent export and metadata.")
|
|
237
|
+
table.add_row(self.STATUS_COMMAND, "Display connection status without leaving.")
|
|
238
|
+
table.add_row("/verbose", "Toggle verbose streaming output (Ctrl+T works too).")
|
|
239
|
+
table.add_row("/exit (/back)", "Return to the slash home screen.")
|
|
240
|
+
table.add_row("/help (/?)", "Display this context-aware menu.")
|
|
241
|
+
self.console.print(table)
|
|
242
|
+
if self.last_run_input:
|
|
243
|
+
self.console.print(f"[dim]Last run input:[/] {self.last_run_input}")
|
|
244
|
+
self.console.print(
|
|
245
|
+
"[dim]Global commands (e.g. `/login`, `/status`) remain available inside the agent prompt.[/dim]"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def _render_global_help(self) -> None:
|
|
249
|
+
table = Table(title="Slash Commands")
|
|
250
|
+
table.add_column("Command", style="cyan", no_wrap=True)
|
|
251
|
+
table.add_column("Description", style="green")
|
|
252
|
+
|
|
253
|
+
for cmd in sorted(self._unique_commands.values(), key=lambda c: c.name):
|
|
254
|
+
aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
|
|
255
|
+
verb = f"/{cmd.name}"
|
|
256
|
+
if aliases:
|
|
257
|
+
verb = f"{verb} ({aliases})"
|
|
258
|
+
table.add_row(verb, cmd.help)
|
|
259
|
+
|
|
260
|
+
self.console.print(table)
|
|
261
|
+
self.console.print(
|
|
262
|
+
"[dim]Tip: `{self.AGENTS_COMMAND}` lets you jump into an agent run prompt quickly.[/dim]"
|
|
263
|
+
)
|
|
264
|
+
|
|
252
265
|
def _cmd_login(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
253
266
|
self.console.print("[cyan]Launching configuration wizard...[/cyan]")
|
|
254
267
|
try:
|
|
255
268
|
self.ctx.invoke(configure_command)
|
|
256
269
|
self._config_cache = None
|
|
257
|
-
self.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
270
|
+
if self._suppress_login_layout:
|
|
271
|
+
self._welcome_rendered = False
|
|
272
|
+
self._default_actions_shown = False
|
|
273
|
+
else:
|
|
274
|
+
self._render_header(initial=True)
|
|
275
|
+
self._show_default_quick_actions()
|
|
264
276
|
except click.ClickException as exc:
|
|
265
277
|
self.console.print(f"[red]{exc}[/red]")
|
|
266
278
|
return True
|
|
@@ -343,7 +355,7 @@ class SlashSession:
|
|
|
343
355
|
# running.
|
|
344
356
|
return True
|
|
345
357
|
|
|
346
|
-
self.console.print("[cyan]Closing the command palette.")
|
|
358
|
+
self.console.print("[cyan]Closing the command palette.[/cyan]")
|
|
347
359
|
return False
|
|
348
360
|
|
|
349
361
|
# ------------------------------------------------------------------
|
|
@@ -388,12 +400,119 @@ class SlashSession:
|
|
|
388
400
|
aliases=("q",),
|
|
389
401
|
)
|
|
390
402
|
)
|
|
403
|
+
self._register(
|
|
404
|
+
SlashCommand(
|
|
405
|
+
name="verbose",
|
|
406
|
+
help="Toggle verbose streaming output.",
|
|
407
|
+
handler=SlashSession._cmd_verbose,
|
|
408
|
+
)
|
|
409
|
+
)
|
|
391
410
|
|
|
392
411
|
def _register(self, command: SlashCommand) -> None:
|
|
393
412
|
self._unique_commands[command.name] = command
|
|
394
413
|
for key in (command.name, *command.aliases):
|
|
395
414
|
self._commands[key] = command
|
|
396
415
|
|
|
416
|
+
# ------------------------------------------------------------------
|
|
417
|
+
# Verbose mode helpers
|
|
418
|
+
# ------------------------------------------------------------------
|
|
419
|
+
@property
|
|
420
|
+
def verbose_enabled(self) -> bool:
|
|
421
|
+
"""Return whether verbose agent runs are enabled."""
|
|
422
|
+
|
|
423
|
+
return self._verbose_enabled
|
|
424
|
+
|
|
425
|
+
def set_verbose(self, enabled: bool, *, announce: bool = True) -> None:
|
|
426
|
+
"""Enable or disable verbose mode with optional announcement."""
|
|
427
|
+
|
|
428
|
+
if self._verbose_enabled == enabled:
|
|
429
|
+
if announce:
|
|
430
|
+
self._print_verbose_status(context="already")
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
self._verbose_enabled = enabled
|
|
434
|
+
self._sync_active_renderer()
|
|
435
|
+
if announce:
|
|
436
|
+
self._print_verbose_status(context="changed")
|
|
437
|
+
|
|
438
|
+
def toggle_verbose(self, *, announce: bool = True) -> None:
|
|
439
|
+
"""Flip verbose mode state."""
|
|
440
|
+
|
|
441
|
+
self.set_verbose(not self._verbose_enabled, announce=announce)
|
|
442
|
+
|
|
443
|
+
def _cmd_verbose(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
444
|
+
"""Slash handler for `/verbose` command."""
|
|
445
|
+
|
|
446
|
+
if args:
|
|
447
|
+
self.console.print(
|
|
448
|
+
"Usage: `/verbose` toggles verbose streaming output. Press Ctrl+T as a shortcut."
|
|
449
|
+
)
|
|
450
|
+
else:
|
|
451
|
+
self.toggle_verbose()
|
|
452
|
+
|
|
453
|
+
return True
|
|
454
|
+
|
|
455
|
+
def _print_verbose_status(self, *, context: str) -> None:
|
|
456
|
+
state_word = "on" if self._verbose_enabled else "off"
|
|
457
|
+
if context == "already":
|
|
458
|
+
self.console.print(
|
|
459
|
+
f"Verbose mode already {state_word}. Use Ctrl+T or `/verbose` to toggle."
|
|
460
|
+
)
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
change_word = "enabled" if self._verbose_enabled else "disabled"
|
|
464
|
+
self.console.print(
|
|
465
|
+
f"Verbose mode {change_word}. Use Ctrl+T or `/verbose` to toggle."
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# ------------------------------------------------------------------
|
|
469
|
+
# Agent run coordination helpers
|
|
470
|
+
# ------------------------------------------------------------------
|
|
471
|
+
def register_active_renderer(self, renderer: Any) -> None:
|
|
472
|
+
"""Register the renderer currently streaming an agent run."""
|
|
473
|
+
|
|
474
|
+
self._active_renderer = renderer
|
|
475
|
+
self._sync_active_renderer()
|
|
476
|
+
|
|
477
|
+
def clear_active_renderer(self, renderer: Any | None = None) -> None:
|
|
478
|
+
"""Clear the active renderer if it matches the provided instance."""
|
|
479
|
+
|
|
480
|
+
if renderer is not None and renderer is not self._active_renderer:
|
|
481
|
+
return
|
|
482
|
+
self._active_renderer = None
|
|
483
|
+
|
|
484
|
+
def notify_agent_run_started(self) -> None:
|
|
485
|
+
"""Mark that an agent run is in progress."""
|
|
486
|
+
|
|
487
|
+
self.clear_active_renderer()
|
|
488
|
+
|
|
489
|
+
def notify_agent_run_finished(self) -> None:
|
|
490
|
+
"""Mark that the active agent run has completed."""
|
|
491
|
+
|
|
492
|
+
self.clear_active_renderer()
|
|
493
|
+
|
|
494
|
+
def _sync_active_renderer(self) -> None:
|
|
495
|
+
"""Ensure the active renderer reflects the current verbose state."""
|
|
496
|
+
|
|
497
|
+
renderer = self._active_renderer
|
|
498
|
+
if renderer is None:
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
applied = False
|
|
502
|
+
apply_verbose = getattr(renderer, "apply_verbosity", None)
|
|
503
|
+
if callable(apply_verbose):
|
|
504
|
+
try:
|
|
505
|
+
apply_verbose(self._verbose_enabled)
|
|
506
|
+
applied = True
|
|
507
|
+
except Exception:
|
|
508
|
+
pass
|
|
509
|
+
|
|
510
|
+
if not applied and hasattr(renderer, "verbose"):
|
|
511
|
+
try:
|
|
512
|
+
renderer.verbose = self._verbose_enabled
|
|
513
|
+
except Exception:
|
|
514
|
+
pass
|
|
515
|
+
|
|
397
516
|
def _parse(self, raw: str) -> tuple[str, list[str]]:
|
|
398
517
|
try:
|
|
399
518
|
tokens = shlex.split(raw)
|
|
@@ -416,33 +535,83 @@ class SlashSession:
|
|
|
416
535
|
match = get_close_matches(verb, keys, n=1)
|
|
417
536
|
return match[0] if match else None
|
|
418
537
|
|
|
419
|
-
def
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
538
|
+
def _convert_message(self, value: Any) -> Any:
|
|
539
|
+
"""Convert a message value to the appropriate format for display."""
|
|
540
|
+
if FormattedText is not None and to_formatted_text is not None:
|
|
541
|
+
return to_formatted_text(value)
|
|
542
|
+
if FormattedText is not None:
|
|
543
|
+
return FormattedText([("class:prompt", str(value))])
|
|
544
|
+
return str(value)
|
|
545
|
+
|
|
546
|
+
def _get_prompt_kwargs(self, placeholder: str | None) -> dict[str, Any]:
|
|
547
|
+
"""Get prompt kwargs with optional placeholder styling."""
|
|
548
|
+
prompt_kwargs: dict[str, Any] = {"style": self._ptk_style}
|
|
549
|
+
if placeholder:
|
|
550
|
+
placeholder_text = (
|
|
551
|
+
FormattedText([("class:placeholder", placeholder)])
|
|
552
|
+
if FormattedText is not None
|
|
553
|
+
else placeholder
|
|
554
|
+
)
|
|
555
|
+
prompt_kwargs["placeholder"] = placeholder_text
|
|
556
|
+
return prompt_kwargs
|
|
557
|
+
|
|
558
|
+
def _prompt_with_prompt_toolkit(
|
|
559
|
+
self, message: str | Callable[[], Any], placeholder: str | None
|
|
560
|
+
) -> str:
|
|
561
|
+
"""Handle prompting with prompt_toolkit."""
|
|
562
|
+
with patch_stdout(): # pragma: no cover - UI specific
|
|
563
|
+
if callable(message):
|
|
564
|
+
|
|
565
|
+
def prompt_text() -> Any:
|
|
566
|
+
return self._convert_message(message())
|
|
567
|
+
else:
|
|
568
|
+
prompt_text = self._convert_message(message)
|
|
569
|
+
|
|
570
|
+
prompt_kwargs = self._get_prompt_kwargs(placeholder)
|
|
442
571
|
|
|
572
|
+
try:
|
|
573
|
+
return self._ptk_session.prompt(prompt_text, **prompt_kwargs)
|
|
574
|
+
except (
|
|
575
|
+
TypeError
|
|
576
|
+
): # pragma: no cover - compatibility with older prompt_toolkit
|
|
577
|
+
prompt_kwargs.pop("placeholder", None)
|
|
578
|
+
return self._ptk_session.prompt(prompt_text, **prompt_kwargs)
|
|
579
|
+
|
|
580
|
+
def _extract_message_text(self, raw_value: Any) -> str:
|
|
581
|
+
"""Extract text content from various message formats."""
|
|
582
|
+
if isinstance(raw_value, str):
|
|
583
|
+
return raw_value
|
|
584
|
+
|
|
585
|
+
try:
|
|
586
|
+
if FormattedText is not None and isinstance(raw_value, FormattedText):
|
|
587
|
+
return "".join(text for _style, text in raw_value)
|
|
588
|
+
elif isinstance(raw_value, list):
|
|
589
|
+
return "".join(segment[1] for segment in raw_value)
|
|
590
|
+
else:
|
|
591
|
+
return str(raw_value)
|
|
592
|
+
except Exception:
|
|
593
|
+
return str(raw_value)
|
|
594
|
+
|
|
595
|
+
def _prompt_with_basic_input(
|
|
596
|
+
self, message: str | Callable[[], Any], placeholder: str | None
|
|
597
|
+
) -> str:
|
|
598
|
+
"""Handle prompting with basic input."""
|
|
443
599
|
if placeholder:
|
|
444
600
|
self.console.print(f"[dim]{placeholder}[/dim]")
|
|
445
|
-
|
|
601
|
+
|
|
602
|
+
raw_value = message() if callable(message) else message
|
|
603
|
+
actual_message = self._extract_message_text(raw_value)
|
|
604
|
+
|
|
605
|
+
return input(actual_message)
|
|
606
|
+
|
|
607
|
+
def _prompt(
|
|
608
|
+
self, message: str | Callable[[], Any], *, placeholder: str | None = None
|
|
609
|
+
) -> str:
|
|
610
|
+
"""Main prompt function with reduced complexity."""
|
|
611
|
+
if self._ptk_session and self._ptk_style and patch_stdout:
|
|
612
|
+
return self._prompt_with_prompt_toolkit(message, placeholder)
|
|
613
|
+
|
|
614
|
+
return self._prompt_with_basic_input(message, placeholder)
|
|
446
615
|
|
|
447
616
|
def _get_client(self) -> Any: # type: ignore[no-any-return]
|
|
448
617
|
if self._client is None:
|
|
@@ -503,12 +672,17 @@ class SlashSession:
|
|
|
503
672
|
agent_type = getattr(active_agent, "type", "") or "-"
|
|
504
673
|
description = getattr(active_agent, "description", "") or ""
|
|
505
674
|
|
|
675
|
+
verbose_label = "verbose on" if self._verbose_enabled else "verbose off"
|
|
676
|
+
|
|
506
677
|
header_grid = Table.grid(expand=True)
|
|
507
678
|
header_grid.add_column(ratio=3)
|
|
508
679
|
header_grid.add_column(ratio=1, justify="right")
|
|
509
680
|
|
|
510
681
|
primary_line = f"[bold]{agent_name}[/bold] · [dim]{agent_type}[/dim] · [cyan]{agent_id}[/cyan]"
|
|
511
|
-
header_grid.add_row(
|
|
682
|
+
header_grid.add_row(
|
|
683
|
+
primary_line,
|
|
684
|
+
f"[green]ready[/green] · {verbose_label}",
|
|
685
|
+
)
|
|
512
686
|
|
|
513
687
|
if description:
|
|
514
688
|
header_grid.add_row(f"[dim]{description}[/dim]", "")
|
|
@@ -517,10 +691,12 @@ class SlashSession:
|
|
|
517
691
|
keybar.add_column(justify="left")
|
|
518
692
|
keybar.add_column(justify="left")
|
|
519
693
|
keybar.add_column(justify="left")
|
|
694
|
+
keybar.add_column(justify="left")
|
|
520
695
|
keybar.add_row(
|
|
521
696
|
"[bold]/help[/bold] [dim]Show commands[/dim]",
|
|
522
697
|
"[bold]/details[/bold] [dim]Agent config[/dim]",
|
|
523
698
|
"[bold]/exit[/bold] [dim]Back[/dim]",
|
|
699
|
+
"[bold]Ctrl+T[/bold] [dim]Toggle verbose[/dim]",
|
|
524
700
|
)
|
|
525
701
|
|
|
526
702
|
header_grid.add_row(keybar, "")
|
|
@@ -544,6 +720,8 @@ class SlashSession:
|
|
|
544
720
|
"",
|
|
545
721
|
f"[dim]API URL[/dim]: {api_url or 'Not configured'}",
|
|
546
722
|
f"[dim]Credentials[/dim]: {status}",
|
|
723
|
+
f"[dim]Verbose mode[/dim]: {'on' if self._verbose_enabled else 'off'}",
|
|
724
|
+
"[dim]Tip[/dim]: Press Ctrl+T or run `/verbose` to toggle verbose streaming.",
|
|
547
725
|
]
|
|
548
726
|
extra: list[str] = []
|
|
549
727
|
self._add_agent_info_to_header(extra, active_agent)
|
|
@@ -560,8 +738,10 @@ class SlashSession:
|
|
|
560
738
|
status_bar.add_row(
|
|
561
739
|
"[bold cyan]AIP Palette[/bold cyan]",
|
|
562
740
|
f"[dim]API[/dim]: {api_url or 'Not configured'}",
|
|
563
|
-
"[dim]
|
|
741
|
+
f"[dim]Verbose[/dim]: {'on' if self._verbose_enabled else 'off'}",
|
|
564
742
|
)
|
|
743
|
+
status_bar.add_row("[dim]Ctrl+T toggles verbose[/dim]", "", "")
|
|
744
|
+
status_bar.add_row("[dim]Type /help for shortcuts[/dim]", "", "")
|
|
565
745
|
|
|
566
746
|
if active_agent is not None:
|
|
567
747
|
agent_id = str(getattr(active_agent, "id", ""))
|
|
@@ -598,10 +778,20 @@ class SlashSession:
|
|
|
598
778
|
label = recent.get("name") or recent.get("id") or "-"
|
|
599
779
|
lines.append(f"[dim]Recent agent[/dim]: {label} [{recent.get('id', '-')}]")
|
|
600
780
|
|
|
781
|
+
def _show_default_quick_actions(self) -> None:
|
|
782
|
+
self._show_quick_actions(
|
|
783
|
+
[
|
|
784
|
+
(self.STATUS_COMMAND, "Verify the connection"),
|
|
785
|
+
(self.AGENTS_COMMAND, "Pick an agent to inspect or run"),
|
|
786
|
+
]
|
|
787
|
+
)
|
|
788
|
+
self._default_actions_shown = True
|
|
789
|
+
|
|
601
790
|
def _render_home_hint(self) -> None:
|
|
602
791
|
self.console.print(
|
|
603
792
|
AIPPanel(
|
|
604
793
|
"Type `/help` for command palette commands, `/agents` to browse agents, or `/exit` (`/q`) to leave the palette.\n"
|
|
794
|
+
"Press Ctrl+T to toggle verbose output.\n"
|
|
605
795
|
"Press Ctrl+C to cancel the current entry, Ctrl+D to quit immediately.",
|
|
606
796
|
title="✨ Getting Started",
|
|
607
797
|
border_style="cyan",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Utility helpers for checking and displaying SDK update notifications.
|
|
2
|
+
|
|
3
|
+
Author:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from packaging.version import InvalidVersion, Version
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
17
|
+
|
|
18
|
+
FetchLatestVersion = Callable[[], str | None]
|
|
19
|
+
|
|
20
|
+
PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
|
|
21
|
+
DEFAULT_TIMEOUT = 1.5 # seconds
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_version(value: str) -> Version | None:
|
|
25
|
+
"""Parse a version string into a `Version`, returning None on failure."""
|
|
26
|
+
try:
|
|
27
|
+
return Version(value)
|
|
28
|
+
except InvalidVersion:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _fetch_latest_version(package_name: str) -> str | None:
|
|
33
|
+
"""Fetch the latest published version from PyPI."""
|
|
34
|
+
url = PYPI_JSON_URL.format(package=package_name)
|
|
35
|
+
timeout = httpx.Timeout(DEFAULT_TIMEOUT)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
with httpx.Client(timeout=timeout) as client:
|
|
39
|
+
response = client.get(url, headers={"Accept": "application/json"})
|
|
40
|
+
response.raise_for_status()
|
|
41
|
+
payload = response.json()
|
|
42
|
+
except httpx.HTTPError:
|
|
43
|
+
return None
|
|
44
|
+
except ValueError:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
info = payload.get("info") if isinstance(payload, dict) else None
|
|
48
|
+
latest_version = info.get("version") if isinstance(info, dict) else None
|
|
49
|
+
if isinstance(latest_version, str) and latest_version.strip():
|
|
50
|
+
return latest_version.strip()
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _should_check_for_updates() -> bool:
|
|
55
|
+
"""Return False when update checks are explicitly disabled."""
|
|
56
|
+
return os.getenv("AIP_NO_UPDATE_CHECK") is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_update_panel(
|
|
60
|
+
current_version: str,
|
|
61
|
+
latest_version: str,
|
|
62
|
+
) -> AIPPanel:
|
|
63
|
+
"""Create a Rich panel that prompts the user to update."""
|
|
64
|
+
message = (
|
|
65
|
+
f"[bold yellow]✨ Update available![/bold yellow] "
|
|
66
|
+
f"{current_version} → {latest_version}\n\n"
|
|
67
|
+
"See the latest release notes:\n"
|
|
68
|
+
f"https://pypi.org/project/glaip-sdk/{latest_version}/\n\n"
|
|
69
|
+
"[cyan]Run[/cyan] [bold]aip update[/bold] to install."
|
|
70
|
+
)
|
|
71
|
+
return AIPPanel(
|
|
72
|
+
message,
|
|
73
|
+
title="[bold green]AIP SDK Update[/bold green]",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def maybe_notify_update(
|
|
78
|
+
current_version: str,
|
|
79
|
+
*,
|
|
80
|
+
package_name: str = "glaip-sdk",
|
|
81
|
+
console: Console | None = None,
|
|
82
|
+
fetch_latest_version: FetchLatestVersion | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Check PyPI for a newer version and display a prompt if one exists.
|
|
85
|
+
|
|
86
|
+
This function deliberately swallows network errors to avoid impacting CLI
|
|
87
|
+
startup time when offline or when PyPI is unavailable.
|
|
88
|
+
"""
|
|
89
|
+
if not _should_check_for_updates():
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
fetcher = fetch_latest_version or (lambda: _fetch_latest_version(package_name))
|
|
93
|
+
latest_version = fetcher()
|
|
94
|
+
if not latest_version:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
current = _parse_version(current_version)
|
|
98
|
+
latest = _parse_version(latest_version)
|
|
99
|
+
if current is None or latest is None or latest <= current:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
active_console = console or Console()
|
|
103
|
+
panel = _build_update_panel(current_version, latest_version)
|
|
104
|
+
active_console.print(panel)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ["maybe_notify_update"]
|
glaip_sdk/cli/utils.py
CHANGED
|
@@ -828,7 +828,7 @@ def _build_table_group(
|
|
|
828
828
|
table = _create_table(columns, title)
|
|
829
829
|
for row in rows:
|
|
830
830
|
table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
|
|
831
|
-
footer = Text(f"\n[dim]Total {len(rows)} items[/dim]")
|
|
831
|
+
footer = Text.from_markup(f"\n[dim]Total {len(rows)} items[/dim]")
|
|
832
832
|
return Group(table, footer)
|
|
833
833
|
|
|
834
834
|
|
|
@@ -872,7 +872,7 @@ def _handle_markdown_output(
|
|
|
872
872
|
|
|
873
873
|
def _handle_empty_items(title: str) -> None:
|
|
874
874
|
"""Handle case when no items are found."""
|
|
875
|
-
console.print(Text(f"[yellow]No {title.lower()} found.[/yellow]"))
|
|
875
|
+
console.print(Text.from_markup(f"[yellow]No {title.lower()} found.[/yellow]"))
|
|
876
876
|
|
|
877
877
|
|
|
878
878
|
def _should_use_fuzzy_picker() -> bool:
|
|
@@ -916,7 +916,9 @@ def _print_selection_tip(title: str) -> None:
|
|
|
916
916
|
"""Print the contextual follow-up tip after a fuzzy selection."""
|
|
917
917
|
|
|
918
918
|
tip_cmd = _resource_tip_command(title)
|
|
919
|
-
console.print(
|
|
919
|
+
console.print(
|
|
920
|
+
Text.from_markup(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]")
|
|
921
|
+
)
|
|
920
922
|
|
|
921
923
|
|
|
922
924
|
def _handle_fuzzy_pick_selection(
|
|
@@ -936,11 +938,21 @@ def _handle_fuzzy_pick_selection(
|
|
|
936
938
|
|
|
937
939
|
|
|
938
940
|
def _handle_table_output(
|
|
939
|
-
rows: list[dict[str, Any]],
|
|
941
|
+
rows: list[dict[str, Any]],
|
|
942
|
+
columns: list[tuple],
|
|
943
|
+
title: str,
|
|
944
|
+
*,
|
|
945
|
+
use_pager: bool | None = None,
|
|
940
946
|
) -> None:
|
|
941
947
|
"""Handle table output with paging."""
|
|
942
948
|
content = _build_table_group(rows, columns, title)
|
|
943
|
-
|
|
949
|
+
should_page = (
|
|
950
|
+
_should_page_output(len(rows), console.is_terminal and os.isatty(1))
|
|
951
|
+
if use_pager is None
|
|
952
|
+
else use_pager
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
if should_page:
|
|
944
956
|
ansi = _render_ansi(content)
|
|
945
957
|
if not _page_with_system_pager(ansi):
|
|
946
958
|
with console.pager(styles=True):
|
|
@@ -955,8 +967,11 @@ def output_list(
|
|
|
955
967
|
title: str,
|
|
956
968
|
columns: list[tuple[str, str, str, int | None]],
|
|
957
969
|
transform_func: Callable | None = None,
|
|
970
|
+
*,
|
|
971
|
+
skip_picker: bool = False,
|
|
972
|
+
use_pager: bool | None = None,
|
|
958
973
|
) -> None:
|
|
959
|
-
"""Display a list with fuzzy palette
|
|
974
|
+
"""Display a list with optional fuzzy palette for quick selection."""
|
|
960
975
|
fmt = _get_view(ctx)
|
|
961
976
|
rows = _normalise_rows(items, transform_func)
|
|
962
977
|
rows = _mask_rows_if_configured(rows)
|
|
@@ -983,10 +998,10 @@ def output_list(
|
|
|
983
998
|
except Exception:
|
|
984
999
|
pass
|
|
985
1000
|
|
|
986
|
-
if _handle_fuzzy_pick_selection(rows, columns, title):
|
|
1001
|
+
if not skip_picker and _handle_fuzzy_pick_selection(rows, columns, title):
|
|
987
1002
|
return
|
|
988
1003
|
|
|
989
|
-
_handle_table_output(rows, columns, title)
|
|
1004
|
+
_handle_table_output(rows, columns, title, use_pager=use_pager)
|
|
990
1005
|
|
|
991
1006
|
|
|
992
1007
|
# ------------------------- Output flags decorator ------------------------ #
|
|
@@ -1108,6 +1123,17 @@ def build_renderer(
|
|
|
1108
1123
|
verbose=verbose,
|
|
1109
1124
|
)
|
|
1110
1125
|
|
|
1126
|
+
# Link the renderer back to the slash session when running from the palette.
|
|
1127
|
+
try:
|
|
1128
|
+
ctx_obj = getattr(_ctx, "obj", None)
|
|
1129
|
+
if isinstance(ctx_obj, dict):
|
|
1130
|
+
session = ctx_obj.get("_slash_session")
|
|
1131
|
+
if session and hasattr(session, "register_active_renderer"):
|
|
1132
|
+
session.register_active_renderer(renderer)
|
|
1133
|
+
except Exception:
|
|
1134
|
+
# Never let session bookkeeping break renderer creation
|
|
1135
|
+
pass
|
|
1136
|
+
|
|
1111
1137
|
return renderer, working_console
|
|
1112
1138
|
|
|
1113
1139
|
|
glaip_sdk/config/constants.py
CHANGED
glaip_sdk/exceptions.py
CHANGED
|
@@ -98,9 +98,3 @@ class AgentTimeoutError(TimeoutError):
|
|
|
98
98
|
super().__init__(message)
|
|
99
99
|
self.timeout_seconds = timeout_seconds
|
|
100
100
|
self.agent_name = agent_name
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class ClientError(APIError):
|
|
104
|
-
"""Client-side error (e.g., invalid request format, missing parameters)."""
|
|
105
|
-
|
|
106
|
-
pass
|