glaip-sdk 0.0.9__py3-none-any.whl → 0.0.10__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 CHANGED
@@ -4,7 +4,7 @@ Simple, friendly CLI branding for the GL AIP (GDP Labs AI Agent Package) SDK.
4
4
 
5
5
  - Package name: GL AIP (GDP Labs AI Agent Package)
6
6
  - Version: auto-detected (AIP_VERSION env or importlib.metadata), or passed in
7
- - Colors: blue / bright_blue, with NO_COLOR/AIP_NO_COLOR fallbacks
7
+ - Colors: GDP Labs brand palette with NO_COLOR/AIP_NO_COLOR fallbacks
8
8
 
9
9
  Author:
10
10
  Raymond Christopher (raymond.christopher@gdplabs.id)
@@ -30,9 +30,13 @@ except Exception: # pragma: no cover
30
30
  PackageNotFoundError = Exception
31
31
 
32
32
 
33
- # ---- minimal, readable styles (light blue + white theme) -----------------
34
- PRIMARY = "#15a2d8" # GDP Labs brand blue
35
- BORDER = PRIMARY # Keep borders aligned with brand tone
33
+ # ---- GDP Labs Brand Color Palette -----------------------------------------
34
+ PRIMARY = "#004987" # Primary brand blue (dark blue)
35
+ SECONDARY_DARK = "#003A5C" # Darkest variant for emphasis
36
+ SECONDARY_MEDIUM = "#005CB8" # Medium variant for UI elements
37
+ SECONDARY_LIGHT = "#40B4E5" # Light variant for highlights
38
+
39
+ BORDER = PRIMARY # Keep borders aligned with primary brand tone
36
40
  TITLE_STYLE = f"bold {PRIMARY}"
37
41
  LABEL = "bold"
38
42
 
@@ -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",
@@ -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
 
@@ -209,46 +212,49 @@ class SlashSession:
209
212
  def _cmd_help(self, _args: list[str], invoked_from_agent: bool) -> bool:
210
213
  try:
211
214
  if invoked_from_agent:
212
- table = Table(title="Agent Context")
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
- )
215
+ self._render_agent_help()
230
216
  else:
231
- table = Table(title="Slash Commands")
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
- )
217
+ self._render_global_help()
246
218
  except Exception as exc: # pragma: no cover - UI/display errors
247
219
  self.console.print(f"[red]Error displaying help: {exc}[/red]")
248
220
  return False
249
221
 
250
222
  return True
251
223
 
224
+ def _render_agent_help(self) -> None:
225
+ table = Table(title="Agent Context")
226
+ table.add_column("Input", style="cyan", no_wrap=True)
227
+ table.add_column("What happens", style="green")
228
+ table.add_row("<message>", "Run the active agent once with that prompt.")
229
+ table.add_row("/details", "Show the full agent export and metadata.")
230
+ table.add_row(self.STATUS_COMMAND, "Display connection status without leaving.")
231
+ table.add_row("/verbose", "Toggle verbose streaming output (Ctrl+T works too).")
232
+ table.add_row("/exit (/back)", "Return to the slash home screen.")
233
+ table.add_row("/help (/?)", "Display this context-aware menu.")
234
+ self.console.print(table)
235
+ if self.last_run_input:
236
+ self.console.print(f"[dim]Last run input:[/] {self.last_run_input}")
237
+ self.console.print(
238
+ "[dim]Global commands (e.g. `/login`, `/status`) remain available inside the agent prompt.[/dim]"
239
+ )
240
+
241
+ def _render_global_help(self) -> None:
242
+ table = Table(title="Slash Commands")
243
+ table.add_column("Command", style="cyan", no_wrap=True)
244
+ table.add_column("Description", style="green")
245
+
246
+ for cmd in sorted(self._unique_commands.values(), key=lambda c: c.name):
247
+ aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
248
+ verb = f"/{cmd.name}"
249
+ if aliases:
250
+ verb = f"{verb} ({aliases})"
251
+ table.add_row(verb, cmd.help)
252
+
253
+ self.console.print(table)
254
+ self.console.print(
255
+ "[dim]Tip: `{self.AGENTS_COMMAND}` lets you jump into an agent run prompt quickly.[/dim]"
256
+ )
257
+
252
258
  def _cmd_login(self, _args: list[str], _invoked_from_agent: bool) -> bool:
253
259
  self.console.print("[cyan]Launching configuration wizard...[/cyan]")
