glaip-sdk 0.2.1__py3-none-any.whl → 0.2.2__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.
@@ -14,6 +14,7 @@ import click
14
14
  from glaip_sdk.branding import ERROR_STYLE, HINT_PREFIX_STYLE
15
15
  from glaip_sdk.cli.commands.agents import get as agents_get_command
16
16
  from glaip_sdk.cli.commands.agents import run as agents_run_command
17
+ from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
17
18
  from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
18
19
  from glaip_sdk.cli.utils import format_command_hint
19
20
 
@@ -38,11 +39,12 @@ class AgentRunSession:
38
39
  self._agent_name = getattr(agent, "name", "") or self._agent_id
39
40
  self._prompt_placeholder: str = "Chat with this agent here; use / for shortcuts. Alt+Enter inserts a newline."
40
41
  self._contextual_completion_help: dict[str, str] = {
41
- "details": "Show this agent's full configuration.",
42
+ "details": "Show this agent's configuration (+ expands prompt).",
42
43
  "help": "Display this context-aware menu.",
43
44
  "exit": "Return to the command palette.",
44
45
  "q": "Return to the command palette.",
45
46
  }
47
+ self._instruction_preview_limit = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
46
48
 
47
49
  def run(self) -> None:
48
50
  """Run the interactive agent session loop."""
@@ -86,6 +88,7 @@ class AgentRunSession:
86
88
  try:
87
89
 
88
90
  def _prompt_message() -> Any:
91
+ """Get formatted prompt message for agent session."""
89
92
  prompt_prefix = f"{self._agent_name} ({self._agent_id}) "
90
93
 
91
94
  # Use FormattedText if prompt_toolkit is available, otherwise use simple string
@@ -122,7 +125,7 @@ class AgentRunSession:
122
125
  if raw in {"/exit", "/back", "/q"}:
123
126
  return self._handle_exit_command()
124
127
 
125
- if raw in {"/details", "/detail"}:
128
+ if raw == "/details":
126
129
  return self._handle_details_command(agent_id)
127
130
 
128
131
  if raw in {"/help", "/?"}:
@@ -151,16 +154,59 @@ class AgentRunSession:
151
154
  self.session.handle_command(raw, invoked_from_agent=True)
152
155
  return not self.session._should_exit
153
156
 
154
- def _show_details(self, agent_id: str) -> None:
157
+ def _show_details(self, agent_id: str, *, enable_prompt: bool = True) -> None:
158
+ """Render the agent's configuration export inside the command palette."""
155
159
  try:
156
- self.session.ctx.invoke(agents_get_command, agent_ref=agent_id)
157
- self.console.print(
158
- f"[{HINT_PREFIX_STYLE}]Tip:[/] Continue the conversation in this prompt, or use "
159
- f"{format_command_hint('/help') or '/help'} for shortcuts."
160
+ self.session.ctx.invoke(
161
+ agents_get_command,
162
+ agent_ref=agent_id,
163
+ instruction_preview=self._instruction_preview_limit,
160
164
  )
165
+ if enable_prompt:
166
+ self._prompt_instruction_view_toggle(agent_id)
167
+ self.console.print(
168
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] Continue the conversation in this prompt, or use "
169
+ f"{format_command_hint('/help') or '/help'} for shortcuts."
170
+ )
161
171
  except click.ClickException as exc:
162
172
  self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
163
173
 
