glaip-sdk 0.1.4__py3-none-any.whl → 0.2.1__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/__init__.py +1 -1
- glaip_sdk/_version.py +1 -0
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/agents.py +22 -37
- glaip_sdk/cli/commands/configure.py +67 -6
- glaip_sdk/cli/commands/mcps.py +19 -23
- glaip_sdk/cli/commands/tools.py +17 -41
- glaip_sdk/cli/commands/transcripts.py +747 -0
- glaip_sdk/cli/config.py +6 -1
- glaip_sdk/cli/display.py +1 -0
- glaip_sdk/cli/main.py +12 -31
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/prompt.py +2 -0
- glaip_sdk/cli/slash/session.py +259 -90
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +32 -0
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +6 -2
- glaip_sdk/cli/update_notifier.py +5 -2
- glaip_sdk/cli/utils.py +170 -0
- glaip_sdk/client/_agent_payloads.py +5 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/utils/__init__.py +12 -7
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/general.py +0 -33
- glaip_sdk/utils/import_export.py +9 -1
- glaip_sdk/utils/rendering/renderer/__init__.py +0 -20
- glaip_sdk/utils/rendering/renderer/debug.py +3 -20
- glaip_sdk/utils/rendering/steps.py +5 -6
- glaip_sdk/utils/resource_refs.py +2 -1
- glaip_sdk/utils/serialization.py +2 -0
- {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.1.dist-info}/METADATA +1 -1
- {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.1.dist-info}/RECORD +37 -34
- {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.1.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.1.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/config.py
CHANGED
|
@@ -12,7 +12,12 @@ import yaml
|
|
|
12
12
|
|
|
13
13
|
CONFIG_DIR = Path.home() / ".aip"
|
|
14
14
|
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
15
|
-
_ALLOWED_KEYS = {
|
|
15
|
+
_ALLOWED_KEYS = {
|
|
16
|
+
"api_url",
|
|
17
|
+
"api_key",
|
|
18
|
+
"timeout",
|
|
19
|
+
"history_default_limit",
|
|
20
|
+
}
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
def _sanitize_config(data: dict[str, Any] | None) -> dict[str, Any]:
|
glaip_sdk/cli/display.py
CHANGED
|
@@ -208,6 +208,7 @@ def build_resource_result_data(resource: Any, fields: list[str]) -> dict[str, An
|
|
|
208
208
|
|
|
209
209
|
|
|
210
210
|
def _normalise_field_value(field: str, value: Any) -> Any:
|
|
211
|
+
"""Convert special sentinel values into display-friendly text."""
|
|
211
212
|
if value is _MISSING:
|
|
212
213
|
return "N/A"
|
|
213
214
|
if hasattr(value, "_mock_name"):
|
glaip_sdk/cli/main.py
CHANGED
|
@@ -33,11 +33,12 @@ from glaip_sdk.cli.commands.configure import (
|
|
|
33
33
|
from glaip_sdk.cli.commands.mcps import mcps_group
|
|
34
34
|
from glaip_sdk.cli.commands.models import models_group
|
|
35
35
|
from glaip_sdk.cli.commands.tools import tools_group
|
|
36
|
+
from glaip_sdk.cli.commands.transcripts import transcripts_group
|
|
36
37
|
from glaip_sdk.cli.commands.update import update_command
|
|
37
38
|
from glaip_sdk.cli.config import load_config
|
|
38
39
|
from glaip_sdk.cli.transcript import get_transcript_cache_stats
|
|
39
40
|
from glaip_sdk.cli.update_notifier import maybe_notify_update
|
|
40
|
-
from glaip_sdk.cli.utils import in_slash_mode, sdk_version, spinner_context, update_spinner
|
|
41
|
+
from glaip_sdk.cli.utils import format_size, in_slash_mode, sdk_version, spinner_context, update_spinner
|
|
41
42
|
from glaip_sdk.config.constants import (
|
|
42
43
|
DEFAULT_AGENT_RUN_TIMEOUT,
|
|
43
44
|
)
|
|
@@ -55,24 +56,6 @@ except ImportError: # pragma: no cover - optional slash dependencies
|
|
|
55
56
|
AVAILABLE_STATUS = "✅ Available"
|
|
56
57
|
|
|
57
58
|
|
|
58
|
-
def _format_size(num: int) -> str:
|
|
59
|
-
"""Return a human-readable byte size."""
|
|
60
|
-
if num <= 0:
|
|
61
|
-
return "0B"
|
|
62
|
-
|
|
63
|
-
units = ["B", "KB", "MB", "GB", "TB"]
|
|
64
|
-
value = float(num)
|
|
65
|
-
for unit in units:
|
|
66
|
-
if value < 1024 or unit == units[-1]:
|
|
67
|
-
if value >= 100 or unit == "B":
|
|
68
|
-
return f"{value:.0f}{unit}"
|
|
69
|
-
if value >= 10:
|
|
70
|
-
return f"{value:.1f}{unit}"
|
|
71
|
-
return f"{value:.2f}{unit}"
|
|
72
|
-
value /= 1024
|
|
73
|
-
return f"{value:.1f}TB" # pragma: no cover - defensive fallback
|
|
74
|
-
|
|
75
|
-
|
|
76
59
|
@click.group(invoke_without_command=True)
|
|
77
60
|
@click.version_option(package_name="glaip-sdk", prog_name="aip")
|
|
78
61
|
@click.option(
|
|
@@ -157,6 +140,7 @@ main.add_command(config_group)
|
|
|
157
140
|
main.add_command(tools_group)
|
|
158
141
|
main.add_command(mcps_group)
|
|
159
142
|
main.add_command(models_group)
|
|
143
|
+
main.add_command(transcripts_group)
|
|
160
144
|
|
|
161
145
|
# Add top-level commands
|
|
162
146
|
main.add_command(configure_command)
|
|
@@ -240,15 +224,15 @@ def _collect_cache_summary() -> tuple[str | None, str | None]:
|
|
|
240
224
|
try:
|
|
241
225
|
cache_stats = get_transcript_cache_stats()
|
|
242
226
|
except Exception:
|
|
243
|
-
return "[dim]Saved
|
|
227
|
+
return "[dim]Saved transcripts[/dim]: unavailable", None
|
|
244
228
|
|
|
245
229
|
runs_text = f"{cache_stats.entry_count} runs saved"
|
|
246
230
|
if cache_stats.total_bytes:
|
|
247
|
-
size_part = f" · {
|
|
231
|
+
size_part = f" · {format_size(cache_stats.total_bytes)} used"
|
|
248
232
|
else:
|
|
249
233
|
size_part = ""
|
|
250
234
|
|
|
251
|
-
cache_line = f"[dim]Saved
|
|
235
|
+
cache_line = f"[dim]Saved transcripts[/dim]: {runs_text}{size_part} · {cache_stats.cache_dir}"
|
|
252
236
|
return cache_line, None
|
|
253
237
|
|
|
254
238
|
|
|
@@ -429,14 +413,11 @@ def update(check_only: bool, force: bool) -> None:
|
|
|
429
413
|
|
|
430
414
|
# Update using pip
|
|
431
415
|
try:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
"--upgrade",
|
|
438
|
-
"glaip-sdk",
|
|
439
|
-
]
|
|
416
|
+
from glaip_sdk.cli.commands.update import _build_upgrade_command
|
|
417
|
+
|
|
418
|
+
cmd = list(_build_upgrade_command(include_prerelease=False))
|
|
419
|
+
# Replace package name with "glaip-sdk" (main.py uses different name)
|
|
420
|
+
cmd[-1] = "glaip-sdk"
|
|
440
421
|
if force:
|
|
441
422
|
cmd.insert(5, "--force-reinstall")
|
|
442
423
|
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
@@ -491,4 +472,4 @@ def update(check_only: bool, force: bool) -> None:
|
|
|
491
472
|
|
|
492
473
|
|
|
493
474
|
if __name__ == "__main__":
|
|
494
|
-
main()
|
|
475
|
+
main() # pylint: disable=no-value-for-parameter
|
glaip_sdk/cli/slash/__init__.py
CHANGED
|
@@ -6,19 +6,10 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
from glaip_sdk.cli.commands.agents import get as agents_get_command
|
|
8
8
|
from glaip_sdk.cli.commands.agents import run as agents_run_command
|
|
9
|
-
from glaip_sdk.cli.commands.configure import configure_command, load_config
|
|
10
|
-
from glaip_sdk.cli.slash.agent_session import AgentRunSession
|
|
11
|
-
from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT
|
|
12
9
|
from glaip_sdk.cli.slash.session import SlashSession
|
|
13
|
-
from glaip_sdk.cli.utils import get_client
|
|
14
10
|
|
|
15
11
|
__all__ = [
|
|
16
|
-
"AgentRunSession",
|
|
17
12
|
"SlashSession",
|
|
18
|
-
"_HAS_PROMPT_TOOLKIT",
|
|
19
13
|
"agents_get_command",
|
|
20
14
|
"agents_run_command",
|
|
21
|
-
"configure_command",
|
|
22
|
-
"get_client",
|
|
23
|
-
"load_config",
|
|
24
15
|
]
|
glaip_sdk/cli/slash/prompt.py
CHANGED
|
@@ -163,6 +163,7 @@ def _create_key_bindings(_session: SlashSession) -> Any:
|
|
|
163
163
|
def _iter_command_completions(
|
|
164
164
|
session: SlashSession, text: str
|
|
165
165
|
) -> Iterable[Completion]: # pragma: no cover - thin wrapper
|
|
166
|
+
"""Yield completions for global slash commands."""
|
|
166
167
|
prefix = text[1:]
|
|
167
168
|
seen: set[str] = set()
|
|
168
169
|
|
|
@@ -203,6 +204,7 @@ def _generate_command_completions(cmd: Any, prefix: str, text: str, seen: set[st
|
|
|
203
204
|
def _iter_contextual_completions(
|
|
204
205
|
session: SlashSession, text: str
|
|
205
206
|
) -> Iterable[Completion]: # pragma: no cover - thin wrapper
|
|
207
|
+
"""Yield completions for context-specific slash commands."""
|
|
206
208
|
prefix = text[1:]
|
|
207
209
|
seen: set[str] = set()
|
|
208
210
|
|
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -33,6 +33,7 @@ from glaip_sdk.branding import (
|
|
|
33
33
|
WARNING_STYLE,
|
|
34
34
|
AIPBranding,
|
|
35
35
|
)
|
|
36
|
+
from glaip_sdk.cli.commands import transcripts as transcripts_cmd
|
|
36
37
|
from glaip_sdk.cli.commands.configure import configure_command, load_config
|
|
37
38
|
from glaip_sdk.cli.commands.update import update_command
|
|
38
39
|
from glaip_sdk.cli.slash.agent_session import AgentRunSession
|
|
@@ -46,9 +47,7 @@ from glaip_sdk.cli.slash.prompt import (
|
|
|
46
47
|
)
|
|
47
48
|
from glaip_sdk.cli.transcript import (
|
|
48
49
|
export_cached_transcript,
|
|
49
|
-
|
|
50
|
-
resolve_manifest_for_export,
|
|
51
|
-
suggest_filename,
|
|
50
|
+
load_history_snapshot,
|
|
52
51
|
)
|
|
53
52
|
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
54
53
|
from glaip_sdk.cli.update_notifier import maybe_notify_update
|
|
@@ -56,6 +55,7 @@ from glaip_sdk.cli.utils import (
|
|
|
56
55
|
_fuzzy_pick_for_resources,
|
|
57
56
|
command_hint,
|
|
58
57
|
format_command_hint,
|
|
58
|
+
format_size,
|
|
59
59
|
get_client,
|
|
60
60
|
)
|
|
61
61
|
from glaip_sdk.rich_components import AIPGrid, AIPPanel, AIPTable
|
|
@@ -73,6 +73,39 @@ class SlashCommand:
|
|
|
73
73
|
aliases: tuple[str, ...] = ()
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
NEW_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
|
|
77
|
+
{
|
|
78
|
+
"cli": "transcripts",
|
|
79
|
+
"slash": "transcripts",
|
|
80
|
+
"description": "Review transcript cache",
|
|
81
|
+
"tag": "NEW",
|
|
82
|
+
"priority": 10,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
DEFAULT_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
|
|
88
|
+
{
|
|
89
|
+
"cli": "status",
|
|
90
|
+
"slash": "status",
|
|
91
|
+
"description": "Connection check",
|
|
92
|
+
"priority": 0,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"cli": "agents list",
|
|
96
|
+
"slash": "agents",
|
|
97
|
+
"description": "Browse agents",
|
|
98
|
+
"priority": 0,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"cli": "help",
|
|
102
|
+
"slash": "help",
|
|
103
|
+
"description": "Show all commands",
|
|
104
|
+
"priority": 0,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
76
109
|
class SlashSession:
|
|
77
110
|
"""Interactive command palette controller."""
|
|
78
111
|
|
|
@@ -99,7 +132,7 @@ class SlashSession:
|
|
|
99
132
|
self._active_renderer: Any | None = None
|
|
100
133
|
self._current_agent: Any | None = None
|
|
101
134
|
|
|
102
|
-
self._home_placeholder = "
|
|
135
|
+
self._home_placeholder = "Hint: type / to explore commands · Ctrl+D exits"
|
|
103
136
|
|
|
104
137
|
# Command string constants to avoid duplication
|
|
105
138
|
self.STATUS_COMMAND = "/status"
|
|
@@ -120,9 +153,15 @@ class SlashSession:
|
|
|
120
153
|
# ------------------------------------------------------------------
|
|
121
154
|
# Session orchestration
|
|
122
155
|
# ------------------------------------------------------------------
|
|
123
|
-
def refresh_branding(
|
|
156
|
+
def refresh_branding(
|
|
157
|
+
self,
|
|
158
|
+
sdk_version: str | None = None,
|
|
159
|
+
*,
|
|
160
|
+
branding_cls: type[AIPBranding] | None = None,
|
|
161
|
+
) -> None:
|
|
124
162
|
"""Refresh branding assets after an in-session SDK upgrade."""
|
|
125
|
-
|
|
163
|
+
branding_type = branding_cls or AIPBranding
|
|
164
|
+
self._branding = branding_type.create_from_sdk(
|
|
126
165
|
sdk_version=sdk_version,
|
|
127
166
|
package_name="glaip-sdk",
|
|
128
167
|
)
|
|
@@ -263,6 +302,10 @@ class SlashSession:
|
|
|
263
302
|
return False
|
|
264
303
|
return True
|
|
265
304
|
|
|
305
|
+
def _continue_session(self) -> bool:
|
|
306
|
+
"""Signal that the slash session should remain active."""
|
|
307
|
+
return not self._should_exit
|
|
308
|
+
|
|
266
309
|
# ------------------------------------------------------------------
|
|
267
310
|
# Command handlers
|
|
268
311
|
# ------------------------------------------------------------------
|
|
@@ -345,7 +388,7 @@ class SlashSession:
|
|
|
345
388
|
self._show_default_quick_actions()
|
|
346
389
|
except click.ClickException as exc:
|
|
347
390
|
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
348
|
-
return
|
|
391
|
+
return self._continue_session()
|
|
349
392
|
|
|
350
393
|
def _cmd_status(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
351
394
|
ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
|
|
@@ -374,7 +417,116 @@ class SlashSession:
|
|
|
374
417
|
ctx_obj.pop("_slash_console", None)
|
|
375
418
|
else:
|
|
376
419
|
ctx_obj["_slash_console"] = previous_console
|
|
377
|
-
return
|
|
420
|
+
return self._continue_session()
|
|
421
|
+
|
|
422
|
+
def _cmd_transcripts(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
423
|
+
if args and args[0].lower() in {"detail", "show"}:
|
|
424
|
+
if len(args) < 2:
|
|
425
|
+
self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts detail <run_id>[/]")
|
|
426
|
+
return self._continue_session()
|
|
427
|
+
self._show_transcript_detail(args[1])
|
|
428
|
+
return self._continue_session()
|
|
429
|
+
|
|
430
|
+
limit, ok = self._parse_transcripts_limit(args)
|
|
431
|
+
if not ok:
|
|
432
|
+
return self._continue_session()
|
|
433
|
+
|
|
434
|
+
snapshot = load_history_snapshot(limit=limit, ctx=self.ctx)
|
|
435
|
+
|
|
436
|
+
if self._handle_transcripts_empty(snapshot, limit):
|
|
437
|
+
return self._continue_session()
|
|
438
|
+
|
|
439
|
+
self._render_transcripts_snapshot(snapshot)
|
|
440
|
+
return self._continue_session()
|
|
441
|
+
|
|
442
|
+
def _parse_transcripts_limit(self, args: list[str]) -> tuple[int | None, bool]:
|
|
443
|
+
if not args:
|
|
444
|
+
return None, True
|
|
445
|
+
try:
|
|
446
|
+
limit = int(args[0])
|
|
447
|
+
except ValueError:
|
|
448
|
+
self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts [limit][/]")
|
|
449
|
+
return None, False
|
|
450
|
+
if limit < 0:
|
|
451
|
+
self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts [limit][/]")
|
|
452
|
+
return None, False
|
|
453
|
+
return limit, True
|
|
454
|
+
|
|
455
|
+
def _handle_transcripts_empty(self, snapshot: Any, limit: int | None) -> bool:
|
|
456
|
+
if snapshot.cached_entries == 0:
|
|
457
|
+
self.console.print(f"[{WARNING_STYLE}]No cached transcripts yet. Run an agent first.[/]")
|
|
458
|
+
for warning in snapshot.warnings:
|
|
459
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
460
|
+
return True
|
|
461
|
+
if limit == 0 and snapshot.cached_entries:
|
|
462
|
+
self.console.print(f"[{WARNING_STYLE}]Limit is 0; nothing to display.[/]")
|
|
463
|
+
return True
|
|
464
|
+
return False
|
|
465
|
+
|
|
466
|
+
def _render_transcripts_snapshot(self, snapshot: Any) -> None:
|
|
467
|
+
size_text = format_size(snapshot.total_size_bytes)
|
|
468
|
+
header = f"[dim]Manifest: {snapshot.manifest_path} · {snapshot.total_entries} runs · {size_text} used[/]"
|
|
469
|
+
self.console.print(header)
|
|
470
|
+
|
|
471
|
+
if snapshot.limit_clamped:
|
|
472
|
+
self.console.print(
|
|
473
|
+
f"[{WARNING_STYLE}]Requested limit exceeded maximum; showing first {snapshot.limit_applied} runs.[/]"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
if snapshot.total_entries > len(snapshot.entries):
|
|
477
|
+
subset_message = (
|
|
478
|
+
f"[dim]Showing {len(snapshot.entries)} of {snapshot.total_entries} "
|
|
479
|
+
f"runs (limit={snapshot.limit_applied}).[/]"
|
|
480
|
+
)
|
|
481
|
+
self.console.print(subset_message)
|
|
482
|
+
self.console.print("[dim]Hint: run `/transcripts <limit>` to change how many rows are displayed.[/]")
|
|
483
|
+
|
|
484
|
+
if snapshot.migration_summary:
|
|
485
|
+
self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
486
|
+
|
|
487
|
+
for warning in snapshot.warnings:
|
|
488
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
489
|
+
|
|
490
|
+
table = transcripts_cmd._build_table(snapshot.entries)
|
|
491
|
+
self.console.print(table)
|
|
492
|
+
self.console.print("[dim]! Missing transcript[/]")
|
|
493
|
+
|
|
494
|
+
def _show_transcript_detail(self, run_id: str) -> None:
|
|
495
|
+
"""Render the cached transcript log for a single run."""
|
|
496
|
+
snapshot = load_history_snapshot(ctx=self.ctx)
|
|
497
|
+
entry = snapshot.index.get(run_id)
|
|
498
|
+
if entry is None:
|
|
499
|
+
self.console.print(f"[{WARNING_STYLE}]Run id {run_id} was not found in the cache manifest.[/]")
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
transcript_path, transcript_text = transcripts_cmd._load_transcript_text(entry)
|
|
504
|
+
except click.ClickException as exc:
|
|
505
|
+
self.console.print(f"[{WARNING_STYLE}]{exc}[/]")
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
meta, events = transcripts_cmd._decode_transcript(transcript_text)
|
|
509
|
+
if transcripts_cmd._maybe_launch_transcript_viewer(
|
|
510
|
+
self.ctx,
|
|
511
|
+
entry,
|
|
512
|
+
meta,
|
|
513
|
+
events,
|
|
514
|
+
console_override=self.console,
|
|
515
|
+
force=True,
|
|
516
|
+
initial_view="transcript",
|
|
517
|
+
):
|
|
518
|
+
if snapshot.migration_summary:
|
|
519
|
+
self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
520
|
+
for warning in snapshot.warnings:
|
|
521
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
522
|
+
return
|
|
523
|
+
|
|
524
|
+
if snapshot.migration_summary:
|
|
525
|
+
self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
526
|
+
for warning in snapshot.warnings:
|
|
527
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
528
|
+
view = transcripts_cmd._render_transcript_display(entry, snapshot.manifest_path, transcript_path, meta, events)
|
|
529
|
+
self.console.print(view, markup=False, highlight=False, soft_wrap=True, end="")
|
|
378
530
|
|
|
379
531
|
def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
380
532
|
client = self._get_client_or_fail()
|
|
@@ -442,7 +594,7 @@ class SlashSession:
|
|
|
442
594
|
self._render_header()
|
|
443
595
|
|
|
444
596
|
self._show_agent_followup_actions(picked_agent)
|
|
445
|
-
return
|
|
597
|
+
return self._continue_session()
|
|
446
598
|
|
|
447
599
|
def _show_agent_followup_actions(self, picked_agent: Any) -> None:
|
|
448
600
|
"""Show follow-up action hints after agent session."""
|
|
@@ -498,6 +650,13 @@ class SlashSession:
|
|
|
498
650
|
handler=SlashSession._cmd_status,
|
|
499
651
|
)
|
|
500
652
|
)
|
|
653
|
+
self._register(
|
|
654
|
+
SlashCommand(
|
|
655
|
+
name="transcripts",
|
|
656
|
+
help="Review cached transcript history. Add a number (e.g. `/transcripts 5`) to change the row limit.",
|
|
657
|
+
handler=SlashSession._cmd_transcripts,
|
|
658
|
+
)
|
|
659
|
+
)
|
|
501
660
|
self._register(
|
|
502
661
|
SlashCommand(
|
|
503
662
|
name="agents",
|
|
@@ -574,55 +733,13 @@ class SlashSession:
|
|
|
574
733
|
manifest = ctx_obj.get("_last_transcript_manifest")
|
|
575
734
|
return payload, manifest
|
|
576
735
|
|
|
577
|
-
def _cmd_export(self,
|
|
736
|
+
def _cmd_export(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
578
737
|
"""Slash handler for `/export` command."""
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if run_id:
|
|
585
|
-
self.console.print(
|
|
586
|
-
f"[{WARNING_STYLE}]No cached transcript found with run id {run_id!r}. "
|
|
587
|
-
"Omit the run id to export the most recent run.[/]"
|
|
588
|
-
)
|
|
589
|
-
else:
|
|
590
|
-
self.console.print(f"[{WARNING_STYLE}]No cached transcripts available yet. Run an agent first.[/]")
|
|
591
|
-
return False
|
|
592
|
-
|
|
593
|
-
destination = self._resolve_export_destination(path_arg, manifest_entry)
|
|
594
|
-
if destination is None:
|
|
595
|
-
return False
|
|
596
|
-
|
|
597
|
-
try:
|
|
598
|
-
exported = export_cached_transcript(
|
|
599
|
-
destination=destination,
|
|
600
|
-
run_id=manifest_entry.get("run_id"),
|
|
601
|
-
)
|
|
602
|
-
except FileNotFoundError as exc:
|
|
603
|
-
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
604
|
-
return False
|
|
605
|
-
except Exception as exc: # pragma: no cover - unexpected IO failures
|
|
606
|
-
self.console.print(f"[{ERROR_STYLE}]Failed to export transcript: {exc}[/]")
|
|
607
|
-
return False
|
|
608
|
-
else:
|
|
609
|
-
self.console.print(f"[{SUCCESS_STYLE}]Transcript exported to[/] {exported}")
|
|
610
|
-
return True
|
|
611
|
-
|
|
612
|
-
def _resolve_export_destination(self, path_arg: str | None, manifest_entry: dict[str, Any]) -> Path | None:
|
|
613
|
-
if path_arg:
|
|
614
|
-
return normalise_export_destination(Path(path_arg))
|
|
615
|
-
|
|
616
|
-
default_name = suggest_filename(manifest_entry)
|
|
617
|
-
prompt = f"Save transcript to [{default_name}]: "
|
|
618
|
-
try:
|
|
619
|
-
response = self.console.input(prompt)
|
|
620
|
-
except EOFError:
|
|
621
|
-
self.console.print("[dim]Export cancelled.[/dim]")
|
|
622
|
-
return None
|
|
623
|
-
|
|
624
|
-
chosen = response.strip() or default_name
|
|
625
|
-
return normalise_export_destination(Path(chosen))
|
|
738
|
+
self.console.print(
|
|
739
|
+
f"[{WARNING_STYLE}]`/export` is deprecated. Use `/transcripts`, select a run, "
|
|
740
|
+
"and open the transcript viewer to export.[/]"
|
|
741
|
+
)
|
|
742
|
+
return True
|
|
626
743
|
|
|
627
744
|
def _cmd_update(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
628
745
|
"""Slash handler for `/update` command."""
|
|
@@ -981,35 +1098,73 @@ class SlashSession:
|
|
|
981
1098
|
return None
|
|
982
1099
|
|
|
983
1100
|
def _show_default_quick_actions(self) -> None:
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
command_hint("agents list", slash_command="agents", ctx=self.ctx),
|
|
991
|
-
"Browse agents",
|
|
992
|
-
),
|
|
993
|
-
(
|
|
994
|
-
command_hint("help", slash_command="help", ctx=self.ctx),
|
|
995
|
-
"Show all commands",
|
|
996
|
-
),
|
|
997
|
-
]
|
|
998
|
-
filtered = [(cmd, desc) for cmd, desc in hints if cmd]
|
|
999
|
-
if filtered:
|
|
1000
|
-
self._show_quick_actions(filtered, title="Quick actions")
|
|
1101
|
+
new_hints = self._collect_quick_action_hints(NEW_QUICK_ACTIONS, highlight_new=True)
|
|
1102
|
+
evergreen_hints = self._collect_quick_action_hints(DEFAULT_QUICK_ACTIONS)
|
|
1103
|
+
if new_hints or evergreen_hints:
|
|
1104
|
+
self.console.print(f"[dim]{'─' * 40}[/]")
|
|
1105
|
+
self._render_quick_action_group(new_hints, "New commands")
|
|
1106
|
+
self._render_quick_action_group(evergreen_hints, "Quick actions")
|
|
1001
1107
|
self._default_actions_shown = True
|
|
1002
1108
|
|
|
1109
|
+
def _collect_quick_action_hints(
|
|
1110
|
+
self,
|
|
1111
|
+
actions: Iterable[dict[str, Any]],
|
|
1112
|
+
*,
|
|
1113
|
+
highlight_new: bool = False,
|
|
1114
|
+
) -> list[tuple[str, str]]:
|
|
1115
|
+
collected: list[tuple[str, str]] = []
|
|
1116
|
+
for action in sorted(actions, key=lambda payload: payload.get("priority", 0), reverse=True):
|
|
1117
|
+
hint = self._build_quick_action_hint(action, highlight_new=highlight_new)
|
|
1118
|
+
if hint:
|
|
1119
|
+
collected.append(hint)
|
|
1120
|
+
return collected
|
|
1121
|
+
|
|
1122
|
+
def _build_quick_action_hint(
|
|
1123
|
+
self,
|
|
1124
|
+
action: dict[str, Any],
|
|
1125
|
+
*,
|
|
1126
|
+
highlight_new: bool = False,
|
|
1127
|
+
) -> tuple[str, str] | None:
|
|
1128
|
+
command = command_hint(action.get("cli"), slash_command=action.get("slash"), ctx=self.ctx)
|
|
1129
|
+
if not command:
|
|
1130
|
+
return None
|
|
1131
|
+
description = action.get("description", "")
|
|
1132
|
+
tag = action.get("tag")
|
|
1133
|
+
if tag:
|
|
1134
|
+
description = f"[{ACCENT_STYLE}]{tag}[/] · {description}"
|
|
1135
|
+
if highlight_new:
|
|
1136
|
+
description = f"✨ {description}"
|
|
1137
|
+
return command, description
|
|
1138
|
+
|
|
1139
|
+
def _render_quick_action_group(self, hints: list[tuple[str, str]], title: str) -> None:
|
|
1140
|
+
if not hints:
|
|
1141
|
+
return
|
|
1142
|
+
formatted_tokens: list[str] = []
|
|
1143
|
+
for command, description in hints:
|
|
1144
|
+
formatted = format_command_hint(command, description)
|
|
1145
|
+
if formatted:
|
|
1146
|
+
formatted_tokens.append(f"• {formatted}")
|
|
1147
|
+
if not formatted_tokens:
|
|
1148
|
+
return
|
|
1149
|
+
header = f"[dim]{title}[/dim] · {formatted_tokens[0]}"
|
|
1150
|
+
self.console.print(header)
|
|
1151
|
+
remaining = formatted_tokens[1:]
|
|
1152
|
+
for chunk in self._chunk_tokens(remaining, size=3):
|
|
1153
|
+
self.console.print(" " + " ".join(chunk))
|
|
1154
|
+
|
|
1155
|
+
def _chunk_tokens(self, tokens: list[str], *, size: int) -> Iterable[list[str]]:
|
|
1156
|
+
for index in range(0, len(tokens), size):
|
|
1157
|
+
yield tokens[index : index + size]
|
|
1158
|
+
|
|
1003
1159
|
def _render_home_hint(self) -> None:
|
|
1004
1160
|
if self._home_hint_shown:
|
|
1005
1161
|
return
|
|
1006
|
-
|
|
1007
|
-
f"[{HINT_PREFIX_STYLE}]Hint:[/]"
|
|
1008
|
-
f"
|
|
1009
|
-
"
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
self.console.print("\n".join(hint_lines))
|
|
1162
|
+
hint_text = (
|
|
1163
|
+
f"[{HINT_PREFIX_STYLE}]Hint:[/] "
|
|
1164
|
+
f"Type {format_command_hint('/') or '/'} to explore commands · "
|
|
1165
|
+
"Press [dim]Ctrl+D[/] to quit"
|
|
1166
|
+
)
|
|
1167
|
+
self.console.print(hint_text)
|
|
1013
1168
|
self._home_hint_shown = True
|
|
1014
1169
|
|
|
1015
1170
|
def _show_quick_actions(
|
|
@@ -1019,26 +1174,40 @@ class SlashSession:
|
|
|
1019
1174
|
title: str = "Quick actions",
|
|
1020
1175
|
inline: bool = False,
|
|
1021
1176
|
) -> None:
|
|
1022
|
-
hint_list =
|
|
1177
|
+
hint_list = self._normalize_quick_action_hints(hints)
|
|
1023
1178
|
if not hint_list:
|
|
1024
1179
|
return
|
|
1025
1180
|
|
|
1026
1181
|
if inline:
|
|
1027
|
-
|
|
1028
|
-
for command, description in hint_list:
|
|
1029
|
-
formatted = format_command_hint(command, description)
|
|
1030
|
-
if formatted:
|
|
1031
|
-
lines.append(formatted)
|
|
1032
|
-
if lines:
|
|
1033
|
-
self.console.print("\n".join(lines))
|
|
1182
|
+
self._render_inline_quick_actions(hint_list, title)
|
|
1034
1183
|
return
|
|
1035
1184
|
|
|
1185
|
+
self._render_panel_quick_actions(hint_list, title)
|
|
1186
|
+
|
|
1187
|
+
def _normalize_quick_action_hints(self, hints: Iterable[tuple[str, str]]) -> list[tuple[str, str]]:
|
|
1188
|
+
return [(command, description) for command, description in hints if command]
|
|
1189
|
+
|
|
1190
|
+
def _render_inline_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
|
|
1191
|
+
tokens: list[str] = []
|
|
1192
|
+
for command, description in hint_list:
|
|
1193
|
+
formatted = format_command_hint(command, description)
|
|
1194
|
+
if formatted:
|
|
1195
|
+
tokens.append(formatted)
|
|
1196
|
+
if not tokens:
|
|
1197
|
+
return
|
|
1198
|
+
prefix = f"[dim]{title}:[/]" if title else ""
|
|
1199
|
+
body = " ".join(tokens)
|
|
1200
|
+
text = f"{prefix} {body}" if prefix else body
|
|
1201
|
+
self.console.print(text.strip())
|
|
1202
|
+
|
|
1203
|
+
def _render_panel_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
|
|
1036
1204
|
body_lines: list[Text] = []
|
|
1037
1205
|
for command, description in hint_list:
|
|
1038
1206
|
formatted = format_command_hint(command, description)
|
|
1039
1207
|
if formatted:
|
|
1040
1208
|
body_lines.append(Text.from_markup(formatted))
|
|
1041
|
-
|
|
1209
|
+
if not body_lines:
|
|
1210
|
+
return
|
|
1042
1211
|
panel_content = Group(*body_lines)
|
|
1043
1212
|
self.console.print(AIPPanel(panel_content, title=title, border_style=SECONDARY_LIGHT, expand=False))
|
|
1044
1213
|
|