254
260
  try:
@@ -388,12 +394,119 @@ class SlashSession:
388
394
  aliases=("q",),
389
395
  )
390
396
  )
397
+ self._register(
398
+ SlashCommand(
399
+ name="verbose",
400
+ help="Toggle verbose streaming output.",
401
+ handler=SlashSession._cmd_verbose,
402
+ )
403
+ )
391
404
 
392
405
  def _register(self, command: SlashCommand) -> None:
393
406
  self._unique_commands[command.name] = command
394
407
  for key in (command.name, *command.aliases):
395
408
  self._commands[key] = command
396
409
 
410
+ # ------------------------------------------------------------------
411
+ # Verbose mode helpers
412
+ # ------------------------------------------------------------------
413
+ @property
414
+ def verbose_enabled(self) -> bool:
415
+ """Return whether verbose agent runs are enabled."""
416
+
417
+ return self._verbose_enabled
418
+
419
+ def set_verbose(self, enabled: bool, *, announce: bool = True) -> None:
420
+ """Enable or disable verbose mode with optional announcement."""
421
+
422
+ if self._verbose_enabled == enabled:
423
+ if announce:
424
+ self._print_verbose_status(context="already")
425
+ return
426
+
427
+ self._verbose_enabled = enabled
428
+ self._sync_active_renderer()
429
+ if announce:
430
+ self._print_verbose_status(context="changed")
431
+
432
+ def toggle_verbose(self, *, announce: bool = True) -> None:
433
+ """Flip verbose mode state."""
434
+
435
+ self.set_verbose(not self._verbose_enabled, announce=announce)
436
+
437
+ def _cmd_verbose(self, args: list[str], _invoked_from_agent: bool) -> bool:
438
+ """Slash handler for `/verbose` command."""
439
+
440
+ if args:
441
+ self.console.print(
442
+ "Usage: `/verbose` toggles verbose streaming output. Press Ctrl+T as a shortcut."
443
+ )
444
+ else:
445
+ self.toggle_verbose()
446
+
447
+ return True
448
+
449
+ def _print_verbose_status(self, *, context: str) -> None:
450
+ state_word = "on" if self._verbose_enabled else "off"
451
+ if context == "already":
452
+ self.console.print(
453
+ f"Verbose mode already {state_word}. Use Ctrl+T or `/verbose` to toggle."
454
+ )
455
+ return
456
+
457
+ change_word = "enabled" if self._verbose_enabled else "disabled"
458
+ self.console.print(
459
+ f"Verbose mode {change_word}. Use Ctrl+T or `/verbose` to toggle."
460
+ )
461
+
462
+ # ------------------------------------------------------------------
463
+ # Agent run coordination helpers
464
+ # ------------------------------------------------------------------
465
+ def register_active_renderer(self, renderer: Any) -> None:
466
+ """Register the renderer currently streaming an agent run."""
467
+
468
+ self._active_renderer = renderer
469
+ self._sync_active_renderer()
470
+
471
+ def clear_active_renderer(self, renderer: Any | None = None) -> None:
472
+ """Clear the active renderer if it matches the provided instance."""
473
+
474
+ if renderer is not None and renderer is not self._active_renderer:
475
+ return
476
+ self._active_renderer = None
477
+
478
+ def notify_agent_run_started(self) -> None:
479
+ """Mark that an agent run is in progress."""
480
+
481
+ self.clear_active_renderer()
482
+
483
+ def notify_agent_run_finished(self) -> None:
484
+ """Mark that the active agent run has completed."""
485
+
486
+ self.clear_active_renderer()
487
+
488
+ def _sync_active_renderer(self) -> None:
489
+ """Ensure the active renderer reflects the current verbose state."""
490
+
491
+ renderer = self._active_renderer
492
+ if renderer is None:
493
+ return
494
+
495
+ applied = False
496
+ apply_verbose = getattr(renderer, "apply_verbosity", None)
497
+ if callable(apply_verbose):
498
+ try:
499
+ apply_verbose(self._verbose_enabled)
500
+ applied = True
501
+ except Exception:
502
+ pass
503
+
504
+ if not applied and hasattr(renderer, "verbose"):
505
+ try:
506
+ renderer.verbose = self._verbose_enabled
507
+ except Exception:
508
+ pass
509
+
397
510
  def _parse(self, raw: str) -> tuple[str, list[str]]:
398
511
  try:
399
512
  tokens = shlex.split(raw)
