glaip-sdk 0.0.19__py3-none-any.whl → 0.1.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 (56) hide show
  1. glaip_sdk/_version.py +2 -2
  2. glaip_sdk/branding.py +27 -2
  3. glaip_sdk/cli/auth.py +93 -28
  4. glaip_sdk/cli/commands/__init__.py +2 -2
  5. glaip_sdk/cli/commands/agents.py +127 -21
  6. glaip_sdk/cli/commands/configure.py +141 -90
  7. glaip_sdk/cli/commands/mcps.py +82 -31
  8. glaip_sdk/cli/commands/models.py +4 -3
  9. glaip_sdk/cli/commands/tools.py +27 -14
  10. glaip_sdk/cli/commands/update.py +66 -0
  11. glaip_sdk/cli/config.py +13 -2
  12. glaip_sdk/cli/display.py +35 -26
  13. glaip_sdk/cli/io.py +14 -5
  14. glaip_sdk/cli/main.py +185 -73
  15. glaip_sdk/cli/pager.py +2 -1
  16. glaip_sdk/cli/resolution.py +4 -1
  17. glaip_sdk/cli/slash/__init__.py +3 -4
  18. glaip_sdk/cli/slash/agent_session.py +88 -36
  19. glaip_sdk/cli/slash/prompt.py +20 -48
  20. glaip_sdk/cli/slash/session.py +437 -189
  21. glaip_sdk/cli/transcript/__init__.py +71 -0
  22. glaip_sdk/cli/transcript/cache.py +338 -0
  23. glaip_sdk/cli/transcript/capture.py +278 -0
  24. glaip_sdk/cli/transcript/export.py +38 -0
  25. glaip_sdk/cli/transcript/launcher.py +79 -0
  26. glaip_sdk/cli/transcript/viewer.py +794 -0
  27. glaip_sdk/cli/update_notifier.py +29 -5
  28. glaip_sdk/cli/utils.py +255 -74
  29. glaip_sdk/client/agents.py +3 -1
  30. glaip_sdk/client/run_rendering.py +126 -21
  31. glaip_sdk/icons.py +25 -0
  32. glaip_sdk/models.py +6 -0
  33. glaip_sdk/rich_components.py +29 -1
  34. glaip_sdk/utils/__init__.py +1 -1
  35. glaip_sdk/utils/client_utils.py +6 -4
  36. glaip_sdk/utils/display.py +61 -32
  37. glaip_sdk/utils/rendering/formatting.py +55 -11
  38. glaip_sdk/utils/rendering/models.py +15 -2
  39. glaip_sdk/utils/rendering/renderer/__init__.py +0 -2
  40. glaip_sdk/utils/rendering/renderer/base.py +1287 -227
  41. glaip_sdk/utils/rendering/renderer/config.py +3 -5
  42. glaip_sdk/utils/rendering/renderer/debug.py +73 -16
  43. glaip_sdk/utils/rendering/renderer/panels.py +27 -15
  44. glaip_sdk/utils/rendering/renderer/progress.py +61 -38
  45. glaip_sdk/utils/rendering/renderer/stream.py +3 -3
  46. glaip_sdk/utils/rendering/renderer/toggle.py +184 -0
  47. glaip_sdk/utils/rendering/step_tree_state.py +102 -0
  48. glaip_sdk/utils/rendering/steps.py +944 -16
  49. glaip_sdk/utils/serialization.py +5 -2
  50. glaip_sdk/utils/validation.py +1 -2
  51. {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.1.0.dist-info}/METADATA +12 -1
  52. glaip_sdk-0.1.0.dist-info/RECORD +82 -0
  53. glaip_sdk/utils/rich_utils.py +0 -29
  54. glaip_sdk-0.0.19.dist-info/RECORD +0 -73
  55. {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.1.0.dist-info}/WHEEL +0 -0
  56. {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.1.0.dist-info}/entry_points.txt +0 -0
@@ -6,24 +6,37 @@ Authors:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import importlib
9
10
  import os
10
11
  import shlex
11
12
  import sys
12
13
  from collections.abc import Callable, Iterable
13
14
  from dataclasses import dataclass
15
+ from difflib import get_close_matches
16
+ from pathlib import Path
14
17
  from typing import Any
15
18
 
16
19
  import click
17
- from rich.console import Console
18
- from rich.table import Table
19
-
20
- from glaip_sdk.branding import AIPBranding
20
+ from rich.console import Console, Group
21
+ from rich.text import Text
22
+
23
+ from glaip_sdk.branding import (
24
+ ACCENT_STYLE,
25
+ ERROR_STYLE,
26
+ HINT_COMMAND_STYLE,
27
+ HINT_DESCRIPTION_COLOR,
28
+ HINT_PREFIX_STYLE,
29
+ INFO_STYLE,
30
+ PRIMARY,
31
+ SECONDARY_LIGHT,
32
+ SUCCESS_STYLE,
33
+ WARNING_STYLE,
34
+ AIPBranding,
35
+ )
21
36
  from glaip_sdk.cli.commands.configure import configure_command, load_config
