glaip-sdk 0.1.3__py3-none-any.whl → 0.2.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 (36) hide show
  1. glaip_sdk/__init__.py +1 -1
  2. glaip_sdk/_version.py +1 -0
  3. glaip_sdk/cli/commands/__init__.py +2 -2
  4. glaip_sdk/cli/commands/agents.py +22 -37
  5. glaip_sdk/cli/commands/configure.py +69 -9
  6. glaip_sdk/cli/commands/mcps.py +19 -23
  7. glaip_sdk/cli/commands/tools.py +17 -41
  8. glaip_sdk/cli/commands/transcripts.py +747 -0
  9. glaip_sdk/cli/config.py +6 -1
  10. glaip_sdk/cli/display.py +1 -0
  11. glaip_sdk/cli/main.py +17 -37
  12. glaip_sdk/cli/parsers/__init__.py +1 -3
  13. glaip_sdk/cli/slash/__init__.py +0 -9
  14. glaip_sdk/cli/slash/prompt.py +2 -0
  15. glaip_sdk/cli/slash/session.py +251 -88
  16. glaip_sdk/cli/transcript/__init__.py +12 -52
  17. glaip_sdk/cli/transcript/cache.py +255 -44
  18. glaip_sdk/cli/transcript/capture.py +32 -0
  19. glaip_sdk/cli/transcript/history.py +815 -0
  20. glaip_sdk/cli/transcript/viewer.py +6 -2
  21. glaip_sdk/cli/utils.py +187 -0
  22. glaip_sdk/client/_agent_payloads.py +5 -0
  23. glaip_sdk/payload_schemas/__init__.py +1 -13
  24. glaip_sdk/utils/__init__.py +12 -7
  25. glaip_sdk/utils/datetime_helpers.py +58 -0
  26. glaip_sdk/utils/general.py +0 -33
  27. glaip_sdk/utils/import_export.py +9 -1
  28. glaip_sdk/utils/rendering/renderer/__init__.py +0 -20
  29. glaip_sdk/utils/rendering/renderer/debug.py +3 -20
  30. glaip_sdk/utils/rendering/steps.py +5 -6
  31. glaip_sdk/utils/resource_refs.py +2 -1
  32. glaip_sdk/utils/serialization.py +2 -0
  33. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.2.0.dist-info}/METADATA +1 -1
  34. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.2.0.dist-info}/RECORD +36 -33
  35. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.2.0.dist-info}/WHEEL +0 -0
  36. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.2.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/config.py CHANGED
@@ -12,7 +12,12 @@ import yaml
12
12
 
13
13
  CONFIG_DIR = Path.home() / ".aip"
14
14
  CONFIG_FILE = CONFIG_DIR / "config.yaml"
15
- _ALLOWED_KEYS = {"api_url", "api_key", "timeout"}
15
+ _ALLOWED_KEYS = {
16
+ "api_url",
17
+ "api_key",
18
+ "timeout",
19
+ "history_default_limit",
20
+ }
16
21
 
17
22
 
18
23
  def _sanitize_config(data: dict[str, Any] | None) -> dict[str, Any]:
glaip_sdk/cli/display.py CHANGED
@@ -208,6 +208,7 @@ def build_resource_result_data(resource: Any, fields: list[str]) -> dict[str, An
208
208
 
209
209
 
210
210
  def _normalise_field_value(field: str, value: Any) -> Any:
211
+ """Convert special sentinel values into display-friendly text."""
211
212
  if value is _MISSING:
212
213
  return "N/A"
213
214
  if hasattr(value, "_mock_name"):
glaip_sdk/cli/main.py CHANGED
@@ -13,7 +13,6 @@ import click
13
13
  from rich.console import Console
14
14
 
15
15
  from glaip_sdk import Client
16
- from glaip_sdk._version import __version__ as _SDK_VERSION
17
16
  from glaip_sdk.branding import (
18
17
  ERROR,
19
18
  ERROR_STYLE,
@@ -34,11 +33,12 @@ from glaip_sdk.cli.commands.configure import (
34
33
  from glaip_sdk.cli.commands.mcps import mcps_group
35
34
  from glaip_sdk.cli.commands.models import models_group
36
35
  from glaip_sdk.cli.commands.tools import tools_group
36
+ from glaip_sdk.cli.commands.transcripts import transcripts_group
37
37
  from glaip_sdk.cli.commands.update import update_command
38
38
  from glaip_sdk.cli.config import load_config
39
39
  from glaip_sdk.cli.transcript import get_transcript_cache_stats
40
40
  from glaip_sdk.cli.update_notifier import maybe_notify_update
41
- from glaip_sdk.cli.utils import in_slash_mode, spinner_context, update_spinner
41
+ from glaip_sdk.cli.utils import format_size, in_slash_mode, sdk_version, spinner_context, update_spinner
42
42
  from glaip_sdk.config.constants import (
43
43
  DEFAULT_AGENT_RUN_TIMEOUT,
44
44
  )
@@ -56,26 +56,8 @@ except ImportError: # pragma: no cover - optional slash dependencies
56
56
  AVAILABLE_STATUS = "✅ Available"
57
57
 
58
58
 
59
- def _format_size(num: int) -> str:
60
- """Return a human-readable byte size."""
61
- if num <= 0:
62
- return "0B"
63
-
64
- units = ["B", "KB", "MB", "GB", "TB"]
65
- value = float(num)
66
- for unit in units:
67
- if value < 1024 or unit == units[-1]:
68
- if value >= 100 or unit == "B":
69
- return f"{value:.0f}{unit}"
70
- if value >= 10:
71
- return f"{value:.1f}{unit}"
72
- return f"{value:.2f}{unit}"
73
- value /= 1024
74
- return f"{value:.1f}TB" # pragma: no cover - defensive fallback
75
-
76
-
77
59
  @click.group(invoke_without_command=True)
78
- @click.version_option(version=_SDK_VERSION, prog_name="aip")
60
+ @click.version_option(package_name="glaip-sdk", prog_name="aip")
79
61
  @click.option(
80
62
  "--api-url",
81
63
  envvar="AIP_API_URL",
@@ -136,7 +118,7 @@ def main(
136
118
  if not ctx.resilient_parsing and ctx.obj["tty"] and not launching_slash:
137
119
  console = Console()
138
120
  maybe_notify_update(
139
- _SDK_VERSION,
121
+ sdk_version(),
140
122
  console=console,
141
123
  ctx=ctx,
142
124
  slash_command="update",
@@ -158,6 +140,7 @@ main.add_command(config_group)
158
140
  main.add_command(tools_group)
159
141
  main.add_command(mcps_group)
160
142
  main.add_command(models_group)
143
+ main.add_command(transcripts_group)
161
144
 
162
145
  # Add top-level commands
163
146
  main.add_command(configure_command)
@@ -215,7 +198,7 @@ def _validate_config_and_show_error(config: dict, console: Console) -> None:
215
198
  border_style=ERROR,
216
199
  )
217
200
  )
218
- console.print(f"\n[{SUCCESS_STYLE}]✅ AIP - Ready[/] (SDK v{_SDK_VERSION}) - Configure to connect")
201
+ console.print(f"\n[{SUCCESS_STYLE}]✅ AIP - Ready[/] (SDK v{sdk_version()}) - Configure to connect")
219
202
  sys.exit(1)
220
203
 
221
204
 
@@ -233,7 +216,7 @@ def _render_status_heading(console: Console, slash_mode: bool) -> None:
233
216
  del slash_mode # heading now consistent across invocation contexts
234
217
  console.print(f"[{INFO_STYLE}]GL AIP status[/]")
235
218
  console.print()
236
- console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{_SDK_VERSION})")
219
+ console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
237
220
 
238
221
 
239
222
  def _collect_cache_summary() -> tuple[str | None, str | None]:
@@ -241,15 +224,15 @@ def _collect_cache_summary() -> tuple[str | None, str | None]:
241
224
  try:
242
225
  cache_stats = get_transcript_cache_stats()
243
226
  except Exception:
244
- return "[dim]Saved run history[/dim]: unavailable", None
227
+ return "[dim]Saved transcripts[/dim]: unavailable", None
245
228
 
246
229
  runs_text = f"{cache_stats.entry_count} runs saved"
247
230
  if cache_stats.total_bytes:
248
- size_part = f" · {_format_size(cache_stats.total_bytes)} used"
231
+ size_part = f" · {format_size(cache_stats.total_bytes)} used"
249
232
  else:
250
233
  size_part = ""
251
234
 
252
- cache_line = f"[dim]Saved run history[/dim]: {runs_text}{size_part} · {cache_stats.cache_dir}"
235
+ cache_line = f"[dim]Saved transcripts[/dim]: {runs_text}{size_part} · {cache_stats.cache_dir}"
253
236
  return cache_line, None
254
237
 
255
238
 
@@ -391,7 +374,7 @@ def status(ctx: Any) -> None:
391
374
  @main.command()
392
375
  def version() -> None:
393
376
  """Show version information."""
394
- branding = AIPBranding.create_from_sdk(sdk_version=_SDK_VERSION, package_name="glaip-sdk")
377
+ branding = AIPBranding.create_from_sdk(sdk_version=sdk_version(), package_name="glaip-sdk")
395
378
  branding.display_version_panel()
396
379
 
397
380
 
@@ -430,14 +413,11 @@ def update(check_only: bool, force: bool) -> None:
430
413
 
431
414
  # Update using pip
432
415
  try:
433
- cmd = [
434
- sys.executable,
435
- "-m",
436
- "pip",
437
- "install",
438
- "--upgrade",
439
- "glaip-sdk",
440
- ]
416
+ from glaip_sdk.cli.commands.update import _build_upgrade_command
417
+
418
+ cmd = list(_build_upgrade_command(include_prerelease=False))
419
+ # Replace package name with "glaip-sdk" (main.py uses different name)
420
+ cmd[-1] = "glaip-sdk"
441
421
  if force:
442
422
  cmd.insert(5, "--force-reinstall")
443
423
  subprocess.run(cmd, capture_output=True, text=True, check=True)
@@ -492,4 +472,4 @@ def update(check_only: bool, force: bool) -> None:
492
472
 
493
473
 
494
474
  if __name__ == "__main__":
495
- main()
475
+ main() # pylint: disable=no-value-for-parameter
@@ -4,6 +4,4 @@ Authors:
4
4
  Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
5
5
  """
6
6
 
7
- from glaip_sdk.cli.parsers.json_input import parse_json_input
8
-
9
- __all__ = ["parse_json_input"]
7
+ __all__: list[str] = []
@@ -6,19 +6,10 @@ Authors:
6
6
 
7
7
  from glaip_sdk.cli.commands.agents import get as agents_get_command
8
8
  from glaip_sdk.cli.commands.agents import run as agents_run_command
9
- from glaip_sdk.cli.commands.configure import configure_command, load_config
10
- from glaip_sdk.cli.slash.agent_session import AgentRunSession
11
- from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT
12
9
  from glaip_sdk.cli.slash.session import SlashSession
13
- from glaip_sdk.cli.utils import get_client
14
10
 
15
11
  __all__ = [
16
- "AgentRunSession",
17
12
  "SlashSession",
18
- "_HAS_PROMPT_TOOLKIT",
19
13
  "agents_get_command",
20
14
  "agents_run_command",
21
- "configure_command",
22
- "get_client",
23
- "load_config",
24
15
  ]
@@ -163,6 +163,7 @@ def _create_key_bindings(_session: SlashSession) -> Any:
163
163
  def _iter_command_completions(
164
164
  session: SlashSession, text: str
165
165
  ) -> Iterable[Completion]: # pragma: no cover - thin wrapper
166
+ """Yield completions for global slash commands."""
166
167
  prefix = text[1:]
167
168
  seen: set[str] = set()
168
169
 
@@ -203,6 +204,7 @@ def _generate_command_completions(cmd: Any, prefix: str, text: str, seen: set[st
203
204
  def _iter_contextual_completions(
204
205
  session: SlashSession, text: str
205
206
  ) -> Iterable[Completion]: # pragma: no cover - thin wrapper
207
+ """Yield completions for context-specific slash commands."""
206
208
  prefix = text[1:]
207
209
  seen: set[str] = set()
208
210
 
@@ -33,6 +33,7 @@ from glaip_sdk.branding import (
33
33
  WARNING_STYLE,
34
34
  AIPBranding,
35
35
  )
36
+ from glaip_sdk.cli.commands import transcripts as transcripts_cmd
36
37
  from glaip_sdk.cli.commands.configure import configure_command, load_config
37
38
  from glaip_sdk.cli.commands.update import update_command
38
39
  from glaip_sdk.cli.slash.agent_session import AgentRunSession
@@ -46,9 +47,7 @@ from glaip_sdk.cli.slash.prompt import (
46
47
  )
47
48
  from glaip_sdk.cli.transcript import (
48
49
  export_cached_transcript,
49
- normalise_export_destination,
50
- resolve_manifest_for_export,
51
- suggest_filename,
50
+ load_history_snapshot,
52
51
  )
53
52
  from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
54
53
  from glaip_sdk.cli.update_notifier import maybe_notify_update
@@ -56,6 +55,7 @@ from glaip_sdk.cli.utils import (
56
55
  _fuzzy_pick_for_resources,
57
56
  command_hint,
58
57
  format_command_hint,
58
+ format_size,
59
59
  get_client,
60
60
  )
61
61
  from glaip_sdk.rich_components import AIPGrid, AIPPanel, AIPTable
@@ -73,6 +73,39 @@ class SlashCommand:
73
73
  aliases: tuple[str, ...] = ()
74
74
 
75
75
 
76
+ NEW_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
77
+ {
78
+ "cli": "transcripts",
79
+ "slash": "transcripts",
80
+ "description": "Review transcript cache",
81
+ "tag": "NEW",
82
+ "priority": 10,
83
+ },
84
+ )
85
+
86
+
87
+ DEFAULT_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
88
+ {
89
+ "cli": "status",
90
+ "slash": "status",
91
+ "description": "Connection check",
92
+ "priority": 0,
93
+ },
94
+ {
95
+ "cli": "agents list",
96
+ "slash": "agents",
97
+ "description": "Browse agents",
98
+ "priority": 0,
99
+ },
100
+ {
101
+ "cli": "help",
102
+ "slash": "help",
103
+ "description": "Show all commands",
104
+ "priority": 0,
105
+ },
106
+ )
107
+
108
+
76
109
  class SlashSession:
77
110
  """Interactive command palette controller."""
78
111
 
@@ -99,7 +132,7 @@ class SlashSession:
99
132
  self._active_renderer: Any | None = None
100
133
  self._current_agent: Any | None = None
101
134
 
102
- self._home_placeholder = "Start with / to browse commands"
135
+ self._home_placeholder = "Hint: type / to explore commands · Ctrl+D exits"
103
136
 
104
137
  # Command string constants to avoid duplication
105
138
  self.STATUS_COMMAND = "/status"
@@ -263,6 +296,10 @@ class SlashSession:
263
296
  return False
264
297
  return True
265
298
 
299
+ def _continue_session(self) -> bool:
300
+ """Signal that the slash session should remain active."""
301
+ return not self._should_exit
302
+
266
303
  # ------------------------------------------------------------------
267
304
  # Command handlers
268
305
  # ------------------------------------------------------------------
@@ -345,7 +382,7 @@ class SlashSession:
345
382
  self._show_default_quick_actions()
346
383
  except click.ClickException as exc:
347
384
  self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
348
- return True
385
+ return self._continue_session()
349
386
 
350
387
  def _cmd_status(self, _args: list[str], _invoked_from_agent: bool) -> bool:
351
388
  ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
@@ -374,7 +411,116 @@ class SlashSession:
374
411
  ctx_obj.pop("_slash_console", None)
375
412
  else:
376
413
  ctx_obj["_slash_console"] = previous_console
377
- return True
414
+ return self._continue_session()
415
+
416
+ def _cmd_transcripts(self, args: list[str], _invoked_from_agent: bool) -> bool:
417
+ if args and args[0].lower() in {"detail", "show"}:
418
+ if len(args) < 2:
419
+ self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts detail <run_id>[/]")
420
+ return self._continue_session()
421
+ self._show_transcript_detail(args[1])
422
+ return self._continue_session()
423
+
424
+ limit, ok = self._parse_transcripts_limit(args)
425
+ if not ok:
426
+ return self._continue_session()
427
+
428
+ snapshot = load_history_snapshot(limit=limit, ctx=self.ctx)
429
+
430
+ if self._handle_transcripts_empty(snapshot, limit):
431
+ return self._continue_session()
432
+
433
+ self._render_transcripts_snapshot(snapshot)
434
+ return self._continue_session()
435
+
436
+ def _parse_transcripts_limit(self, args: list[str]) -> tuple[int | None, bool]:
437
+ if not args:
438
+ return None, True
439
+ try:
440
+ limit = int(args[0])
441
+ except ValueError:
442
+ self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts [limit][/]")
443
+ return None, False
444
+ if limit < 0:
445
+ self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts [limit][/]")
446
+ return None, False
447
+ return limit, True
448
+
449
+ def _handle_transcripts_empty(self, snapshot: Any, limit: int | None) -> bool:
450
+ if snapshot.cached_entries == 0:
451
+ self.console.print(f"[{WARNING_STYLE}]No cached transcripts yet. Run an agent first.[/]")
452
+ for warning in snapshot.warnings:
453
+ self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
454
+ return True
455
+ if limit == 0 and snapshot.cached_entries:
456
+ self.console.print(f"[{WARNING_STYLE}]Limit is 0; nothing to display.[/]")
457
+ return True
458
+ return False
459
+
460
+ def _render_transcripts_snapshot(self, snapshot: Any) -> None:
461
+ size_text = format_size(snapshot.total_size_bytes)
462
+ header = f"[dim]Manifest: {snapshot.manifest_path} · {snapshot.total_entries} runs · {size_text} used[/]"
463
+ self.console.print(header)
464
+
465
+ if snapshot.limit_clamped:
466
+ self.console.print(
467
+ f"[{WARNING_STYLE}]Requested limit exceeded maximum; showing first {snapshot.limit_applied} runs.[/]"
468
+ )
469
+
470
+ if snapshot.total_entries > len(snapshot.entries):
471
+ subset_message = (
472
+ f"[dim]Showing {len(snapshot.entries)} of {snapshot.total_entries} "
473
+ f"runs (limit={snapshot.limit_applied}).[/]"
474
+ )
475
+ self.console.print(subset_message)
476
+ self.console.print("[dim]Hint: run `/transcripts <limit>` to change how many rows are displayed.[/]")
477
+
478
+ if snapshot.migration_summary:
479
+ self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
480
+
481
+ for warning in snapshot.warnings:
482
+ self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
483
+
484
+ table = transcripts_cmd._build_table(snapshot.entries)
485
+ self.console.print(table)
486
+ self.console.print("[dim]! Missing transcript[/]")
487
+
488
+ def _show_transcript_detail(self, run_id: str) -> None:
489
+ """Render the cached transcript log for a single run."""
490
+ snapshot = load_history_snapshot(ctx=self.ctx)
491
+ entry = snapshot.index.get(run_id)
492
+ if entry is None:
493
+ self.console.print(f"[{WARNING_STYLE}]Run id {run_id} was not found in the cache manifest.[/]")
494
+ return
495
+
496
+ try:
497
+ transcript_path, transcript_text = transcripts_cmd._load_transcript_text(entry)
498
+ except click.ClickException as exc:
499
+ self.console.print(f"[{WARNING_STYLE}]{exc}[/]")
500
+ return
501
+
502
+ meta, events = transcripts_cmd._decode_transcript(transcript_text)
503
+ if transcripts_cmd._maybe_launch_transcript_viewer(
504
+ self.ctx,
505
+ entry,
506
+ meta,
507
+ events,
508
+ console_override=self.console,
509
+ force=True,
510
+ initial_view="transcript",
511
+ ):
512
+ if snapshot.migration_summary:
513
+ self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
514
+ for warning in snapshot.warnings:
515
+ self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
516
+ return
517
+
518
+ if snapshot.migration_summary:
519
+ self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
520
+ for warning in snapshot.warnings:
521
+ self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
522
+ view = transcripts_cmd._render_transcript_display(entry, snapshot.manifest_path, transcript_path, meta, events)
523
+ self.console.print(view, markup=False, highlight=False, soft_wrap=True, end="")
378
524
 
379
525
  def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
380
526
  client = self._get_client_or_fail()
@@ -442,7 +588,7 @@ class SlashSession:
442
588
  self._render_header()
443
589
 
444
590
  self._show_agent_followup_actions(picked_agent)
445
- return True
591
+ return self._continue_session()
446
592
 
447
593
  def _show_agent_followup_actions(self, picked_agent: Any) -> None:
448
594
  """Show follow-up action hints after agent session."""
@@ -498,6 +644,13 @@ class SlashSession:
498
644
  handler=SlashSession._cmd_status,
499
645
  )
500
646
  )
647
+ self._register(
648
+ SlashCommand(
649
+ name="transcripts",
650
+ help="Review cached transcript history. Add a number (e.g. `/transcripts 5`) to change the row limit.",
651
+ handler=SlashSession._cmd_transcripts,
652
+ )
653
+ )
501
654
  self._register(
502
655
  SlashCommand(
503
656
  name="agents",
@@ -574,55 +727,13 @@ class SlashSession:
574
727
  manifest = ctx_obj.get("_last_transcript_manifest")
575
728
  return payload, manifest
576
729
 
577
- def _cmd_export(self, args: list[str], _invoked_from_agent: bool) -> bool:
730
+ def _cmd_export(self, _args: list[str], _invoked_from_agent: bool) -> bool:
578
731
  """Slash handler for `/export` command."""
579
- path_arg = args[0] if args else None
580
- run_id = args[1] if len(args) > 1 else None
581
-
582
- manifest_entry = resolve_manifest_for_export(self.ctx, run_id)
583
- if manifest_entry is None:
584
- if run_id:
585
- self.console.print(
586
- f"[{WARNING_STYLE}]No cached transcript found with run id {run_id!r}. "
587
- "Omit the run id to export the most recent run.[/]"
588
- )
589
- else:
590
- self.console.print(f"[{WARNING_STYLE}]No cached transcripts available yet. Run an agent first.[/]")
591
- return False
592
-
593
- destination = self._resolve_export_destination(path_arg, manifest_entry)
594
- if destination is None:
595
- return False
596
-
597
- try:
598
- exported = export_cached_transcript(
599
- destination=destination,
600
- run_id=manifest_entry.get("run_id"),
601
- )
602
- except FileNotFoundError as exc:
603
- self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
604
- return False
605
- except Exception as exc: # pragma: no cover - unexpected IO failures
606
- self.console.print(f"[{ERROR_STYLE}]Failed to export transcript: {exc}[/]")
607
- return False
608
- else:
609
- self.console.print(f"[{SUCCESS_STYLE}]Transcript exported to[/] {exported}")
610
- return True
611
-
612
- def _resolve_export_destination(self, path_arg: str | None, manifest_entry: dict[str, Any]) -> Path | None:
613
- if path_arg:
614
- return normalise_export_destination(Path(path_arg))
615
-
616
- default_name = suggest_filename(manifest_entry)
617
- prompt = f"Save transcript to [{default_name}]: "
618
- try:
619
- response = self.console.input(prompt)
620
- except EOFError:
621
- self.console.print("[dim]Export cancelled.[/dim]")
622
- return None
623
-
624
- chosen = response.strip() or default_name
625
- return normalise_export_destination(Path(chosen))
732
+ self.console.print(
733
+ f"[{WARNING_STYLE}]`/export` is deprecated. Use `/transcripts`, select a run, "
734
+ "and open the transcript viewer to export.[/]"
735
+ )
736
+ return True
626
737
 
627
738
  def _cmd_update(self, args: list[str], _invoked_from_agent: bool) -> bool:
628
739
  """Slash handler for `/update` command."""
@@ -981,35 +1092,73 @@ class SlashSession:
981
1092
  return None
982
1093
 
983
1094
  def _show_default_quick_actions(self) -> None:
984
- hints: list[tuple[str | None, str]] = [
985
- (
986
- command_hint("status", slash_command="status", ctx=self.ctx),
987
- "Connection check",
988
- ),
989
- (
990
- command_hint("agents list", slash_command="agents", ctx=self.ctx),
991
- "Browse agents",
992
- ),
993
- (
994
- command_hint("help", slash_command="help", ctx=self.ctx),
995
- "Show all commands",
996
- ),
997
- ]
998
- filtered = [(cmd, desc) for cmd, desc in hints if cmd]
999
- if filtered:
1000
- self._show_quick_actions(filtered, title="Quick actions")
1095
+ new_hints = self._collect_quick_action_hints(NEW_QUICK_ACTIONS, highlight_new=True)
1096
+ evergreen_hints = self._collect_quick_action_hints(DEFAULT_QUICK_ACTIONS)
1097
+ if new_hints or evergreen_hints:
1098
+ self.console.print(f"[dim]{'─' * 40}[/]")
1099
+ self._render_quick_action_group(new_hints, "New commands")
1100
+ self._render_quick_action_group(evergreen_hints, "Quick actions")
1001
1101
  self._default_actions_shown = True
1002
1102
 
1103
+ def _collect_quick_action_hints(
1104
+ self,
1105
+ actions: Iterable[dict[str, Any]],
1106
+ *,
1107
+ highlight_new: bool = False,
1108
+ ) -> list[tuple[str, str]]:
1109
+ collected: list[tuple[str, str]] = []
1110
+ for action in sorted(actions, key=lambda payload: payload.get("priority", 0), reverse=True):
1111
+ hint = self._build_quick_action_hint(action, highlight_new=highlight_new)
1112
+ if hint:
1113
+ collected.append(hint)
1114
+ return collected
1115
+
1116
+ def _build_quick_action_hint(
1117
+ self,
1118
+ action: dict[str, Any],
1119
+ *,
1120
+ highlight_new: bool = False,
1121
+ ) -> tuple[str, str] | None:
1122
+ command = command_hint(action.get("cli"), slash_command=action.get("slash"), ctx=self.ctx)
1123
+ if not command:
1124
+ return None
1125
+ description = action.get("description", "")
1126
+ tag = action.get("tag")
1127
+ if tag:
1128
+ description = f"[{ACCENT_STYLE}]{tag}[/] · {description}"
1129
+ if highlight_new:
1130
+ description = f"✨ {description}"
1131
+ return command, description
1132
+
1133
+ def _render_quick_action_group(self, hints: list[tuple[str, str]], title: str) -> None:
1134
+ if not hints:
1135
+ return
1136
+ formatted_tokens: list[str] = []
1137
+ for command, description in hints:
1138
+ formatted = format_command_hint(command, description)
1139
+ if formatted:
1140
+ formatted_tokens.append(f"• {formatted}")
1141
+ if not formatted_tokens:
1142
+ return
1143
+ header = f"[dim]{title}[/dim] · {formatted_tokens[0]}"
1144
+ self.console.print(header)
1145
+ remaining = formatted_tokens[1:]
1146
+ for chunk in self._chunk_tokens(remaining, size=3):
1147
+ self.console.print(" " + " ".join(chunk))
1148
+
1149
+ def _chunk_tokens(self, tokens: list[str], *, size: int) -> Iterable[list[str]]:
1150
+ for index in range(0, len(tokens), size):
1151
+ yield tokens[index : index + size]
1152
+
1003
1153
  def _render_home_hint(self) -> None:
1004
1154
  if self._home_hint_shown:
1005
1155
  return
1006
- hint_lines = [
1007
- f"[{HINT_PREFIX_STYLE}]Hint:[/]",
1008
- f" Type {format_command_hint('/') or '/'} to explore commands",
1009
- " Press [dim]Ctrl+C[/] to cancel the current entry",
1010
- " Press [dim]Ctrl+D[/] to quit",
1011
- ]
1012
- self.console.print("\n".join(hint_lines))
1156
+ hint_text = (
1157
+ f"[{HINT_PREFIX_STYLE}]Hint:[/] "
1158
+ f"Type {format_command_hint('/') or '/'} to explore commands · "
1159
+ "Press [dim]Ctrl+D[/] to quit"
1160
+ )
1161
+ self.console.print(hint_text)
1013
1162
  self._home_hint_shown = True
1014
1163
 
1015
1164
  def _show_quick_actions(
@@ -1019,26 +1168,40 @@ class SlashSession:
1019
1168
  title: str = "Quick actions",
1020
1169
  inline: bool = False,
1021
1170
  ) -> None:
1022
- hint_list = [(command, description) for command, description in hints if command]
1171
+ hint_list = self._normalize_quick_action_hints(hints)
1023
1172
  if not hint_list:
1024
1173
  return
1025
1174
 
1026
1175
  if inline:
1027
- lines: list[str] = []
1028
- for command, description in hint_list:
1029
- formatted = format_command_hint(command, description)
1030
- if formatted:
1031
- lines.append(formatted)
1032
- if lines:
1033
- self.console.print("\n".join(lines))
1176
+ self._render_inline_quick_actions(hint_list, title)
1034
1177
  return
1035
1178
 
1179
+ self._render_panel_quick_actions(hint_list, title)
1180
+
1181
+ def _normalize_quick_action_hints(self, hints: Iterable[tuple[str, str]]) -> list[tuple[str, str]]:
1182
+ return [(command, description) for command, description in hints if command]
1183
+
1184
+ def _render_inline_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
1185
+ tokens: list[str] = []
1186
+ for command, description in hint_list:
1187
+ formatted = format_command_hint(command, description)
1188
+ if formatted:
1189
+ tokens.append(formatted)
1190
+ if not tokens:
1191
+ return
1192
+ prefix = f"[dim]{title}:[/]" if title else ""
1193
+ body = " ".join(tokens)
1194
+ text = f"{prefix} {body}" if prefix else body
1195
+ self.console.print(text.strip())
1196
+
1197
+ def _render_panel_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
1036
1198
  body_lines: list[Text] = []
1037
1199
  for command, description in hint_list:
1038
1200
  formatted = format_command_hint(command, description)
1039
1201
  if formatted:
1040
1202
  body_lines.append(Text.from_markup(formatted))
1041
-
1203
+ if not body_lines:
1204
+ return
1042
1205
  panel_content = Group(*body_lines)
1043
1206
  self.console.print(AIPPanel(panel_content, title=title, border_style=SECONDARY_LIGHT, expand=False))
1044
1207