glaip-sdk 0.2.2__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 (38) hide show
  1. glaip_sdk/cli/commands/agents.py +50 -35
  2. glaip_sdk/cli/commands/mcps.py +28 -18
  3. glaip_sdk/cli/commands/models.py +3 -5
  4. glaip_sdk/cli/commands/tools.py +27 -16
  5. glaip_sdk/cli/constants.py +3 -0
  6. glaip_sdk/cli/main.py +1 -3
  7. glaip_sdk/cli/slash/agent_session.py +3 -13
  8. glaip_sdk/cli/slash/prompt.py +3 -0
  9. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  10. glaip_sdk/cli/slash/session.py +138 -47
  11. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  12. glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
  13. glaip_sdk/cli/transcript/viewer.py +6 -32
  14. glaip_sdk/cli/utils.py +183 -9
  15. glaip_sdk/cli/validators.py +5 -6
  16. glaip_sdk/client/__init__.py +2 -1
  17. glaip_sdk/client/agent_runs.py +147 -0
  18. glaip_sdk/client/agents.py +42 -22
  19. glaip_sdk/client/main.py +18 -6
  20. glaip_sdk/client/mcps.py +2 -4
  21. glaip_sdk/client/tools.py +2 -3
  22. glaip_sdk/config/constants.py +11 -0
  23. glaip_sdk/models/__init__.py +56 -0
  24. glaip_sdk/models/agent_runs.py +117 -0
  25. glaip_sdk/rich_components.py +58 -2
  26. glaip_sdk/utils/client_utils.py +13 -0
  27. glaip_sdk/utils/export.py +143 -0
  28. glaip_sdk/utils/import_export.py +6 -9
  29. glaip_sdk/utils/rendering/__init__.py +122 -1
  30. glaip_sdk/utils/rendering/renderer/base.py +3 -7
  31. glaip_sdk/utils/rendering/renderer/debug.py +0 -1
  32. glaip_sdk/utils/rendering/renderer/stream.py +4 -12
  33. glaip_sdk/utils/rendering/steps.py +1 -0
  34. glaip_sdk/utils/resource_refs.py +26 -15
  35. {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.3.0.dist-info}/METADATA +24 -2
  36. {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.3.0.dist-info}/RECORD +38 -31
  37. {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.3.0.dist-info}/WHEEL +0 -0
  38. {glaip_sdk-0.2.2.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
 
@@ -199,10 +229,7 @@ class SlashSession:
199
229
  self._run_interactive_loop()
200
230
  finally:
201
231
  if ctx_obj is not None:
202
- if previous_session is None:
203
- ctx_obj.pop("_slash_session", None)
204
- else:
205
- ctx_obj["_slash_session"] = previous_session
232
+ restore_slash_session_context(ctx_obj, previous_session)
206
233
 
207
234
  def _run_interactive_loop(self) -> None:
208
235
  """Run the main interactive command loop."""
@@ -290,7 +317,7 @@ class SlashSession:
290
317
  if suggestion:
291
318
  self.console.print(f"[{WARNING_STYLE}]Unknown command '{verb}'. Did you mean '/{suggestion}'?[/]")
292
319
  else:
293
- help_command = "/help"
320
+ help_command = HELP_COMMAND
294
321
  help_hint = format_command_hint(help_command) or help_command
295
322
  self.console.print(
296
323
  f"[{WARNING_STYLE}]Unknown command '{verb}'. Type {help_hint} for a list of options.[/]"
@@ -324,7 +351,7 @@ class SlashSession:
324
351
  if invoked_from_agent:
325
352
  self._render_agent_help()
326
353
  else:
327
- self._render_global_help()
354
+ self._render_global_help(include_agent_hint=True)
328
355
  except Exception as exc: # pragma: no cover - UI/display errors
329
356
  self.console.print(f"[{ERROR_STYLE}]Error displaying help: {exc}[/]")
330
357
  return False
@@ -339,9 +366,10 @@ class SlashSession:
339
366
  table.add_row("<message>", "Run the active agent once with that prompt.")
340
367
  table.add_row("/details", "Show the agent export (prompts to expand instructions).")
341
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.")
342
370
  table.add_row("/export [path]", "Export the latest agent transcript as JSONL.")
343
371
  table.add_row("/exit (/back)", "Return to the slash home screen.")
344
- table.add_row("/help (/?)", "Display this context-aware menu.")
372
+ table.add_row(f"{HELP_COMMAND} (/?)", "Display this context-aware menu.")
345
373
 
346
374
  panel_items = [table]
347
375
  if self.last_run_input:
@@ -359,14 +387,28 @@ class SlashSession:
359
387
  border_style=PRIMARY,
360
388
  )
361
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
+ )
362
404
 