174
+ def _prompt_instruction_view_toggle(self, agent_id: str) -> None:
175
+ """Offer a prompt to expand or collapse the instruction preview after details."""
176
+ if not getattr(self.console, "is_terminal", False):
177
+ return
178
+
179
+ while True:
180
+ mode = "expanded" if self._instruction_preview_limit == 0 else "trimmed"
181
+ self.console.print(f"[dim]Instruction view is {mode}. Press Ctrl+T to toggle, Enter to continue.[/dim]")
182
+ try:
183
+ ch = click.getchar()
184
+ except (EOFError, KeyboardInterrupt): # pragma: no cover - defensive guard
185
+ return
186
+
187
+ if not self._handle_instruction_toggle_input(agent_id, ch):
188
+ break
189
+
190
+ if self._instruction_preview_limit == 0:
191
+ self._instruction_preview_limit = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
192
+ self.console.print("")
193
+
194
+ def _handle_instruction_toggle_input(self, agent_id: str, ch: str) -> bool:
195
+ """Process a single toggle keypress; return False when the loop should exit."""
196
+ if ch in {"\r", "\n"}:
197
+ return False
198
+
199
+ lowered = ch.lower()
200
+ if lowered == "t" or ch == "\x14": # support literal 't' or Ctrl+T
201
+ self._instruction_preview_limit = (
202
+ DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT if self._instruction_preview_limit == 0 else 0
203
+ )
204
+ self._show_details(agent_id, enable_prompt=False)
205
+ return True
206
+
207
+ # Ignore other keys and continue prompting.
208
+ return True
209
+
164
210
  def _after_agent_run(self) -> None:
165
211
  """Handle transcript viewer behaviour after a successful run."""
166
212
  payload, manifest = self.session._get_last_transcript()
@@ -221,6 +267,7 @@ class AgentRunSession:
221
267
  ctx_obj["_slash_session"] = previous_session
222
268
 
223
269
  def _run_agent(self, agent_id: str, message: str) -> None:
270
+ """Execute the agents run command for the active agent."""
224
271
  if not message:
225
272
  return
226
273
 
@@ -123,6 +123,11 @@ def _create_key_bindings(_session: SlashSession) -> Any:
123
123
  bindings = KeyBindings()
124
124
 
125
125
  def _refresh_completions(buffer: Any) -> None: # type: ignore[no-any-return]
126
+ """Refresh completions when slash command is typed.
127
+
128
+ Args:
129
+ buffer: Prompt buffer instance.
130
+ """
126
131
  text = buffer.document.text_before_cursor or ""
127
132
  if text.startswith("/") and " " not in text:
128
133
  buffer.start_completion(select_first=False)
@@ -171,6 +171,7 @@ class SlashSession:
171
171
  self._render_header(initial=True)
172
172
 
173
173
  def _setup_prompt_toolkit(self) -> None:
174
+ """Initialize prompt_toolkit session and style."""
174
175
  session, style = setup_prompt_toolkit(self, interactive=self._interactive)
175
176
  self._ptk_session = session
176
177
  self._ptk_style = style
@@ -310,6 +311,15 @@ class SlashSession:
310
311
  # Command handlers
311
312
  # ------------------------------------------------------------------
312
313
  def _cmd_help(self, _args: list[str], invoked_from_agent: bool) -> bool:
314
+ """Handle the /help command.
315
+
316
+ Args:
317
+ _args: Command arguments (unused).
318
+ invoked_from_agent: Whether invoked from agent context.
319
+
320
+ Returns:
321
+ True to continue session.
322
+ """
313
323
  try:
314
324
  if invoked_from_agent:
315
325
  self._render_agent_help()
@@ -322,11 +332,12 @@ class SlashSession:
322
332
  return True
323
333
 
324
334
  def _render_agent_help(self) -> None:
335
+ """Render help text for agent context commands."""
325
336
  table = AIPTable()
326
337
  table.add_column("Input", style=HINT_COMMAND_STYLE, no_wrap=True)
327
338
  table.add_column("What happens", style=HINT_DESCRIPTION_COLOR)
328
339
  table.add_row("<message>", "Run the active agent once with that prompt.")
329
- table.add_row("/details", "Show the full agent export and metadata.")
340
+ table.add_row("/details", "Show the agent export (prompts to expand instructions).")
330
341
  table.add_row(self.STATUS_COMMAND, "Display connection status without leaving.")
331
342
  table.add_row("/export [path]", "Export the latest agent transcript as JSONL.")
332
343
  table.add_row("/exit (/back)", "Return to the slash home screen.")
@@ -350,6 +361,7 @@ class SlashSession:
350
361
  )
351
362
 
352
363
  def _render_global_help(self) -> None:
364
+ """Render help text for global slash commands."""
353
365
  table = AIPTable()
354
366
  table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
355
367
  table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
@@ -376,6 +388,15 @@ class SlashSession:
376
388
  )
377
389
 
378
390
  def _cmd_login(self, _args: list[str], _invoked_from_agent: bool) -> bool:
391
+ """Handle the /login command.
392
+
393
+ Args:
394
+ _args: Command arguments (unused).
395
+ _invoked_from_agent: Whether invoked from agent context (unused).
396
+
397
+ Returns:
398
+ True to continue session.
399
+ """
379
400
  self.console.print(f"[{ACCENT_STYLE}]Launching configuration wizard...[/]")
380
401
  try:
381
402
  self.ctx.invoke(configure_command)
@@ -391,6 +412,15 @@ class SlashSession:
391
412
  return self._continue_session()
392
413
 
393
414
  def _cmd_status(self, _args: list[str], _invoked_from_agent: bool) -> bool:
415
+ """Handle the /status command.
416
+
417
+ Args:
418
+ _args: Command arguments (unused).
419
+ _invoked_from_agent: Whether invoked from agent context (unused).
420
+
421
+ Returns:
422
+ True to continue session.
423
+ """
394
424
  ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
395
425
  previous_console = None
396
426
  try:
@@ -420,6 +450,15 @@ class SlashSession:
420
450
  return self._continue_session()
421
451
 
422
452
  def _cmd_transcripts(self, args: list[str], _invoked_from_agent: bool) -> bool:
453
+ """Handle the /transcripts command.
454
+
455
+ Args:
456
+ args: Command arguments (limit or detail/show with run_id).
457
+ _invoked_from_agent: Whether invoked from agent context (unused).
458
+
459
+ Returns:
460
+ True to continue session.
461
+ """
423
462
  if args and args[0].lower() in {"detail", "show"}:
424
463
  if len(args) < 2:
425
464
  self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts detail <run_id>[/]")
@@ -440,6 +479,14 @@ class SlashSession:
440
479
  return self._continue_session()
441
480
 
442
481
  def _parse_transcripts_limit(self, args: list[str]) -> tuple[int | None, bool]:
482
+ """Parse limit argument from transcripts command.
483
+
484
+ Args:
485
+ args: Command arguments.
486
+
487
+ Returns:
488
+ Tuple of (limit value or None, success boolean).
489
+ """
443
490
  if not args:
444
491
  return None, True
445
492
  try:
@@ -453,6 +500,15 @@ class SlashSession:
453
500
  return limit, True
454
501
 
455
502
  def _handle_transcripts_empty(self, snapshot: Any, limit: int | None) -> bool:
503
+ """Handle empty transcript snapshot cases.
504
+
505
+ Args:
506
+ snapshot: Transcript snapshot object.
507
+ limit: Limit value or None.
508
+
509
+ Returns:
510
+ True if empty case was handled, False otherwise.
511
+ """
456
512
  if snapshot.cached_entries == 0:
457
513
  self.console.print(f"[{WARNING_STYLE}]No cached transcripts yet. Run an agent first.[/]")
458
514
  for warning in snapshot.warnings:
@@ -464,6 +520,11 @@ class SlashSession:
464
520
  return False
465
521
 
466
522
  def _render_transcripts_snapshot(self, snapshot: Any) -> None:
523
+ """Render transcript snapshot table and metadata.
524
+
525
+ Args:
526
+ snapshot: Transcript snapshot object to render.
527
+ """
467
528
  size_text = format_size(snapshot.total_size_bytes)
468
529
  header = f"[dim]Manifest: {snapshot.manifest_path} · {snapshot.total_entries} runs · {size_text} used[/]"
469
530
  self.console.print(header)
@@ -529,6 +590,15 @@ class SlashSession:
529
590
  self.console.print(view, markup=False, highlight=False, soft_wrap=True, end="")
530
591
 
531
592
  def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
