glaip-sdk 0.0.7__py3-none-any.whl → 0.0.8__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/branding.py +3 -3
- glaip_sdk/cli/commands/agents.py +119 -12
- glaip_sdk/cli/display.py +4 -3
- glaip_sdk/cli/main.py +51 -5
- glaip_sdk/cli/resolution.py +17 -9
- glaip_sdk/cli/slash/__init__.py +25 -0
- glaip_sdk/cli/slash/agent_session.py +146 -0
- glaip_sdk/cli/slash/prompt.py +198 -0
- glaip_sdk/cli/slash/session.py +665 -0
- glaip_sdk/cli/utils.py +99 -5
- glaip_sdk/utils/rendering/renderer/base.py +19 -1
- glaip_sdk/utils/serialization.py +49 -17
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.0.8.dist-info}/METADATA +1 -1
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.0.8.dist-info}/RECORD +16 -12
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.0.8.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.0.8.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"""SlashSession orchestrates the interactive command palette.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shlex
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Callable, Iterable
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
from glaip_sdk.branding import AIPBranding
|
|
21
|
+
from glaip_sdk.cli.commands.configure import configure_command, load_config
|
|
22
|
+
from glaip_sdk.cli.utils import _fuzzy_pick_for_resources, get_client
|
|
23
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
24
|
+
|
|
25
|
+
from .agent_session import AgentRunSession
|
|
26
|
+
from .prompt import (
|
|
27
|
+
FormattedText,
|
|
28
|
+
PromptSession,
|
|
29
|
+
Style,
|
|
30
|
+
patch_stdout,
|
|
31
|
+
setup_prompt_toolkit,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
SlashHandler = Callable[["SlashSession", list[str], bool], bool]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class SlashCommand:
|
|
39
|
+
"""Metadata for a slash command entry."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
help: str
|
|
43
|
+
handler: SlashHandler
|
|
44
|
+
aliases: tuple[str, ...] = ()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SlashSession:
|
|
48
|
+
"""Interactive command palette controller."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, ctx: click.Context, *, console: Console | None = None) -> None:
|
|
51
|
+
self.ctx = ctx
|
|
52
|
+
self.console = console or Console()
|
|
53
|
+
self._commands: dict[str, SlashCommand] = {}
|
|
54
|
+
self._unique_commands: dict[str, SlashCommand] = {}
|
|
55
|
+
self._contextual_commands: dict[str, str] = {}
|
|
56
|
+
self._contextual_include_global: bool = True
|
|
57
|
+
self._client: Any | None = None
|
|
58
|
+
self.recent_agents: list[dict[str, str]] = []
|
|
59
|
+
self.last_run_input: str | None = None
|
|
60
|
+
self._should_exit = False
|
|
61
|
+
self._interactive = bool(sys.stdin.isatty() and sys.stdout.isatty())
|
|
62
|
+
self._config_cache: dict[str, Any] | None = None
|
|
63
|
+
self._welcome_rendered = False
|
|
64
|
+
|
|
65
|
+
self._home_placeholder = "Start with / to browse commands"
|
|
66
|
+
|
|
67
|
+
# Command string constants to avoid duplication
|
|
68
|
+
self.STATUS_COMMAND = "/status"
|
|
69
|
+
self.AGENTS_COMMAND = "/agents"
|
|
70
|
+
|
|
71
|
+
self._ptk_session: PromptSession | None = None
|
|
72
|
+
self._ptk_style: Style | None = None
|
|
73
|
+
self._setup_prompt_toolkit()
|
|
74
|
+
self._register_defaults()
|
|
75
|
+
self._branding = AIPBranding.create_from_sdk()
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# Session orchestration
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def _setup_prompt_toolkit(self) -> None:
|
|
82
|
+
session, style = setup_prompt_toolkit(self, interactive=self._interactive)
|
|
83
|
+
self._ptk_session = session
|
|
84
|
+
self._ptk_style = style
|
|
85
|
+
|
|
86
|
+
def run(self, initial_commands: Iterable[str] | None = None) -> None:
|
|
87
|
+
"""Start the command palette session loop."""
|
|
88
|
+
|
|
89
|
+
if not self._interactive:
|
|
90
|
+
self._run_non_interactive(initial_commands)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
if not self._ensure_configuration():
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
self._render_header(initial=True)
|
|
97
|
+
self._render_home_hint()
|
|
98
|
+
self._run_interactive_loop()
|
|
99
|
+
|
|
100
|
+
def _run_interactive_loop(self) -> None:
|
|
101
|
+
"""Run the main interactive command loop."""
|
|
102
|
+
while not self._should_exit:
|
|
103
|
+
try:
|
|
104
|
+
raw = self._prompt("› ", placeholder=self._home_placeholder)
|
|
105
|
+
except EOFError:
|
|
106
|
+
self.console.print("\n👋 Closing the command palette.")
|
|
107
|
+
break
|
|
108
|
+
except KeyboardInterrupt:
|
|
109
|
+
self.console.print("")
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
if not self._process_command(raw):
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
def _process_command(self, raw: str) -> bool:
|
|
116
|
+
"""Process a single command input. Returns False if should exit."""
|
|
117
|
+
raw = raw.strip()
|
|
118
|
+
if not raw:
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
if raw == "/":
|
|
122
|
+
self._cmd_help([], invoked_from_agent=False)
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
if not raw.startswith("/"):
|
|
126
|
+
self.console.print(
|
|
127
|
+
"[yellow]Hint:[/] start commands with `/`. Try `/agents` to select an agent."
|
|
128
|
+
)
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
return self.handle_command(raw)
|
|
132
|
+
|
|
133
|
+
def _run_non_interactive(
|
|
134
|
+
self, initial_commands: Iterable[str] | None = None
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Run slash commands in non-interactive mode."""
|
|
137
|
+
commands = list(initial_commands or [])
|
|
138
|
+
if not commands:
|
|
139
|
+
commands = [line.strip() for line in sys.stdin if line.strip()]
|
|
140
|
+
|
|
141
|
+
for raw in commands:
|
|
142
|
+
if not raw.startswith("/"):
|
|
143
|
+
continue
|
|
144
|
+
if not self.handle_command(raw):
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
def _ensure_configuration(self) -> bool:
|
|
148
|
+
"""Ensure the CLI has both API URL and credentials before continuing."""
|
|
149
|
+
|
|
150
|
+
while not self._configuration_ready():
|
|
151
|
+
self.console.print(
|
|
152
|
+
"[yellow]Configuration required.[/] Launching `/login` wizard..."
|
|
153
|
+
)
|
|
154
|
+
try:
|
|
155
|
+
self._cmd_login([], False)
|
|
156
|
+
except KeyboardInterrupt:
|
|
157
|
+
self.console.print(
|
|
158
|
+
"[red]Configuration aborted. Closing the command palette.[/red]"
|
|
159
|
+
)
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def _configuration_ready(self) -> bool:
|
|
165
|
+
"""Check whether API URL and credentials are available."""
|
|
166
|
+
|
|
167
|
+
config = self._load_config()
|
|
168
|
+
api_url = self._get_api_url(config)
|
|
169
|
+
if not api_url:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
api_key: str | None = None
|
|
173
|
+
if isinstance(self.ctx.obj, dict):
|
|
174
|
+
api_key = self.ctx.obj.get("api_key")
|
|
175
|
+
|
|
176
|
+
api_key = api_key or config.get("api_key") or os.getenv("AIP_API_KEY")
|
|
177
|
+
return bool(api_key)
|
|
178
|
+
|
|
179
|
+
def handle_command(self, raw: str, *, invoked_from_agent: bool = False) -> bool:
|
|
180
|
+
"""Parse and execute a single slash command string."""
|
|
181
|
+
|
|
182
|
+
verb, args = self._parse(raw)
|
|
183
|
+
if not verb:
|
|
184
|
+
self.console.print("[red]Unrecognised command[/red]")
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
command = self._commands.get(verb)
|
|
188
|
+
if command is None:
|
|
189
|
+
suggestion = self._suggest(verb)
|
|
190
|
+
if suggestion:
|
|
191
|
+
self.console.print(
|
|
192
|
+
f"[yellow]Unknown command '{verb}'. Did you mean '/{suggestion}'?[/yellow]"
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
self.console.print(
|
|
196
|
+
"[yellow]Unknown command '{verb}'. Type `/help` for a list of options.[/yellow]"
|
|
197
|
+
)
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
should_continue = command.handler(self, args, invoked_from_agent)
|
|
201
|
+
if not should_continue:
|
|
202
|
+
self._should_exit = True
|
|
203
|
+
return False
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
# ------------------------------------------------------------------
|
|
207
|
+
# Command handlers
|
|
208
|
+
# ------------------------------------------------------------------
|
|
209
|
+
def _cmd_help(self, _args: list[str], invoked_from_agent: bool) -> bool:
|
|
210
|
+
try:
|
|
211
|
+
if invoked_from_agent:
|
|
212
|
+
table = Table(title="Agent Context")
|
|
213
|
+
table.add_column("Input", style="cyan", no_wrap=True)
|
|
214
|
+
table.add_column("What happens", style="green")
|
|
215
|
+
table.add_row(
|
|
216
|
+
"<message>", "Run the active agent once with that prompt."
|
|
217
|
+
)
|
|
218
|
+
table.add_row("/details", "Show the full agent export and metadata.")
|
|
219
|
+
table.add_row(
|
|
220
|
+
self.STATUS_COMMAND, "Display connection status without leaving."
|
|
221
|
+
)
|
|
222
|
+
table.add_row("/exit (/back)", "Return to the slash home screen.")
|
|
223
|
+
table.add_row("/help (/?)", "Display this context-aware menu.")
|
|
224
|
+
self.console.print(table)
|
|
225
|
+
if self.last_run_input:
|
|
226
|
+
self.console.print(f"[dim]Last run input:[/] {self.last_run_input}")
|
|
227
|
+
self.console.print(
|
|
228
|
+
"[dim]Global commands (e.g. `/login`, `/status`) remain available inside the agent prompt.[/dim]"
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
table = Table(title="Slash Commands")
|
|
232
|
+
table.add_column("Command", style="cyan", no_wrap=True)
|
|
233
|
+
table.add_column("Description", style="green")
|
|
234
|
+
|
|
235
|
+
for cmd in sorted(self._unique_commands.values(), key=lambda c: c.name):
|
|
236
|
+
aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
|
|
237
|
+
verb = f"/{cmd.name}"
|
|
238
|
+
if aliases:
|
|
239
|
+
verb = f"{verb} ({aliases})"
|
|
240
|
+
table.add_row(verb, cmd.help)
|
|
241
|
+
|
|
242
|
+
self.console.print(table)
|
|
243
|
+
self.console.print(
|
|
244
|
+
"[dim]Tip: `{self.AGENTS_COMMAND}` lets you jump into an agent run prompt quickly.[/dim]"
|
|
245
|
+
)
|
|
246
|
+
except Exception as exc: # pragma: no cover - UI/display errors
|
|
247
|
+
self.console.print(f"[red]Error displaying help: {exc}[/red]")
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
def _cmd_login(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
253
|
+
self.console.print("[cyan]Launching configuration wizard...[/cyan]")
|
|
254
|
+
try:
|
|
255
|
+
self.ctx.invoke(configure_command)
|
|
256
|
+
self._config_cache = None
|
|
257
|
+
self._render_header(initial=True)
|
|
258
|
+
self._show_quick_actions(
|
|
259
|
+
[
|
|
260
|
+
(self.STATUS_COMMAND, "Verify the connection"),
|
|
261
|
+
(self.AGENTS_COMMAND, "Pick an agent to inspect or run"),
|
|
262
|
+
]
|
|
263
|
+
)
|
|
264
|
+
except click.ClickException as exc:
|
|
265
|
+
self.console.print(f"[red]{exc}[/red]")
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
def _cmd_status(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
269
|
+
try:
|
|
270
|
+
from glaip_sdk.cli.main import status as status_command
|
|
271
|
+
|
|
272
|
+
self.ctx.invoke(status_command)
|
|
273
|
+
hints: list[tuple[str, str]] = [
|
|
274
|
+
(self.AGENTS_COMMAND, "Browse agents and run them")
|
|
275
|
+
]
|
|
276
|
+
if self.recent_agents:
|
|
277
|
+
top = self.recent_agents[0]
|
|
278
|
+
label = top.get("name") or top.get("id")
|
|
279
|
+
hints.append((f"/agents {top.get('id')}", f"Reopen {label}"))
|
|
280
|
+
self._show_quick_actions(hints, title="Next actions")
|
|
281
|
+
except click.ClickException as exc:
|
|
282
|
+
self.console.print(f"[red]{exc}[/red]")
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
286
|
+
try:
|
|
287
|
+
client = self._get_client()
|
|
288
|
+
except click.ClickException as exc:
|
|
289
|
+
self.console.print(f"[red]{exc}[/red]")
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
agents = client.list_agents()
|
|
294
|
+
except Exception as exc: # pragma: no cover - API failures
|
|
295
|
+
self.console.print(f"[red]Failed to load agents: {exc}[/red]")
|
|
296
|
+
return True
|
|
297
|
+
|
|
298
|
+
if not agents:
|
|
299
|
+
self.console.print(
|
|
300
|
+
"[yellow]No agents available. Use `aip agents create` to add one.[/yellow]"
|
|
301
|
+
)
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
if args:
|
|
305
|
+
picked_agent = self._resolve_agent_from_ref(client, agents, args[0])
|
|
306
|
+
if picked_agent is None:
|
|
307
|
+
self.console.print(
|
|
308
|
+
f"[yellow]Could not resolve agent '{args[0]}'. Try `/agents` to browse interactively.[/yellow]"
|
|
309
|
+
)
|
|
310
|
+
return True
|
|
311
|
+
else:
|
|
312
|
+
picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
|
|
313
|
+
|
|
314
|
+
if not picked_agent:
|
|
315
|
+
return True
|
|
316
|
+
|
|
317
|
+
self._remember_agent(picked_agent)
|
|
318
|
+
|
|
319
|
+
AgentRunSession(self, picked_agent).run()
|
|
320
|
+
|
|
321
|
+
# Refresh the main palette header and surface follow-up actions so the
|
|
322
|
+
# user has immediate cues after leaving the agent context.
|
|
323
|
+
self._render_header()
|
|
324
|
+
|
|
325
|
+
agent_id = str(getattr(picked_agent, "id", ""))
|
|
326
|
+
agent_label = getattr(picked_agent, "name", "") or agent_id or "this agent"
|
|
327
|
+
hints: list[tuple[str, str]] = []
|
|
328
|
+
if agent_id:
|
|
329
|
+
hints.append((f"/agents {agent_id}", f"Reopen {agent_label}"))
|
|
330
|
+
hints.extend(
|
|
331
|
+
[
|
|
332
|
+
(self.AGENTS_COMMAND, "Browse agents"),
|
|
333
|
+
(self.STATUS_COMMAND, "Check connection"),
|
|
334
|
+
]
|
|
335
|
+
)
|
|
336
|
+
self._show_quick_actions(hints, title="Next actions")
|
|
337
|
+
return True
|
|
338
|
+
|
|
339
|
+
def _cmd_exit(self, _args: list[str], invoked_from_agent: bool) -> bool:
|
|
340
|
+
if invoked_from_agent:
|
|
341
|
+
# Returning False would stop the full session; we only want to exit
|
|
342
|
+
# the agent context. Raising a custom flag keeps the outer loop
|
|
343
|
+
# running.
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
self.console.print("[cyan]Closing the command palette.")
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
# ------------------------------------------------------------------
|
|
350
|
+
# Utilities
|
|
351
|
+
# ------------------------------------------------------------------
|
|
352
|
+
def _register_defaults(self) -> None:
|
|
353
|
+
self._register(
|
|
354
|
+
SlashCommand(
|
|
355
|
+
name="help",
|
|
356
|
+
help="Show available command palette commands.",
|
|
357
|
+
handler=SlashSession._cmd_help,
|
|
358
|
+
aliases=("?",),
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
self._register(
|
|
362
|
+
SlashCommand(
|
|
363
|
+
name="login",
|
|
364
|
+
help="Run `aip configure` to set credentials.",
|
|
365
|
+
handler=SlashSession._cmd_login,
|
|
366
|
+
aliases=("configure",),
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
self._register(
|
|
370
|
+
SlashCommand(
|
|
371
|
+
name="status",
|
|
372
|
+
help="Display connection status summary.",
|
|
373
|
+
handler=SlashSession._cmd_status,
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
self._register(
|
|
377
|
+
SlashCommand(
|
|
378
|
+
name="agents",
|
|
379
|
+
help="Pick an agent and enter a focused run prompt.",
|
|
380
|
+
handler=SlashSession._cmd_agents,
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
self._register(
|
|
384
|
+
SlashCommand(
|
|
385
|
+
name="exit",
|
|
386
|
+
help="Exit the command palette.",
|
|
387
|
+
handler=SlashSession._cmd_exit,
|
|
388
|
+
aliases=("q",),
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def _register(self, command: SlashCommand) -> None:
|
|
393
|
+
self._unique_commands[command.name] = command
|
|
394
|
+
for key in (command.name, *command.aliases):
|
|
395
|
+
self._commands[key] = command
|
|
396
|
+
|
|
397
|
+
def _parse(self, raw: str) -> tuple[str, list[str]]:
|
|
398
|
+
try:
|
|
399
|
+
tokens = shlex.split(raw)
|
|
400
|
+
except ValueError:
|
|
401
|
+
return "", []
|
|
402
|
+
|
|
403
|
+
if not tokens:
|
|
404
|
+
return "", []
|
|
405
|
+
|
|
406
|
+
head = tokens[0]
|
|
407
|
+
if head.startswith("/"):
|
|
408
|
+
head = head[1:]
|
|
409
|
+
|
|
410
|
+
return head, tokens[1:]
|
|
411
|
+
|
|
412
|
+
def _suggest(self, verb: str) -> str | None:
|
|
413
|
+
from difflib import get_close_matches
|
|
414
|
+
|
|
415
|
+
keys = [cmd.name for cmd in self._unique_commands.values()]
|
|
416
|
+
match = get_close_matches(verb, keys, n=1)
|
|
417
|
+
return match[0] if match else None
|
|
418
|
+
|
|
419
|
+
def _prompt(self, message: str, *, placeholder: str | None = None) -> str:
|
|
420
|
+
if self._ptk_session and self._ptk_style and patch_stdout:
|
|
421
|
+
with patch_stdout(): # pragma: no cover - UI specific
|
|
422
|
+
prompt_text = (
|
|
423
|
+
FormattedText([("class:prompt", message)])
|
|
424
|
+
if FormattedText is not None
|
|
425
|
+
else message
|
|
426
|
+
)
|
|
427
|
+
prompt_kwargs: dict[str, Any] = {"style": self._ptk_style}
|
|
428
|
+
if placeholder:
|
|
429
|
+
placeholder_text = (
|
|
430
|
+
FormattedText([("class:placeholder", placeholder)])
|
|
431
|
+
if FormattedText is not None
|
|
432
|
+
else placeholder
|
|
433
|
+
)
|
|
434
|
+
prompt_kwargs["placeholder"] = placeholder_text
|
|
435
|
+
try:
|
|
436
|
+
return self._ptk_session.prompt(prompt_text, **prompt_kwargs)
|
|
437
|
+
except (
|
|
438
|
+
TypeError
|
|
439
|
+
): # pragma: no cover - compatibility with older prompt_toolkit
|
|
440
|
+
prompt_kwargs.pop("placeholder", None)
|
|
441
|
+
return self._ptk_session.prompt(prompt_text, **prompt_kwargs)
|
|
442
|
+
|
|
443
|
+
if placeholder:
|
|
444
|
+
self.console.print(f"[dim]{placeholder}[/dim]")
|
|
445
|
+
return input(message)
|
|
446
|
+
|
|
447
|
+
def _get_client(self) -> Any: # type: ignore[no-any-return]
|
|
448
|
+
if self._client is None:
|
|
449
|
+
self._client = get_client(self.ctx)
|
|
450
|
+
return self._client
|
|
451
|
+
|
|
452
|
+
def set_contextual_commands(
|
|
453
|
+
self, commands: dict[str, str] | None, *, include_global: bool = True
|
|
454
|
+
) -> None:
|
|
455
|
+
"""Set context-specific commands that should appear in completions."""
|
|
456
|
+
|
|
457
|
+
self._contextual_commands = dict(commands or {})
|
|
458
|
+
self._contextual_include_global = include_global if commands else True
|
|
459
|
+
|
|
460
|
+
def get_contextual_commands(self) -> dict[str, str]: # type: ignore[no-any-return]
|
|
461
|
+
"""Return a copy of the currently active contextual commands."""
|
|
462
|
+
|
|
463
|
+
return dict(self._contextual_commands)
|
|
464
|
+
|
|
465
|
+
def should_include_global_commands(self) -> bool:
|
|
466
|
+
"""Return whether global slash commands should appear in completions."""
|
|
467
|
+
|
|
468
|
+
return self._contextual_include_global
|
|
469
|
+
|
|
470
|
+
def _remember_agent(self, agent: Any) -> None: # type: ignore[no-any-return]
|
|
471
|
+
agent_data = {
|
|
472
|
+
"id": str(getattr(agent, "id", "")),
|
|
473
|
+
"name": getattr(agent, "name", "") or "",
|
|
474
|
+
"type": getattr(agent, "type", "") or "",
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
self.recent_agents = [
|
|
478
|
+
a for a in self.recent_agents if a.get("id") != agent_data["id"]
|
|
479
|
+
]
|
|
480
|
+
self.recent_agents.insert(0, agent_data)
|
|
481
|
+
self.recent_agents = self.recent_agents[:5]
|
|
482
|
+
|
|
483
|
+
def _render_header(
|
|
484
|
+
self,
|
|
485
|
+
active_agent: Any | None = None,
|
|
486
|
+
*,
|
|
487
|
+
focus_agent: bool = False,
|
|
488
|
+
initial: bool = False,
|
|
489
|
+
) -> None:
|
|
490
|
+
if focus_agent and active_agent is not None:
|
|
491
|
+
self._render_focused_agent_header(active_agent)
|
|
492
|
+
return
|
|
493
|
+
|
|
494
|
+
full_header = initial or not self._welcome_rendered
|
|
495
|
+
self._render_main_header(active_agent, full=full_header)
|
|
496
|
+
if full_header:
|
|
497
|
+
self._welcome_rendered = True
|
|
498
|
+
|
|
499
|
+
def _render_focused_agent_header(self, active_agent: Any) -> None:
|
|
500
|
+
"""Render header when focusing on a specific agent."""
|
|
501
|
+
agent_id = str(getattr(active_agent, "id", ""))
|
|
502
|
+
agent_name = getattr(active_agent, "name", "") or agent_id
|
|
503
|
+
agent_type = getattr(active_agent, "type", "") or "-"
|
|
504
|
+
description = getattr(active_agent, "description", "") or ""
|
|
505
|
+
|
|
506
|
+
header_grid = Table.grid(expand=True)
|
|
507
|
+
header_grid.add_column(ratio=3)
|
|
508
|
+
header_grid.add_column(ratio=1, justify="right")
|
|
509
|
+
|
|
510
|
+
primary_line = f"[bold]{agent_name}[/bold] · [dim]{agent_type}[/dim] · [cyan]{agent_id}[/cyan]"
|
|
511
|
+
header_grid.add_row(primary_line, "[green]ready[/green]")
|
|
512
|
+
|
|
513
|
+
if description:
|
|
514
|
+
header_grid.add_row(f"[dim]{description}[/dim]", "")
|
|
515
|
+
|
|
516
|
+
keybar = Table.grid(expand=True)
|
|
517
|
+
keybar.add_column(justify="left")
|
|
518
|
+
keybar.add_column(justify="left")
|
|
519
|
+
keybar.add_column(justify="left")
|
|
520
|
+
keybar.add_row(
|
|
521
|
+
"[bold]/help[/bold] [dim]Show commands[/dim]",
|
|
522
|
+
"[bold]/details[/bold] [dim]Agent config[/dim]",
|
|
523
|
+
"[bold]/exit[/bold] [dim]Back[/dim]",
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
header_grid.add_row(keybar, "")
|
|
527
|
+
|
|
528
|
+
self.console.print(
|
|
529
|
+
AIPPanel(header_grid, title="Agent Session", border_style="blue")
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
def _render_main_header(
|
|
533
|
+
self, active_agent: Any | None = None, *, full: bool = False
|
|
534
|
+
) -> None:
|
|
535
|
+
"""Render the main AIP environment header."""
|
|
536
|
+
config = self._load_config()
|
|
537
|
+
|
|
538
|
+
api_url = self._get_api_url(config)
|
|
539
|
+
status = "Configured" if config.get("api_key") else "Not configured"
|
|
540
|
+
|
|
541
|
+
if full:
|
|
542
|
+
lines = [
|
|
543
|
+
self._branding.get_welcome_banner(),
|
|
544
|
+
"",
|
|
545
|
+
f"[dim]API URL[/dim]: {api_url or 'Not configured'}",
|
|
546
|
+
f"[dim]Credentials[/dim]: {status}",
|
|
547
|
+
]
|
|
548
|
+
extra: list[str] = []
|
|
549
|
+
self._add_agent_info_to_header(extra, active_agent)
|
|
550
|
+
lines.extend(extra)
|
|
551
|
+
self.console.print(
|
|
552
|
+
AIPPanel("\n".join(lines), title="GL AIP Session", border_style="cyan")
|
|
553
|
+
)
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
status_bar = Table.grid(expand=True)
|
|
557
|
+
status_bar.add_column(ratio=2)
|
|
558
|
+
status_bar.add_column(ratio=2)
|
|
559
|
+
status_bar.add_column(ratio=1, justify="right")
|
|
560
|
+
status_bar.add_row(
|
|
561
|
+
"[bold cyan]AIP Palette[/bold cyan]",
|
|
562
|
+
f"[dim]API[/dim]: {api_url or 'Not configured'}",
|
|
563
|
+
"[dim]Type /help for shortcuts[/dim]",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
if active_agent is not None:
|
|
567
|
+
agent_id = str(getattr(active_agent, "id", ""))
|
|
568
|
+
agent_name = getattr(active_agent, "name", "") or agent_id
|
|
569
|
+
status_bar.add_row(f"[dim]Active[/dim]: {agent_name} [{agent_id}]", "", "")
|
|
570
|
+
elif self.recent_agents:
|
|
571
|
+
recent = self.recent_agents[0]
|
|
572
|
+
label = recent.get("name") or recent.get("id") or "-"
|
|
573
|
+
status_bar.add_row(
|
|
574
|
+
f"[dim]Recent[/dim]: {label} [{recent.get('id', '-')}]",
|
|
575
|
+
"",
|
|
576
|
+
"",
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
self.console.print(AIPPanel(status_bar, border_style="cyan"))
|
|
580
|
+
|
|
581
|
+
def _get_api_url(self, config: dict[str, Any]) -> str | None:
|
|
582
|
+
"""Get the API URL from various sources."""
|
|
583
|
+
api_url = None
|
|
584
|
+
if isinstance(self.ctx.obj, dict):
|
|
585
|
+
api_url = self.ctx.obj.get("api_url")
|
|
586
|
+
return api_url or config.get("api_url") or os.getenv("AIP_API_URL")
|
|
587
|
+
|
|
588
|
+
def _add_agent_info_to_header(
|
|
589
|
+
self, lines: list[str], active_agent: Any | None
|
|
590
|
+
) -> None:
|
|
591
|
+
"""Add agent information to header lines."""
|
|
592
|
+
if active_agent is not None:
|
|
593
|
+
agent_id = str(getattr(active_agent, "id", ""))
|
|
594
|
+
agent_name = getattr(active_agent, "name", "") or agent_id
|
|
595
|
+
lines.append(f"[dim]Active agent[/dim]: {agent_name} [{agent_id}]")
|
|
596
|
+
elif self.recent_agents:
|
|
597
|
+
recent = self.recent_agents[0]
|
|
598
|
+
label = recent.get("name") or recent.get("id") or "-"
|
|
599
|
+
lines.append(f"[dim]Recent agent[/dim]: {label} [{recent.get('id', '-')}]")
|
|
600
|
+
|
|
601
|
+
def _render_home_hint(self) -> None:
|
|
602
|
+
self.console.print(
|
|
603
|
+
AIPPanel(
|
|
604
|
+
"Type `/help` for command palette commands, `/agents` to browse agents, or `/exit` (`/q`) to leave the palette.\n"
|
|
605
|
+
"Press Ctrl+C to cancel the current entry, Ctrl+D to quit immediately.",
|
|
606
|
+
title="✨ Getting Started",
|
|
607
|
+
border_style="cyan",
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
def _show_quick_actions(
|
|
612
|
+
self, hints: Iterable[tuple[str, str]], *, title: str = "Quick actions"
|
|
613
|
+
) -> None:
|
|
614
|
+
hint_list = list(hints)
|
|
615
|
+
if not hint_list:
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
grid = Table.grid(expand=True)
|
|
619
|
+
for _ in hint_list:
|
|
620
|
+
grid.add_column(ratio=1, no_wrap=False)
|
|
621
|
+
|
|
622
|
+
grid.add_row(
|
|
623
|
+
*[
|
|
624
|
+
f"[bold]{command}[/bold]\n[dim]{description}[/dim]"
|
|
625
|
+
for command, description in hint_list
|
|
626
|
+
]
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
self.console.print(AIPPanel(grid, title=title, border_style="magenta"))
|
|
630
|
+
|
|
631
|
+
def _load_config(self) -> dict[str, Any]:
|
|
632
|
+
if self._config_cache is None:
|
|
633
|
+
try:
|
|
634
|
+
self._config_cache = load_config() or {}
|
|
635
|
+
except Exception:
|
|
636
|
+
self._config_cache = {}
|
|
637
|
+
return self._config_cache
|
|
638
|
+
|
|
639
|
+
def _resolve_agent_from_ref(
|
|
640
|
+
self, client: Any, available_agents: list[Any], ref: str
|
|
641
|
+
) -> Any | None:
|
|
642
|
+
ref = ref.strip()
|
|
643
|
+
if not ref:
|
|
644
|
+
return None
|
|
645
|
+
|
|
646
|
+
try:
|
|
647
|
+
agent = client.get_agent_by_id(ref)
|
|
648
|
+
if agent:
|
|
649
|
+
return agent
|
|
650
|
+
except Exception: # pragma: no cover - passthrough
|
|
651
|
+
pass
|
|
652
|
+
|
|
653
|
+
matches = [a for a in available_agents if str(getattr(a, "id", "")) == ref]
|
|
654
|
+
if matches:
|
|
655
|
+
return matches[0]
|
|
656
|
+
|
|
657
|
+
try:
|
|
658
|
+
found = client.find_agents(name=ref)
|
|
659
|
+
except Exception: # pragma: no cover - passthrough
|
|
660
|
+
found = []
|
|
661
|
+
|
|
662
|
+
if len(found) == 1:
|
|
663
|
+
return found[0]
|
|
664
|
+
|
|
665
|
+
return None
|