363
- def _render_global_help(self) -> None:
405
+ def _render_global_help(self, *, include_agent_hint: bool = False) -> None:
364
406
  """Render help text for global slash commands."""
365
407
  table = AIPTable()
366
408
  table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
367
409
  table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
368
410
 
369
- for cmd in sorted(self._unique_commands.values(), key=lambda c: c.name):
411
+ for cmd in self._visible_commands(include_agent_only=False):
370
412
  aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
371
413
  verb = f"/{cmd.name}"
372
414
  if aliases:
@@ -386,6 +428,11 @@ class SlashSession:
386
428
  border_style=PRIMARY,
387
429
  )
388
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
+ )
389
436
 
390
437
  def _cmd_login(self, _args: list[str], _invoked_from_agent: bool) -> bool:
391
438
  """Handle the /login command.
@@ -589,6 +636,19 @@ class SlashSession:
589
636
  view = transcripts_cmd._render_transcript_display(entry, snapshot.manifest_path, transcript_path, meta, events)
590
637
  self.console.print(view, markup=False, highlight=False, soft_wrap=True, end="")
591
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
+
592
652
  def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
593
653
  """Handle the /agents command.
594
654
 
@@ -718,7 +778,7 @@ class SlashSession:
718
778
  self._register(
719
779
  SlashCommand(
720
780
  name="login",
721
- help="Run `/login` (alias `/configure`) to set credentials.",
781
+ help="Configure API credentials (alias `/configure`).",
722
782
  handler=SlashSession._cmd_login,
723
783
  aliases=("configure",),
724
784
  )
@@ -733,7 +793,10 @@ class SlashSession:
733
793
  self._register(
734
794
  SlashCommand(
735
795
  name="transcripts",
736
- 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
+ ),
737
800
  handler=SlashSession._cmd_transcripts,
738
801
  )
739
802
  )
@@ -766,6 +829,14 @@ class SlashSession:
766
829
  handler=SlashSession._cmd_update,
767
830
  )
768
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
+ )
769
840
 
770
841
  def _register(self, command: SlashCommand) -> None:
771
842
  """Register a slash command.
@@ -777,6 +848,13 @@ class SlashSession:
777
848
  for key in (command.name, *command.aliases):
778
849
  self._commands[key] = command
779
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
+
780
858
  def open_transcript_viewer(self, *, announce: bool = True) -> None:
781
859
  """Launch the transcript viewer for the most recent run."""
782
860
  payload, manifest = self._get_last_transcript()
@@ -1104,8 +1182,9 @@ class SlashSession:
1104
1182
 
1105
1183
  header_grid = self._build_header_grid(agent_info, transcript_status)
1106
1184
  keybar = self._build_keybar()
1107
-
1108
1185
  header_grid.add_row(keybar, "")
1186
+
1187
+ # Agent-scoped commands like /runs will appear in /help, no need to duplicate here
1109
1188
  self.console.print(AIPPanel(header_grid, title="Agent Session", border_style=PRIMARY))
1110
1189
 
1111
1190
  def _get_agent_info(self, active_agent: Any) -> dict[str, str]:
@@ -1163,9 +1242,10 @@ class SlashSession:
1163
1242
  keybar = AIPGrid(expand=True)
1164
1243
  keybar.add_column(justify="left", ratio=1)
1165
1244
  keybar.add_column(justify="left", ratio=1)
1245
+ keybar.add_column(justify="left", ratio=1)
1166
1246
 
1167
1247
  keybar.add_row(
1168
- format_command_hint("/help", "Show commands") or "",
1248
+ format_command_hint(HELP_COMMAND, "Show commands") or "",
1169
1249
  format_command_hint("/details", "Agent config (expand prompt)") or "",
1170
1250
  format_command_hint("/exit", "Back") or "",
1171
1251
  )
@@ -1225,33 +1305,39 @@ class SlashSession:
1225
1305
  return None
1226
1306
 
1227
1307
  def _show_default_quick_actions(self) -> None:
1228
- """Show default quick action hints for new and evergreen commands."""
1229
- new_hints = self._collect_quick_action_hints(NEW_QUICK_ACTIONS, highlight_new=True)
1230
- evergreen_hints = self._collect_quick_action_hints(DEFAULT_QUICK_ACTIONS)
1231
- if new_hints or evergreen_hints:
1232
- self.console.print(f"[dim]{'─' * 40}[/]")
1233
- self._render_quick_action_group(new_hints, "New commands")
1234
- 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}")
1235
1312
  self._default_actions_shown = True
1236
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
+
1237
1320
  def _collect_quick_action_hints(
1238
1321
  self,
1239
1322
  actions: Iterable[dict[str, Any]],
1240
- *,
1241
- highlight_new: bool = False,
1242
1323
  ) -> list[tuple[str, str]]:
1243
1324
  """Collect quick action hints from action definitions.
