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.
- glaip_sdk/_version.py +8 -0
- glaip_sdk/branding.py +13 -0
- glaip_sdk/cli/commands/agents.py +180 -39
- glaip_sdk/cli/commands/mcps.py +44 -18
- glaip_sdk/cli/commands/models.py +11 -5
- glaip_sdk/cli/commands/tools.py +35 -16
- glaip_sdk/cli/commands/transcripts.py +8 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/display.py +34 -19
- glaip_sdk/cli/main.py +14 -7
- glaip_sdk/cli/masking.py +8 -33
- glaip_sdk/cli/pager.py +9 -10
- glaip_sdk/cli/slash/agent_session.py +57 -20
- glaip_sdk/cli/slash/prompt.py +8 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +341 -46
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
- glaip_sdk/cli/transcript/viewer.py +232 -32
- glaip_sdk/cli/update_notifier.py +2 -2
- glaip_sdk/cli/utils.py +266 -35
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +30 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +186 -22
- glaip_sdk/client/main.py +23 -6
- glaip_sdk/client/mcps.py +2 -4
- glaip_sdk/client/run_rendering.py +66 -0
- glaip_sdk/client/tools.py +2 -3
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/models/__init__.py +56 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/utils/client_utils.py +13 -0
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/import_export.py +6 -9
- glaip_sdk/utils/rendering/__init__.py +122 -1
- glaip_sdk/utils/rendering/renderer/base.py +3 -7
- glaip_sdk/utils/rendering/renderer/debug.py +0 -1
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/steps.py +1 -0
- glaip_sdk/utils/resource_refs.py +26 -15
- glaip_sdk/utils/serialization.py +16 -0
- {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/METADATA +24 -2
- glaip_sdk-0.3.0.dist-info/RECORD +94 -0
- glaip_sdk-0.2.1.dist-info/RECORD +0 -86
- {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
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("
|
|
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
|
|
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="
|
|
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=
|
|
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(
|
|
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
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
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
|
|
1133
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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
|