593
+ """Handle the /agents command.
594
+
595
+ Args:
596
+ args: Command arguments (optional agent reference).
597
+ _invoked_from_agent: Whether invoked from agent context (unused).
598
+
599
+ Returns:
600
+ True to continue session.
601
+ """
532
602
  client = self._get_client_or_fail()
533
603
  if not client:
534
604
  return True
@@ -614,6 +684,15 @@ class SlashSession:
614
684
  self._show_quick_actions(hints, title="Next actions")
615
685
 
616
686
  def _cmd_exit(self, _args: list[str], invoked_from_agent: bool) -> bool:
687
+ """Handle the /exit command.
688
+
689
+ Args:
690
+ _args: Command arguments (unused).
691
+ invoked_from_agent: Whether invoked from agent context.
692
+
693
+ Returns:
694
+ False to exit session, True to continue.
695
+ """
617
696
  if invoked_from_agent:
618
697
  # Returning False would stop the full session; we only want to exit
619
698
  # the agent context. Raising a custom flag keeps the outer loop
@@ -627,6 +706,7 @@ class SlashSession:
627
706
  # Utilities
628
707
  # ------------------------------------------------------------------
629
708
  def _register_defaults(self) -> None:
709
+ """Register default slash commands."""
630
710
  self._register(
631
711
  SlashCommand(
632
712
  name="help",
@@ -688,6 +768,11 @@ class SlashSession:
688
768
  )
689
769
 
690
770
  def _register(self, command: SlashCommand) -> None:
771
+ """Register a slash command.
772
+
773
+ Args:
774
+ command: SlashCommand to register.
775
+ """
691
776
  self._unique_commands[command.name] = command
692
777
  for key in (command.name, *command.aliases):
693
778
  self._commands[key] = command
@@ -716,6 +801,14 @@ class SlashSession:
716
801
  )
717
802
 
718
803
  def _export(destination: Path) -> Path:
804
+ """Export cached transcript to destination.
805
+
806
+ Args:
807
+ destination: Path to export transcript to.
808
+
809
+ Returns:
810
+ Path to exported transcript file.
811
+ """
719
812
  return export_cached_transcript(destination=destination, run_id=run_id)
720
813
 
721
814
  try:
@@ -812,6 +905,14 @@ class SlashSession:
812
905
  pass
813
906
 
814
907
  def _parse(self, raw: str) -> tuple[str, list[str]]:
908
+ """Parse a raw command string into verb and arguments.
909
+
910
+ Args:
911
+ raw: Raw command string.
912
+
913
+ Returns:
914
+ Tuple of (verb, args).
915
+ """
815
916
  try:
816
917
  tokens = shlex.split(raw)
817
918
  except ValueError:
@@ -827,6 +928,14 @@ class SlashSession:
827
928
  return head, tokens[1:]
828
929
 
829
930
  def _suggest(self, verb: str) -> str | None:
931
+ """Suggest a similar command name for an unknown verb.
932
+
933
+ Args:
934
+ verb: Unknown command verb.
935
+
936
+ Returns:
937
+ Suggested command name or None.
938
+ """
830
939
  keys = [cmd.name for cmd in self._unique_commands.values()]
831
940
  match = get_close_matches(verb, keys, n=1)
832
941
  return match[0] if match else None
@@ -855,6 +964,7 @@ class SlashSession:
855
964
  if callable(message):
856
965
 
857
966
  def prompt_text() -> Any:
967
+ """Get formatted prompt text from callable message."""
858
968
  return self._convert_message(message())
859
969
  else:
860
970
  prompt_text = self._convert_message(message)
@@ -900,6 +1010,11 @@ class SlashSession:
900
1010
  return self._prompt_with_basic_input(message, placeholder)
901
1011
 
902
1012
  def _get_client(self) -> Any: # type: ignore[no-any-return]
1013
+ """Get or create the API client instance.
1014
+
1015
+ Returns:
1016
+ API client instance.
1017
+ """
903
1018
  if self._client is None:
904
1019
  self._client = get_client(self.ctx)
905
1020
  return self._client
@@ -918,6 +1033,11 @@ class SlashSession:
918
1033
  return self._contextual_include_global