1244
1325
 
1245
1326
  Args:
1246
1327
  actions: Iterable of action dictionaries.
1247
- highlight_new: Whether to highlight new actions.
1248
1328
 
1249
1329
  Returns:
1250
1330
  List of (command, description) tuples.
1251
1331
  """
1252
1332
  collected: list[tuple[str, str]] = []
1253
- for action in sorted(actions, key=lambda payload: payload.get("priority", 0), reverse=True):
1254
- 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)
1255
1341
  if hint:
1256
1342
  collected.append(hint)
1257
1343
  return collected
@@ -1259,14 +1345,11 @@ class SlashSession:
1259
1345
  def _build_quick_action_hint(
1260
1346
  self,
1261
1347
  action: dict[str, Any],
1262
- *,
1263
- highlight_new: bool = False,
1264
1348
  ) -> tuple[str, str] | None:
1265
1349
  """Build a quick action hint from an action definition.
1266
1350
 
1267
1351
  Args:
1268
1352
  action: Action dictionary.
1269
- highlight_new: Whether to highlight as new.
1270
1353
 
1271
1354
  Returns:
1272
1355
  Tuple of (command, description) or None.
@@ -1275,11 +1358,8 @@ class SlashSession:
1275
1358
  if not command:
1276
1359
  return None
1277
1360
  description = action.get("description", "")
1278
- tag = action.get("tag")
1279
- if tag:
1280
- description = f"[{ACCENT_STYLE}]{tag}[/] · {description}"
1281
- if highlight_new:
1282
- 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)
1283
1363
  return command, description
1284
1364
 
1285
1365
  def _render_quick_action_group(self, hints: list[tuple[str, str]], title: str) -> None:
@@ -1289,20 +1369,8 @@ class SlashSession:
1289
1369
  hints: List of (command, description) tuples.
1290
1370
  title: Group title.
1291
1371
  """
1292
- if not hints:
1293
- return
1294
- formatted_tokens: list[str] = []
1295
- for command, description in hints:
1296
- formatted = format_command_hint(command, description)
1297
- if formatted:
1298
- formatted_tokens.append(f"• {formatted}")
1299
- if not formatted_tokens:
1300
- return
1301
- header = f"[dim]{title}[/dim] · {formatted_tokens[0]}"
1302
- self.console.print(header)
1303
- remaining = formatted_tokens[1:]
1304
- for chunk in self._chunk_tokens(remaining, size=3):
1305
- self.console.print(" " + " ".join(chunk))
1372
+ for line in self._format_quick_action_lines(hints, title):
1373
+ self.console.print(line)
1306
1374
 
1307
1375
  def _chunk_tokens(self, tokens: list[str], *, size: int) -> Iterable[list[str]]:
1308
1376
  """Chunk tokens into groups of specified size.
@@ -1400,6 +1468,29 @@ class SlashSession:
1400
1468
  panel_content = Group(*body_lines)
1401
1469
  self.console.print(AIPPanel(panel_content, title=title, border_style=SECONDARY_LIGHT, expand=False))
1402
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
+
1403
1494
  def _load_config(self) -> dict[str, Any]:
1404
1495
  """Load configuration with caching.
1405
1496
 
@@ -0,0 +1,9 @@
1
+ """Textual UI helpers for slash commands."""
2
+
3
+ from glaip_sdk.cli.slash.tui.remote_runs_app import (
4
+ RemoteRunsTextualApp,
5
+ RemoteRunsTUICallbacks,
6
+ run_remote_runs_textual,
7
+ )
8
+
9
+ __all__ = ["RemoteRunsTextualApp", "RemoteRunsTUICallbacks", "run_remote_runs_textual"]