22
- from glaip_sdk.cli.utils import _fuzzy_pick_for_resources, command_hint, get_client
23
- from glaip_sdk.rich_components import AIPPanel
24
-
25
- from .agent_session import AgentRunSession
26
- from .prompt import (
37
+ from glaip_sdk.cli.commands.update import update_command
38
+ from glaip_sdk.cli.slash.agent_session import AgentRunSession
39
+ from glaip_sdk.cli.slash.prompt import (
27
40
  FormattedText,
28
41
  PromptSession,
29
42
  Style,
@@ -31,6 +44,21 @@ from .prompt import (
31
44
  setup_prompt_toolkit,
32
45
  to_formatted_text,
33
46
  )
47
+ from glaip_sdk.cli.transcript import (
48
+ export_cached_transcript,
49
+ normalise_export_destination,
50
+ resolve_manifest_for_export,
51
+ suggest_filename,
52
+ )
53
+ from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
54
+ from glaip_sdk.cli.update_notifier import maybe_notify_update
55
+ from glaip_sdk.cli.utils import (
56
+ _fuzzy_pick_for_resources,
57
+ command_hint,
58
+ format_command_hint,
59
+ get_client,
60
+ )
61
+ from glaip_sdk.rich_components import AIPGrid, AIPPanel, AIPTable
34
62
 
35
63
  SlashHandler = Callable[["SlashSession", list[str], bool], bool]
36
64
 
@@ -68,8 +96,8 @@ class SlashSession:
68
96
  self._interactive = bool(sys.stdin.isatty() and sys.stdout.isatty())
69
97
  self._config_cache: dict[str, Any] | None = None
70
98
  self._welcome_rendered = False
71
- self._verbose_enabled = False
72
99
  self._active_renderer: Any | None = None
100
+ self._current_agent: Any | None = None
73
101
 
74
102
  self._home_placeholder = "Start with / to browse commands"
75
103
 
@@ -84,6 +112,10 @@ class SlashSession:
84
112
  self._branding = AIPBranding.create_from_sdk()
85
113
  self._suppress_login_layout = False
86
114
  self._default_actions_shown = False
115
+ self._update_prompt_shown = False
116
+ self._update_notifier = maybe_notify_update
117
+ self._home_hint_shown = False
118
+ self._agent_transcript_ready: dict[str, str] = {}
87
119
 
88
120
  # ------------------------------------------------------------------
89
121
  # Session orchestration
@@ -96,18 +128,31 @@ class SlashSession:
96
128
 
97
129
  def run(self, initial_commands: Iterable[str] | None = None) -> None:
98
130
  """Start the command palette session loop."""
99
- if not self._interactive:
100
- self._run_non_interactive(initial_commands)
101
- return
131
+ ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
132
+ previous_session = None
133
+ if ctx_obj is not None:
134
+ previous_session = ctx_obj.get("_slash_session")
135
+ ctx_obj["_slash_session"] = self
102
136
 
103
- if not self._ensure_configuration():
104
- return
137
+ try:
138
+ if not self._interactive:
139
+ self._run_non_interactive(initial_commands)
140
+ return
141
+
142
+ if not self._ensure_configuration():
143
+ return
105
144
 
106
- self._render_header(initial=not self._welcome_rendered)
107
- if not self._default_actions_shown:
108
- self._show_default_quick_actions()
109
- self._render_home_hint()
110
- self._run_interactive_loop()
145
+ self._maybe_show_update_prompt()
146
+ self._render_header(initial=not self._welcome_rendered)
147
+ if not self._default_actions_shown:
148
+ self._show_default_quick_actions()
149
+ self._run_interactive_loop()
150
+ finally:
151
+ if ctx_obj is not None:
152
+ if previous_session is None:
153
+ ctx_obj.pop("_slash_session", None)
154
+ else:
155
+ ctx_obj["_slash_session"] = previous_session
111
156
 
112
157
  def _run_interactive_loop(self) -> None:
113
158
  """Run the main interactive command loop."""
@@ -131,12 +176,13 @@ class SlashSession:
131
176
  return True
132
177
 
133
178
  if raw == "/":
179
+ self._render_home_hint()
134
180
  self._cmd_help([], invoked_from_agent=False)
135
181
  return True
136
182
 
137
183
  if not raw.startswith("/"):
138
184
  self.console.print(
139
- "[yellow]Hint:[/] start commands with `/`. Try `/agents` to select an agent."
185
+ f"[{INFO_STYLE}]Hint:[/] start commands with `/`. Try `/agents` to select an agent."
140
186
  )
141
187
  return True
142
188
 
@@ -160,14 +206,14 @@ class SlashSession:
160
206
  """Ensure the CLI has both API URL and credentials before continuing."""
161
207
  while not self._configuration_ready():
162
208
  self.console.print(
163
- "[yellow]Configuration required.[/] Launching `/login` wizard..."
209
+ f"[{WARNING_STYLE}]Configuration required.[/] Launching `/login` wizard..."
164
210
  )
165
211
  self._suppress_login_layout = True
166
212
  try:
167
213
  self._cmd_login([], False)
168
214
  except KeyboardInterrupt:
169
215
  self.console.print(
170
- "[red]Configuration aborted. Closing the command palette.[/red]"
216
+ f"[{ERROR_STYLE}]Configuration aborted. Closing the command palette.[/]"
171
217
  )
172
218
  return False
173
219
  finally:
@@ -193,7 +239,7 @@ class SlashSession:
193
239
  """Parse and execute a single slash command string."""
194
240
  verb, args = self._parse(raw)
195
241
  if not verb:
196
- self.console.print("[red]Unrecognised command[/red]")
242
+ self.console.print(f"[{ERROR_STYLE}]Unrecognised command[/]")
197
243
  return True
198
244
 
199
245
  command = self._commands.get(verb)
@@ -201,11 +247,13 @@ class SlashSession:
201
247
  suggestion = self._suggest(verb)
202
248
  if suggestion:
203
249
  self.console.print(
204
- f"[yellow]Unknown command '{verb}'. Did you mean '/{suggestion}'?[/yellow]"
250
+ f"[{WARNING_STYLE}]Unknown command '{verb}'. Did you mean '/{suggestion}'?[/]"
205
251
  )
206
252
  else:
253
+ help_command = "/help"
254
+ help_hint = format_command_hint(help_command) or help_command
207
255
  self.console.print(
208
- "[yellow]Unknown command '{verb}'. Type `/help` for a list of options.[/yellow]"
256
+ f"[{WARNING_STYLE}]Unknown command '{verb}'. Type {help_hint} for a list of options.[/]"
209
257
  )
210
258
  return True
211
259
 
@@ -225,32 +273,45 @@ class SlashSession:
225
273
  else:
226
274
  self._render_global_help()
227
275
  except Exception as exc: # pragma: no cover - UI/display errors
228
- self.console.print(f"[red]Error displaying help: {exc}[/red]")
276
+ self.console.print(f"[{ERROR_STYLE}]Error displaying help: {exc}[/]")
229
277
  return False
230
278
 
231
279
  return True
232
280
 
233
281
  def _render_agent_help(self) -> None:
234
- table = Table(title="Agent Context")
235
- table.add_column("Input", style="cyan", no_wrap=True)
236
- table.add_column("What happens", style="green")
282
+ table = AIPTable()
283
+ table.add_column("Input", style=HINT_COMMAND_STYLE, no_wrap=True)
284
+ table.add_column("What happens", style=HINT_DESCRIPTION_COLOR)
237
285
  table.add_row("<message>", "Run the active agent once with that prompt.")
238
286
  table.add_row("/details", "Show the full agent export and metadata.")
239
287
  table.add_row(self.STATUS_COMMAND, "Display connection status without leaving.")
240
- table.add_row("/verbose", "Toggle verbose streaming output (Ctrl+T works too).")
288
+ table.add_row("/export [path]", "Export the latest agent transcript as JSONL.")
241
289
  table.add_row("/exit (/back)", "Return to the slash home screen.")
242
290
  table.add_row("/help (/?)", "Display this context-aware menu.")
243
- self.console.print(table)
291
+
292
+ panel_items = [table]
244
293
  if self.last_run_input:
245
- self.console.print(f"[dim]Last run input:[/] {self.last_run_input}")
294
+ panel_items.append(
295
+ Text.from_markup(f"[dim]Last run input:[/] {self.last_run_input}")
296
+ )
297
+ panel_items.append(
298
+ Text.from_markup(
299
+ "[dim]Global commands (e.g. `/login`, `/status`) remain available inside the agent prompt.[/dim]"
300
+ )
301
+ )
302
+
246
303
  self.console.print(
247
- "[dim]Global commands (e.g. `/login`, `/status`) remain available inside the agent prompt.[/dim]"
304
+ AIPPanel(
305
+ Group(*panel_items),
306
+ title="Agent Context",
307
+ border_style=PRIMARY,
308
+ )
248
309
  )
249
310
 
250
311
  def _render_global_help(self) -> None:
251
- table = Table(title="Slash Commands")
252
- table.add_column("Command", style="cyan", no_wrap=True)
253
- table.add_column("Description", style="green")
312
+ table = AIPTable()
313
+ table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
314
+ table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
254
315
 
255
316
  for cmd in sorted(self._unique_commands.values(), key=lambda c: c.name):
256
317
  aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
@@ -259,13 +320,20 @@ class SlashSession:
259
320
  verb = f"{verb} ({aliases})"
260
321
  table.add_row(verb, cmd.help)
261
322
 
262
- self.console.print(table)
323
+ tip = Text.from_markup(
324
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] {format_command_hint(self.AGENTS_COMMAND) or self.AGENTS_COMMAND} lets you jump into an agent run prompt quickly."
325
+ )
326
+
263
327
  self.console.print(
264
- "[dim]Tip: `{self.AGENTS_COMMAND}` lets you jump into an agent run prompt quickly.[/dim]"
328
+ AIPPanel(
329
+ Group(table, tip),
330
+ title="Slash Commands",
331
+ border_style=PRIMARY,
332
+ )
265
333
  )
266
334
 
267
335
  def _cmd_login(self, _args: list[str], _invoked_from_agent: bool) -> bool:
268
- self.console.print("[cyan]Launching configuration wizard...[/cyan]")
336
+ self.console.print(f"[{ACCENT_STYLE}]Launching configuration wizard...[/]")
269
337
  try:
270
338
  self.ctx.invoke(configure_command)
271
339
  self._config_cache = None
@@ -276,14 +344,22 @@ class SlashSession:
276
344
  self._render_header(initial=True)
277
345
  self._show_default_quick_actions()
278
346
  except click.ClickException as exc:
279
- self.console.print(f"[red]{exc}[/red]")
347
+ self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
280
348
  return True
281
349
 
282
350
  def _cmd_status(self, _args: list[str], _invoked_from_agent: bool) -> bool:
351
+ ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
352
+ previous_console = None
283
353
  try:
284
- from glaip_sdk.cli.main import status as status_command
354
+ status_module = importlib.import_module("glaip_sdk.cli.main")
355
+ status_command = getattr(status_module, "status")
356
+
357
+ if ctx_obj is not None:
358
+ previous_console = ctx_obj.get("_slash_console")
359
+ ctx_obj["_slash_console"] = self.console
285
360
 
286
361
  self.ctx.invoke(status_command)
362
+
287
363
  hints: list[tuple[str, str]] = [
288
364
  (self.AGENTS_COMMAND, "Browse agents and run them")
289
365
  ]
@@ -293,7 +369,13 @@ class SlashSession:
293
369
  hints.append((f"/agents {top.get('id')}", f"Reopen {label}"))
294
370
  self._show_quick_actions(hints, title="Next actions")
295
371
  except click.ClickException as exc:
296
- self.console.print(f"[red]{exc}[/red]")
372
+ self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
373
+ finally:
374
+ if ctx_obj is not None:
375
+ if previous_console is None:
376
+ ctx_obj.pop("_slash_console", None)
377
+ else:
378
+ ctx_obj["_slash_console"] = previous_console
297
379
  return True
298
380
 
299
381
  def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
@@ -317,7 +399,7 @@ class SlashSession:
317
399
  try:
318
400
  return self._get_client()
319
401
  except click.ClickException as exc:
320
- self.console.print(f"[red]{exc}[/red]")
402
+ self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
321
403
  return None
322
404
 
323
405
  def _get_agents_or_fail(self, client: Any) -> list:
@@ -328,7 +410,7 @@ class SlashSession:
328
410
  self._handle_no_agents()
329
411
  return agents
330
412
  except Exception as exc: # pragma: no cover - API failures
331
- self.console.print(f"[red]Failed to load agents: {exc}[/red]")
413
+ self.console.print(f"[{ERROR_STYLE}]Failed to load agents: {exc}[/]")
332
414
  return []
333
415
 
334
416
  def _handle_no_agents(self) -> None:
@@ -336,10 +418,10 @@ class SlashSession:
336
418
  hint = command_hint("agents create", slash_command=None, ctx=self.ctx)
337
419
  if hint:
338
420
  self.console.print(
339
- f"[yellow]No agents available. Use `{hint}` to add one.[/yellow]"
421
+ f"[{WARNING_STYLE}]No agents available. Use `{hint}` to add one.[/]"
340
422
  )
341
423
  else:
342
- self.console.print("[yellow]No agents available.[/yellow]")
424
+ self.console.print(f"[{WARNING_STYLE}]No agents available.[/]")
343
425
 
344
426
  def _resolve_or_pick_agent(self, client: Any, agents: list, args: list[str]) -> Any:
345
427
  """Resolve agent from args or pick interactively."""
@@ -347,7 +429,7 @@ class SlashSession:
347
429
  picked_agent = self._resolve_agent_from_ref(client, agents, args[0])
348
430
  if picked_agent is None:
349
431
  self.console.print(
350
- f"[yellow]Could not resolve agent '{args[0]}'. Try `/agents` to browse interactively.[/yellow]"
432
+ f"[{WARNING_STYLE}]Could not resolve agent '{args[0]}'. Try `/agents` to browse interactively.[/]"
351
433
  )
352
434
  return None
353
435
  else:
@@ -390,7 +472,7 @@ class SlashSession:
390
472
  # running.
391
473
  return True
392
474
 
393
- self.console.print("[cyan]Closing the command palette.[/cyan]")
475
+ self.console.print(f"[{ACCENT_STYLE}]Closing the command palette.[/]")
394
476
  return False
395
477
 
396
478
  # ------------------------------------------------------------------
@@ -437,9 +519,16 @@ class SlashSession:
437
519
  )