@@ -416,33 +529,83 @@ class SlashSession:
416
529
  match = get_close_matches(verb, keys, n=1)
417
530
  return match[0] if match else None
418
531
 
419
- def _prompt(self, message: str, *, placeholder: str | None = None) -> str:
420
- if self._ptk_session and self._ptk_style and patch_stdout:
421
- with patch_stdout(): # pragma: no cover - UI specific
422
- prompt_text = (
423
- FormattedText([("class:prompt", message)])
424
- if FormattedText is not None
425
- else message
426
- )
427
- prompt_kwargs: dict[str, Any] = {"style": self._ptk_style}
428
- if placeholder:
429
- placeholder_text = (
430
- FormattedText([("class:placeholder", placeholder)])
431
- if FormattedText is not None
432
- else placeholder
433
- )
434
- prompt_kwargs["placeholder"] = placeholder_text
435
- try:
436
- return self._ptk_session.prompt(prompt_text, **prompt_kwargs)
437
- except (
438
- TypeError
439
- ): # pragma: no cover - compatibility with older prompt_toolkit
440
- prompt_kwargs.pop("placeholder", None)
441
- return self._ptk_session.prompt(prompt_text, **prompt_kwargs)
532
+ def _convert_message(self, value: Any) -> Any:
533
+ """Convert a message value to the appropriate format for display."""
534
+ if FormattedText is not None and to_formatted_text is not None:
535
+ return to_formatted_text(value)
536
+ if FormattedText is not None:
537
+ return FormattedText([("class:prompt", str(value))])
538
+ return str(value)
539
+
540
+ def _get_prompt_kwargs(self, placeholder: str | None) -> dict[str, Any]:
541
+ """Get prompt kwargs with optional placeholder styling."""
542
+ prompt_kwargs: dict[str, Any] = {"style": self._ptk_style}
543
+ if placeholder:
544
+ placeholder_text = (
545
+ FormattedText([("class:placeholder", placeholder)])
546
+ if FormattedText is not None
547
+ else placeholder
548
+ )
549
+ prompt_kwargs["placeholder"] = placeholder_text
550
+ return prompt_kwargs
551
+
552
+ def _prompt_with_prompt_toolkit(
553
+ self, message: str | Callable[[], Any], placeholder: str | None
554
+ ) -> str:
555
+ """Handle prompting with prompt_toolkit."""
556
+ with patch_stdout(): # pragma: no cover - UI specific
557
+ if callable(message):
558
+
559
+ def prompt_text() -> Any:
560
+ return self._convert_message(message())
561
+ else:
562
+ prompt_text = self._convert_message(message)
563
+
564
+ prompt_kwargs = self._get_prompt_kwargs(placeholder)
565
+
566
+ try:
567
+ return self._ptk_session.prompt(prompt_text, **prompt_kwargs)
568
+ except (
569
+ TypeError
570
+ ): # pragma: no cover - compatibility with older prompt_toolkit
571
+ prompt_kwargs.pop("placeholder", None)
572
+ return self._ptk_session.prompt(prompt_text, **prompt_kwargs)
573
+
574
+ def _extract_message_text(self, raw_value: Any) -> str:
575
+ """Extract text content from various message formats."""
576
+ if isinstance(raw_value, str):
577
+ return raw_value
442
578
 
579
+ try:
580
+ if FormattedText is not None and isinstance(raw_value, FormattedText):
581
+ return "".join(text for _style, text in raw_value)
582
+ elif isinstance(raw_value, list):
583
+ return "".join(segment[1] for segment in raw_value)
584
+ else:
585
+ return str(raw_value)
586
+ except Exception:
587
+ return str(raw_value)
588
+
589
+ def _prompt_with_basic_input(
590
+ self, message: str | Callable[[], Any], placeholder: str | None
591
+ ) -> str:
592
+ """Handle prompting with basic input."""
443
593
  if placeholder:
444
594
  self.console.print(f"[dim]{placeholder}[/dim]")
445
- return input(message)
595
+
596
+ raw_value = message() if callable(message) else message
597
+ actual_message = self._extract_message_text(raw_value)
598
+
599
+ return input(actual_message)
600
+
601
+ def _prompt(
602
+ self, message: str | Callable[[], Any], *, placeholder: str | None = None
603
+ ) -> str:
604
+ """Main prompt function with reduced complexity."""
605
+ if self._ptk_session and self._ptk_style and patch_stdout:
606
+ return self._prompt_with_prompt_toolkit(message, placeholder)
607
+
608
+ return self._prompt_with_basic_input(message, placeholder)
446
609
 
