glaip-sdk 0.0.19__py3-none-any.whl → 0.1.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 +2 -2
- glaip_sdk/branding.py +27 -2
- glaip_sdk/cli/auth.py +93 -28
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/agents.py +127 -21
- glaip_sdk/cli/commands/configure.py +141 -90
- glaip_sdk/cli/commands/mcps.py +82 -31
- glaip_sdk/cli/commands/models.py +4 -3
- glaip_sdk/cli/commands/tools.py +27 -14
- glaip_sdk/cli/commands/update.py +66 -0
- glaip_sdk/cli/config.py +13 -2
- glaip_sdk/cli/display.py +35 -26
- glaip_sdk/cli/io.py +14 -5
- glaip_sdk/cli/main.py +185 -73
- glaip_sdk/cli/pager.py +2 -1
- glaip_sdk/cli/resolution.py +4 -1
- glaip_sdk/cli/slash/__init__.py +3 -4
- glaip_sdk/cli/slash/agent_session.py +88 -36
- glaip_sdk/cli/slash/prompt.py +20 -48
- glaip_sdk/cli/slash/session.py +437 -189
- glaip_sdk/cli/transcript/__init__.py +71 -0
- glaip_sdk/cli/transcript/cache.py +338 -0
- glaip_sdk/cli/transcript/capture.py +278 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/launcher.py +79 -0
- glaip_sdk/cli/transcript/viewer.py +794 -0
- glaip_sdk/cli/update_notifier.py +29 -5
- glaip_sdk/cli/utils.py +255 -74
- glaip_sdk/client/agents.py +3 -1
- glaip_sdk/client/run_rendering.py +126 -21
- glaip_sdk/icons.py +25 -0
- glaip_sdk/models.py +6 -0
- glaip_sdk/rich_components.py +29 -1
- glaip_sdk/utils/__init__.py +1 -1
- glaip_sdk/utils/client_utils.py +6 -4
- glaip_sdk/utils/display.py +61 -32
- glaip_sdk/utils/rendering/formatting.py +55 -11
- glaip_sdk/utils/rendering/models.py +15 -2
- glaip_sdk/utils/rendering/renderer/__init__.py +0 -2
- glaip_sdk/utils/rendering/renderer/base.py +1287 -227
- glaip_sdk/utils/rendering/renderer/config.py +3 -5
- glaip_sdk/utils/rendering/renderer/debug.py +73 -16
- glaip_sdk/utils/rendering/renderer/panels.py +27 -15
- glaip_sdk/utils/rendering/renderer/progress.py +61 -38
- glaip_sdk/utils/rendering/renderer/stream.py +3 -3
- glaip_sdk/utils/rendering/renderer/toggle.py +184 -0
- glaip_sdk/utils/rendering/step_tree_state.py +102 -0
- glaip_sdk/utils/rendering/steps.py +944 -16
- glaip_sdk/utils/serialization.py +5 -2
- glaip_sdk/utils/validation.py +1 -2
- {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.1.0.dist-info}/METADATA +12 -1
- glaip_sdk-0.1.0.dist-info/RECORD +82 -0
- glaip_sdk/utils/rich_utils.py +0 -29
- glaip_sdk-0.0.19.dist-info/RECORD +0 -73
- {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.1.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.19.dist-info → glaip_sdk-0.1.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -6,24 +6,37 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import importlib
|
|
9
10
|
import os
|
|
10
11
|
import shlex
|
|
11
12
|
import sys
|
|
12
13
|
from collections.abc import Callable, Iterable
|
|
13
14
|
from dataclasses import dataclass
|
|
15
|
+
from difflib import get_close_matches
|
|
16
|
+
from pathlib import Path
|
|
14
17
|
from typing import Any
|
|
15
18
|
|
|
16
19
|
import click
|
|
17
|
-
from rich.console import Console
|
|
18
|
-
from rich.
|
|
19
|
-
|
|
20
|
-
from glaip_sdk.branding import
|
|
20
|
+
from rich.console import Console, Group
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
|
|
23
|
+
from glaip_sdk.branding import (
|
|
24
|
+
ACCENT_STYLE,
|
|
25
|
+
ERROR_STYLE,
|
|
26
|
+
HINT_COMMAND_STYLE,
|
|
27
|
+
HINT_DESCRIPTION_COLOR,
|
|
28
|
+
HINT_PREFIX_STYLE,
|
|
29
|
+
INFO_STYLE,
|
|
30
|
+
PRIMARY,
|
|
31
|
+
SECONDARY_LIGHT,
|
|
32
|
+
SUCCESS_STYLE,
|
|
33
|
+
WARNING_STYLE,
|
|
34
|
+
AIPBranding,
|
|
35
|
+
)
|
|
21
36
|
from glaip_sdk.cli.commands.configure import configure_command, load_config
|
|
22
|
-
from glaip_sdk.cli.
|
|
23
|
-
from glaip_sdk.
|
|
24
|
-
|
|
25
|
-
from .agent_session import AgentRunSession
|
|
26
|
-
from .prompt import (
|
|
37
|
+
from glaip_sdk.cli.commands.update import update_command
|
|
38
|
+
from glaip_sdk.cli.slash.agent_session import AgentRunSession
|
|
39
|
+
from glaip_sdk.cli.slash.prompt import (
|
|
27
40
|
FormattedText,
|
|
28
41
|
PromptSession,
|
|
29
42
|
Style,
|
|
@@ -31,6 +44,21 @@ from .prompt import (
|
|
|
31
44
|
setup_prompt_toolkit,
|
|
32
45
|
to_formatted_text,
|
|
33
46
|
)
|
|
47
|
+
from glaip_sdk.cli.transcript import (
|
|
48
|
+
export_cached_transcript,
|
|
49
|
+
normalise_export_destination,
|
|
50
|
+
resolve_manifest_for_export,
|
|
51
|
+
suggest_filename,
|
|
52
|
+
)
|
|
53
|
+
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
54
|
+
from glaip_sdk.cli.update_notifier import maybe_notify_update
|
|
55
|
+
from glaip_sdk.cli.utils import (
|
|
56
|
+
_fuzzy_pick_for_resources,
|
|
57
|
+
command_hint,
|
|
58
|
+
format_command_hint,
|
|
59
|
+
get_client,
|
|
60
|
+
)
|
|
61
|
+
from glaip_sdk.rich_components import AIPGrid, AIPPanel, AIPTable
|
|
34
62
|
|
|
35
63
|
SlashHandler = Callable[["SlashSession", list[str], bool], bool]
|
|
36
64
|
|
|
@@ -68,8 +96,8 @@ class SlashSession:
|
|
|
68
96
|
self._interactive = bool(sys.stdin.isatty() and sys.stdout.isatty())
|
|
69
97
|
self._config_cache: dict[str, Any] | None = None
|
|
70
98
|
self._welcome_rendered = False
|
|
71
|
-
self._verbose_enabled = False
|
|
72
99
|
self._active_renderer: Any | None = None
|
|
100
|
+
self._current_agent: Any | None = None
|
|
73
101
|
|
|
74
102
|
self._home_placeholder = "Start with / to browse commands"
|
|
75
103
|
|
|
@@ -84,6 +112,10 @@ class SlashSession:
|
|
|
84
112
|
self._branding = AIPBranding.create_from_sdk()
|
|
85
113
|
self._suppress_login_layout = False
|
|
86
114
|
self._default_actions_shown = False
|
|
115
|
+
self._update_prompt_shown = False
|
|
116
|
+
self._update_notifier = maybe_notify_update
|
|
117
|
+
self._home_hint_shown = False
|
|
118
|
+
self._agent_transcript_ready: dict[str, str] = {}
|
|
87
119
|
|
|
88
120
|
# ------------------------------------------------------------------
|
|
89
121
|
# Session orchestration
|
|
@@ -96,18 +128,31 @@ class SlashSession:
|
|
|
96
128
|
|
|
97
129
|
def run(self, initial_commands: Iterable[str] | None = None) -> None:
|
|
98
130
|
"""Start the command palette session loop."""
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
131
|
+
ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
|
|
132
|
+
previous_session = None
|
|
133
|
+
if ctx_obj is not None:
|
|
134
|
+
previous_session = ctx_obj.get("_slash_session")
|
|
135
|
+
ctx_obj["_slash_session"] = self
|
|
102
136
|
|
|
103
|
-
|
|
104
|
-
|
|
137
|
+
try:
|
|
138
|
+
if not self._interactive:
|
|
139
|
+
self._run_non_interactive(initial_commands)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
if not self._ensure_configuration():
|
|
143
|
+
return
|
|
105
144
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
self.
|
|
109
|
-
|
|
110
|
-
|
|
145
|
+
self._maybe_show_update_prompt()
|
|
146
|
+
self._render_header(initial=not self._welcome_rendered)
|
|
147
|
+
if not self._default_actions_shown:
|
|
148
|
+
self._show_default_quick_actions()
|
|
149
|
+
self._run_interactive_loop()
|
|
150
|
+
finally:
|
|
151
|
+
if ctx_obj is not None:
|
|
152
|
+
if previous_session is None:
|
|
153
|
+
ctx_obj.pop("_slash_session", None)
|
|
154
|
+
else:
|
|
155
|
+
ctx_obj["_slash_session"] = previous_session
|
|
111
156
|
|
|
112
157
|
def _run_interactive_loop(self) -> None:
|
|
113
158
|
"""Run the main interactive command loop."""
|
|
@@ -131,12 +176,13 @@ class SlashSession:
|
|
|
131
176
|
return True
|
|
132
177
|
|
|
133
178
|
if raw == "/":
|
|
179
|
+
self._render_home_hint()
|
|
134
180
|
self._cmd_help([], invoked_from_agent=False)
|
|
135
181
|
return True
|
|
136
182
|
|
|
137
183
|
if not raw.startswith("/"):
|
|
138
184
|
self.console.print(
|
|
139
|
-
"[
|
|
185
|
+
f"[{INFO_STYLE}]Hint:[/] start commands with `/`. Try `/agents` to select an agent."
|
|
140
186
|
)
|
|
141
187
|
return True
|
|
142
188
|
|
|
@@ -160,14 +206,14 @@ class SlashSession:
|
|
|
160
206
|
"""Ensure the CLI has both API URL and credentials before continuing."""
|
|
161
207
|
while not self._configuration_ready():
|
|
162
208
|
self.console.print(
|
|
163
|
-
"[
|
|
209
|
+
f"[{WARNING_STYLE}]Configuration required.[/] Launching `/login` wizard..."
|
|
164
210
|
)
|
|
165
211
|
self._suppress_login_layout = True
|
|
166
212
|
try:
|
|
167
213
|
self._cmd_login([], False)
|
|
168
214
|
except KeyboardInterrupt:
|
|
169
215
|
self.console.print(
|
|
170
|
-
"[
|
|
216
|
+
f"[{ERROR_STYLE}]Configuration aborted. Closing the command palette.[/]"
|
|
171
217
|
)
|
|
172
218
|
return False
|
|
173
219
|
finally:
|
|
@@ -193,7 +239,7 @@ class SlashSession:
|
|
|
193
239
|
"""Parse and execute a single slash command string."""
|
|
194
240
|
verb, args = self._parse(raw)
|
|
195
241
|
if not verb:
|
|
196
|
-
self.console.print("[
|
|
242
|
+
self.console.print(f"[{ERROR_STYLE}]Unrecognised command[/]")
|
|
197
243
|
return True
|
|
198
244
|
|
|
199
245
|
command = self._commands.get(verb)
|
|
@@ -201,11 +247,13 @@ class SlashSession:
|
|
|
201
247
|
suggestion = self._suggest(verb)
|
|
202
248
|
if suggestion:
|
|
203
249
|
self.console.print(
|
|
204
|
-
f"[
|
|
250
|
+
f"[{WARNING_STYLE}]Unknown command '{verb}'. Did you mean '/{suggestion}'?[/]"
|
|
205
251
|
)
|
|
206
252
|
else:
|
|
253
|
+
help_command = "/help"
|
|
254
|
+
help_hint = format_command_hint(help_command) or help_command
|
|
207
255
|
self.console.print(
|
|
208
|
-
"[
|
|
256
|
+
f"[{WARNING_STYLE}]Unknown command '{verb}'. Type {help_hint} for a list of options.[/]"
|
|
209
257
|
)
|
|
210
258
|
return True
|
|
211
259
|
|
|
@@ -225,32 +273,45 @@ class SlashSession:
|
|
|
225
273
|
else:
|
|
226
274
|
self._render_global_help()
|
|
227
275
|
except Exception as exc: # pragma: no cover - UI/display errors
|
|
228
|
-
self.console.print(f"[
|
|
276
|
+
self.console.print(f"[{ERROR_STYLE}]Error displaying help: {exc}[/]")
|
|
229
277
|
return False
|
|
230
278
|
|
|
231
279
|
return True
|
|
232
280
|
|
|
233
281
|
def _render_agent_help(self) -> None:
|
|
234
|
-
table =
|
|
235
|
-
table.add_column("Input", style=
|
|
236
|
-
table.add_column("What happens", style=
|
|
282
|
+
table = AIPTable()
|
|
283
|
+
table.add_column("Input", style=HINT_COMMAND_STYLE, no_wrap=True)
|
|
284
|
+
table.add_column("What happens", style=HINT_DESCRIPTION_COLOR)
|
|
237
285
|
table.add_row("<message>", "Run the active agent once with that prompt.")
|
|
238
286
|
table.add_row("/details", "Show the full agent export and metadata.")
|
|
239
287
|
table.add_row(self.STATUS_COMMAND, "Display connection status without leaving.")
|
|
240
|
-
table.add_row("/
|
|
288
|
+
table.add_row("/export [path]", "Export the latest agent transcript as JSONL.")
|
|
241
289
|
table.add_row("/exit (/back)", "Return to the slash home screen.")
|
|
242
290
|
table.add_row("/help (/?)", "Display this context-aware menu.")
|
|
243
|
-
|
|
291
|
+
|
|
292
|
+
panel_items = [table]
|
|
244
293
|
if self.last_run_input:
|
|
245
|
-
|
|
294
|
+
panel_items.append(
|
|
295
|
+
Text.from_markup(f"[dim]Last run input:[/] {self.last_run_input}")
|
|
296
|
+
)
|
|
297
|
+
panel_items.append(
|
|
298
|
+
Text.from_markup(
|
|
299
|
+
"[dim]Global commands (e.g. `/login`, `/status`) remain available inside the agent prompt.[/dim]"
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
|
|
246
303
|
self.console.print(
|
|
247
|
-
|
|
304
|
+
AIPPanel(
|
|
305
|
+
Group(*panel_items),
|
|
306
|
+
title="Agent Context",
|
|
307
|
+
border_style=PRIMARY,
|
|
308
|
+
)
|
|
248
309
|
)
|
|
249
310
|
|
|
250
311
|
def _render_global_help(self) -> None:
|
|
251
|
-
table =
|
|
252
|
-
table.add_column("Command", style=
|
|
253
|
-
table.add_column("Description", style=
|
|
312
|
+
table = AIPTable()
|
|
313
|
+
table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
|
|
314
|
+
table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
|
|
254
315
|
|
|
255
316
|
for cmd in sorted(self._unique_commands.values(), key=lambda c: c.name):
|
|
256
317
|
aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
|
|
@@ -259,13 +320,20 @@ class SlashSession:
|
|
|
259
320
|
verb = f"{verb} ({aliases})"
|
|
260
321
|
table.add_row(verb, cmd.help)
|
|
261
322
|
|
|
262
|
-
|
|
323
|
+
tip = Text.from_markup(
|
|
324
|
+
f"[{HINT_PREFIX_STYLE}]Tip:[/] {format_command_hint(self.AGENTS_COMMAND) or self.AGENTS_COMMAND} lets you jump into an agent run prompt quickly."
|
|
325
|
+
)
|
|
326
|
+
|
|
263
327
|
self.console.print(
|
|
264
|
-
|
|
328
|
+
AIPPanel(
|
|
329
|
+
Group(table, tip),
|
|
330
|
+
title="Slash Commands",
|
|
331
|
+
border_style=PRIMARY,
|
|
332
|
+
)
|
|
265
333
|
)
|
|
266
334
|
|
|
267
335
|
def _cmd_login(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
268
|
-
self.console.print("[
|
|
336
|
+
self.console.print(f"[{ACCENT_STYLE}]Launching configuration wizard...[/]")
|
|
269
337
|
try:
|
|
270
338
|
self.ctx.invoke(configure_command)
|
|
271
339
|
self._config_cache = None
|
|
@@ -276,14 +344,22 @@ class SlashSession:
|
|
|
276
344
|
self._render_header(initial=True)
|
|
277
345
|
self._show_default_quick_actions()
|
|
278
346
|
except click.ClickException as exc:
|
|
279
|
-
self.console.print(f"[
|
|
347
|
+
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
280
348
|
return True
|
|
281
349
|
|
|
282
350
|
def _cmd_status(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
351
|
+
ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
|
|
352
|
+
previous_console = None
|
|
283
353
|
try:
|
|
284
|
-
|
|
354
|
+
status_module = importlib.import_module("glaip_sdk.cli.main")
|
|
355
|
+
status_command = getattr(status_module, "status")
|
|
356
|
+
|
|
357
|
+
if ctx_obj is not None:
|
|
358
|
+
previous_console = ctx_obj.get("_slash_console")
|
|
359
|
+
ctx_obj["_slash_console"] = self.console
|
|
285
360
|
|
|
286
361
|
self.ctx.invoke(status_command)
|
|
362
|
+
|
|
287
363
|
hints: list[tuple[str, str]] = [
|
|
288
364
|
(self.AGENTS_COMMAND, "Browse agents and run them")
|
|
289
365
|
]
|
|
@@ -293,7 +369,13 @@ class SlashSession:
|
|
|
293
369
|
hints.append((f"/agents {top.get('id')}", f"Reopen {label}"))
|
|
294
370
|
self._show_quick_actions(hints, title="Next actions")
|
|
295
371
|
except click.ClickException as exc:
|
|
296
|
-
self.console.print(f"[
|
|
372
|
+
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
373
|
+
finally:
|
|
374
|
+
if ctx_obj is not None:
|
|
375
|
+
if previous_console is None:
|
|
376
|
+
ctx_obj.pop("_slash_console", None)
|
|
377
|
+
else:
|
|
378
|
+
ctx_obj["_slash_console"] = previous_console
|
|
297
379
|
return True
|
|
298
380
|
|
|
299
381
|
def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
@@ -317,7 +399,7 @@ class SlashSession:
|
|
|
317
399
|
try:
|
|
318
400
|
return self._get_client()
|
|
319
401
|
except click.ClickException as exc:
|
|
320
|
-
self.console.print(f"[
|
|
402
|
+
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
321
403
|
return None
|
|
322
404
|
|
|
323
405
|
def _get_agents_or_fail(self, client: Any) -> list:
|
|
@@ -328,7 +410,7 @@ class SlashSession:
|
|
|
328
410
|
self._handle_no_agents()
|
|
329
411
|
return agents
|
|
330
412
|
except Exception as exc: # pragma: no cover - API failures
|
|
331
|
-
self.console.print(f"[
|
|
413
|
+
self.console.print(f"[{ERROR_STYLE}]Failed to load agents: {exc}[/]")
|
|
332
414
|
return []
|
|
333
415
|
|
|
334
416
|
def _handle_no_agents(self) -> None:
|
|
@@ -336,10 +418,10 @@ class SlashSession:
|
|
|
336
418
|
hint = command_hint("agents create", slash_command=None, ctx=self.ctx)
|
|
337
419
|
if hint:
|
|
338
420
|
self.console.print(
|
|
339
|
-
f"[
|
|
421
|
+
f"[{WARNING_STYLE}]No agents available. Use `{hint}` to add one.[/]"
|
|
340
422
|
)
|
|
341
423
|
else:
|
|
342
|
-
self.console.print("[
|
|
424
|
+
self.console.print(f"[{WARNING_STYLE}]No agents available.[/]")
|
|
343
425
|
|
|
344
426
|
def _resolve_or_pick_agent(self, client: Any, agents: list, args: list[str]) -> Any:
|
|
345
427
|
"""Resolve agent from args or pick interactively."""
|
|
@@ -347,7 +429,7 @@ class SlashSession:
|
|
|
347
429
|
picked_agent = self._resolve_agent_from_ref(client, agents, args[0])
|
|
348
430
|
if picked_agent is None:
|
|
349
431
|
self.console.print(
|
|
350
|
-
f"[
|
|
432
|
+
f"[{WARNING_STYLE}]Could not resolve agent '{args[0]}'. Try `/agents` to browse interactively.[/]"
|
|
351
433
|
)
|
|
352
434
|
return None
|
|
353
435
|
else:
|
|
@@ -390,7 +472,7 @@ class SlashSession:
|
|
|
390
472
|
# running.
|
|
391
473
|
return True
|
|
392
474
|
|
|
393
|
-
self.console.print("[
|
|
475
|
+
self.console.print(f"[{ACCENT_STYLE}]Closing the command palette.[/]")
|
|
394
476
|
return False
|
|
395
477
|
|
|
396
478
|
# ------------------------------------------------------------------
|
|
@@ -437,9 +519,16 @@ class SlashSession:
|
|
|
437
519
|
)
|
|
438
520
|
self._register(
|
|
439
521
|
SlashCommand(
|
|
440
|
-
name="
|
|
441
|
-
help="
|
|
442
|
-
handler=SlashSession.
|
|
522
|
+
name="export",
|
|
523
|
+
help="Export the most recent agent transcript.",
|
|
524
|
+
handler=SlashSession._cmd_export,
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
self._register(
|
|
528
|
+
SlashCommand(
|
|
529
|
+
name="update",
|
|
530
|
+
help="Upgrade the glaip-sdk package to the latest version.",
|
|
531
|
+
handler=SlashSession._cmd_update,
|
|
443
532
|
)
|
|
444
533
|
)
|
|
445
534
|
|
|
@@ -448,53 +537,121 @@ class SlashSession:
|
|
|
448
537
|
for key in (command.name, *command.aliases):
|
|
449
538
|
self._commands[key] = command
|
|
450
539
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
def verbose_enabled(self) -> bool:
|
|
456
|
-
"""Return whether verbose agent runs are enabled."""
|
|
457
|
-
return self._verbose_enabled
|
|
458
|
-
|
|
459
|
-
def set_verbose(self, enabled: bool, *, announce: bool = True) -> None:
|
|
460
|
-
"""Enable or disable verbose mode with optional announcement."""
|
|
461
|
-
if self._verbose_enabled == enabled:
|
|
540
|
+
def open_transcript_viewer(self, *, announce: bool = True) -> None:
|
|
541
|
+
"""Launch the transcript viewer for the most recent run."""
|
|
542
|
+
payload, manifest = self._get_last_transcript()
|
|
543
|
+
if payload is None or manifest is None:
|
|
462
544
|
if announce:
|
|
463
|
-
self.
|
|
545
|
+
self.console.print(
|
|
546
|
+
f"[{WARNING_STYLE}]No transcript is available yet. Run an agent first.[/]"
|
|
547
|
+
)
|
|
464
548
|
return
|
|
465
549
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
550
|
+
run_id = manifest.get("run_id")
|
|
551
|
+
if not run_id:
|
|
552
|
+
if announce:
|
|
553
|
+
self.console.print(
|
|
554
|
+
f"[{WARNING_STYLE}]Latest transcript is missing run metadata.[/]"
|
|
555
|
+
)
|
|
556
|
+
return
|
|
470
557
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
558
|
+
viewer_ctx = ViewerContext(
|
|
559
|
+
manifest_entry=manifest,
|
|
560
|
+
events=list(getattr(payload, "events", []) or []),
|
|
561
|
+
default_output=getattr(payload, "default_output", ""),
|
|
562
|
+
final_output=getattr(payload, "final_output", ""),
|
|
563
|
+
stream_started_at=getattr(payload, "started_at", None),
|
|
564
|
+
meta=getattr(payload, "meta", {}) or {},
|
|
565
|
+
)
|
|
474
566
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
567
|
+
def _export(destination: Path) -> Path:
|
|
568
|
+
return export_cached_transcript(destination=destination, run_id=run_id)
|
|
569
|
+
|
|
570
|
+
try:
|
|
571
|
+
run_viewer_session(self.console, viewer_ctx, _export)
|
|
572
|
+
except Exception as exc: # pragma: no cover - interactive failures
|
|
573
|
+
if announce:
|
|
574
|
+
self.console.print(
|
|
575
|
+
f"[{ERROR_STYLE}]Failed to launch transcript viewer: {exc}[/]"
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
def _get_last_transcript(self) -> tuple[Any | None, dict[str, Any] | None]:
|
|
579
|
+
"""Fetch the most recently stored transcript payload and manifest."""
|
|
580
|
+
ctx_obj = getattr(self.ctx, "obj", None)
|
|
581
|
+
if not isinstance(ctx_obj, dict):
|
|
582
|
+
return None, None
|
|
583
|
+
payload = ctx_obj.get("_last_transcript_payload")
|
|
584
|
+
manifest = ctx_obj.get("_last_transcript_manifest")
|
|
585
|
+
return payload, manifest
|
|
586
|
+
|
|
587
|
+
def _cmd_export(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
588
|
+
"""Slash handler for `/export` command."""
|
|
589
|
+
path_arg = args[0] if args else None
|
|
590
|
+
run_id = args[1] if len(args) > 1 else None
|
|
591
|
+
|
|
592
|
+
manifest_entry = resolve_manifest_for_export(self.ctx, run_id)
|
|
593
|
+
if manifest_entry is None:
|
|
594
|
+
if run_id:
|
|
595
|
+
self.console.print(
|
|
596
|
+
f"[{WARNING_STYLE}]No cached transcript found with run id {run_id!r}. Omit the run id to export the most recent run.[/]"
|
|
597
|
+
)
|
|
598
|
+
else:
|
|
599
|
+
self.console.print(
|
|
600
|
+
f"[{WARNING_STYLE}]No cached transcripts available yet. Run an agent first.[/]"
|
|
601
|
+
)
|
|
602
|
+
return False
|
|
603
|
+
|
|
604
|
+
destination = self._resolve_export_destination(path_arg, manifest_entry)
|
|
605
|
+
if destination is None:
|
|
606
|
+
return False
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
exported = export_cached_transcript(
|
|
610
|
+
destination=destination,
|
|
611
|
+
run_id=manifest_entry.get("run_id"),
|
|
480
612
|
)
|
|
613
|
+
except FileNotFoundError as exc:
|
|
614
|
+
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
615
|
+
return False
|
|
616
|
+
except Exception as exc: # pragma: no cover - unexpected IO failures
|
|
617
|
+
self.console.print(f"[{ERROR_STYLE}]Failed to export transcript: {exc}[/]")
|
|
618
|
+
return False
|
|
481
619
|
else:
|
|
482
|
-
self.
|
|
620
|
+
self.console.print(f"[{SUCCESS_STYLE}]Transcript exported to[/] {exported}")
|
|
621
|
+
return True
|
|
483
622
|
|
|
484
|
-
|
|
623
|
+
def _resolve_export_destination(
|
|
624
|
+
self, path_arg: str | None, manifest_entry: dict[str, Any]
|
|
625
|
+
) -> Path | None:
|
|
626
|
+
if path_arg:
|
|
627
|
+
return normalise_export_destination(Path(path_arg))
|
|
628
|
+
|
|
629
|
+
default_name = suggest_filename(manifest_entry)
|
|
630
|
+
prompt = f"Save transcript to [{default_name}]: "
|
|
631
|
+
try:
|
|
632
|
+
response = self.console.input(prompt)
|
|
633
|
+
except EOFError:
|
|
634
|
+
self.console.print("[dim]Export cancelled.[/dim]")
|
|
635
|
+
return None
|
|
485
636
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
637
|
+
chosen = response.strip() or default_name
|
|
638
|
+
return normalise_export_destination(Path(chosen))
|
|
639
|
+
|
|
640
|
+
def _cmd_update(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
641
|
+
"""Slash handler for `/update` command."""
|
|
642
|
+
if args:
|
|
489
643
|
self.console.print(
|
|
490
|
-
|
|
644
|
+
"Usage: `/update` upgrades glaip-sdk to the latest published version."
|
|
491
645
|
)
|
|
492
|
-
return
|
|
646
|
+
return True
|
|
493
647
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
648
|
+
try:
|
|
649
|
+
self.ctx.invoke(update_command)
|
|
650
|
+
return True
|
|
651
|
+
except click.ClickException as exc:
|
|
652
|
+
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
653
|
+
# Return False for update command failures to indicate the command didn't complete successfully
|
|
654
|
+
return False
|
|
498
655
|
|
|
499
656
|
# ------------------------------------------------------------------
|
|
500
657
|
# Agent run coordination helpers
|
|
@@ -510,6 +667,19 @@ class SlashSession:
|
|
|
510
667
|
return
|
|
511
668
|
self._active_renderer = None
|
|
512
669
|
|
|
670
|
+
def mark_agent_transcript_ready(self, agent_id: str, run_id: str | None) -> None:
|
|
671
|
+
"""Record that an agent has a transcript ready for the current session."""
|
|
672
|
+
if not agent_id or not run_id:
|
|
673
|
+
return
|
|
674
|
+
self._agent_transcript_ready[agent_id] = run_id
|
|
675
|
+
|
|
676
|
+
def clear_agent_transcript_ready(self, agent_id: str | None = None) -> None:
|
|
677
|
+
"""Reset transcript-ready state for an agent or for all agents."""
|
|
678
|
+
if agent_id:
|
|
679
|
+
self._agent_transcript_ready.pop(agent_id, None)
|
|
680
|
+
return
|
|
681
|
+
self._agent_transcript_ready.clear()
|
|
682
|
+
|
|
513
683
|
def notify_agent_run_started(self) -> None:
|
|
514
684
|
"""Mark that an agent run is in progress."""
|
|
515
685
|
self.clear_active_renderer()
|
|
@@ -519,7 +689,7 @@ class SlashSession:
|
|
|
519
689
|
self.clear_active_renderer()
|
|
520
690
|
|
|
521
691
|
def _sync_active_renderer(self) -> None:
|
|
522
|
-
"""Ensure the active renderer
|
|
692
|
+
"""Ensure the active renderer stays in standard (non-verbose) mode."""
|
|
523
693
|
renderer = self._active_renderer
|
|
524
694
|
if renderer is None:
|
|
525
695
|
return
|
|
@@ -528,14 +698,14 @@ class SlashSession:
|
|
|
528
698
|
apply_verbose = getattr(renderer, "apply_verbosity", None)
|
|
529
699
|
if callable(apply_verbose):
|
|
530
700
|
try:
|
|
531
|
-
apply_verbose(
|
|
701
|
+
apply_verbose(False)
|
|
532
702
|
applied = True
|
|
533
703
|
except Exception:
|
|
534
704
|
pass
|
|
535
705
|
|
|
536
706
|
if not applied and hasattr(renderer, "verbose"):
|
|
537
707
|
try:
|
|
538
|
-
renderer.verbose =
|
|
708
|
+
renderer.verbose = False
|
|
539
709
|
except Exception:
|
|
540
710
|
pass
|
|
541
711
|
|
|
@@ -555,8 +725,6 @@ class SlashSession:
|
|
|
555
725
|
return head, tokens[1:]
|
|
556
726
|
|
|
557
727
|
def _suggest(self, verb: str) -> str | None:
|
|
558
|
-
from difflib import get_close_matches
|
|
559
|
-
|
|
560
728
|
keys = [cmd.name for cmd in self._unique_commands.values()]
|
|
561
729
|
match = get_close_matches(verb, keys, n=1)
|
|
562
730
|
return match[0] if match else None
|
|
@@ -684,50 +852,121 @@ class SlashSession:
|
|
|
684
852
|
return
|
|
685
853
|
|
|
686
854
|
full_header = initial or not self._welcome_rendered
|
|
855
|
+
if full_header:
|
|
856
|
+
self._render_branding_banner()
|
|
857
|
+
self.console.rule(style=PRIMARY)
|
|
687
858
|
self._render_main_header(active_agent, full=full_header)
|
|
688
859
|
if full_header:
|
|
689
860
|
self._welcome_rendered = True
|
|
861
|
+
self.console.print()
|
|
862
|
+
|
|
863
|
+
def _render_branding_banner(self) -> None:
|
|
864
|
+
"""Render the GL AIP branding banner."""
|
|
865
|
+
banner = self._branding.get_welcome_banner()
|
|
866
|
+
heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
|
|
867
|
+
self.console.print(heading)
|
|
868
|
+
self.console.print()
|
|
869
|
+
self.console.print(banner)
|
|
870
|
+
|
|
871
|
+
def _maybe_show_update_prompt(self) -> None:
|
|
872
|
+
"""Display update prompt once per session when applicable."""
|
|
873
|
+
if self._update_prompt_shown:
|
|
874
|
+
return
|
|
875
|
+
|
|
876
|
+
self._update_notifier(
|
|
877
|
+
self._branding.version,
|
|
878
|
+
console=self.console,
|
|
879
|
+
ctx=self.ctx,
|
|
880
|
+
slash_command="update",
|
|
881
|
+
style="panel",
|
|
882
|
+
)
|
|
883
|
+
self._update_prompt_shown = True
|
|
690
884
|
|
|
691
885
|
def _render_focused_agent_header(self, active_agent: Any) -> None:
|
|
692
886
|
"""Render header when focusing on a specific agent."""
|
|
887
|
+
agent_info = self._get_agent_info(active_agent)
|
|
888
|
+
transcript_status = self._get_transcript_status(active_agent)
|
|
889
|
+
|
|
890
|
+
header_grid = self._build_header_grid(agent_info, transcript_status)
|
|
891
|
+
keybar = self._build_keybar()
|
|
892
|
+
|
|
893
|
+
header_grid.add_row(keybar, "")
|
|
894
|
+
self.console.print(
|
|
895
|
+
AIPPanel(header_grid, title="Agent Session", border_style=PRIMARY)
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
def _get_agent_info(self, active_agent: Any) -> dict[str, str]:
|
|
899
|
+
"""Extract agent information for display."""
|
|
693
900
|
agent_id = str(getattr(active_agent, "id", ""))
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
901
|
+
return {
|
|
902
|
+
"id": agent_id,
|
|
903
|
+
"name": getattr(active_agent, "name", "") or agent_id,
|
|
904
|
+
"type": getattr(active_agent, "type", "") or "-",
|
|
905
|
+
"description": getattr(active_agent, "description", "") or "",
|
|
906
|
+
}
|
|
697
907
|
|
|
698
|
-
|
|
908
|
+
def _get_transcript_status(self, active_agent: Any) -> dict[str, Any]:
|
|
909
|
+
"""Get transcript status for the active agent."""
|
|
910
|
+
agent_id = str(getattr(active_agent, "id", ""))
|
|
911
|
+
payload, manifest = self._get_last_transcript()
|
|
912
|
+
|
|
913
|
+
latest_agent_id = (manifest or {}).get("agent_id")
|
|
914
|
+
has_transcript = bool(payload and manifest and manifest.get("run_id"))
|
|
915
|
+
run_id = (manifest or {}).get("run_id")
|
|
916
|
+
transcript_ready = (
|
|
917
|
+
has_transcript
|
|
918
|
+
and latest_agent_id == agent_id
|
|
919
|
+
and self._agent_transcript_ready.get(agent_id) == run_id
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
"has_transcript": has_transcript,
|
|
924
|
+
"transcript_ready": transcript_ready,
|
|
925
|
+
"run_id": run_id,
|
|
926
|
+
}
|
|
699
927
|
|
|
700
|
-
|
|
928
|
+
def _build_header_grid(
|
|
929
|
+
self, agent_info: dict[str, str], transcript_status: dict[str, Any]
|
|
930
|
+
) -> AIPGrid:
|
|
931
|
+
"""Build the main header grid with agent information."""
|
|
932
|
+
header_grid = AIPGrid(expand=True)
|
|
701
933
|
header_grid.add_column(ratio=3)
|
|
702
934
|
header_grid.add_column(ratio=1, justify="right")
|
|
703
935
|
|
|
704
|
-
primary_line =
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
f"[green]ready[/green] · {verbose_label}",
|
|
936
|
+
primary_line = (
|
|
937
|
+
f"[bold]{agent_info['name']}[/bold] · [dim]{agent_info['type']}[/dim] · "
|
|
938
|
+
f"[{ACCENT_STYLE}]{agent_info['id']}[/]"
|
|
708
939
|
)
|
|
940
|
+
status_line = f"[{SUCCESS_STYLE}]ready[/]"
|
|
941
|
+
status_line += (
|
|
942
|
+
" · transcript ready"
|
|
943
|
+
if transcript_status["transcript_ready"]
|
|
944
|
+
else " · transcript pending"
|
|
945
|
+
)
|
|
946
|
+
header_grid.add_row(primary_line, status_line)
|
|
709
947
|
|
|
710
|
-
if description:
|
|
948
|
+
if agent_info["description"]:
|
|
949
|
+
description = agent_info["description"]
|
|
950
|
+
if not transcript_status["transcript_ready"]:
|
|
951
|
+
description = f"{description} (transcript pending)"
|
|
711
952
|
header_grid.add_row(f"[dim]{description}[/dim]", "")
|
|
712
953
|
|
|
713
|
-
|
|
714
|
-
keybar.add_column(justify="left")
|
|
715
|
-
keybar.add_column(justify="left")
|
|
716
|
-
keybar.add_column(justify="left")
|
|
717
|
-
keybar.add_column(justify="left")
|
|
718
|
-
keybar.add_row(
|
|
719
|
-
"[bold]/help[/bold] [dim]Show commands[/dim]",
|
|
720
|
-
"[bold]/details[/bold] [dim]Agent config[/dim]",
|
|
721
|
-
"[bold]/exit[/bold] [dim]Back[/dim]",
|
|
722
|
-
"[bold]Ctrl+T[/bold] [dim]Toggle verbose[/dim]",
|
|
723
|
-
)
|
|
954
|
+
return header_grid
|
|
724
955
|
|
|
725
|
-
|
|
956
|
+
def _build_keybar(self) -> AIPGrid:
|
|
957
|
+
"""Build the keybar with command hints."""
|
|
958
|
+
keybar = AIPGrid(expand=True)
|
|
959
|
+
keybar.add_column(justify="left", ratio=1)
|
|
960
|
+
keybar.add_column(justify="left", ratio=1)
|
|
726
961
|
|
|
727
|
-
|
|
728
|
-
|
|
962
|
+
keybar.add_row(
|
|
963
|
+
format_command_hint("/help", "Show commands") or "",
|
|
964
|
+
format_command_hint("/details", "Agent config") or "",
|
|
965
|
+
format_command_hint("/exit", "Back") or "",
|
|
729
966
|
)
|
|
730
967
|
|
|
968
|
+
return keybar
|
|
969
|
+
|
|
731
970
|
def _render_main_header(
|
|
732
971
|
self, active_agent: Any | None = None, *, full: bool = False
|
|
733
972
|
) -> None:
|
|
@@ -737,49 +976,31 @@ class SlashSession:
|
|
|
737
976
|
api_url = self._get_api_url(config)
|
|
738
977
|
status = "Configured" if config.get("api_key") else "Not configured"
|
|
739
978
|
|
|
979
|
+
segments = [
|
|
980
|
+
f"[dim]Base URL[/dim] • {api_url or 'Not configured'}",
|
|
981
|
+
f"[dim]Credentials[/dim] • {status}",
|
|
982
|
+
]
|
|
983
|
+
agent_info = self._build_agent_status_line(active_agent)
|
|
984
|
+
if agent_info:
|
|
985
|
+
segments.append(agent_info)
|
|
986
|
+
|
|
987
|
+
rendered_line = " ".join(segments)
|
|
988
|
+
|
|
740
989
|
if full:
|
|
741
|
-
|
|
742
|
-
f"GL AIP v{self._branding.version} · GDP Labs AI Agents Package",
|
|
743
|
-
f"API: {api_url or 'Not configured'} · Credentials: {status}",
|
|
744
|
-
(
|
|
745
|
-
f"Verbose: {'on' if self._verbose_enabled else 'off'} "
|
|
746
|
-
"(Ctrl+T toggles verbose streaming)"
|
|
747
|
-
),
|
|
748
|
-
]
|
|
749
|
-
extra: list[str] = []
|
|
750
|
-
self._add_agent_info_to_header(extra, active_agent)
|
|
751
|
-
lines.extend(extra)
|
|
752
|
-
self.console.print(
|
|
753
|
-
AIPPanel("\n".join(lines), title="GL AIP Session", border_style="cyan")
|
|
754
|
-
)
|
|
990
|
+
self.console.print(rendered_line)
|
|
755
991
|
return
|
|
756
992
|
|
|
757
|
-
status_bar =
|
|
758
|
-
status_bar.add_column(ratio=
|
|
759
|
-
status_bar.
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
status_bar.add_row("[dim]Ctrl+T toggles verbose[/dim]", "", "")
|
|
767
|
-
status_bar.add_row("[dim]Type /help for shortcuts[/dim]", "", "")
|
|
768
|
-
|
|
769
|
-
if active_agent is not None:
|
|
770
|
-
agent_id = str(getattr(active_agent, "id", ""))
|
|
771
|
-
agent_name = getattr(active_agent, "name", "") or agent_id
|
|
772
|
-
status_bar.add_row(f"[dim]Active[/dim]: {agent_name} [{agent_id}]", "", "")
|
|
773
|
-
elif self.recent_agents:
|
|
774
|
-
recent = self.recent_agents[0]
|
|
775
|
-
label = recent.get("name") or recent.get("id") or "-"
|
|
776
|
-
status_bar.add_row(
|
|
777
|
-
f"[dim]Recent[/dim]: {label} [{recent.get('id', '-')}]",
|
|
778
|
-
"",
|
|
779
|
-
"",
|
|
993
|
+
status_bar = AIPGrid(expand=True)
|
|
994
|
+
status_bar.add_column(ratio=1)
|
|
995
|
+
status_bar.add_row(rendered_line)
|
|
996
|
+
self.console.print(
|
|
997
|
+
AIPPanel(
|
|
998
|
+
status_bar,
|
|
999
|
+
border_style=PRIMARY,
|
|
1000
|
+
padding=(0, 1),
|
|
1001
|
+
expand=False,
|
|
780
1002
|
)
|
|
781
|
-
|
|
782
|
-
self.console.print(AIPPanel(status_bar, border_style="cyan"))
|
|
1003
|
+
)
|
|
783
1004
|
|
|
784
1005
|
def _get_api_url(self, config: dict[str, Any]) -> str | None:
|
|
785
1006
|
"""Get the API URL from various sources."""
|
|
@@ -788,58 +1009,85 @@ class SlashSession:
|
|
|
788
1009
|
api_url = self.ctx.obj.get("api_url")
|
|
789
1010
|
return api_url or config.get("api_url") or os.getenv("AIP_API_URL")
|
|
790
1011
|
|
|
791
|
-
def
|
|
792
|
-
|
|
793
|
-
) -> None:
|
|
794
|
-
"""Add agent information to header lines."""
|
|
1012
|
+
def _build_agent_status_line(self, active_agent: Any | None) -> str | None:
|
|
1013
|
+
"""Return a short status line about the active or recent agent."""
|
|
795
1014
|
if active_agent is not None:
|
|
796
1015
|
agent_id = str(getattr(active_agent, "id", ""))
|
|
797
1016
|
agent_name = getattr(active_agent, "name", "") or agent_id
|
|
798
|
-
|
|
799
|
-
|
|
1017
|
+
return f"[dim]Active[/dim]: {agent_name} ({agent_id})"
|
|
1018
|
+
if self.recent_agents:
|
|
800
1019
|
recent = self.recent_agents[0]
|
|
801
1020
|
label = recent.get("name") or recent.get("id") or "-"
|
|
802
|
-
|
|
1021
|
+
return f"[dim]Recent[/dim]: {label} ({recent.get('id', '-')})"
|
|
1022
|
+
return None
|
|
803
1023
|
|
|
804
1024
|
def _show_default_quick_actions(self) -> None:
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
(
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1025
|
+
hints: list[tuple[str | None, str]] = [
|
|
1026
|
+
(
|
|
1027
|
+
command_hint("status", slash_command="status", ctx=self.ctx),
|
|
1028
|
+
"Connection check",
|
|
1029
|
+
),
|
|
1030
|
+
(
|
|
1031
|
+
command_hint("agents list", slash_command="agents", ctx=self.ctx),
|
|
1032
|
+
"Browse agents",
|
|
1033
|
+
),
|
|
1034
|
+
(
|
|
1035
|
+
command_hint("help", slash_command="help", ctx=self.ctx),
|
|
1036
|
+
"Show all commands",
|
|
1037
|
+
),
|
|
1038
|
+
]
|
|
1039
|
+
filtered = [(cmd, desc) for cmd, desc in hints if cmd]
|
|
1040
|
+
if filtered:
|
|
1041
|
+
self._show_quick_actions(filtered, title="Quick actions")
|
|
811
1042
|
self._default_actions_shown = True
|
|
812
1043
|
|
|
813
1044
|
def _render_home_hint(self) -> None:
|
|
814
|
-
self.
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
)
|
|
1045
|
+
if self._home_hint_shown:
|
|
1046
|
+
return
|
|
1047
|
+
hint_lines = [
|
|
1048
|
+
f"[{HINT_PREFIX_STYLE}]Hint:[/]",
|
|
1049
|
+
f" Type {format_command_hint('/') or '/'} to explore commands",
|
|
1050
|
+
" Press [dim]Ctrl+C[/] to cancel the current entry",
|
|
1051
|
+
" Press [dim]Ctrl+D[/] to quit",
|
|
1052
|
+
]
|
|
1053
|
+
self.console.print("\n".join(hint_lines))
|
|
1054
|
+
self._home_hint_shown = True
|
|
823
1055
|
|
|
824
1056
|
def _show_quick_actions(
|
|
825
|
-
self,
|
|
1057
|
+
self,
|
|
1058
|
+
hints: Iterable[tuple[str, str]],
|
|
1059
|
+
*,
|
|
1060
|
+
title: str = "Quick actions",
|
|
1061
|
+
inline: bool = False,
|
|
826
1062
|
) -> None:
|
|
827
|
-
hint_list =
|
|
1063
|
+
hint_list = [
|
|
1064
|
+
(command, description) for command, description in hints if command
|
|
1065
|
+
]
|
|
828
1066
|
if not hint_list:
|
|
829
1067
|
return
|
|
830
1068
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1069
|
+
if inline:
|
|
1070
|
+
lines: list[str] = []
|
|
1071
|
+
for command, description in hint_list:
|
|
1072
|
+
formatted = format_command_hint(command, description)
|
|
1073
|
+
if formatted:
|
|
1074
|
+
lines.append(formatted)
|
|
1075
|
+
if lines:
|
|
1076
|
+
self.console.print("\n".join(lines))
|
|
1077
|
+
return
|
|
834
1078
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
)
|
|
1079
|
+
body_lines: list[Text] = []
|
|
1080
|
+
for command, description in hint_list:
|
|
1081
|
+
formatted = format_command_hint(command, description)
|
|
1082
|
+
if formatted:
|
|
1083
|
+
body_lines.append(Text.from_markup(formatted))
|
|
841
1084
|
|
|
842
|
-
|
|
1085
|
+
panel_content = Group(*body_lines)
|
|
1086
|
+
self.console.print(
|
|
1087
|
+
AIPPanel(
|
|
1088
|
+
panel_content, title=title, border_style=SECONDARY_LIGHT, expand=False
|
|
1089
|
+
)
|
|
1090
|
+
)
|
|
843
1091
|
|
|
844
1092
|
def _load_config(self) -> dict[str, Any]:
|
|
845
1093
|
if self._config_cache is None:
|