919
1034
 
920
1035
  def _remember_agent(self, agent: Any) -> None: # type: ignore[no-any-return]
1036
+ """Remember an agent in recent agents list.
1037
+
1038
+ Args:
1039
+ agent: Agent object to remember.
1040
+ """
921
1041
  agent_data = {
922
1042
  "id": str(getattr(agent, "id", "")),
923
1043
  "name": getattr(agent, "name", "") or "",
@@ -935,6 +1055,13 @@ class SlashSession:
935
1055
  focus_agent: bool = False,
936
1056
  initial: bool = False,
937
1057
  ) -> None:
1058
+ """Render the session header with branding and status.
1059
+
1060
+ Args:
1061
+ active_agent: Optional active agent to display.
1062
+ focus_agent: Whether to focus on agent display.
1063
+ initial: Whether this is the initial render.
1064
+ """
938
1065
  if focus_agent and active_agent is not None:
939
1066
  self._render_focused_agent_header(active_agent)
940
1067
  return
@@ -1039,7 +1166,7 @@ class SlashSession:
1039
1166
 
1040
1167
  keybar.add_row(
1041
1168
  format_command_hint("/help", "Show commands") or "",
1042
- format_command_hint("/details", "Agent config") or "",
1169
+ format_command_hint("/details", "Agent config (expand prompt)") or "",
1043
1170
  format_command_hint("/exit", "Back") or "",
1044
1171
  )
1045
1172
 
@@ -1098,6 +1225,7 @@ class SlashSession:
1098
1225
  return None
1099
1226
 
1100
1227
  def _show_default_quick_actions(self) -> None:
1228
+ """Show default quick action hints for new and evergreen commands."""
1101
1229
  new_hints = self._collect_quick_action_hints(NEW_QUICK_ACTIONS, highlight_new=True)
1102
1230
  evergreen_hints = self._collect_quick_action_hints(DEFAULT_QUICK_ACTIONS)
1103
1231
  if new_hints or evergreen_hints:
@@ -1112,6 +1240,15 @@ class SlashSession:
1112
1240
  *,
1113
1241
  highlight_new: bool = False,
1114
1242
  ) -> list[tuple[str, str]]:
1243
+ """Collect quick action hints from action definitions.
1244
+
1245
+ Args:
1246
+ actions: Iterable of action dictionaries.
1247
+ highlight_new: Whether to highlight new actions.
1248
+
1249
+ Returns:
1250
+ List of (command, description) tuples.
1251
+ """
1115
1252
  collected: list[tuple[str, str]] = []
1116
1253
  for action in sorted(actions, key=lambda payload: payload.get("priority", 0), reverse=True):
1117
1254
  hint = self._build_quick_action_hint(action, highlight_new=highlight_new)
@@ -1125,6 +1262,15 @@ class SlashSession:
1125
1262
  *,
1126
1263
  highlight_new: bool = False,
1127
1264
  ) -> tuple[str, str] | None:
1265
+ """Build a quick action hint from an action definition.
1266
+
1267
+ Args:
1268
+ action: Action dictionary.
1269
+ highlight_new: Whether to highlight as new.
1270
+
1271
+ Returns:
1272
+ Tuple of (command, description) or None.
1273
+ """
1128
1274
  command = command_hint(action.get("cli"), slash_command=action.get("slash"), ctx=self.ctx)
1129
1275
  if not command:
1130
1276
  return None
@@ -1137,6 +1283,12 @@ class SlashSession:
1137
1283
  return command, description
1138
1284
 
1139
1285
  def _render_quick_action_group(self, hints: list[tuple[str, str]], title: str) -> None:
1286
+ """Render a group of quick action hints.
1287
+
1288
+ Args:
1289
+ hints: List of (command, description) tuples.
1290
+ title: Group title.
1291
+ """
1140
1292
  if not hints:
1141
1293
  return
1142
1294
  formatted_tokens: list[str] = []
@@ -1153,10 +1305,20 @@ class SlashSession:
1153
1305
  self.console.print(" " + " ".join(chunk))