447
610
  def _get_client(self) -> Any: # type: ignore[no-any-return]
448
611
  if self._client is None:
@@ -503,12 +666,17 @@ class SlashSession:
503
666
  agent_type = getattr(active_agent, "type", "") or "-"
504
667
  description = getattr(active_agent, "description", "") or ""
505
668
 
669
+ verbose_label = "verbose on" if self._verbose_enabled else "verbose off"
670
+
506
671
  header_grid = Table.grid(expand=True)
507
672
  header_grid.add_column(ratio=3)
508
673
  header_grid.add_column(ratio=1, justify="right")
509
674
 
510
675
  primary_line = f"[bold]{agent_name}[/bold] · [dim]{agent_type}[/dim] · [cyan]{agent_id}[/cyan]"
511
- header_grid.add_row(primary_line, "[green]ready[/green]")
676
+ header_grid.add_row(
677
+ primary_line,
678
+ f"[green]ready[/green] · {verbose_label}",
679
+ )
512
680
 
513
681
  if description:
514
682
  header_grid.add_row(f"[dim]{description}[/dim]", "")
@@ -517,10 +685,12 @@ class SlashSession:
517
685
  keybar.add_column(justify="left")
518
686
  keybar.add_column(justify="left")
519
687
  keybar.add_column(justify="left")
688
+ keybar.add_column(justify="left")
520
689
  keybar.add_row(
521
690
  "[bold]/help[/bold] [dim]Show commands[/dim]",
522
691
  "[bold]/details[/bold] [dim]Agent config[/dim]",
523
692
  "[bold]/exit[/bold] [dim]Back[/dim]",
693
+ "[bold]Ctrl+T[/bold] [dim]Toggle verbose[/dim]",
524
694
  )
525
695
 
526
696
  header_grid.add_row(keybar, "")
@@ -544,6 +714,8 @@ class SlashSession:
544
714
  "",
545
715
  f"[dim]API URL[/dim]: {api_url or 'Not configured'}",
546
716
  f"[dim]Credentials[/dim]: {status}",
717
+ f"[dim]Verbose mode[/dim]: {'on' if self._verbose_enabled else 'off'}",
718
+ "[dim]Tip[/dim]: Press Ctrl+T or run `/verbose` to toggle verbose streaming.",
547
719
  ]
548
720
  extra: list[str] = []
549
721
  self._add_agent_info_to_header(extra, active_agent)
@@ -560,8 +732,10 @@ class SlashSession:
560
732
  status_bar.add_row(
561
733
  "[bold cyan]AIP Palette[/bold cyan]",
562
734
  f"[dim]API[/dim]: {api_url or 'Not configured'}",
563
- "[dim]Type /help for shortcuts[/dim]",
735
+ f"[dim]Verbose[/dim]: {'on' if self._verbose_enabled else 'off'}",
564
736
  )
737
+ status_bar.add_row("[dim]Ctrl+T toggles verbose[/dim]", "", "")
738
+ status_bar.add_row("[dim]Type /help for shortcuts[/dim]", "", "")
565
739
 
566
740
  if active_agent is not None:
567
741
  agent_id = str(getattr(active_agent, "id", ""))
