glaip-sdk 0.0.19__py3-none-any.whl → 0.0.20__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 (49) hide show
  1. glaip_sdk/_version.py +2 -2
  2. glaip_sdk/branding.py +27 -2
  3. glaip_sdk/cli/auth.py +93 -28
  4. glaip_sdk/cli/commands/__init__.py +2 -2
  5. glaip_sdk/cli/commands/agents.py +108 -21
  6. glaip_sdk/cli/commands/configure.py +141 -90
  7. glaip_sdk/cli/commands/mcps.py +81 -29
  8. glaip_sdk/cli/commands/models.py +4 -3
  9. glaip_sdk/cli/commands/tools.py +27 -14
  10. glaip_sdk/cli/commands/update.py +66 -0
  11. glaip_sdk/cli/config.py +13 -2
  12. glaip_sdk/cli/display.py +35 -26
  13. glaip_sdk/cli/io.py +14 -5
  14. glaip_sdk/cli/main.py +185 -73
  15. glaip_sdk/cli/pager.py +2 -1
  16. glaip_sdk/cli/resolution.py +4 -1
  17. glaip_sdk/cli/slash/__init__.py +3 -4
  18. glaip_sdk/cli/slash/agent_session.py +88 -36
  19. glaip_sdk/cli/slash/prompt.py +20 -48
  20. glaip_sdk/cli/slash/session.py +440 -189
  21. glaip_sdk/cli/transcript/__init__.py +71 -0
  22. glaip_sdk/cli/transcript/cache.py +338 -0
  23. glaip_sdk/cli/transcript/capture.py +278 -0
  24. glaip_sdk/cli/transcript/export.py +38 -0
  25. glaip_sdk/cli/transcript/launcher.py +79 -0
  26. glaip_sdk/cli/transcript/viewer.py +624 -0
  27. glaip_sdk/cli/update_notifier.py +29 -5
  28. glaip_sdk/cli/utils.py +256 -74
  29. glaip_sdk/client/agents.py +3 -1
  30. glaip_sdk/client/run_rendering.py +2 -2
  31. glaip_sdk/icons.py +19 -0
  32. glaip_sdk/models.py +6 -0
  33. glaip_sdk/rich_components.py +29 -1
  34. glaip_sdk/utils/__init__.py +1 -1
  35. glaip_sdk/utils/client_utils.py +6 -4
  36. glaip_sdk/utils/display.py +61 -32
  37. glaip_sdk/utils/rendering/formatting.py +6 -5
  38. glaip_sdk/utils/rendering/renderer/base.py +213 -66
  39. glaip_sdk/utils/rendering/renderer/debug.py +73 -16
  40. glaip_sdk/utils/rendering/renderer/panels.py +27 -15
  41. glaip_sdk/utils/rendering/renderer/progress.py +61 -38
  42. glaip_sdk/utils/serialization.py +5 -2
  43. glaip_sdk/utils/validation.py +1 -2
  44. {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.0.20.dist-info}/METADATA +1 -1
  45. glaip_sdk-0.0.20.dist-info/RECORD +80 -0
  46. glaip_sdk/utils/rich_utils.py +0 -29
  47. glaip_sdk-0.0.19.dist-info/RECORD +0 -73
  48. {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.0.20.dist-info}/WHEEL +0 -0
  49. {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.0.20.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/utils.py CHANGED
@@ -7,29 +7,44 @@ Authors:
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import importlib
10
11
  import json
11
12
  import logging
12
13
  import os
13
14
  import sys
14
- from collections.abc import Callable
15
+ from collections.abc import Callable, Iterable
15
16
  from contextlib import AbstractContextManager, nullcontext
16
- from typing import TYPE_CHECKING, Any
17
+ from typing import TYPE_CHECKING, Any, cast
17
18
 
18
19
  import click
19
20
  from rich.console import Console, Group
20
21
  from rich.markdown import Markdown
21
22
  from rich.pretty import Pretty
22
23
 
24
+ from glaip_sdk.branding import (
25
+ ACCENT_STYLE,
26
+ HINT_COMMAND_STYLE,
27
+ HINT_DESCRIPTION_COLOR,
28
+ SUCCESS_STYLE,
29
+ WARNING_STYLE,
30
+ )
23
31
  from glaip_sdk.cli.rich_helpers import markup_text
32
+ from glaip_sdk.icons import ICON_AGENT
24
33
  from glaip_sdk.rich_components import AIPPanel
25
34
 
26
35
  # Optional interactive deps (fuzzy palette)
27
36
  try:
37
+ from prompt_toolkit.buffer import Buffer
28
38
  from prompt_toolkit.completion import Completion
29
- from prompt_toolkit.shortcuts import prompt
39
+ from prompt_toolkit.selection import SelectionType
40
+ from prompt_toolkit.shortcuts import PromptSession, prompt
30
41
 
31
42
  _HAS_PTK = True
32
43
  except Exception: # pragma: no cover - optional dependency
44
+ Buffer = None # type: ignore[assignment]
45
+ SelectionType = None # type: ignore[assignment]
46
+ PromptSession = None # type: ignore[assignment]
47
+ prompt = None # type: ignore[assignment]
33
48
  _HAS_PTK = False
34
49
 
35
50
  try:
@@ -114,25 +129,54 @@ def command_hint(
114
129
  return f"aip {cli_command}"
115
130
 
116
131
 
132
+ def format_command_hint(
133
+ command: str | None,
134
+ description: str | None = None,
135
+ ) -> str | None:
136
+ """Return a Rich markup string that highlights a command hint.
137
+
138
+ Args:
139
+ command: Command text to highlight (already formatted for the active mode).
140
+ description: Optional short description to display alongside the command.
141
+
142
+ Returns:
143
+ Markup string suitable for Rich rendering, or ``None`` when ``command`` is falsy.
144
+ """
145
+ if not command:
146
+ return None
147
+
148
+ highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
149
+ if description:
150
+ highlighted += (
151
+ f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
152
+ )
153
+ return highlighted
154
+
155
+
117
156
  def spinner_context(
118
157
  ctx: Any | None,
119
158
  message: str,
120
159
  *,
121
160
  console_override: Console | None = None,
122
161
  spinner: str = "dots",
123
- spinner_style: str = "cyan",
162
+ spinner_style: str = ACCENT_STYLE,
124
163
  ) -> AbstractContextManager[Any]:
125
164
  """Return a context manager that renders a spinner when appropriate."""
126
165
  active_console = console_override or console
127
166
  if not _can_use_spinner(ctx, active_console):
128
167
  return nullcontext()
129
168
 
130
- return active_console.status(
169
+ status = active_console.status(
131
170
  message,
132
171
  spinner=spinner,
133
172
  spinner_style=spinner_style,
134
173
  )
135
174
 
175
+ if not hasattr(status, "__enter__") or not hasattr(status, "__exit__"):
176
+ return nullcontext()
177
+
178
+ return status
179
+
136
180
 
137
181
  def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
138
182
  """Check if spinner output is allowed in the current environment."""
@@ -189,8 +233,8 @@ _spinner_stop = stop_spinner
189
233
 
190
234
  def get_client(ctx: Any) -> Client: # pragma: no cover
191
235
  """Get configured client from context, env, and config file (ctx > env > file)."""
192
- from glaip_sdk import Client
193
-
236
+ module = importlib.import_module("glaip_sdk")
237
+ client_class = cast("type[Client]", getattr(module, "Client"))
194
238
  file_config = load_config() or {}
195
239
  context_config_obj = getattr(ctx, "obj", None)
196
240
  context_config = context_config_obj or {}
@@ -223,7 +267,7 @@ def get_client(ctx: Any) -> Client: # pragma: no cover
223
267
  actions.append("set AIP_* env vars")
224
268
  raise click.ClickException(f"Missing api_url/api_key. {' or '.join(actions)}.")
225
269
 
226
- return Client(
270
+ return client_class(
227
271
  api_url=config.get("api_url"),
228
272
  api_key=config.get("api_key"),
229
273
  timeout=float(config.get("timeout") or 30.0),
@@ -335,6 +379,86 @@ def _build_unique_labels(
335
379
  return labels, by_label
336
380
 
337
381
 
382
+ def _basic_prompt(
383
+ message: str,
384
+ completer: Any,
385
+ ) -> str | None:
386
+ """Fallback prompt handler when PromptSession is unavailable or fails."""
387
+ if prompt is None: # pragma: no cover - optional dependency path
388
+ return None
389
+
390
+ try:
391
+ return prompt(
392
+ message=message,
393
+ completer=completer,
394
+ complete_in_thread=True,
395
+ complete_while_typing=True,
396
+ )
397
+ except (KeyboardInterrupt, EOFError):
398
+ return None
399
+ except Exception as exc: # pragma: no cover - defensive
400
+ logger.debug("Fallback prompt failed: %s", exc)
401
+ return None
402
+
403
+
404
+ def _prompt_with_auto_select(
405
+ message: str,
406
+ completer: Any,
407
+ choices: Iterable[str],
408
+ ) -> str | None:
409
+ """Prompt with fuzzy completer that auto-selects suggested matches."""
410
+ if not _HAS_PTK or PromptSession is None or Buffer is None or SelectionType is None:
411
+ return _basic_prompt(message, completer)
412
+
413
+ try:
414
+ session = PromptSession(
415
+ message,
416
+ completer=completer,
417
+ complete_in_thread=True,
418
+ complete_while_typing=True,
419
+ reserve_space_for_menu=8,
420
+ )
421
+ except Exception as exc: # pragma: no cover - depends on prompt_toolkit
422
+ logger.debug(
423
+ "PromptSession init failed (%s); falling back to basic prompt.", exc
424
+ )
425
+ return _basic_prompt(message, completer)
426
+
427
+ buffer = session.default_buffer
428
+ valid_choices = set(choices)
429
+
430
+ def _auto_select(_: Buffer) -> None:
431
+ text = buffer.text
432
+ if not text or text not in valid_choices:
433
+ return
434
+ buffer.cursor_position = 0
435
+ buffer.start_selection(selection_type=SelectionType.CHARACTERS)
436
+ buffer.cursor_position = len(text)
437
+
438
+ handler_attached = False
439
+ try:
440
+ buffer.on_text_changed += _auto_select
441
+ handler_attached = True
442
+ except Exception as exc: # pragma: no cover - defensive
443
+ logger.debug("Failed to attach auto-select handler: %s", exc)
444
+
445
+ try:
446
+ return session.prompt()
447
+ except (KeyboardInterrupt, EOFError):
448
+ return None
449
+ except Exception as exc: # pragma: no cover - defensive
450
+ logger.debug(
451
+ "PromptSession prompt failed (%s); falling back to basic prompt.", exc
452
+ )
453
+ return _basic_prompt(message, completer)
454
+ finally:
455
+ if handler_attached:
456
+ try:
457
+ buffer.on_text_changed -= _auto_select
458
+ except Exception: # pragma: no cover - defensive
459
+ pass
460
+
461
+
338
462
  class _FuzzyCompleter:
339
463
  """Fuzzy completer for prompt_toolkit."""
340
464
 
@@ -404,17 +528,12 @@ def _fuzzy_pick(
404
528
 
405
529
  # Create fuzzy completer
406
530
  completer = _FuzzyCompleter(labels)
407
-
408
- try:
409
- answer = prompt(
410
- message=f"Find {title.rstrip('s')}: ",
411
- completer=completer,
412
- complete_in_thread=True,
413
- complete_while_typing=True,
414
- )
415
- except (KeyboardInterrupt, EOFError): # pragma: no cover - user cancelled input
416
- return None
417
- except Exception: # pragma: no cover - prompt_toolkit not available in headless env
531
+ answer = _prompt_with_auto_select(
532
+ f"Find {title.rstrip('s')}: ",
533
+ completer,
534
+ labels,
535
+ )
536
+ if answer is None:
418
537
  return None
419
538
 
420
539
  return _perform_fuzzy_search(answer, labels, by_label) if answer else None
@@ -557,7 +676,7 @@ def output_result(
557
676
  if panel_title:
558
677
  console.print(AIPPanel(renderable, title=panel_title))
559
678
  else:
560
- console.print(markup_text(f"[cyan]{title}:[/cyan]"))
679
+ console.print(markup_text(f"[{ACCENT_STYLE}]{title}:[/]"))
561
680
  console.print(renderable)
562
681
 
563
682
 
@@ -665,7 +784,7 @@ def _handle_markdown_output(
665
784
 
666
785
  def _handle_empty_items(title: str) -> None:
667
786
  """Handle case when no items are found."""
668
- console.print(markup_text(f"[yellow]No {title.lower()} found.[/yellow]"))
787
+ console.print(markup_text(f"[{WARNING_STYLE}]No {title.lower()} found.[/]"))
669
788
 
670
789
 
671
790
  def _should_use_fuzzy_picker() -> bool:
@@ -816,6 +935,18 @@ def coerce_to_row(item: Any, keys: list[str]) -> dict[str, Any]:
816
935
  return result
817
936
 
818
937
 
938
+ def _register_renderer_with_session(ctx: Any, renderer: RichStreamRenderer) -> None:
939
+ """Attach renderer to an active slash session when present."""
940
+ try:
941
+ ctx_obj = getattr(ctx, "obj", None)
942
+ session = ctx_obj.get("_slash_session") if isinstance(ctx_obj, dict) else None
943
+ if session and hasattr(session, "register_active_renderer"):
944
+ session.register_active_renderer(renderer)
945
+ except Exception:
946
+ # Never let session bookkeeping break renderer creation
947
+ pass
948
+
949
+
819
950
  def build_renderer(
820
951
  _ctx: Any,
821
952
  *,
@@ -841,21 +972,17 @@ def build_renderer(
841
972
  Tuple of (renderer, capturing_console) for streaming output.
842
973
  """
843
974
  # Use capturing console if saving output
844
- working_console = console
845
- if save_path:
846
- working_console = CapturingConsole(console, capture=True)
975
+ working_console = CapturingConsole(console, capture=True) if save_path else console
847
976
 
848
977
  # Configure renderer based on verbose mode and explicit overrides
849
- if live is None:
850
- live_enabled = not verbose # Disable live mode in verbose (unless overridden)
851
- else:
852
- live_enabled = bool(live)
978
+ live_enabled = bool(live) if live is not None else not verbose
979
+ style = "debug" if verbose else "pretty"
853
980
 
854
981
  renderer_cfg = RendererConfig(
855
982
  theme=theme,
856
- style="debug" if verbose else "pretty",
983
+ style=style,
857
984
  live=live_enabled,
858
- show_delegate_tool_panels=True,
985
+ show_delegate_tool_panels=False,
859
986
  append_finished_snapshots=bool(snapshots)
860
987
  if snapshots is not None
861
988
  else RendererConfig.append_finished_snapshots,
@@ -871,15 +998,7 @@ def build_renderer(
871
998
  )
872
999
 
873
1000
  # Link the renderer back to the slash session when running from the palette.
874
- try:
875
- ctx_obj = getattr(_ctx, "obj", None)
876
- if isinstance(ctx_obj, dict):
877
- session = ctx_obj.get("_slash_session")
878
- if session and hasattr(session, "register_active_renderer"):
879
- session.register_active_renderer(renderer)
880
- except Exception:
881
- # Never let session bookkeeping break renderer creation
882
- pass
1001
+ _register_renderer_with_session(_ctx, renderer)
883
1002
 
884
1003
  return renderer, working_console
885
1004
 
@@ -935,15 +1054,12 @@ def _fuzzy_pick_for_resources(
935
1054
 
936
1055
  # Create fuzzy completer
937
1056
  completer = _FuzzyCompleter(labels)
938
-
939
- try:
940
- answer = prompt(
941
- message=f"Find 🤖 {resource_type.title()}: ",
942
- completer=completer,
943
- complete_in_thread=True,
944
- complete_while_typing=True,
945
- )
946
- except (KeyboardInterrupt, EOFError):
1057
+ answer = _prompt_with_auto_select(
1058
+ f"Find {ICON_AGENT} {resource_type.title()}: ",
1059
+ completer,
1060
+ labels,
1061
+ )
1062
+ if answer is None:
947
1063
  return None
948
1064
 
949
1065
  return _perform_fuzzy_search(answer, labels, by_label) if answer else None
@@ -967,19 +1083,19 @@ def _resolve_by_name_multiple_with_select(matches: list[Any], select: int) -> An
967
1083
  def _resolve_by_name_multiple_fuzzy(
968
1084
  ctx: Any, ref: str, matches: list[Any], label: str
969
1085
  ) -> Any:
970
- """Resolve multiple matches using fuzzy picker interface."""
971
- picked = _fuzzy_pick_for_resources(matches, label.lower(), ref)
972
- if picked:
973
- return picked
974
- # Fallback to original ambiguity handler if fuzzy picker fails
975
- return handle_ambiguous_resource(ctx, label.lower(), ref, matches)
1086
+ """Resolve multiple matches preferring the fuzzy picker interface."""
1087
+ return handle_ambiguous_resource(
1088
+ ctx, label.lower(), ref, matches, interface_preference="fuzzy"
1089
+ )
976
1090
 
977
1091
 
978
1092
  def _resolve_by_name_multiple_questionary(
979
1093
  ctx: Any, ref: str, matches: list[Any], label: str
980
1094
  ) -> Any:
981
- """Resolve multiple matches using questionary interface."""
982
- return handle_ambiguous_resource(ctx, label.lower(), ref, matches)
1095
+ """Resolve multiple matches preferring the questionary interface."""
1096
+ return handle_ambiguous_resource(
1097
+ ctx, label.lower(), ref, matches, interface_preference="questionary"
1098
+ )
983
1099
 
984
1100
 
985
1101
  def resolve_resource(
@@ -1015,7 +1131,7 @@ def resolve_resource(
1015
1131
  _spinner_update(spinner, f"[bold blue]Fetching {label} by ID…[/bold blue]")
1016
1132
  result = _resolve_by_id(ref, get_by_id)
1017
1133
  if result is not None:
1018
- _spinner_update(spinner, f"[bold green]{label} found[/bold green]")
1134
+ _spinner_update(spinner, f"[{SUCCESS_STYLE}]{label} found[/]")
1019
1135
  return result
1020
1136
 
1021
1137
  # If get_by_id returned None, the resource doesn't exist
@@ -1033,7 +1149,7 @@ def resolve_resource(
1033
1149
  raise click.ClickException(f"{label} '{ref}' not found")
1034
1150
 
1035
1151
  if len(matches) == 1:
1036
- _spinner_update(spinner, f"[bold green]{label} found[/bold green]")
1152
+ _spinner_update(spinner, f"[{SUCCESS_STYLE}]{label} found[/]")
1037
1153
  return matches[0]
1038
1154
 
1039
1155
  # Multiple matches found, handle ambiguity
@@ -1043,7 +1159,10 @@ def resolve_resource(
1043
1159
 
1044
1160
  # Choose interface based on preference
1045
1161
  _spinner_stop(spinner)
1046
- if interface_preference == "fuzzy":
1162
+ preference = (interface_preference or "fuzzy").lower()
1163
+ if preference not in {"fuzzy", "questionary"}:
1164
+ preference = "fuzzy"
1165
+ if preference == "fuzzy":
1047
1166
  return _resolve_by_name_multiple_fuzzy(ctx, ref, matches, label)
1048
1167
  else:
1049
1168
  return _resolve_by_name_multiple_questionary(ctx, ref, matches, label)
@@ -1093,7 +1212,7 @@ def _handle_fallback_numeric_ambiguity(
1093
1212
 
1094
1213
  console.print(
1095
1214
  markup_text(
1096
- f"[yellow]Multiple {safe_resource_type}s found matching '{safe_ref}':[/yellow]"
1215
+ f"[{WARNING_STYLE}]Multiple {safe_resource_type}s found matching '{safe_ref}':[/]"
1097
1216
  )
1098
1217
  )
1099
1218
  table = AIPTable(
@@ -1101,7 +1220,7 @@ def _handle_fallback_numeric_ambiguity(
1101
1220
  )
1102
1221
  table.add_column("#", style="dim", width=3)
1103
1222
  table.add_column("ID", style="dim", width=36)
1104
- table.add_column("Name", style="cyan")
1223
+ table.add_column("Name", style=ACCENT_STYLE)
1105
1224
  for i, m in enumerate(matches, 1):
1106
1225
  table.add_row(str(i), str(getattr(m, "id", "")), str(getattr(m, "name", "")))
1107
1226
  console.print(table)
@@ -1127,22 +1246,85 @@ def _should_fallback_to_numeric_prompt(exception: Exception) -> bool:
1127
1246
  return True
1128
1247
 
1129
1248
 
1249
+ def _normalize_interface_preference(preference: str) -> str:
1250
+ """Normalize and validate interface preference."""
1251
+ normalized = (preference or "questionary").lower()
1252
+ return normalized if normalized in {"fuzzy", "questionary"} else "questionary"
1253
+
1254
+
1255
+ def _get_interface_order(preference: str) -> tuple[str, str]:
1256
+ """Get the ordered interface preferences."""
1257
+ interface_orders = {
1258
+ "fuzzy": ("fuzzy", "questionary"),
1259
+ "questionary": ("questionary", "fuzzy"),
1260
+ }
1261
+ return interface_orders.get(preference, ("questionary", "fuzzy"))
1262
+
1263
+
1264
+ def _try_fuzzy_selection(
1265
+ resource_type: str,
1266
+ ref: str,
1267
+ matches: list[Any],
1268
+ ) -> Any | None:
1269
+ """Try fuzzy interface selection."""
1270
+ picked = _fuzzy_pick_for_resources(matches, resource_type, ref)
1271
+ return picked if picked else None
1272
+
1273
+
1274
+ def _try_questionary_selection(
1275
+ resource_type: str,
1276
+ ref: str,
1277
+ matches: list[Any],
1278
+ ) -> Any | None:
1279
+ """Try questionary interface selection."""
1280
+ try:
1281
+ return _handle_questionary_ambiguity(resource_type, ref, matches)
1282
+ except Exception as exc:
1283
+ if not _should_fallback_to_numeric_prompt(exc):
1284
+ raise
1285
+ return None
1286
+
1287
+
1288
+ def _try_interface_selection(
1289
+ interface_order: tuple[str, str],
1290
+ resource_type: str,
1291
+ ref: str,
1292
+ matches: list[Any],
1293
+ ) -> Any | None:
1294
+ """Try interface selection in order, return result or None if all failed."""
1295
+ interface_handlers = {
1296
+ "fuzzy": _try_fuzzy_selection,
1297
+ "questionary": _try_questionary_selection,
1298
+ }
1299
+
1300
+ for interface in interface_order:
1301
+ handler = interface_handlers.get(interface)
1302
+ if handler:
1303
+ result = handler(resource_type, ref, matches)
1304
+ if result:
1305
+ return result
1306
+
1307
+ return None
1308
+
1309
+
1130
1310
  def handle_ambiguous_resource(
1131
- ctx: Any, resource_type: str, ref: str, matches: list[Any]
1311
+ ctx: Any,
1312
+ resource_type: str,
1313
+ ref: str,
1314
+ matches: list[Any],
1315
+ *,
1316
+ interface_preference: str = "questionary",
1132
1317
  ) -> Any:
1133
1318
  """Handle multiple resource matches gracefully."""
1134
1319
  if _get_view(ctx) == "json":
1135
1320
  return _handle_json_view_ambiguity(matches)
1136
1321
 
1137
- try:
1138
- return _handle_questionary_ambiguity(resource_type, ref, matches)
1139
- except Exception as e:
1140
- if _should_fallback_to_numeric_prompt(e):
1141
- try:
1142
- return _handle_fallback_numeric_ambiguity(resource_type, ref, matches)
1143
- except Exception:
1144
- # If fallback also fails, re-raise the original exception
1145
- raise e
1146
- else:
1147
- # Re-raise cancellation exceptions
1148
- raise
1322
+ preference = _normalize_interface_preference(interface_preference)
1323
+ interface_order = _get_interface_order(preference)
1324
+
1325
+ result = _try_interface_selection(interface_order, resource_type, ref, matches)
1326
+
1327
+ if result is not None:
1328
+ return result
1329
+
1330
+ return _handle_fallback_numeric_ambiguity(resource_type, ref, matches)
@@ -928,6 +928,8 @@ class AgentClient(BaseClient):
928
928
  started_monotonic: float | None = None
929
929
  finished_monotonic: float | None = None
930
930
 
931
+ timeout_seconds = compute_timeout_seconds(kwargs)
932
+
931
933
  try:
932
934
  response = self.http_client.stream(
933
935
  "POST",
@@ -936,12 +938,12 @@ class AgentClient(BaseClient):
936
938
  data=data_payload,
937
939
  files=files_payload,
938
940
  headers=headers,
941
+ timeout=timeout_seconds,
939
942
  )
940
943
 
941
944
  with response as stream_response:
942
945
  stream_response.raise_for_status()
943
946
 
944
- timeout_seconds = compute_timeout_seconds(kwargs)
945
947
  agent_name = kwargs.get("agent_name")
946
948
 
947
949
  (
@@ -106,7 +106,7 @@ class AgentRunRenderingManager:
106
106
  theme="dark",
107
107
  style="debug",
108
108
  live=False,
109
- show_delegate_tool_panels=True,
109
+ show_delegate_tool_panels=False,
110
110
  append_finished_snapshots=False,
111
111
  )
112
112
  return RichStreamRenderer(
@@ -118,7 +118,7 @@ class AgentRunRenderingManager:
118
118
  def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
119
119
  if verbose:
120
120
  return self._create_verbose_renderer()
121
- default_config = RendererConfig(show_delegate_tool_panels=True)
121
+ default_config = RendererConfig()
122
122
  return RichStreamRenderer(console=_Console(), cfg=default_config)
123
123
 
124
124
  # --------------------------------------------------------------------- #
glaip_sdk/icons.py ADDED
@@ -0,0 +1,19 @@
1
+ """Lightweight icon definitions used across the CLI.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ ICON_AGENT = "🤖"
8
+ ICON_AGENT_STEP = "🧠"
9
+ ICON_TOOL = "🔧"
10
+ ICON_TOOL_STEP = "⚙️"
11
+ ICON_DELEGATE = ICON_AGENT
12
+
13
+ __all__ = [
14
+ "ICON_AGENT",
15
+ "ICON_AGENT_STEP",
16
+ "ICON_TOOL",
17
+ "ICON_TOOL_STEP",
18
+ "ICON_DELEGATE",
19
+ ]
glaip_sdk/models.py CHANGED
@@ -58,6 +58,9 @@ class Agent(BaseModel):
58
58
  )
59
59
  # Automatically pass the agent name for better renderer display
60
60
  kwargs.setdefault("agent_name", self.name)
61
+ # Pass the agent's configured timeout if not explicitly overridden
62
+ if "timeout" not in kwargs:
63
+ kwargs["timeout"] = self.timeout
61
64
  # Pass verbose flag through to enable event JSON output
62
65
  return self._client.run_agent(self.id, message, verbose=verbose, **kwargs)
63
66
 
@@ -82,6 +85,9 @@ class Agent(BaseModel):
82
85
  )
83
86
  # Automatically pass the agent name for better context
84
87
  kwargs.setdefault("agent_name", self.name)
88
+ # Pass the agent's configured timeout if not explicitly overridden
89
+ if "timeout" not in kwargs:
90
+ kwargs["timeout"] = self.timeout
85
91
 
86
92
  async for chunk in self._client.arun_agent(self.id, message, **kwargs):
87
93
  yield chunk
@@ -38,4 +38,32 @@ class AIPTable(Table):
38
38
  super().__init__(*args, **kwargs)
39
39
 
40
40
 
41
- __all__ = ["AIPPanel", "AIPTable"]
41
+ class AIPGrid(Table):
42
+ """Table-based grid with GL AIP defaults for layout blocks."""
43
+
44
+ def __init__(
45
+ self,
46
+ *,
47
+ expand: bool = True,
48
+ padding: tuple[int, int] = (0, 1),
49
+ collapse_padding: bool = True,
50
+ ):
51
+ """Initialize AIPGrid with zero-edge borders and optional expansion.
52
+
53
+ Args:
54
+ expand: Whether the grid should expand to fill available width.
55
+ padding: Cell padding for the grid (row, column).
56
+ collapse_padding: Collapse padding between renderables.
57
+ """
58
+ super().__init__(
59
+ show_header=False,
60
+ show_edge=False,
61
+ pad_edge=False,
62
+ box=None,
63
+ expand=expand,
64
+ padding=padding,
65
+ collapse_padding=collapse_padding,
66
+ )
67
+
68
+
69
+ __all__ = ["AIPPanel", "AIPTable", "AIPGrid"]
@@ -5,6 +5,7 @@ Authors:
5
5
  """
6
6
 
7
7
  from glaip_sdk.utils.display import (
8
+ RICH_AVAILABLE,
8
9
  print_agent_created,
9
10
  print_agent_deleted,
10
11
  print_agent_output,
@@ -19,7 +20,6 @@ from glaip_sdk.utils.general import (
19
20
  )
20
21
  from glaip_sdk.utils.rendering.models import RunStats, Step
21
22
  from glaip_sdk.utils.rendering.steps import StepManager
22
- from glaip_sdk.utils.rich_utils import RICH_AVAILABLE
23
23
  from glaip_sdk.utils.run_renderer import RichStreamRenderer
24
24
 
25
25
  __all__ = [
@@ -17,6 +17,12 @@ from typing import Any, BinaryIO, NoReturn
17
17
  import httpx
18
18
 
19
19
  from glaip_sdk.exceptions import AgentTimeoutError
20
+ from glaip_sdk.utils.resource_refs import (
21
+ extract_ids as extract_ids_new,
22
+ )
23
+ from glaip_sdk.utils.resource_refs import (
24
+ find_by_name as find_by_name_new,
25
+ )
20
26
 
21
27
  # Set up module-level logger
22
28
  logger = logging.getLogger("glaip_sdk.client_utils")
@@ -77,8 +83,6 @@ def extract_ids(items: list[str | Any] | None) -> list[str] | None:
77
83
  This function maintains backward compatibility by returning None for empty input.
78
84
  New code should use glaip_sdk.utils.resource_refs.extract_ids which returns [].
79
85
  """
80
- from .resource_refs import extract_ids as extract_ids_new
81
-
82
86
  if not items:
83
87
  return None
84
88
 
@@ -127,8 +131,6 @@ def find_by_name(
127
131
  Note:
128
132
  This function now delegates to glaip_sdk.utils.resource_refs.find_by_name.
129
133
  """
130
- from .resource_refs import find_by_name as find_by_name_new
131
-
132
134
  return find_by_name_new(items, name, case_sensitive)
133
135
 
134
136