1154
1306
 
1155
1307
  def _chunk_tokens(self, tokens: list[str], *, size: int) -> Iterable[list[str]]:
1308
+ """Chunk tokens into groups of specified size.
1309
+
1310
+ Args:
1311
+ tokens: List of tokens to chunk.
1312
+ size: Size of each chunk.
1313
+
1314
+ Yields:
1315
+ Lists of tokens.
1316
+ """
1156
1317
  for index in range(0, len(tokens), size):
1157
1318
  yield tokens[index : index + size]
1158
1319
 
1159
1320
  def _render_home_hint(self) -> None:
1321
+ """Render hint text for home screen."""
1160
1322
  if self._home_hint_shown:
1161
1323
  return
1162
1324
  hint_text = (
@@ -1174,6 +1336,13 @@ class SlashSession:
1174
1336
  title: str = "Quick actions",
1175
1337
  inline: bool = False,
1176
1338
  ) -> None:
1339
+ """Show quick action hints.
1340
+
1341
+ Args:
1342
+ hints: Iterable of (command, description) tuples.
1343
+ title: Title for the hints.
1344
+ inline: Whether to render inline or in a panel.
1345
+ """
1177
1346
  hint_list = self._normalize_quick_action_hints(hints)
1178
1347
  if not hint_list:
1179
1348
  return
@@ -1185,9 +1354,23 @@ class SlashSession:
1185
1354
  self._render_panel_quick_actions(hint_list, title)
1186
1355
 
1187
1356
  def _normalize_quick_action_hints(self, hints: Iterable[tuple[str, str]]) -> list[tuple[str, str]]:
1357
+ """Normalize quick action hints by filtering out empty commands.
1358
+
1359
+ Args:
1360
+ hints: Iterable of (command, description) tuples.
1361
+
1362
+ Returns:
1363
+ List of normalized hints.
1364
+ """
1188
1365
  return [(command, description) for command, description in hints if command]
1189
1366
 
1190
1367
  def _render_inline_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
1368
+ """Render quick actions inline.
1369
+
1370
+ Args:
1371
+ hint_list: List of (command, description) tuples.
1372
+ title: Title for the hints.
1373
+ """
1191
1374
  tokens: list[str] = []
1192
1375
  for command, description in hint_list:
1193
1376
  formatted = format_command_hint(command, description)
@@ -1201,6 +1384,12 @@ class SlashSession:
1201
1384
  self.console.print(text.strip())
1202
1385
 
1203
1386
  def _render_panel_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
1387
+ """Render quick actions in a panel.
1388
+
1389
+ Args:
1390
+ hint_list: List of (command, description) tuples.
1391
+ title: Panel title.
1392
+ """
1204
1393
  body_lines: list[Text] = []
1205
1394
  for command, description in hint_list:
1206
1395
  formatted = format_command_hint(command, description)
@@ -1212,6 +1401,11 @@ class SlashSession:
1212
1401
  self.console.print(AIPPanel(panel_content, title=title, border_style=SECONDARY_LIGHT, expand=False))
1213
1402
 
1214
1403
  def _load_config(self) -> dict[str, Any]:
1404
+ """Load configuration with caching.
1405
+
1406
+ Returns:
1407
+ Configuration dictionary.
1408
+ """
1215
1409
  if self._config_cache is None:
1216
1410
  try:
1217
1411
  self._config_cache = load_config() or {}
@@ -1220,6 +1414,16 @@ class SlashSession:
1220
1414
  return self._config_cache
1221
1415
 
1222
1416
  def _resolve_agent_from_ref(self, client: Any, available_agents: list[Any], ref: str) -> Any | None:
1417
+ """Resolve an agent from a reference string.
1418
+
1419
+ Args:
1420
+ client: API client instance.
1421
+ available_agents: List of available agents.
1422
+ ref: Reference string (ID or name).
1423
+
1424
+ Returns:
1425
+ Resolved agent or None.
1426
+ """
1223
1427
  ref = ref.strip()
1224
1428
  if not ref:
1225
1429
  return None