438
520
  self._register(
439
521
  SlashCommand(
440
- name="verbose",
441
- help="Toggle verbose streaming output.",
442
- handler=SlashSession._cmd_verbose,
522
+ name="export",
523
+ help="Export the most recent agent transcript.",
524
+ handler=SlashSession._cmd_export,
525
+ )
526
+ )
527
+ self._register(
528
+ SlashCommand(
529
+ name="update",
530
+ help="Upgrade the glaip-sdk package to the latest version.",
531
+ handler=SlashSession._cmd_update,
443
532
  )
444
533
  )
445
534
 
@@ -448,53 +537,121 @@ class SlashSession:
448
537
  for key in (command.name, *command.aliases):
449
538
  self._commands[key] = command
450
539
 
451
- # ------------------------------------------------------------------
452
- # Verbose mode helpers
453
- # ------------------------------------------------------------------
454
- @property
455
- def verbose_enabled(self) -> bool:
456
- """Return whether verbose agent runs are enabled."""
457
- return self._verbose_enabled
458
-
459
- def set_verbose(self, enabled: bool, *, announce: bool = True) -> None:
460
- """Enable or disable verbose mode with optional announcement."""
461
- if self._verbose_enabled == enabled:
540
+ def open_transcript_viewer(self, *, announce: bool = True) -> None:
541
+ """Launch the transcript viewer for the most recent run."""
542
+ payload, manifest = self._get_last_transcript()
543
+ if payload is None or manifest is None:
462
544
  if announce:
463
- self._print_verbose_status(context="already")
545
+ self.console.print(
546
+ f"[{WARNING_STYLE}]No transcript is available yet. Run an agent first.[/]"
547
+ )
464
548
  return
465
549
 
466
- self._verbose_enabled = enabled
467
- self._sync_active_renderer()
468
- if announce:
469
- self._print_verbose_status(context="changed")
550
+ run_id = manifest.get("run_id")
551
+ if not run_id:
552
+ if announce:
553
+ self.console.print(
554
+ f"[{WARNING_STYLE}]Latest transcript is missing run metadata.[/]"
555
+ )
556
+ return
470
557
 
471
- def toggle_verbose(self, *, announce: bool = True) -> None:
472
- """Flip verbose mode state."""
473
- self.set_verbose(not self._verbose_enabled, announce=announce)
558
+ viewer_ctx = ViewerContext(
559
+ manifest_entry=manifest,
560
+ events=list(getattr(payload, "events", []) or []),
561
+ default_output=getattr(payload, "default_output", ""),
562
+ final_output=getattr(payload, "final_output", ""),
563
+ stream_started_at=getattr(payload, "started_at", None),
564
+ meta=getattr(payload, "meta", {}) or {},
565
+ )
474
566
 
475
- def _cmd_verbose(self, args: list[str], _invoked_from_agent: bool) -> bool:
476
- """Slash handler for `/verbose` command."""
477
- if args:
478
- self.console.print(
479
- "Usage: `/verbose` toggles verbose streaming output. Press Ctrl+T as a shortcut."
567
+ def _export(destination: Path) -> Path:
568
+ return export_cached_transcript(destination=destination, run_id=run_id)
569
+
570
+ try:
571
+ run_viewer_session(self.console, viewer_ctx, _export)
572
+ except Exception as exc: # pragma: no cover - interactive failures
573
+ if announce:
574
+ self.console.print(
575
+ f"[{ERROR_STYLE}]Failed to launch transcript viewer: {exc}[/]"
576
+ )
577
+
578
+ def _get_last_transcript(self) -> tuple[Any | None, dict[str, Any] | None]:
579
+ """Fetch the most recently stored transcript payload and manifest."""
580
+ ctx_obj = getattr(self.ctx, "obj", None)
581
+ if not isinstance(ctx_obj, dict):
582
+ return None, None
583
+ payload = ctx_obj.get("_last_transcript_payload")
584
+ manifest = ctx_obj.get("_last_transcript_manifest")
585
+ return payload, manifest
586
+
587
+ def _cmd_export(self, args: list[str], _invoked_from_agent: bool) -> bool:
588
+ """Slash handler for `/export` command."""
589
+ path_arg = args[0] if args else None
590
+ run_id = args[1] if len(args) > 1 else None
591
+
592
+ manifest_entry = resolve_manifest_for_export(self.ctx, run_id)
593
+ if manifest_entry is None:
594
+ if run_id:
595
+ self.console.print(
596
+ f"[{WARNING_STYLE}]No cached transcript found with run id {run_id!r}. Omit the run id to export the most recent run.[/]"
597
+ )
598
+ else:
599
+ self.console.print(
600
+ f"[{WARNING_STYLE}]No cached transcripts available yet. Run an agent first.[/]"
601
+ )
602
+ return False
603
+
604
+ destination = self._resolve_export_destination(path_arg, manifest_entry)
605
+ if destination is None:
606
+ return False
607
+
608
+ try:
609
+ exported = export_cached_transcript(
610
+ destination=destination,
611
+ run_id=manifest_entry.get("run_id"),
480
612
  )
613
+ except FileNotFoundError as exc:
614
+ self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
615
+ return False
616
+ except Exception as exc: # pragma: no cover - unexpected IO failures
617
+ self.console.print(f"[{ERROR_STYLE}]Failed to export transcript: {exc}[/]")
618
+ return False
481
619
  else:
482
- self.toggle_verbose()
620
+ self.console.print(f"[{SUCCESS_STYLE}]Transcript exported to[/] {exported}")
621
+ return True
483
622
 
484
- return True
623
+ def _resolve_export_destination(
624
+ self, path_arg: str | None, manifest_entry: dict[str, Any]
625
+ ) -> Path | None:
626
+ if path_arg:
627
+ return normalise_export_destination(Path(path_arg))
628
+
629
+ default_name = suggest_filename(manifest_entry)
630
+ prompt = f"Save transcript to [{default_name}]: "
631
+ try:
632
+ response = self.console.input(prompt)
633
+ except EOFError:
634
+ self.console.print("[dim]Export cancelled.[/dim]")
635
+ return None
485
636
 
486
- def _print_verbose_status(self, *, context: str) -> None:
487
- state_word = "on" if self._verbose_enabled else "off"
488
- if context == "already":
637
+ chosen = response.strip() or default_name
638
+ return normalise_export_destination(Path(chosen))
639
+
640
+ def _cmd_update(self, args: list[str], _invoked_from_agent: bool) -> bool:
641
+ """Slash handler for `/update` command."""
642
+ if args:
489
643
  self.console.print(
490
- f"Verbose mode already {state_word}. Use Ctrl+T or `/verbose` to toggle."
644
+ "Usage: `/update` upgrades glaip-sdk to the latest published version."
491
645
  )
492
- return
646
+ return True
493
647
 
494
- change_word = "enabled" if self._verbose_enabled else "disabled"
495
- self.console.print(
496
- f"Verbose mode {change_word}. Use Ctrl+T or `/verbose` to toggle."
497
- )
648
+ try:
649
+ self.ctx.invoke(update_command)
650
+ return True
651
+ except click.ClickException as exc:
652
+ self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
653
+ # Return False for update command failures to indicate the command didn't complete successfully
654
+ return False
498
655
 
499
656
  # ------------------------------------------------------------------
500
657
  # Agent run coordination helpers
@@ -510,6 +667,19 @@ class SlashSession:
510
667
  return
511
668
  self._active_renderer = None
512
669
 
670
+ def mark_agent_transcript_ready(self, agent_id: str, run_id: str | None) -> None:
671
+ """Record that an agent has a transcript ready for the current session."""
672
+ if not agent_id or not run_id:
673
+ return
674
+ self._agent_transcript_ready[agent_id] = run_id
675
+
676
+ def clear_agent_transcript_ready(self, agent_id: str | None = None) -> None:
677
+ """Reset transcript-ready state for an agent or for all agents."""
678
+ if agent_id:
679
+ self._agent_transcript_ready.pop(agent_id, None)
680
+ return
681
+ self._agent_transcript_ready.clear()
682
+
513
683
  def notify_agent_run_started(self) -> None:
514
684
  """Mark that an agent run is in progress."""
515
685
  self.clear_active_renderer()
@@ -519,7 +689,7 @@ class SlashSession:
519
689
  self.clear_active_renderer()
520
690
 
521
691
  def _sync_active_renderer(self) -> None:
522
- """Ensure the active renderer reflects the current verbose state."""
692
+ """Ensure the active renderer stays in standard (non-verbose) mode."""
523
693
  renderer = self._active_renderer
524
694
  if renderer is None:
525
695
  return
@@ -528,14 +698,14 @@ class SlashSession:
528
698
  apply_verbose = getattr(renderer, "apply_verbosity", None)
529
699
  if callable(apply_verbose):
530
700
  try:
531
- apply_verbose(self._verbose_enabled)
701
+ apply_verbose(False)
532
702
  applied = True
533
703
  except Exception:
534
704
  pass
535
705
 
536
706
  if not applied and hasattr(renderer, "verbose"):
537
707
  try:
538
- renderer.verbose = self._verbose_enabled
708
+ renderer.verbose = False
539
709
  except Exception:
540
710
  pass
541
711
 
@@ -555,8 +725,6 @@ class SlashSession:
555
725
  return head, tokens[1:]
556
726
 
557
727
  def _suggest(self, verb: str) -> str | None:
558
- from difflib import get_close_matches
559
-
560
728
  keys = [cmd.name for cmd in self._unique_commands.values()]
561
729
  match = get_close_matches(verb, keys, n=1)
562
730
  return match[0] if match else None
@@ -684,50 +852,121 @@ class SlashSession:
684
852
  return
685
853
 
686
854
  full_header = initial or not self._welcome_rendered
855
+ if full_header:
856
+ self._render_branding_banner()
857
+ self.console.rule(style=PRIMARY)
687
858
  self._render_main_header(active_agent, full=full_header)
688
859
  if full_header:
689
860
  self._welcome_rendered = True
861
+ self.console.print()
862
+
863
+ def _render_branding_banner(self) -> None:
864
+ """Render the GL AIP branding banner."""
865
+ banner = self._branding.get_welcome_banner()
866
+ heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
867
+ self.console.print(heading)
868
+ self.console.print()
869
+ self.console.print(banner)
870
+
871
+ def _maybe_show_update_prompt(self) -> None:
872
+ """Display update prompt once per session when applicable."""
873
+ if self._update_prompt_shown:
874
+ return
875
+
876
+ self._update_notifier(
877
+ self._branding.version,
878
+ console=self.console,
879
+ ctx=self.ctx,
880
+ slash_command="update",
881
+ style="panel",
882
+ )
883
+ self._update_prompt_shown = True
690
884
 
691
885
  def _render_focused_agent_header(self, active_agent: Any) -> None:
692
886
  """Render header when focusing on a specific agent."""
887
+ agent_info = self._get_agent_info(active_agent)
888
+ transcript_status = self._get_transcript_status(active_agent)
889
+
890
+ header_grid = self._build_header_grid(agent_info, transcript_status)
891
+ keybar = self._build_keybar()
892
+
893
+ header_grid.add_row(keybar, "")
894
+ self.console.print(
895
+ AIPPanel(header_grid, title="Agent Session", border_style=PRIMARY)
896
+ )
897
+
898
+ def _get_agent_info(self, active_agent: Any) -> dict[str, str]:
899
+ """Extract agent information for display."""
693
900
  agent_id = str(getattr(active_agent, "id", ""))
694
- agent_name = getattr(active_agent, "name", "") or agent_id
695
- agent_type = getattr(active_agent, "type", "") or "-"
696
- description = getattr(active_agent, "description", "") or ""
901
+ return {
902
+ "id": agent_id,
903
+ "name": getattr(active_agent, "name", "") or agent_id,
904
+ "type": getattr(active_agent, "type", "") or "-",
905
+ "description": getattr(active_agent, "description", "") or "",
906
+ }
697
907
 
698
- verbose_label = "verbose on" if self._verbose_enabled else "verbose off"
908
+ def _get_transcript_status(self, active_agent: Any) -> dict[str, Any]:
909
+ """Get transcript status for the active agent."""
910
+ agent_id = str(getattr(active_agent, "id", ""))
911
+ payload, manifest = self._get_last_transcript()
912
+
913
+ latest_agent_id = (manifest or {}).get("agent_id")
914
+ has_transcript = bool(payload and manifest and manifest.get("run_id"))
915
+ run_id = (manifest or {}).get("run_id")
916
+ transcript_ready = (
917
+ has_transcript
918
+ and latest_agent_id == agent_id
919
+ and self._agent_transcript_ready.get(agent_id) == run_id
920
+ )
921
+
922
+ return {
923
+ "has_transcript": has_transcript,
924
+ "transcript_ready": transcript_ready,
925
+ "run_id": run_id,
926
+ }
699
927
 
700
- header_grid = Table.grid(expand=True)
928
+ def _build_header_grid(
929
+ self, agent_info: dict[str, str], transcript_status: dict[str, Any]
930
+ ) -> AIPGrid:
931
+ """Build the main header grid with agent information."""
932
+ header_grid = AIPGrid(expand=True)
701
933
  header_grid.add_column(ratio=3)
702
934
  header_grid.add_column(ratio=1, justify="right")
703
935
 
704
- primary_line = f"[bold]{agent_name}[/bold] · [dim]{agent_type}[/dim] · [cyan]{agent_id}[/cyan]"
705
- header_grid.add_row(
706
- primary_line,
707
- f"[green]ready[/green] · {verbose_label}",
936
+ primary_line = (
937
+ f"[bold]{agent_info['name']}[/bold] · [dim]{agent_info['type']}[/dim] · "
938
+ f"[{ACCENT_STYLE}]{agent_info['id']}[/]"
708
939
  )
940
+ status_line = f"[{SUCCESS_STYLE}]ready[/]"
941
+ status_line += (
942
+ " · transcript ready"
943
+ if transcript_status["transcript_ready"]
944
+ else " · transcript pending"
945
+ )
946
+ header_grid.add_row(primary_line, status_line)
709
947
 
710
- if description:
948
+ if agent_info["description"]:
949
+ description = agent_info["description"]
950
+ if not transcript_status["transcript_ready"]:
951
+ description = f"{description} (transcript pending)"
711
952
  header_grid.add_row(f"[dim]{description}[/dim]", "")
712
953
 
713
- keybar = Table.grid(expand=True)
714
- keybar.add_column(justify="left")
715
- keybar.add_column(justify="left")
716
- keybar.add_column(justify="left")
717
- keybar.add_column(justify="left")
718
- keybar.add_row(
719
- "[bold]/help[/bold] [dim]Show commands[/dim]",
720
- "[bold]/details[/bold] [dim]Agent config[/dim]",
721
- "[bold]/exit[/bold] [dim]Back[/dim]",
722
- "[bold]Ctrl+T[/bold] [dim]Toggle verbose[/dim]",
723
- )
954
+ return header_grid
724
955
 
725
- header_grid.add_row(keybar, "")
956
+ def _build_keybar(self) -> AIPGrid:
957
+ """Build the keybar with command hints."""
958
+ keybar = AIPGrid(expand=True)
959
+ keybar.add_column(justify="left", ratio=1)
960
+ keybar.add_column(justify="left", ratio=1)
726
961
 
727
- self.console.print(
728
- AIPPanel(header_grid, title="Agent Session", border_style="blue")
962
+ keybar.add_row(
963
+ format_command_hint("/help", "Show commands") or "",
964
+ format_command_hint("/details", "Agent config") or "",
965
+ format_command_hint("/exit", "Back") or "",
729
966
  )
730
967
 
968
+ return keybar
969
+
731
970
  def _render_main_header(
732
971
  self, active_agent: Any | None = None, *, full: bool = False
733
972
  ) -> None:
@@ -737,49 +976,31 @@ class SlashSession:
737
976
  api_url = self._get_api_url(config)
738
977
  status = "Configured" if config.get("api_key") else "Not configured"
739
978
 
979
+ segments = [
980
+ f"[dim]Base URL[/dim] • {api_url or 'Not configured'}",
981
+ f"[dim]Credentials[/dim] • {status}",
982
+ ]
983
+ agent_info = self._build_agent_status_line(active_agent)
984
+ if agent_info:
985
+ segments.append(agent_info)
986
+
987
+ rendered_line = " ".join(segments)
988
+
740
989
  if full:
741
- lines = [
742
- f"GL AIP v{self._branding.version} · GDP Labs AI Agents Package",
743
- f"API: {api_url or 'Not configured'} · Credentials: {status}",
744
- (
745
- f"Verbose: {'on' if self._verbose_enabled else 'off'} "
746
- "(Ctrl+T toggles verbose streaming)"
747
- ),
748
- ]
749
- extra: list[str] = []
750
- self._add_agent_info_to_header(extra, active_agent)
751
- lines.extend(extra)
752
- self.console.print(
753
- AIPPanel("\n".join(lines), title="GL AIP Session", border_style="cyan")
754
- )
990
+ self.console.print(rendered_line)
755
991
  return
756
992
 
757
- status_bar = Table.grid(expand=True)
758
- status_bar.add_column(ratio=2)
759
- status_bar.add_column(ratio=2)
760
- status_bar.add_column(ratio=1, justify="right")
761
- status_bar.add_row(
762
- "[bold cyan]AIP Palette[/bold cyan]",
763
- f"[dim]API[/dim]: {api_url or 'Not configured'}",
764
- f"[dim]Verbose[/dim]: {'on' if self._verbose_enabled else 'off'}",
765
- )
766
- status_bar.add_row("[dim]Ctrl+T toggles verbose[/dim]", "", "")
767
- status_bar.add_row("[dim]Type /help for shortcuts[/dim]", "", "")
768
-
769
- if active_agent is not None:
770
- agent_id = str(getattr(active_agent, "id", ""))
771
- agent_name = getattr(active_agent, "name", "") or agent_id
772
- status_bar.add_row(f"[dim]Active[/dim]: {agent_name} [{agent_id}]", "", "")
773
- elif self.recent_agents:
774
- recent = self.recent_agents[0]
775
- label = recent.get("name") or recent.get("id") or "-"
776
- status_bar.add_row(
777
- f"[dim]Recent[/dim]: {label} [{recent.get('id', '-')}]",
778
- "",
779
- "",
993
+ status_bar = AIPGrid(expand=True)
994
+ status_bar.add_column(ratio=1)
995
+ status_bar.add_row(rendered_line)
996
+ self.console.print(
997
+ AIPPanel(
998
+ status_bar,
999
+ border_style=PRIMARY,
1000
+ padding=(0, 1),
1001
+ expand=False,
780
1002
  )
781
-
782
- self.console.print(AIPPanel(status_bar, border_style="cyan"))
1003
+ )
783
1004
 
784
1005
  def _get_api_url(self, config: dict[str, Any]) -> str | None:
785
1006
  """Get the API URL from various sources."""
@@ -788,58 +1009,85 @@ class SlashSession:
788
1009
  api_url = self.ctx.obj.get("api_url")
789
1010
  return api_url or config.get("api_url") or os.getenv("AIP_API_URL")
790
1011
 
791
- def _add_agent_info_to_header(
792
- self, lines: list[str], active_agent: Any | None
793
- ) -> None:
794
- """Add agent information to header lines."""
1012
+ def _build_agent_status_line(self, active_agent: Any | None) -> str | None:
1013
+ """Return a short status line about the active or recent agent."""
795
1014
  if active_agent is not None:
796
1015
  agent_id = str(getattr(active_agent, "id", ""))
797
1016
  agent_name = getattr(active_agent, "name", "") or agent_id
798
- lines.append(f"[dim]Active agent[/dim]: {agent_name} [{agent_id}]")
799
- elif self.recent_agents:
1017
+ return f"[dim]Active[/dim]: {agent_name} ({agent_id})"
1018
+ if self.recent_agents:
800
1019
  recent = self.recent_agents[0]
801
1020
  label = recent.get("name") or recent.get("id") or "-"
802
- lines.append(f"[dim]Recent agent[/dim]: {label} [{recent.get('id', '-')}]")
1021
+ return f"[dim]Recent[/dim]: {label} ({recent.get('id', '-')})"
1022
+ return None
803
1023
 
804
1024
  def _show_default_quick_actions(self) -> None:
805
- self._show_quick_actions(
806
- [
807
- (self.STATUS_COMMAND, "Verify the connection"),
808
- (self.AGENTS_COMMAND, "Pick an agent to inspect or run"),
809
- ]
810
- )
1025
+ hints: list[tuple[str | None, str]] = [
1026
+ (
1027
+ command_hint("status", slash_command="status", ctx=self.ctx),
1028
+ "Connection check",
1029
+ ),
1030
+ (
1031
+ command_hint("agents list", slash_command="agents", ctx=self.ctx),
1032
+ "Browse agents",
1033
+ ),
1034
+ (
1035
+ command_hint("help", slash_command="help", ctx=self.ctx),
1036
+ "Show all commands",
1037
+ ),
1038
+ ]
1039
+ filtered = [(cmd, desc) for cmd, desc in hints if cmd]
1040
+ if filtered:
1041
+ self._show_quick_actions(filtered, title="Quick actions")
811
1042
  self._default_actions_shown = True
812
1043
 
813
1044
  def _render_home_hint(self) -> None:
814
- self.console.print(
815
- AIPPanel(
816
- "Type `/help` for command palette commands, `/agents` to browse agents, or `/exit` (`/q`) to leave the palette.\n"
817
- "Press Ctrl+T to toggle verbose output.\n"
818
- "Press Ctrl+C to cancel the current entry, Ctrl+D to quit immediately.",
819
- title=" Getting Started",
820
- border_style="cyan",
821
- )
822
- )
1045
+ if self._home_hint_shown:
1046
+ return
1047
+ hint_lines = [
1048
+ f"[{HINT_PREFIX_STYLE}]Hint:[/]",
1049
+ f" Type {format_command_hint('/') or '/'} to explore commands",
1050
+ " Press [dim]Ctrl+C[/] to cancel the current entry",
1051
+ " Press [dim]Ctrl+D[/] to quit",
1052
+ ]
1053
+ self.console.print("\n".join(hint_lines))
1054
+ self._home_hint_shown = True
823
1055
 
824
1056
  def _show_quick_actions(
825
- self, hints: Iterable[tuple[str, str]], *, title: str = "Quick actions"
1057
+ self,
1058
+ hints: Iterable[tuple[str, str]],
1059
+ *,
1060
+ title: str = "Quick actions",
1061
+ inline: bool = False,
826
1062
  ) -> None:
827
- hint_list = list(hints)
1063
+ hint_list = [
1064
+ (command, description) for command, description in hints if command
1065
+ ]
828
1066
  if not hint_list:
829
1067
  return
830
1068
 
831
- grid = Table.grid(expand=True)
832
- for _ in hint_list:
833
- grid.add_column(ratio=1, no_wrap=False)
1069
+ if inline:
1070
+ lines: list[str] = []
1071
+ for command, description in hint_list:
1072
+ formatted = format_command_hint(command, description)
1073
+ if formatted:
1074
+ lines.append(formatted)
1075
+ if lines:
1076
+ self.console.print("\n".join(lines))
1077
+ return
834
1078
 
835
- grid.add_row(
836
- *[
837
- f"[bold]{command}[/bold]\n[dim]{description}[/dim]"
838
- for command, description in hint_list
839
- ]
840
- )
1079
+ body_lines: list[Text] = []
1080
+ for command, description in hint_list:
1081
+ formatted = format_command_hint(command, description)
1082
+ if formatted:
1083
+ body_lines.append(Text.from_markup(formatted))
841
1084
 
842
- self.console.print(AIPPanel(grid, title=title, border_style="magenta"))
1085
+ panel_content = Group(*body_lines)
1086
+ self.console.print(
1087
+ AIPPanel(
1088
+ panel_content, title=title, border_style=SECONDARY_LIGHT, expand=False
1089
+ )
1090
+ )
843
1091
 
844
1092
  def _load_config(self) -> dict[str, Any]:
845
1093
  if self._config_cache is None: