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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. glaip_sdk/_version.py +8 -0
  2. glaip_sdk/branding.py +13 -0
  3. glaip_sdk/cli/commands/agents.py +180 -39
  4. glaip_sdk/cli/commands/mcps.py +44 -18
  5. glaip_sdk/cli/commands/models.py +11 -5
  6. glaip_sdk/cli/commands/tools.py +35 -16
  7. glaip_sdk/cli/commands/transcripts.py +8 -0
  8. glaip_sdk/cli/constants.py +38 -0
  9. glaip_sdk/cli/context.py +8 -0
  10. glaip_sdk/cli/display.py +34 -19
  11. glaip_sdk/cli/main.py +14 -7
  12. glaip_sdk/cli/masking.py +8 -33
  13. glaip_sdk/cli/pager.py +9 -10
  14. glaip_sdk/cli/slash/agent_session.py +57 -20
  15. glaip_sdk/cli/slash/prompt.py +8 -0
  16. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  17. glaip_sdk/cli/slash/session.py +341 -46
  18. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  19. glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
  20. glaip_sdk/cli/transcript/viewer.py +232 -32
  21. glaip_sdk/cli/update_notifier.py +2 -2
  22. glaip_sdk/cli/utils.py +266 -35
  23. glaip_sdk/cli/validators.py +5 -6
  24. glaip_sdk/client/__init__.py +2 -1
  25. glaip_sdk/client/_agent_payloads.py +30 -0
  26. glaip_sdk/client/agent_runs.py +147 -0
  27. glaip_sdk/client/agents.py +186 -22
  28. glaip_sdk/client/main.py +23 -6
  29. glaip_sdk/client/mcps.py +2 -4
  30. glaip_sdk/client/run_rendering.py +66 -0
  31. glaip_sdk/client/tools.py +2 -3
  32. glaip_sdk/config/constants.py +11 -0
  33. glaip_sdk/models/__init__.py +56 -0
  34. glaip_sdk/models/agent_runs.py +117 -0
  35. glaip_sdk/rich_components.py +58 -2
  36. glaip_sdk/utils/client_utils.py +13 -0
  37. glaip_sdk/utils/export.py +143 -0
  38. glaip_sdk/utils/import_export.py +6 -9
  39. glaip_sdk/utils/rendering/__init__.py +122 -1
  40. glaip_sdk/utils/rendering/renderer/base.py +3 -7
  41. glaip_sdk/utils/rendering/renderer/debug.py +0 -1
  42. glaip_sdk/utils/rendering/renderer/stream.py +4 -12
  43. glaip_sdk/utils/rendering/steps.py +1 -0
  44. glaip_sdk/utils/resource_refs.py +26 -15
  45. glaip_sdk/utils/serialization.py +16 -0
  46. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/METADATA +24 -2
  47. glaip_sdk-0.3.0.dist-info/RECORD +94 -0
  48. glaip_sdk-0.2.1.dist-info/RECORD +0 -86
  49. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/WHEEL +0 -0
  50. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/entry_points.txt +0 -0
@@ -37,6 +37,7 @@ from glaip_sdk.cli.commands import transcripts as transcripts_cmd
37
37
  from glaip_sdk.cli.commands.configure import configure_command, load_config
38
38
  from glaip_sdk.cli.commands.update import update_command
39
39
  from glaip_sdk.cli.slash.agent_session import AgentRunSession
40
+ from glaip_sdk.cli.slash.remote_runs_controller import RemoteRunsController
40
41
  from glaip_sdk.cli.slash.prompt import (
41
42
  FormattedText,
42
43
  PromptSession,
@@ -57,6 +58,7 @@ from glaip_sdk.cli.utils import (
57
58
  format_command_hint,
58
59
  format_size,
59
60
  get_client,
61
+ restore_slash_session_context,
60
62
  )
61
63
  from glaip_sdk.rich_components import AIPGrid, AIPPanel, AIPTable
62
64
 
@@ -71,6 +73,7 @@ class SlashCommand:
71
73
  help: str
72
74
  handler: SlashHandler
73
75
  aliases: tuple[str, ...] = ()
76
+ agent_only: bool = False
74
77
 
75
78
 
76
79
  NEW_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
@@ -80,6 +83,15 @@ NEW_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
80
83
  "description": "Review transcript cache",
81
84
  "tag": "NEW",
82
85
  "priority": 10,
86
+ "scope": "global",
87
+ },
88
+ {
89
+ "cli": None,
90
+ "slash": "runs",
91
+ "description": "View remote run history for the active agent",
92
+ "tag": "NEW",
93
+ "priority": 8,
94
+ "scope": "agent",
83
95
  },
84
96
  )
85
97
 
@@ -103,9 +115,26 @@ DEFAULT_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
103
115
  "description": "Show all commands",
104
116
  "priority": 0,
105
117
  },
118
+ {
119
+ "cli": "configure",
120
+ "slash": "login",
121
+ "description": f"Configure credentials (alias [{HINT_COMMAND_STYLE}]/configure[/])",
122
+ "priority": -1,
123
+ },
106
124
  )
107
125
 
108
126
 
127
+ HELP_COMMAND = "/help"
128
+
129
+
130
+ def _quick_action_scope(action: dict[str, Any]) -> str:
131
+ """Return the scope for a quick action definition."""
132
+ scope = action.get("scope") or "global"
133
+ if isinstance(scope, str):
134
+ return scope.lower()
135
+ return "global"
136
+
137
+
109
138
  class SlashSession:
110
139
  """Interactive command palette controller."""
111
140
 
@@ -131,6 +160,7 @@ class SlashSession:
131
160
  self._welcome_rendered = False
132
161
  self._active_renderer: Any | None = None
133
162
  self._current_agent: Any | None = None
163
+ self._runs_pagination_state: dict[str, dict[str, Any]] = {} # agent_id -> {page, limit, cursor}
134
164
 
135
165
  self._home_placeholder = "Hint: type / to explore commands · Ctrl+D exits"
136
166
 
@@ -171,6 +201,7 @@ class SlashSession:
171
201
  self._render_header(initial=True)
172
202
 
173
203
  def _setup_prompt_toolkit(self) -> None:
204
+ """Initialize prompt_toolkit session and style."""
174
205
  session, style = setup_prompt_toolkit(self, interactive=self._interactive)
175
206
  self._ptk_session = session
176
207
  self._ptk_style = style
@@ -198,10 +229,7 @@ class SlashSession:
198
229
  self._run_interactive_loop()
199
230
  finally:
200
231
  if ctx_obj is not None:
201
- if previous_session is None:
202
- ctx_obj.pop("_slash_session", None)
203
- else:
204
- ctx_obj["_slash_session"] = previous_session
232
+ restore_slash_session_context(ctx_obj, previous_session)
205
233
 
206
234
  def _run_interactive_loop(self) -> None:
207
235
  """Run the main interactive command loop."""
@@ -289,7 +317,7 @@ class SlashSession:
289
317
  if suggestion:
290
318
  self.console.print(f"[{WARNING_STYLE}]Unknown command '{verb}'. Did you mean '/{suggestion}'?[/]")
291
319
  else:
292
- help_command = "/help"
320
+ help_command = HELP_COMMAND
293
321
  help_hint = format_command_hint(help_command) or help_command
294
322
  self.console.print(
295
323
  f"[{WARNING_STYLE}]Unknown command '{verb}'. Type {help_hint} for a list of options.[/]"
@@ -310,11 +338,20 @@ class SlashSession:
310
338
  # Command handlers
311
339
  # ------------------------------------------------------------------
312
340
  def _cmd_help(self, _args: list[str], invoked_from_agent: bool) -> bool:
341
+ """Handle the /help command.
342
+
343
+ Args:
344
+ _args: Command arguments (unused).
345
+ invoked_from_agent: Whether invoked from agent context.
346
+
347
+ Returns:
348
+ True to continue session.
349
+ """
313
350
  try:
314
351
  if invoked_from_agent:
315
352
  self._render_agent_help()
316
353
  else:
317
- self._render_global_help()
354
+ self._render_global_help(include_agent_hint=True)
318
355
  except Exception as exc: # pragma: no cover - UI/display errors
319
356
  self.console.print(f"[{ERROR_STYLE}]Error displaying help: {exc}[/]")
320
357
  return False
@@ -322,15 +359,17 @@ class SlashSession:
322
359
  return True
323
360
 
324
361
  def _render_agent_help(self) -> None:
362
+ """Render help text for agent context commands."""
325
363
  table = AIPTable()
326
364
  table.add_column("Input", style=HINT_COMMAND_STYLE, no_wrap=True)
327
365
  table.add_column("What happens", style=HINT_DESCRIPTION_COLOR)
328
366
  table.add_row("<message>", "Run the active agent once with that prompt.")
329
- table.add_row("/details", "Show the full agent export and metadata.")
367
+ table.add_row("/details", "Show the agent export (prompts to expand instructions).")
330
368
  table.add_row(self.STATUS_COMMAND, "Display connection status without leaving.")
369
+ table.add_row("/runs", "✨ NEW · Open the remote run browser for this agent.")
331
370
  table.add_row("/export [path]", "Export the latest agent transcript as JSONL.")
332
371
  table.add_row("/exit (/back)", "Return to the slash home screen.")
333
- table.add_row("/help (/?)", "Display this context-aware menu.")
372
+ table.add_row(f"{HELP_COMMAND} (/?)", "Display this context-aware menu.")
334
373
 
335
374
  panel_items = [table]
336
375
  if self.last_run_input:
@@ -348,13 +387,28 @@ class SlashSession:
348
387
  border_style=PRIMARY,
349
388
  )
350
389
  )
390
+ new_commands_table = AIPTable()
391
+ new_commands_table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
392
+ new_commands_table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
393
+ new_commands_table.add_row(
394
+ "/runs",
395
+ "✨ NEW · View remote run history with keyboard navigation and export options.",
396
+ )
397
+ self.console.print(
398
+ AIPPanel(
399
+ new_commands_table,
400
+ title="New commands",
401
+ border_style=SECONDARY_LIGHT,
402
+ )
403
+ )
351
404
 
352
- def _render_global_help(self) -> None:
405
+ def _render_global_help(self, *, include_agent_hint: bool = False) -> None:
406
+ """Render help text for global slash commands."""
353
407
  table = AIPTable()
354
408
  table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
355
409
  table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
356
410
 
357
- for cmd in sorted(self._unique_commands.values(), key=lambda c: c.name):
411
+ for cmd in self._visible_commands(include_agent_only=False):
358
412
  aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
359
413
  verb = f"/{cmd.name}"
360
414
  if aliases:
@@ -374,8 +428,22 @@ class SlashSession:
374
428
  border_style=PRIMARY,
375
429
  )
376
430
  )
431
+ if include_agent_hint:
432
+ self.console.print(
433
+ "[dim]Additional commands (e.g. `/runs`) become available after you pick an agent with `/agents`. "
434
+ "Those agent-only commands stay hidden here to avoid confusion.[/]"
435
+ )
377
436
 
378
437
  def _cmd_login(self, _args: list[str], _invoked_from_agent: bool) -> bool:
438
+ """Handle the /login command.
439
+
440
+ Args:
441
+ _args: Command arguments (unused).
442
+ _invoked_from_agent: Whether invoked from agent context (unused).
443
+
444
+ Returns:
445
+ True to continue session.
446
+ """
379
447
  self.console.print(f"[{ACCENT_STYLE}]Launching configuration wizard...[/]")
380
448
  try:
381
449
  self.ctx.invoke(configure_command)
@@ -391,6 +459,15 @@ class SlashSession:
391
459
  return self._continue_session()
392
460
 
393
461
  def _cmd_status(self, _args: list[str], _invoked_from_agent: bool) -> bool:
462
+ """Handle the /status command.
463
+
464
+ Args:
465
+ _args: Command arguments (unused).
466
+ _invoked_from_agent: Whether invoked from agent context (unused).
467
+
468
+ Returns:
469
+ True to continue session.
470
+ """
394
471
  ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
395
472
  previous_console = None
396
473
  try:
@@ -420,6 +497,15 @@ class SlashSession:
420
497
  return self._continue_session()
421
498
 
422
499
  def _cmd_transcripts(self, args: list[str], _invoked_from_agent: bool) -> bool:
500
+ """Handle the /transcripts command.
501
+
502
+ Args:
503
+ args: Command arguments (limit or detail/show with run_id).
504
+ _invoked_from_agent: Whether invoked from agent context (unused).
505
+
506
+ Returns:
507
+ True to continue session.
508
+ """
423
509
  if args and args[0].lower() in {"detail", "show"}:
424
510
  if len(args) < 2:
425
511
  self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts detail <run_id>[/]")
@@ -440,6 +526,14 @@ class SlashSession:
440
526
  return self._continue_session()
441
527
 
442
528
  def _parse_transcripts_limit(self, args: list[str]) -> tuple[int | None, bool]:
529
+ """Parse limit argument from transcripts command.
530
+
531
+ Args:
532
+ args: Command arguments.
533
+
534
+ Returns:
535
+ Tuple of (limit value or None, success boolean).
536
+ """
443
537
  if not args:
444
538
  return None, True
445
539
  try:
@@ -453,6 +547,15 @@ class SlashSession:
453
547
  return limit, True
454
548
 
455
549
  def _handle_transcripts_empty(self, snapshot: Any, limit: int | None) -> bool:
550
+ """Handle empty transcript snapshot cases.
551
+
552
+ Args:
553
+ snapshot: Transcript snapshot object.
554
+ limit: Limit value or None.
555
+
556
+ Returns:
557
+ True if empty case was handled, False otherwise.
558
+ """
456
559
  if snapshot.cached_entries == 0:
457
560
  self.console.print(f"[{WARNING_STYLE}]No cached transcripts yet. Run an agent first.[/]")
458
561
  for warning in snapshot.warnings:
@@ -464,6 +567,11 @@ class SlashSession:
464
567
  return False
465
568
 
466
569
  def _render_transcripts_snapshot(self, snapshot: Any) -> None:
570
+ """Render transcript snapshot table and metadata.
571
+
572
+ Args:
573
+ snapshot: Transcript snapshot object to render.
574
+ """
467
575
  size_text = format_size(snapshot.total_size_bytes)
468
576
  header = f"[dim]Manifest: {snapshot.manifest_path} · {snapshot.total_entries} runs · {size_text} used[/]"
469
577
  self.console.print(header)
@@ -528,7 +636,29 @@ class SlashSession:
528
636
  view = transcripts_cmd._render_transcript_display(entry, snapshot.manifest_path, transcript_path, meta, events)
529
637
  self.console.print(view, markup=False, highlight=False, soft_wrap=True, end="")
530
638
 
639
+ def _cmd_runs(self, args: list[str], _invoked_from_agent: bool) -> bool:
640
+ """Handle the /runs command for browsing remote agent run history.
641
+
642
+ Args:
643
+ args: Command arguments (optional run_id for detail view).
644
+ _invoked_from_agent: Whether invoked from agent context.
645
+
646
+ Returns:
647
+ True to continue session.
648
+ """
649
+ controller = RemoteRunsController(self)
650
+ return controller.handle_runs_command(args)
651
+
531
652
  def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
653
+ """Handle the /agents command.
654
+
655
+ Args:
656
+ args: Command arguments (optional agent reference).
657
+ _invoked_from_agent: Whether invoked from agent context (unused).
658
+
659
+ Returns:
660
+ True to continue session.
661
+ """
532
662
  client = self._get_client_or_fail()
533
663
  if not client:
534
664
  return True
@@ -614,6 +744,15 @@ class SlashSession:
614
744
  self._show_quick_actions(hints, title="Next actions")
615
745
 
616
746
  def _cmd_exit(self, _args: list[str], invoked_from_agent: bool) -> bool:
747
+ """Handle the /exit command.
748
+
749
+ Args:
750
+ _args: Command arguments (unused).
751
+ invoked_from_agent: Whether invoked from agent context.
752
+
753
+ Returns:
754
+ False to exit session, True to continue.
755
+ """
617
756
  if invoked_from_agent:
618
757
  # Returning False would stop the full session; we only want to exit
619
758
  # the agent context. Raising a custom flag keeps the outer loop
@@ -627,6 +766,7 @@ class SlashSession:
627
766
  # Utilities
628
767
  # ------------------------------------------------------------------
629
768
  def _register_defaults(self) -> None:
769
+ """Register default slash commands."""
630
770
  self._register(
631
771
  SlashCommand(
632
772
  name="help",
@@ -638,7 +778,7 @@ class SlashSession:
638
778
  self._register(
639
779
  SlashCommand(
640
780
  name="login",
641
- help="Run `/login` (alias `/configure`) to set credentials.",
781
+ help="Configure API credentials (alias `/configure`).",
642
782
  handler=SlashSession._cmd_login,
643
783
  aliases=("configure",),
644
784
  )
@@ -653,7 +793,10 @@ class SlashSession:
653
793
  self._register(
654
794
  SlashCommand(
655
795
  name="transcripts",
656
- help="Review cached transcript history. Add a number (e.g. `/transcripts 5`) to change the row limit.",
796
+ help=(
797
+ "✨ NEW · Review cached transcript history. "
798
+ "Add a number (e.g. `/transcripts 5`) to change the row limit."
799
+ ),
657
800
  handler=SlashSession._cmd_transcripts,
658
801
  )
659
802
  )
@@ -686,12 +829,32 @@ class SlashSession:
686
829
  handler=SlashSession._cmd_update,
687
830
  )
688
831
  )
832
+ self._register(
833
+ SlashCommand(
834
+ name="runs",
835
+ help="✨ NEW · Browse remote agent run history (requires active agent session).",
836
+ handler=SlashSession._cmd_runs,
837
+ agent_only=True,
838
+ )
839
+ )
689
840
 
690
841
  def _register(self, command: SlashCommand) -> None:
842
+ """Register a slash command.
843
+
844
+ Args:
845
+ command: SlashCommand to register.
846
+ """
691
847
  self._unique_commands[command.name] = command
692
848
  for key in (command.name, *command.aliases):
693
849
  self._commands[key] = command
694
850
 
851
+ def _visible_commands(self, *, include_agent_only: bool) -> list[SlashCommand]:
852
+ """Return the list of commands that should be shown in global listings."""
853
+ commands = sorted(self._unique_commands.values(), key=lambda c: c.name)
854
+ if include_agent_only:
855
+ return commands
856
+ return [cmd for cmd in commands if not cmd.agent_only]
857
+
695
858
  def open_transcript_viewer(self, *, announce: bool = True) -> None:
696
859
  """Launch the transcript viewer for the most recent run."""
697
860
  payload, manifest = self._get_last_transcript()
@@ -716,6 +879,14 @@ class SlashSession:
716
879
  )
717
880
 
718
881
  def _export(destination: Path) -> Path:
882
+ """Export cached transcript to destination.
883
+
884
+ Args:
885
+ destination: Path to export transcript to.
886
+
887
+ Returns:
888
+ Path to exported transcript file.
889
+ """
719
890
  return export_cached_transcript(destination=destination, run_id=run_id)
720
891
 
721
892
  try:
@@ -812,6 +983,14 @@ class SlashSession:
812
983
  pass
813
984
 
814
985
  def _parse(self, raw: str) -> tuple[str, list[str]]:
986
+ """Parse a raw command string into verb and arguments.
987
+
988
+ Args:
989
+ raw: Raw command string.
990
+
991
+ Returns:
992
+ Tuple of (verb, args).
993
+ """
815
994
  try:
816
995
  tokens = shlex.split(raw)
817
996
  except ValueError:
@@ -827,6 +1006,14 @@ class SlashSession:
827
1006
  return head, tokens[1:]
828
1007
 
829
1008
  def _suggest(self, verb: str) -> str | None:
1009
+ """Suggest a similar command name for an unknown verb.
1010
+
1011
+ Args:
1012
+ verb: Unknown command verb.
1013
+
1014
+ Returns:
1015
+ Suggested command name or None.
1016
+ """
830
1017
  keys = [cmd.name for cmd in self._unique_commands.values()]
831
1018
  match = get_close_matches(verb, keys, n=1)
832
1019
  return match[0] if match else None
@@ -855,6 +1042,7 @@ class SlashSession:
855
1042
  if callable(message):
856
1043
 
857
1044
  def prompt_text() -> Any:
1045
+ """Get formatted prompt text from callable message."""
858
1046
  return self._convert_message(message())
859
1047
  else:
860
1048
  prompt_text = self._convert_message(message)
@@ -900,6 +1088,11 @@ class SlashSession:
900
1088
  return self._prompt_with_basic_input(message, placeholder)
901
1089
 
902
1090
  def _get_client(self) -> Any: # type: ignore[no-any-return]
1091
+ """Get or create the API client instance.
1092
+
1093
+ Returns:
1094
+ API client instance.
1095
+ """
903
1096
  if self._client is None:
904
1097
  self._client = get_client(self.ctx)
905
1098
  return self._client
@@ -918,6 +1111,11 @@ class SlashSession:
918
1111
  return self._contextual_include_global
919
1112
 
920
1113
  def _remember_agent(self, agent: Any) -> None: # type: ignore[no-any-return]
1114
+ """Remember an agent in recent agents list.
1115
+
1116
+ Args:
1117
+ agent: Agent object to remember.
1118
+ """
921
1119
  agent_data = {
922
1120
  "id": str(getattr(agent, "id", "")),
923
1121
  "name": getattr(agent, "name", "") or "",
@@ -935,6 +1133,13 @@ class SlashSession:
935
1133
  focus_agent: bool = False,
936
1134
  initial: bool = False,
937
1135
  ) -> None:
1136
+ """Render the session header with branding and status.
1137
+
1138
+ Args:
1139
+ active_agent: Optional active agent to display.
1140
+ focus_agent: Whether to focus on agent display.
1141
+ initial: Whether this is the initial render.
1142
+ """
938
1143
  if focus_agent and active_agent is not None:
939
1144
  self._render_focused_agent_header(active_agent)
940
1145
  return
@@ -977,8 +1182,9 @@ class SlashSession:
977
1182
 
978
1183
  header_grid = self._build_header_grid(agent_info, transcript_status)
979
1184
  keybar = self._build_keybar()
980
-
981
1185
  header_grid.add_row(keybar, "")
1186
+
1187
+ # Agent-scoped commands like /runs will appear in /help, no need to duplicate here
982
1188
  self.console.print(AIPPanel(header_grid, title="Agent Session", border_style=PRIMARY))
983
1189
 
984
1190
  def _get_agent_info(self, active_agent: Any) -> dict[str, str]:
@@ -1036,10 +1242,11 @@ class SlashSession:
1036
1242
  keybar = AIPGrid(expand=True)
1037
1243
  keybar.add_column(justify="left", ratio=1)
1038
1244
  keybar.add_column(justify="left", ratio=1)
1245
+ keybar.add_column(justify="left", ratio=1)
1039
1246
 
1040
1247
  keybar.add_row(
1041
- format_command_hint("/help", "Show commands") or "",
1042
- format_command_hint("/details", "Agent config") or "",
1248
+ format_command_hint(HELP_COMMAND, "Show commands") or "",
1249
+ format_command_hint("/details", "Agent config (expand prompt)") or "",
1043
1250
  format_command_hint("/exit", "Back") or "",
1044
1251
  )
1045
1252
 
@@ -1098,23 +1305,39 @@ class SlashSession:
1098
1305
  return None
1099
1306
 
1100
1307
  def _show_default_quick_actions(self) -> None:
1101
- new_hints = self._collect_quick_action_hints(NEW_QUICK_ACTIONS, highlight_new=True)
1102
- evergreen_hints = self._collect_quick_action_hints(DEFAULT_QUICK_ACTIONS)
1103
- if new_hints or evergreen_hints:
1104
- self.console.print(f"[dim]{'─' * 40}[/]")
1105
- self._render_quick_action_group(new_hints, "New commands")
1106
- self._render_quick_action_group(evergreen_hints, "Quick actions")
1308
+ """Show simplified help hint to discover commands."""
1309
+ self.console.print(f"[dim]{'─' * 40}[/]")
1310
+ help_hint = format_command_hint(HELP_COMMAND, "Show all commands") or HELP_COMMAND
1311
+ self.console.print(f"{help_hint}")
1107
1312
  self._default_actions_shown = True
1108
1313
 
1314
+ def _collect_scoped_new_action_hints(self, scope: str) -> list[tuple[str, str]]:
1315
+ """Return new quick action hints filtered by scope."""
1316
+ scoped_actions = [action for action in NEW_QUICK_ACTIONS if _quick_action_scope(action) == scope]
1317
+ # Don't highlight with sparkle emoji in quick actions display - it will show in command palette instead
1318
+ return self._collect_quick_action_hints(scoped_actions)
1319
+
1109
1320
  def _collect_quick_action_hints(
1110
1321
  self,
1111
1322
  actions: Iterable[dict[str, Any]],
1112
- *,
1113
- highlight_new: bool = False,
1114
1323
  ) -> list[tuple[str, str]]:
1324
+ """Collect quick action hints from action definitions.
1325
+
1326
+ Args:
1327
+ actions: Iterable of action dictionaries.
1328
+
1329
+ Returns:
1330
+ List of (command, description) tuples.
1331
+ """
1115
1332
  collected: list[tuple[str, str]] = []
1116
- for action in sorted(actions, key=lambda payload: payload.get("priority", 0), reverse=True):
1117
- hint = self._build_quick_action_hint(action, highlight_new=highlight_new)
1333
+
1334
+ def sort_key(payload: dict[str, Any]) -> tuple[int, str]:
1335
+ priority = int(payload.get("priority", 0))
1336
+ label = str(payload.get("slash") or payload.get("cli") or "")
1337
+ return (-priority, label.lower())
1338
+
1339
+ for action in sorted(actions, key=sort_key):
1340
+ hint = self._build_quick_action_hint(action)
1118
1341
  if hint:
1119
1342
  collected.append(hint)
1120
1343
  return collected
@@ -1122,41 +1345,48 @@ class SlashSession:
1122
1345
  def _build_quick_action_hint(
1123
1346
  self,
1124
1347
  action: dict[str, Any],
1125
- *,
1126
- highlight_new: bool = False,
1127
1348
  ) -> tuple[str, str] | None:
1349
+ """Build a quick action hint from an action definition.
1350
+
1351
+ Args:
1352
+ action: Action dictionary.
1353
+
1354
+ Returns:
1355
+ Tuple of (command, description) or None.
1356
+ """
1128
1357
  command = command_hint(action.get("cli"), slash_command=action.get("slash"), ctx=self.ctx)
1129
1358
  if not command:
1130
1359
  return None
1131
1360
  description = action.get("description", "")
1132
- tag = action.get("tag")
1133
- if tag:
1134
- description = f"[{ACCENT_STYLE}]{tag}[/] · {description}"
1135
- if highlight_new:
1136
- description = f"✨ {description}"
1361
+ # Don't include tag or sparkle emoji in quick actions display
1362
+ # The NEW tag will only show in the command dropdown (help text)
1137
1363
  return command, description
1138
1364
 
1139
1365
  def _render_quick_action_group(self, hints: list[tuple[str, str]], title: str) -> None:
1140
- if not hints:
1141
- return
1142
- formatted_tokens: list[str] = []
1143
- for command, description in hints:
1144
- formatted = format_command_hint(command, description)
1145
- if formatted:
1146
- formatted_tokens.append(f"• {formatted}")
1147
- if not formatted_tokens:
1148
- return
1149
- header = f"[dim]{title}[/dim] · {formatted_tokens[0]}"
1150
- self.console.print(header)
1151
- remaining = formatted_tokens[1:]
1152
- for chunk in self._chunk_tokens(remaining, size=3):
1153
- self.console.print(" " + " ".join(chunk))
1366
+ """Render a group of quick action hints.
1367
+
1368
+ Args:
1369
+ hints: List of (command, description) tuples.
1370
+ title: Group title.
1371
+ """
1372
+ for line in self._format_quick_action_lines(hints, title):
1373
+ self.console.print(line)
1154
1374
 
1155
1375
  def _chunk_tokens(self, tokens: list[str], *, size: int) -> Iterable[list[str]]:
1376
+ """Chunk tokens into groups of specified size.
1377
+
1378
+ Args:
1379
+ tokens: List of tokens to chunk.
1380
+ size: Size of each chunk.
1381
+
1382
+ Yields:
1383
+ Lists of tokens.
1384
+ """
1156
1385
  for index in range(0, len(tokens), size):
1157
1386
  yield tokens[index : index + size]
1158
1387
 
1159
1388
  def _render_home_hint(self) -> None:
1389
+ """Render hint text for home screen."""
1160
1390
  if self._home_hint_shown:
1161
1391
  return
1162
1392
  hint_text = (
@@ -1174,6 +1404,13 @@ class SlashSession:
1174
1404
  title: str = "Quick actions",
1175
1405
  inline: bool = False,
1176
1406
  ) -> None:
1407
+ """Show quick action hints.
1408
+
1409
+ Args:
1410
+ hints: Iterable of (command, description) tuples.
1411
+ title: Title for the hints.
1412
+ inline: Whether to render inline or in a panel.
1413
+ """
1177
1414
  hint_list = self._normalize_quick_action_hints(hints)
1178
1415
  if not hint_list:
1179
1416
  return
@@ -1185,9 +1422,23 @@ class SlashSession:
1185
1422
  self._render_panel_quick_actions(hint_list, title)
1186
1423
 
1187
1424
  def _normalize_quick_action_hints(self, hints: Iterable[tuple[str, str]]) -> list[tuple[str, str]]:
1425
+ """Normalize quick action hints by filtering out empty commands.
1426
+
1427
+ Args:
1428
+ hints: Iterable of (command, description) tuples.
1429
+
1430
+ Returns:
1431
+ List of normalized hints.
1432
+ """
1188
1433
  return [(command, description) for command, description in hints if command]
1189
1434
 
1190
1435
  def _render_inline_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
1436
+ """Render quick actions inline.
1437
+
1438
+ Args:
1439
+ hint_list: List of (command, description) tuples.
1440
+ title: Title for the hints.
1441
+ """
1191
1442
  tokens: list[str] = []
1192
1443
  for command, description in hint_list:
1193
1444
  formatted = format_command_hint(command, description)
@@ -1201,6 +1452,12 @@ class SlashSession:
1201
1452
  self.console.print(text.strip())
1202
1453
 
1203
1454
  def _render_panel_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
1455
+ """Render quick actions in a panel.
1456
+
1457
+ Args:
1458
+ hint_list: List of (command, description) tuples.
1459
+ title: Panel title.
1460
+ """
1204
1461
  body_lines: list[Text] = []
1205
1462
  for command, description in hint_list:
1206
1463
  formatted = format_command_hint(command, description)
@@ -1211,7 +1468,35 @@ class SlashSession:
1211
1468
  panel_content = Group(*body_lines)
1212
1469
  self.console.print(AIPPanel(panel_content, title=title, border_style=SECONDARY_LIGHT, expand=False))
1213
1470
 
1471
+ def _format_quick_action_lines(self, hints: list[tuple[str, str]], title: str) -> list[str]:
1472
+ """Return formatted lines for quick action hints."""
1473
+ if not hints:
1474
+ return []
1475
+ formatted_tokens: list[str] = []
1476
+ for command, description in hints:
1477
+ formatted = format_command_hint(command, description)
1478
+ if formatted:
1479
+ formatted_tokens.append(f"• {formatted}")
1480
+ if not formatted_tokens:
1481
+ return []
1482
+ lines: list[str] = []
1483
+ # Use vertical layout (1 per line) for better readability
1484
+ chunks = list(self._chunk_tokens(formatted_tokens, size=1))
1485
+ prefix = f"[dim]{title}[/dim]\n " if title else ""
1486
+ for idx, chunk in enumerate(chunks):
1487
+ row = " ".join(chunk)
1488
+ if idx == 0:
1489
+ lines.append(f"{prefix}{row}" if prefix else row)
1490
+ else:
1491
+ lines.append(f" {row}")
1492
+ return lines
1493
+
1214
1494
  def _load_config(self) -> dict[str, Any]:
1495
+ """Load configuration with caching.
1496
+
1497
+ Returns:
1498
+ Configuration dictionary.
1499
+ """
1215
1500
  if self._config_cache is None:
1216
1501
  try:
1217
1502
  self._config_cache = load_config() or {}
@@ -1220,6 +1505,16 @@ class SlashSession:
1220
1505
  return self._config_cache
1221
1506
 
1222
1507
  def _resolve_agent_from_ref(self, client: Any, available_agents: list[Any], ref: str) -> Any | None:
1508
+ """Resolve an agent from a reference string.
1509
+
1510
+ Args:
1511
+ client: API client instance.
1512
+ available_agents: List of available agents.
1513
+ ref: Reference string (ID or name).
1514
+
1515
+ Returns:
1516
+ Resolved agent or None.
1517
+ """
1223
1518
  ref = ref.strip()
1224
1519
  if not ref:
1225
1520
  return None