@@ -602,6 +776,7 @@ class SlashSession:
602
776
  self.console.print(
603
777
  AIPPanel(
604
778
  "Type `/help` for command palette commands, `/agents` to browse agents, or `/exit` (`/q`) to leave the palette.\n"
779
+ "Press Ctrl+T to toggle verbose output.\n"
605
780
  "Press Ctrl+C to cancel the current entry, Ctrl+D to quit immediately.",
606
781
  title="✨ Getting Started",
607
782
  border_style="cyan",
glaip_sdk/cli/utils.py CHANGED
@@ -1108,6 +1108,17 @@ def build_renderer(
1108
1108
  verbose=verbose,
1109
1109
  )
1110
1110
 
1111
+ # Link the renderer back to the slash session when running from the palette.
1112
+ try:
1113
+ ctx_obj = getattr(_ctx, "obj", None)
1114
+ if isinstance(ctx_obj, dict):
1115
+ session = ctx_obj.get("_slash_session")
1116
+ if session and hasattr(session, "register_active_renderer"):
1117
+ session.register_active_renderer(renderer)
1118
+ except Exception:
1119
+ # Never let session bookkeeping break renderer creation
1120
+ pass
1121
+
1111
1122
  return renderer, working_console
1112
1123
 
1113
1124
 
@@ -6,10 +6,6 @@ Authors:
6
6
 
7
7
  # Default language model configuration
8
8
  DEFAULT_MODEL = "gpt-4.1"
9
- DEFAULT_MODEL_PROVIDER = "openai"
10
-
11
- # Default timeout values
12
- DEFAULT_TIMEOUT = 30.0
13
9
  DEFAULT_AGENT_RUN_TIMEOUT = 300
14
10
 
15
11
  # User agent and version
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
@@ -310,56 +310,115 @@ class RichStreamRenderer:
310
310
 
311
311
  def _ensure_live(self) -> None:
312
312
  """Ensure live display is updated."""
313
- # Lazily create Live if needed
314
- # Rich's Live expects the console to maintain a _live_stack list. When tests
315
- # or callers provide a lightweight console double (e.g. unittest.mock.Mock),
316
- # the attribute might be missing or replaced with another type which breaks
317
- # the background refresh thread. Normal Rich consoles always expose
318
- # _live_stack as a list, so we defensively initialise it if needed.
319
- live_stack = getattr(self.console, "_live_stack", None)
320
- if not isinstance(live_stack, list):
321
- try:
322
- self.console._live_stack = [] # type: ignore[attr-defined]
323
- except Exception:
324
- # If the console forbids attribute assignment we simply skip the
325
- # live update for this cycle and fall back to buffered printing.
326
- logger.debug(
327
- "Console missing _live_stack; skipping live UI initialisation",
328
- exc_info=True,
329
- )
330
- return
331
-
332
- if self.live is None and self.cfg.live:
333
- try:
334
- self.live = Live(
335
- console=self.console,
336
- refresh_per_second=1 / self.cfg.refresh_debounce,
337
- transient=not self.cfg.persist_live,
338
- )
339
- self.live.start()
340
- except Exception:
341
- self.live = None
313
+ if not self._ensure_live_stack():
314
+ return
315
+
316
+ self._start_live_if_needed()
342
317
 
343
318
  if self.live:
344
- panels = [self._render_main_panel()]
345
- steps_renderable = self._render_steps_text()
346
- panels.append(
347
- AIPPanel(
348
- steps_renderable,
349
- title="Steps",
350
- border_style="blue",
351
- )
319
+ self._refresh_live_panels()
320
+
321
+ def _ensure_live_stack(self) -> bool:
322
+ """Guarantee the console exposes the internal live stack Rich expects."""
323
+ live_stack = getattr(self.console, "_live_stack", None)
324
+ if isinstance(live_stack, list):
325
+ return True
326
+
327
+ try:
328
+ self.console._live_stack = [] # type: ignore[attr-defined]
329
+ return True
330
+ except Exception:
331
+ # If the console forbids attribute assignment we simply skip the live
332
+ # update for this cycle and fall back to buffered printing.
333
+ logger.debug(
334
+ "Console missing _live_stack; skipping live UI initialisation",
335
+ exc_info=True,
336
+ )
337
+ return False
338
+
339
+ def _start_live_if_needed(self) -> None:
340
+ """Create and start a Live instance when configuration allows."""
341
+ if self.live is not None or not self.cfg.live:
342
+ return
343
+
344
+ try:
345
+ self.live = Live(
346
+ console=self.console,
347
+ refresh_per_second=1 / self.cfg.refresh_debounce,
348
+ transient=not self.cfg.persist_live,
352
349
  )
353
- panels.extend(self._render_tool_panels())
354
- self.live.update(Group(*panels))
350
+ self.live.start()
351
+ except Exception:
352
+ self.live = None
353
+
354
+ def _refresh_live_panels(self) -> None:
355
+ """Render panels and push them to the active Live display."""
356
+ if not self.live:
357
+ return
358
+
359
+ main_panel = self._render_main_panel()
360
+ steps_renderable = self._render_steps_text()
361
+ steps_panel = AIPPanel(
362
+ steps_renderable,
363
+ title="Steps",
364
+ border_style="blue",
365
+ )
366
+ tool_panels = self._render_tool_panels()
367
+ panels = self._build_live_panels(main_panel, steps_panel, tool_panels)
368
+
369
+ self.live.update(Group(*panels))
370
+
371
+ def _build_live_panels(
372
+ self,
373
+ main_panel: Any,
374
+ steps_panel: Any,
375
+ tool_panels: list[Any],
376
+ ) -> list[Any]:
377
+ """Assemble the panel order for the live display."""
378
+ if self.verbose:
379
+ return [main_panel, steps_panel, *tool_panels]
380
+
381
+ panels: list[Any] = [steps_panel]
382
+ if tool_panels:
383
+ panels.extend(tool_panels)
384
+ panels.append(main_panel)
385
+ return panels
355
386
 
356
387
  def _render_main_panel(self) -> Any:
357
388
  """Render the main content panel."""
358
389
  body = "".join(self.state.buffer).strip()
390
+ if not self.verbose:
391
+ final_content = (self.state.final_text or "").strip()
392
+ if final_content:
393
+ return create_final_panel(
394
+ final_content,
395
+ title="Final Result",
396
+ theme=self.cfg.theme,
397
+ )
359
398
  # Dynamic title with spinner + elapsed/hints
360
399
  title = self._format_enhanced_main_title()
361
400
  return create_main_panel(body, title, self.cfg.theme)
362
401
 
402
+ def apply_verbosity(self, verbose: bool) -> None:
403
+ """Update verbose behaviour at runtime."""
404
+
405
+ if self.verbose == verbose:
406
+ return
407
+
408
+ self.verbose = verbose
409
+ self.cfg.style = "debug" if verbose else "pretty"
410
+
411
+ desired_live = not verbose
412
+ if desired_live != self.cfg.live:
413
+ self.cfg.live = desired_live
414
+ if not desired_live:
415
+ self._shutdown_live()
416
+ else:
417
+ self._ensure_live()
418
+
419
+ if self.cfg.live:
420
+ self._ensure_live()
421
+
363
422
  def _maybe_insert_thinking_gap(
364
423
  self, task_id: str | None, context_id: str | None
365
424
  ) -> None:
@@ -668,15 +727,16 @@ class RichStreamRenderer:
668
727
 
669
728
  def _clamp_snapshot_body(self, body_text: str) -> str:
670
729
  """Clamp snapshot body to configured limits."""
671
- max_lines = int(self.cfg.snapshot_max_lines or 0) or 60
730
+ max_lines = int(self.cfg.snapshot_max_lines or 0)
672
731
  lines = body_text.splitlines()
673
- if len(lines) > max_lines:
732
+ if max_lines > 0 and len(lines) > max_lines:
674
733
  lines = lines[:max_lines] + ["… (truncated)"]
675
- body_text = "\n".join(lines)
734
+ body_text = "\n".join(lines)
676
735
 
677
- max_chars = int(self.cfg.snapshot_max_chars or 0) or 4000
678
- if len(body_text) > max_chars:
679
- body_text = body_text[: max_chars - 12] + "\n… (truncated)"
736
+ max_chars = int(self.cfg.snapshot_max_chars or 0)
737
+ if max_chars > 0 and len(body_text) > max_chars:
738
+ suffix = "\n… (truncated)"
739
+ body_text = body_text[: max_chars - len(suffix)] + suffix
680
740
 
681
741
  return body_text
682
742
 
@@ -29,5 +29,5 @@ class RendererConfig:
29
29
 
30
30
  # Scrollback/append options
31
31
  append_finished_snapshots: bool = False
32
- snapshot_max_chars: int = 4000
33
- snapshot_max_lines: int = 60
32
+ snapshot_max_chars: int = 12000
33
+ snapshot_max_lines: int = 200
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: glaip-sdk
3
- Version: 0.0.9
3
+ Version: 0.0.10
4
4
  Summary: Python SDK for GL AIP (GDP Labs AI Agent Package) - Simplified CLI Design
5
5
  License: MIT
6
6
  Author: Raymond Christopher
@@ -1,6 +1,6 @@
1
1
  glaip_sdk/__init__.py,sha256=FD-oTyFUKsTB9xTuGiqvkhuFXfeZ-TspjkeXERglha8,370
2
2
  glaip_sdk/_version.py,sha256=tGkFWAVu2ry4Hy7j-u7ophGbPRX8y-ngBbXDhN1VBIQ,2007
3
- glaip_sdk/branding.py,sha256=_TiQtiwH3ZSWaQhooWPHioriL0goTiYANeN6DT8lQX8,5398
3
+ glaip_sdk/branding.py,sha256=xsHL7nRAWuwJrAvCi2fZhaPD-AWvdcLNoTVmlw4gZKc,5604
4
4
  glaip_sdk/cli/__init__.py,sha256=xCCfuF1Yc7mpCDcfhHZTX0vizvtrDSLeT8MJ3V7m5A0,156
5
5
  glaip_sdk/cli/agent_config.py,sha256=VHjebw68wAdhGUzYdPH8qz10oADZPRgUQcPW6F7iHIU,2421
6
6
  glaip_sdk/cli/commands/__init__.py,sha256=x0CZlZbZHoHvuzfoTWIyEch6WmNnbPzxajrox6riYp0,173
@@ -14,10 +14,10 @@ glaip_sdk/cli/io.py,sha256=GPkw3pQMLBGoD5GH-KlbKpNRlVWFZOXHE17F7V3kQsI,3343
14
14
  glaip_sdk/cli/main.py,sha256=uWuntvFl0hQSLfNa1rXb-J2zUK_msf3vrSbbrqBcEc4,12692
15
15
  glaip_sdk/cli/resolution.py,sha256=BOw2NchReLKewAwBAZLWw_3_bI7u3tfzQEO7kQbIiGE,2067
16
16
  glaip_sdk/cli/slash/__init__.py,sha256=Vdv6Y8bu-pA8dxDlyP4XrhudBPivztUozhLAz9vaLig,682
17
- glaip_sdk/cli/slash/agent_session.py,sha256=JFIFBxB4xzigqHtpLGbl2fgg7RHNwy3e-kUMPMK9MdM,5006
18
- glaip_sdk/cli/slash/prompt.py,sha256=QFWYgNt5AhBRYGGyUkH30ToZE7B6LS-eZGzoyibYrK8,6064
19
- glaip_sdk/cli/slash/session.py,sha256=GtnnBqV79KtFJHeLfCKKnCXJ6SU1AmeXquPjy72T20E,24215
20
- glaip_sdk/cli/utils.py,sha256=S7aa4-v808BEZGvEhKn8N8HQr2slSpvV-TtWw3j15S8,41779
17
+ glaip_sdk/cli/slash/agent_session.py,sha256=pDOwGXNHuyJIulrGYu1pacyF3oxHWeDQY-Uv92h2qVg,6859
18
+ glaip_sdk/cli/slash/prompt.py,sha256=Pr5SSTOKFssRsi-AujOm5_BCW_f5MxgLwJ3CCji1ogM,7356
19
+ glaip_sdk/cli/slash/session.py,sha256=W8Fk1t9qqr1B9clhIwsAuv3i_6yYk_vEAuM00zGHxHM,30482
20
+ glaip_sdk/cli/utils.py,sha256=7UNg4qDIR0BDgiWJgtEDaVmRa9fhfUsBLiygcuTDxmE,42236
21
21
  glaip_sdk/cli/validators.py,sha256=USbBgY86AwuDHO-Q_g8g7hu-ot4NgITBsWjTWIl62ms,5569
22
22
  glaip_sdk/client/__init__.py,sha256=nYLXfBVTTWwKjP0e63iumPYO4k5FifwWaELQPaPIKIg,188
23
23
  glaip_sdk/client/agents.py,sha256=FSKubF40wptMNIheC3_iawiX2CRbhTcNLFiz4qkPC6k,34659
@@ -26,8 +26,8 @@ glaip_sdk/client/main.py,sha256=LlvYHP7-Hy7Eq1ep1kfk337K-Oue5SdKWJpqYfX9eXY,7993
26
26
  glaip_sdk/client/mcps.py,sha256=yxwrAtztElYDEGhp2EHRpeYUxNsOlTLTqtw9jSKJmcI,8936
27
27
  glaip_sdk/client/tools.py,sha256=n8DIiOOf1YU_j9JK3Bx2-rDnkpckPi0MI9Ok2s1kwa4,16634
28
28
  glaip_sdk/client/validators.py,sha256=NtPsWjQLjj25LiUnmR-WuS8lL5p4MVRaYT9UVRmj9bo,8809
29
- glaip_sdk/config/constants.py,sha256=NgmPqJmpK1wgEds7T-_B6CDUBsvcsmIy27ms-Z8GFgY,902
30
- glaip_sdk/exceptions.py,sha256=QTVtwxRHMN4e8gGn0icXphZvdugiRvcSrlMYylwGsDc,1993
29
+ glaip_sdk/config/constants.py,sha256=ysEobMiXlLZGIOEaqTdHpPF8kmg5nbLn7BIcBvTCuHM,819
30
+ glaip_sdk/exceptions.py,sha256=DJgaIcvGA09qIX10-ypYgQQ5_k5N3qknmiIFP3p4Z7E,1872
31
31
  glaip_sdk/models.py,sha256=Ry_Ihd6FuC0JGb0Obd5T3A22AOp2r9xStQisfE8uo8c,8822
32
32
  glaip_sdk/rich_components.py,sha256=pmJd-81OQE8bC9UOXtga5rsax4zphKlzCZ1JoWbbQzQ,803
33
33
  glaip_sdk/utils/__init__.py,sha256=fmVGcUFa7G0CCfSMSqfNU2BqFl36G1gOFyDfTvtJfVw,926
@@ -40,8 +40,8 @@ glaip_sdk/utils/rendering/__init__.py,sha256=vXjwk5rPhhfPyD8S0DnV4GFFEtPJp4HCCg1
40
40
  glaip_sdk/utils/rendering/formatting.py,sha256=_k8tkcobctmHvdygMljZF7-ALGXpD9-hHF1CNtM2KMU,7201
41
41
  glaip_sdk/utils/rendering/models.py,sha256=SS34_00FaoGuSYn-viBkAtIbq7cJNwwPjpxnvyeUmxI,1567
42
42
  glaip_sdk/utils/rendering/renderer/__init__.py,sha256=EXwVBmGkSYcype4ocAXo69Z1kXu0gpNXmhH5LW0_B7A,2939
43
- glaip_sdk/utils/rendering/renderer/base.py,sha256=HfIeq-izi3WprbKX_qxNeQRrpernFtoCvgQqrIyVYSU,40950
44
- glaip_sdk/utils/rendering/renderer/config.py,sha256=E4ER8TJJbqr1hcWjkwG7XROqLuccQy4EL99CbuLvSXE,783
43
+ glaip_sdk/utils/rendering/renderer/base.py,sha256=A_f8r3hWDVrQjzUR5CYzQMekPRIdG-l7Wjh9Hx492Xk,42425
44
+ glaip_sdk/utils/rendering/renderer/config.py,sha256=-P35z9JO_1ypJXAqxJ1ybHraH4i-I1LPopeW3Lh7ACE,785
45
45
  glaip_sdk/utils/rendering/renderer/console.py,sha256=4cLOw4Q1fkHkApuj6dWW8eYpeYdcT0t2SO5MbVt5UTc,1844
46
46
  glaip_sdk/utils/rendering/renderer/debug.py,sha256=FEYxAu4ZB0CjrJKevqQ2TKDgElA2cf6GqZXCNm12sNQ,3721
47
47
  glaip_sdk/utils/rendering/renderer/panels.py,sha256=05u6SjU53iP9VD0YHTNruzD7sO6qYCp-P5dTTSdSZmU,3364
@@ -53,7 +53,7 @@ glaip_sdk/utils/rich_utils.py,sha256=-Ij-1bIJvnVAi6DrfftchIlMcvOTjVmSE0Qqax0EY_s
53
53
  glaip_sdk/utils/run_renderer.py,sha256=d_VMI6LbvHPUUeRmGqh5wK_lHqDEIAcym2iqpbtDad0,1365
54
54
  glaip_sdk/utils/serialization.py,sha256=cUE6PxqTsfJuEEmsk_Li3QmaDavTIPotEA-BQ-v5exY,9043
55
55
  glaip_sdk/utils/validation.py,sha256=QNORcdyvuliEs4EH2_mkDgmoyT9utgl7YNhaf45SEf8,6992
56
- glaip_sdk-0.0.9.dist-info/METADATA,sha256=f2-GNqMR8cLgyQnA4f7TCPZv1ETZE4S8LSpQaPD6GrE,4949
57
- glaip_sdk-0.0.9.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
58
- glaip_sdk-0.0.9.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
59
- glaip_sdk-0.0.9.dist-info/RECORD,,
56
+ glaip_sdk-0.0.10.dist-info/METADATA,sha256=ryaYhShJ7h43sX_oXbkLdKqpy5-eRgAP0lDczHVkCog,4950
57
+ glaip_sdk-0.0.10.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
58
+ glaip_sdk-0.0.10.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
59
+ glaip_sdk-0.0.10.dist-info/RECORD,,