glaip-sdk 0.6.15b2__py3-none-any.whl → 0.6.16__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/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1196 -0
- glaip_sdk/cli/__init__.py +9 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +78 -0
- glaip_sdk/cli/auth.py +699 -0
- glaip_sdk/cli/commands/__init__.py +5 -0
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +1509 -0
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +896 -0
- glaip_sdk/cli/commands/mcps.py +1356 -0
- glaip_sdk/cli/commands/models.py +69 -0
- glaip_sdk/cli/commands/tools.py +576 -0
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +61 -0
- glaip_sdk/cli/config.py +95 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +150 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +355 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +112 -0
- glaip_sdk/cli/main.py +615 -0
- glaip_sdk/cli/masking.py +136 -0
- glaip_sdk/cli/mcp_validators.py +287 -0
- glaip_sdk/cli/pager.py +266 -0
- glaip_sdk/cli/parsers/__init__.py +7 -0
- glaip_sdk/cli/parsers/json_input.py +177 -0
- glaip_sdk/cli/resolution.py +67 -0
- glaip_sdk/cli/rich_helpers.py +27 -0
- glaip_sdk/cli/slash/__init__.py +15 -0
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +285 -0
- glaip_sdk/cli/slash/prompt.py +256 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +1708 -0
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +31 -0
- glaip_sdk/cli/transcript/cache.py +536 -0
- glaip_sdk/cli/transcript/capture.py +329 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +77 -0
- glaip_sdk/cli/transcript/viewer.py +374 -0
- glaip_sdk/cli/update_notifier.py +290 -0
- glaip_sdk/cli/utils.py +263 -0
- glaip_sdk/cli/validators.py +238 -0
- glaip_sdk/client/__init__.py +11 -0
- glaip_sdk/client/_agent_payloads.py +520 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +1335 -0
- glaip_sdk/client/base.py +502 -0
- glaip_sdk/client/main.py +249 -0
- glaip_sdk/client/mcps.py +370 -0
- glaip_sdk/client/run_rendering.py +700 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +661 -0
- glaip_sdk/client/validators.py +198 -0
- glaip_sdk/config/constants.py +52 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +7 -0
- glaip_sdk/payload_schemas/agent.py +85 -0
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +232 -0
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +782 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +86 -0
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +194 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +486 -0
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +135 -0
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +61 -0
- glaip_sdk/utils/import_export.py +168 -0
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -0
- glaip_sdk/utils/rendering/formatting.py +264 -0
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/layout/panels.py +156 -0
- glaip_sdk/utils/rendering/layout/progress.py +202 -0
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +85 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +55 -0
- glaip_sdk/utils/rendering/renderer/base.py +1024 -0
- glaip_sdk/utils/rendering/renderer/config.py +27 -0
- glaip_sdk/utils/rendering/renderer/console.py +55 -0
- glaip_sdk/utils/rendering/renderer/debug.py +178 -0
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +202 -0
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +195 -0
- glaip_sdk/utils/run_renderer.py +41 -0
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +424 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +264 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/METADATA +4 -5
- glaip_sdk-0.6.16.dist-info/RECORD +160 -0
- glaip_sdk-0.6.15b2.dist-info/RECORD +0 -12
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1708 @@
|
|
|
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 importlib
|
|
10
|
+
import os
|
|
11
|
+
import shlex
|
|
12
|
+
import sys
|
|
13
|
+
from collections.abc import Callable, Iterable
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from difflib import get_close_matches
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import click
|
|
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
|
+
)
|
|
36
|
+
from glaip_sdk.cli.auth import resolve_api_url_from_context
|
|
37
|
+
from glaip_sdk.cli.account_store import get_account_store
|
|
38
|
+
from glaip_sdk.cli.commands import transcripts as transcripts_cmd
|
|
39
|
+
from glaip_sdk.cli.commands.configure import _configure_interactive, load_config
|
|
40
|
+
from glaip_sdk.cli.commands.update import update_command
|
|
41
|
+
from glaip_sdk.cli.hints import format_command_hint
|
|
42
|
+
from glaip_sdk.cli.slash.accounts_controller import AccountsController
|
|
43
|
+
from glaip_sdk.cli.slash.agent_session import AgentRunSession
|
|
44
|
+
from glaip_sdk.cli.slash.accounts_shared import env_credentials_present
|
|
45
|
+
from glaip_sdk.cli.slash.prompt import (
|
|
46
|
+
FormattedText,
|
|
47
|
+
PromptSession,
|
|
48
|
+
Style,
|
|
49
|
+
patch_stdout,
|
|
50
|
+
setup_prompt_toolkit,
|
|
51
|
+
to_formatted_text,
|
|
52
|
+
)
|
|
53
|
+
from glaip_sdk.cli.slash.remote_runs_controller import RemoteRunsController
|
|
54
|
+
from glaip_sdk.cli.transcript import (
|
|
55
|
+
export_cached_transcript,
|
|
56
|
+
load_history_snapshot,
|
|
57
|
+
)
|
|
58
|
+
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
59
|
+
from glaip_sdk.cli.update_notifier import maybe_notify_update
|
|
60
|
+
from glaip_sdk.cli.utils import (
|
|
61
|
+
_fuzzy_pick_for_resources,
|
|
62
|
+
command_hint,
|
|
63
|
+
format_size,
|
|
64
|
+
get_client,
|
|
65
|
+
restore_slash_session_context,
|
|
66
|
+
)
|
|
67
|
+
from glaip_sdk.rich_components import AIPGrid, AIPPanel, AIPTable
|
|
68
|
+
|
|
69
|
+
SlashHandler = Callable[["SlashSession", list[str], bool], bool]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class SlashCommand:
|
|
74
|
+
"""Metadata for a slash command entry."""
|
|
75
|
+
|
|
76
|
+
name: str
|
|
77
|
+
help: str
|
|
78
|
+
handler: SlashHandler
|
|
79
|
+
aliases: tuple[str, ...] = ()
|
|
80
|
+
agent_only: bool = False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
NEW_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
|
|
84
|
+
{
|
|
85
|
+
"cli": "transcripts",
|
|
86
|
+
"slash": "transcripts",
|
|
87
|
+
"description": "Review transcript cache",
|
|
88
|
+
"tag": "NEW",
|
|
89
|
+
"priority": 10,
|
|
90
|
+
"scope": "global",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"cli": None,
|
|
94
|
+
"slash": "runs",
|
|
95
|
+
"description": "View remote run history for the active agent",
|
|
96
|
+
"tag": "NEW",
|
|
97
|
+
"priority": 8,
|
|
98
|
+
"scope": "agent",
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
DEFAULT_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
|
|
104
|
+
{
|
|
105
|
+
"cli": None,
|
|
106
|
+
"slash": "accounts",
|
|
107
|
+
"description": "Switch account profile",
|
|
108
|
+
"priority": 5,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"cli": "status",
|
|
112
|
+
"slash": "status",
|
|
113
|
+
"description": "Connection check",
|
|
114
|
+
"priority": 0,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"cli": "agents list",
|
|
118
|
+
"slash": "agents",
|
|
119
|
+
"description": "Browse agents",
|
|
120
|
+
"priority": 0,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"cli": "help",
|
|
124
|
+
"slash": "help",
|
|
125
|
+
"description": "Show all commands",
|
|
126
|
+
"priority": 0,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"cli": "configure",
|
|
130
|
+
"slash": "login",
|
|
131
|
+
"description": f"Configure credentials (alias [{HINT_COMMAND_STYLE}]/configure[/])",
|
|
132
|
+
"priority": -1,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
HELP_COMMAND = "/help"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _quick_action_scope(action: dict[str, Any]) -> str:
|
|
141
|
+
"""Return the scope for a quick action definition."""
|
|
142
|
+
scope = action.get("scope") or "global"
|
|
143
|
+
if isinstance(scope, str):
|
|
144
|
+
return scope.lower()
|
|
145
|
+
return "global"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SlashSession:
|
|
149
|
+
"""Interactive command palette controller."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, ctx: click.Context, *, console: Console | None = None) -> None:
|
|
152
|
+
"""Initialize the slash session.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
ctx: The Click context
|
|
156
|
+
console: Optional console instance, creates default if None
|
|
157
|
+
"""
|
|
158
|
+
self.ctx = ctx
|
|
159
|
+
self.console = console or Console()
|
|
160
|
+
self._commands: dict[str, SlashCommand] = {}
|
|
161
|
+
self._unique_commands: dict[str, SlashCommand] = {}
|
|
162
|
+
self._contextual_commands: dict[str, str] = {}
|
|
163
|
+
self._contextual_include_global: bool = True
|
|
164
|
+
self._client: Any | None = None
|
|
165
|
+
self.recent_agents: list[dict[str, str]] = []
|
|
166
|
+
self.last_run_input: str | None = None
|
|
167
|
+
self._should_exit = False
|
|
168
|
+
self._interactive = bool(sys.stdin.isatty() and sys.stdout.isatty())
|
|
169
|
+
self._config_cache: dict[str, Any] | None = None
|
|
170
|
+
self._welcome_rendered = False
|
|
171
|
+
self._active_renderer: Any | None = None
|
|
172
|
+
self._current_agent: Any | None = None
|
|
173
|
+
self._runs_pagination_state: dict[str, dict[str, Any]] = {} # agent_id -> {page, limit, cursor}
|
|
174
|
+
|
|
175
|
+
self._home_placeholder = "Hint: type / to explore commands · Ctrl+D exits"
|
|
176
|
+
|
|
177
|
+
# Command string constants to avoid duplication
|
|
178
|
+
self.STATUS_COMMAND = "/status"
|
|
179
|
+
self.AGENTS_COMMAND = "/agents"
|
|
180
|
+
|
|
181
|
+
self._ptk_session: PromptSession | None = None
|
|
182
|
+
self._ptk_style: Style | None = None
|
|
183
|
+
self._setup_prompt_toolkit()
|
|
184
|
+
self._register_defaults()
|
|
185
|
+
self._branding = AIPBranding.create_from_sdk()
|
|
186
|
+
self._suppress_login_layout = False
|
|
187
|
+
self._default_actions_shown = False
|
|
188
|
+
self._update_prompt_shown = False
|
|
189
|
+
self._update_notifier = maybe_notify_update
|
|
190
|
+
self._home_hint_shown = False
|
|
191
|
+
self._agent_transcript_ready: dict[str, str] = {}
|
|
192
|
+
|
|
193
|
+
# ------------------------------------------------------------------
|
|
194
|
+
# Session orchestration
|
|
195
|
+
# ------------------------------------------------------------------
|
|
196
|
+
def refresh_branding(
|
|
197
|
+
self,
|
|
198
|
+
sdk_version: str | None = None,
|
|
199
|
+
*,
|
|
200
|
+
branding_cls: type[AIPBranding] | None = None,
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Refresh branding assets after an in-session SDK upgrade."""
|
|
203
|
+
branding_type = branding_cls or AIPBranding
|
|
204
|
+
self._branding = branding_type.create_from_sdk(
|
|
205
|
+
sdk_version=sdk_version,
|
|
206
|
+
package_name="glaip-sdk",
|
|
207
|
+
)
|
|
208
|
+
self._welcome_rendered = False
|
|
209
|
+
self.console.print()
|
|
210
|
+
self.console.print(f"[{SUCCESS_STYLE}]CLI updated to {self._branding.version}. Refreshing banner...[/]")
|
|
211
|
+
self._render_header(initial=True)
|
|
212
|
+
|
|
213
|
+
def _setup_prompt_toolkit(self) -> None:
|
|
214
|
+
"""Initialize prompt_toolkit session and style."""
|
|
215
|
+
session, style = setup_prompt_toolkit(self, interactive=self._interactive)
|
|
216
|
+
self._ptk_session = session
|
|
217
|
+
self._ptk_style = style
|
|
218
|
+
|
|
219
|
+
def run(self, initial_commands: Iterable[str] | None = None) -> None:
|
|
220
|
+
"""Start the command palette session loop."""
|
|
221
|
+
ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
|
|
222
|
+
previous_session = None
|
|
223
|
+
if ctx_obj is not None:
|
|
224
|
+
previous_session = ctx_obj.get("_slash_session")
|
|
225
|
+
ctx_obj["_slash_session"] = self
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
if not self._interactive:
|
|
229
|
+
self._run_non_interactive(initial_commands)
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
if not self._ensure_configuration():
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
self._maybe_show_update_prompt()
|
|
236
|
+
self._render_header(initial=not self._welcome_rendered)
|
|
237
|
+
if not self._default_actions_shown:
|
|
238
|
+
self._show_default_quick_actions()
|
|
239
|
+
self._run_interactive_loop()
|
|
240
|
+
finally:
|
|
241
|
+
if ctx_obj is not None:
|
|
242
|
+
restore_slash_session_context(ctx_obj, previous_session)
|
|
243
|
+
|
|
244
|
+
def _run_interactive_loop(self) -> None:
|
|
245
|
+
"""Run the main interactive command loop."""
|
|
246
|
+
while not self._should_exit:
|
|
247
|
+
try:
|
|
248
|
+
raw = self._prompt("› ", placeholder=self._home_placeholder)
|
|
249
|
+
except EOFError:
|
|
250
|
+
self.console.print("\n👋 Closing the command palette.")
|
|
251
|
+
break
|
|
252
|
+
except KeyboardInterrupt:
|
|
253
|
+
self.console.print("")
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
if not self._process_command(raw):
|
|
257
|
+
break
|
|
258
|
+
|
|
259
|
+
def _process_command(self, raw: str) -> bool:
|
|
260
|
+
"""Process a single command input. Returns False if should exit."""
|
|
261
|
+
raw = raw.strip()
|
|
262
|
+
if not raw:
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
if raw == "/":
|
|
266
|
+
self._render_home_hint()
|
|
267
|
+
self._cmd_help([], invoked_from_agent=False)
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
if not raw.startswith("/"):
|
|
271
|
+
self.console.print(f"[{INFO_STYLE}]Hint:[/] start commands with `/`. Try `/agents` to select an agent.")
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
return self.handle_command(raw)
|
|
275
|
+
|
|
276
|
+
def _run_non_interactive(self, initial_commands: Iterable[str] | None = None) -> None:
|
|
277
|
+
"""Run slash commands in non-interactive mode."""
|
|
278
|
+
commands = list(initial_commands or [])
|
|
279
|
+
if not commands:
|
|
280
|
+
commands = [line.strip() for line in sys.stdin if line.strip()]
|
|
281
|
+
|
|
282
|
+
for raw in commands:
|
|
283
|
+
if not raw.startswith("/"):
|
|
284
|
+
continue
|
|
285
|
+
if not self.handle_command(raw):
|
|
286
|
+
break
|
|
287
|
+
|
|
288
|
+
def _handle_account_selection(self) -> bool:
|
|
289
|
+
"""Handle account selection when accounts exist but none are active.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
True if configuration is ready after selection, False if user aborted.
|
|
293
|
+
"""
|
|
294
|
+
self.console.print(f"[{INFO_STYLE}]No active account selected. Please choose an account:[/]")
|
|
295
|
+
try:
|
|
296
|
+
self._cmd_accounts([], False)
|
|
297
|
+
self._config_cache = None
|
|
298
|
+
return self._check_configuration_after_selection()
|
|
299
|
+
except KeyboardInterrupt:
|
|
300
|
+
self.console.print(f"[{ERROR_STYLE}]Account selection aborted. Closing the command palette.[/]")
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
def _check_configuration_after_selection(self) -> bool:
|
|
304
|
+
"""Check if configuration is ready after account selection.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
True if configuration is ready, False otherwise.
|
|
308
|
+
"""
|
|
309
|
+
return self._configuration_ready()
|
|
310
|
+
|
|
311
|
+
def _handle_new_account_creation(self) -> bool:
|
|
312
|
+
"""Handle new account creation when no accounts exist.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
True if configuration succeeded, False if user aborted.
|
|
316
|
+
"""
|
|
317
|
+
previous_tip_env = os.environ.get("AIP_SUPPRESS_CONFIGURE_TIP")
|
|
318
|
+
os.environ["AIP_SUPPRESS_CONFIGURE_TIP"] = "1"
|
|
319
|
+
self._suppress_login_layout = True
|
|
320
|
+
try:
|
|
321
|
+
self._cmd_login([], False)
|
|
322
|
+
return True
|
|
323
|
+
except KeyboardInterrupt:
|
|
324
|
+
self.console.print(f"[{ERROR_STYLE}]Configuration aborted. Closing the command palette.[/]")
|
|
325
|
+
return False
|
|
326
|
+
finally:
|
|
327
|
+
self._suppress_login_layout = False
|
|
328
|
+
if previous_tip_env is None:
|
|
329
|
+
os.environ.pop("AIP_SUPPRESS_CONFIGURE_TIP", None)
|
|
330
|
+
else:
|
|
331
|
+
os.environ["AIP_SUPPRESS_CONFIGURE_TIP"] = previous_tip_env
|
|
332
|
+
|
|
333
|
+
def _ensure_configuration(self) -> bool:
|
|
334
|
+
"""Ensure the CLI has both API URL and credentials before continuing."""
|
|
335
|
+
while not self._configuration_ready():
|
|
336
|
+
store = get_account_store()
|
|
337
|
+
accounts = store.list_accounts()
|
|
338
|
+
active_account = store.get_active_account()
|
|
339
|
+
|
|
340
|
+
# If accounts exist but none are active, show accounts list first
|
|
341
|
+
if accounts and (not active_account or active_account not in accounts):
|
|
342
|
+
if not self._handle_account_selection():
|
|
343
|
+
return False
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
# No accounts exist - prompt for configuration
|
|
347
|
+
if not self._handle_new_account_creation():
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
def _get_credentials_from_context_and_env(self) -> tuple[str, str]:
|
|
353
|
+
"""Get credentials from context and environment variables.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Tuple of (api_url, api_key) from context/env overrides.
|
|
357
|
+
"""
|
|
358
|
+
api_url = ""
|
|
359
|
+
api_key = ""
|
|
360
|
+
if isinstance(self.ctx.obj, dict):
|
|
361
|
+
api_url = self.ctx.obj.get("api_url", "")
|
|
362
|
+
api_key = self.ctx.obj.get("api_key", "")
|
|
363
|
+
# Environment variables take precedence
|
|
364
|
+
env_url = os.getenv("AIP_API_URL", "")
|
|
365
|
+
env_key = os.getenv("AIP_API_KEY", "")
|
|
366
|
+
return (env_url or api_url, env_key or api_key)
|
|
367
|
+
|
|
368
|
+
def _get_credentials_from_account_store(self) -> tuple[str, str] | None:
|
|
369
|
+
"""Get credentials from the active account in account store.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Tuple of (api_url, api_key) if active account exists, None otherwise.
|
|
373
|
+
"""
|
|
374
|
+
store = get_account_store()
|
|
375
|
+
active_account = store.get_active_account()
|
|
376
|
+
if not active_account:
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
account = store.get_account(active_account)
|
|
380
|
+
if not account:
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
api_url = account.get("api_url", "")
|
|
384
|
+
api_key = account.get("api_key", "")
|
|
385
|
+
return (api_url, api_key)
|
|
386
|
+
|
|
387
|
+
def _configuration_ready(self) -> bool:
|
|
388
|
+
"""Check whether API URL and credentials are available."""
|
|
389
|
+
# Check for explicit overrides in context/env first
|
|
390
|
+
override_url, override_key = self._get_credentials_from_context_and_env()
|
|
391
|
+
if override_url and override_key:
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
# Read from account store directly to avoid stale cache
|
|
395
|
+
account_creds = self._get_credentials_from_account_store()
|
|
396
|
+
if account_creds is None:
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
store_url, store_key = account_creds
|
|
400
|
+
|
|
401
|
+
# Use override values if available, otherwise use store values
|
|
402
|
+
api_url = override_url or store_url
|
|
403
|
+
api_key = override_key or store_key
|
|
404
|
+
|
|
405
|
+
return bool(api_url and api_key)
|
|
406
|
+
|
|
407
|
+
def handle_command(self, raw: str, *, invoked_from_agent: bool = False) -> bool:
|
|
408
|
+
"""Parse and execute a single slash command string."""
|
|
409
|
+
verb, args = self._parse(raw)
|
|
410
|
+
if not verb:
|
|
411
|
+
self.console.print(f"[{ERROR_STYLE}]Unrecognised command[/]")
|
|
412
|
+
return True
|
|
413
|
+
|
|
414
|
+
command = self._commands.get(verb)
|
|
415
|
+
if command is None:
|
|
416
|
+
suggestion = self._suggest(verb)
|
|
417
|
+
if suggestion:
|
|
418
|
+
self.console.print(f"[{WARNING_STYLE}]Unknown command '{verb}'. Did you mean '/{suggestion}'?[/]")
|
|
419
|
+
else:
|
|
420
|
+
help_command = HELP_COMMAND
|
|
421
|
+
help_hint = format_command_hint(help_command) or help_command
|
|
422
|
+
self.console.print(
|
|
423
|
+
f"[{WARNING_STYLE}]Unknown command '{verb}'. Type {help_hint} for a list of options.[/]"
|
|
424
|
+
)
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
should_continue = command.handler(self, args, invoked_from_agent)
|
|
428
|
+
if not should_continue:
|
|
429
|
+
self._should_exit = True
|
|
430
|
+
return False
|
|
431
|
+
return True
|
|
432
|
+
|
|
433
|
+
def _continue_session(self) -> bool:
|
|
434
|
+
"""Signal that the slash session should remain active."""
|
|
435
|
+
return not self._should_exit
|
|
436
|
+
|
|
437
|
+
# ------------------------------------------------------------------
|
|
438
|
+
# Command handlers
|
|
439
|
+
# ------------------------------------------------------------------
|
|
440
|
+
def _cmd_help(self, _args: list[str], invoked_from_agent: bool) -> bool:
|
|
441
|
+
"""Handle the /help command.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
_args: Command arguments (unused).
|
|
445
|
+
invoked_from_agent: Whether invoked from agent context.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
True to continue session.
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
if invoked_from_agent:
|
|
452
|
+
self._render_agent_help()
|
|
453
|
+
else:
|
|
454
|
+
self._render_global_help(include_agent_hint=True)
|
|
455
|
+
except Exception as exc: # pragma: no cover - UI/display errors
|
|
456
|
+
self.console.print(f"[{ERROR_STYLE}]Error displaying help: {exc}[/]")
|
|
457
|
+
return False
|
|
458
|
+
|
|
459
|
+
return True
|
|
460
|
+
|
|
461
|
+
def _render_agent_help(self) -> None:
|
|
462
|
+
"""Render help text for agent context commands."""
|
|
463
|
+
table = AIPTable()
|
|
464
|
+
table.add_column("Input", style=HINT_COMMAND_STYLE, no_wrap=True)
|
|
465
|
+
table.add_column("What happens", style=HINT_DESCRIPTION_COLOR)
|
|
466
|
+
table.add_row("<message>", "Run the active agent once with that prompt.")
|
|
467
|
+
table.add_row("/details", "Show the agent export (prompts to expand instructions).")
|
|
468
|
+
table.add_row(self.STATUS_COMMAND, "Display connection status without leaving.")
|
|
469
|
+
table.add_row("/runs", "✨ NEW · Open the remote run browser for this agent.")
|
|
470
|
+
table.add_row("/export [path]", "Export the latest agent transcript as JSONL.")
|
|
471
|
+
table.add_row("/exit (/back)", "Return to the slash home screen.")
|
|
472
|
+
table.add_row(f"{HELP_COMMAND} (/?)", "Display this context-aware menu.")
|
|
473
|
+
|
|
474
|
+
panel_items = [table]
|
|
475
|
+
if self.last_run_input:
|
|
476
|
+
panel_items.append(Text.from_markup(f"[dim]Last run input:[/] {self.last_run_input}"))
|
|
477
|
+
panel_items.append(
|
|
478
|
+
Text.from_markup(
|
|
479
|
+
"[dim]Global commands (e.g. `/login`, `/status`) remain available inside the agent prompt.[/dim]"
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
self.console.print(
|
|
484
|
+
AIPPanel(
|
|
485
|
+
Group(*panel_items),
|
|
486
|
+
title="Agent Context",
|
|
487
|
+
border_style=PRIMARY,
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
new_commands_table = AIPTable()
|
|
491
|
+
new_commands_table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
|
|
492
|
+
new_commands_table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
|
|
493
|
+
new_commands_table.add_row(
|
|
494
|
+
"/runs",
|
|
495
|
+
"✨ NEW · View remote run history with keyboard navigation and export options.",
|
|
496
|
+
)
|
|
497
|
+
self.console.print(
|
|
498
|
+
AIPPanel(
|
|
499
|
+
new_commands_table,
|
|
500
|
+
title="New commands",
|
|
501
|
+
border_style=SECONDARY_LIGHT,
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
def _render_global_help(self, *, include_agent_hint: bool = False) -> None:
|
|
506
|
+
"""Render help text for global slash commands."""
|
|
507
|
+
table = AIPTable()
|
|
508
|
+
table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
|
|
509
|
+
table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
|
|
510
|
+
|
|
511
|
+
for cmd in self._visible_commands(include_agent_only=False):
|
|
512
|
+
aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
|
|
513
|
+
verb = f"/{cmd.name}"
|
|
514
|
+
if aliases:
|
|
515
|
+
verb = f"{verb} ({aliases})"
|
|
516
|
+
table.add_row(verb, cmd.help)
|
|
517
|
+
|
|
518
|
+
tip = Text.from_markup(
|
|
519
|
+
f"[{HINT_PREFIX_STYLE}]Tip:[/] "
|
|
520
|
+
f"{format_command_hint(self.AGENTS_COMMAND) or self.AGENTS_COMMAND} "
|
|
521
|
+
"lets you jump into an agent run prompt quickly."
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
self.console.print(
|
|
525
|
+
AIPPanel(
|
|
526
|
+
Group(table, tip),
|
|
527
|
+
title="Slash Commands",
|
|
528
|
+
border_style=PRIMARY,
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
if include_agent_hint:
|
|
532
|
+
self.console.print(
|
|
533
|
+
"[dim]Additional commands (e.g. `/runs`) become available after you pick an agent with `/agents`. "
|
|
534
|
+
"Those agent-only commands stay hidden here to avoid confusion.[/]"
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
def _cmd_login(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
538
|
+
"""Handle the /login command.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
_args: Command arguments (unused).
|
|
542
|
+
_invoked_from_agent: Whether invoked from agent context (unused).
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
True to continue session.
|
|
546
|
+
"""
|
|
547
|
+
self.console.print(f"[{ACCENT_STYLE}]Launching configuration wizard...[/]")
|
|
548
|
+
try:
|
|
549
|
+
# Use the modern account-aware wizard directly (bypasses legacy config gating)
|
|
550
|
+
_configure_interactive(account_name=None)
|
|
551
|
+
self.on_account_switched()
|
|
552
|
+
if self._suppress_login_layout:
|
|
553
|
+
self._welcome_rendered = False
|
|
554
|
+
self._default_actions_shown = False
|
|
555
|
+
else:
|
|
556
|
+
self._render_header(initial=True)
|
|
557
|
+
self._show_default_quick_actions()
|
|
558
|
+
except click.ClickException as exc:
|
|
559
|
+
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
560
|
+
return self._continue_session()
|
|
561
|
+
|
|
562
|
+
def _cmd_status(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
563
|
+
"""Handle the /status command.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
_args: Command arguments (unused).
|
|
567
|
+
_invoked_from_agent: Whether invoked from agent context (unused).
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
True to continue session.
|
|
571
|
+
"""
|
|
572
|
+
ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
|
|
573
|
+
previous_console = None
|
|
574
|
+
try:
|
|
575
|
+
status_module = importlib.import_module("glaip_sdk.cli.main")
|
|
576
|
+
status_command = status_module.status
|
|
577
|
+
|
|
578
|
+
if ctx_obj is not None:
|
|
579
|
+
previous_console = ctx_obj.get("_slash_console")
|
|
580
|
+
ctx_obj["_slash_console"] = self.console
|
|
581
|
+
|
|
582
|
+
self.ctx.invoke(status_command)
|
|
583
|
+
|
|
584
|
+
hints: list[tuple[str, str]] = [(self.AGENTS_COMMAND, "Browse agents and run them")]
|
|
585
|
+
if self.recent_agents:
|
|
586
|
+
top = self.recent_agents[0]
|
|
587
|
+
label = top.get("name") or top.get("id")
|
|
588
|
+
hints.append((f"/agents {top.get('id')}", f"Reopen {label}"))
|
|
589
|
+
self._show_quick_actions(hints, title="Next actions")
|
|
590
|
+
except click.ClickException as exc:
|
|
591
|
+
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
592
|
+
finally:
|
|
593
|
+
if ctx_obj is not None:
|
|
594
|
+
if previous_console is None:
|
|
595
|
+
ctx_obj.pop("_slash_console", None)
|
|
596
|
+
else:
|
|
597
|
+
ctx_obj["_slash_console"] = previous_console
|
|
598
|
+
return self._continue_session()
|
|
599
|
+
|
|
600
|
+
def _cmd_transcripts(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
601
|
+
"""Handle the /transcripts command.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
args: Command arguments (limit or detail/show with run_id).
|
|
605
|
+
_invoked_from_agent: Whether invoked from agent context (unused).
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
True to continue session.
|
|
609
|
+
"""
|
|
610
|
+
if args and args[0].lower() in {"detail", "show"}:
|
|
611
|
+
if len(args) < 2:
|
|
612
|
+
self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts detail <run_id>[/]")
|
|
613
|
+
return self._continue_session()
|
|
614
|
+
self._show_transcript_detail(args[1])
|
|
615
|
+
return self._continue_session()
|
|
616
|
+
|
|
617
|
+
limit, ok = self._parse_transcripts_limit(args)
|
|
618
|
+
if not ok:
|
|
619
|
+
return self._continue_session()
|
|
620
|
+
|
|
621
|
+
snapshot = load_history_snapshot(limit=limit, ctx=self.ctx)
|
|
622
|
+
|
|
623
|
+
if self._handle_transcripts_empty(snapshot, limit):
|
|
624
|
+
return self._continue_session()
|
|
625
|
+
|
|
626
|
+
self._render_transcripts_snapshot(snapshot)
|
|
627
|
+
return self._continue_session()
|
|
628
|
+
|
|
629
|
+
def _parse_transcripts_limit(self, args: list[str]) -> tuple[int | None, bool]:
|
|
630
|
+
"""Parse limit argument from transcripts command.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
args: Command arguments.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Tuple of (limit value or None, success boolean).
|
|
637
|
+
"""
|
|
638
|
+
if not args:
|
|
639
|
+
return None, True
|
|
640
|
+
try:
|
|
641
|
+
limit = int(args[0])
|
|
642
|
+
except ValueError:
|
|
643
|
+
self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts [limit][/]")
|
|
644
|
+
return None, False
|
|
645
|
+
if limit < 0:
|
|
646
|
+
self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts [limit][/]")
|
|
647
|
+
return None, False
|
|
648
|
+
return limit, True
|
|
649
|
+
|
|
650
|
+
def _handle_transcripts_empty(self, snapshot: Any, limit: int | None) -> bool:
|
|
651
|
+
"""Handle empty transcript snapshot cases.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
snapshot: Transcript snapshot object.
|
|
655
|
+
limit: Limit value or None.
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
True if empty case was handled, False otherwise.
|
|
659
|
+
"""
|
|
660
|
+
if snapshot.cached_entries == 0:
|
|
661
|
+
self.console.print(f"[{WARNING_STYLE}]No cached transcripts yet. Run an agent first.[/]")
|
|
662
|
+
for warning in snapshot.warnings:
|
|
663
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
664
|
+
return True
|
|
665
|
+
if limit == 0 and snapshot.cached_entries:
|
|
666
|
+
self.console.print(f"[{WARNING_STYLE}]Limit is 0; nothing to display.[/]")
|
|
667
|
+
return True
|
|
668
|
+
return False
|
|
669
|
+
|
|
670
|
+
def _render_transcripts_snapshot(self, snapshot: Any) -> None:
|
|
671
|
+
"""Render transcript snapshot table and metadata.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
snapshot: Transcript snapshot object to render.
|
|
675
|
+
"""
|
|
676
|
+
size_text = format_size(snapshot.total_size_bytes)
|
|
677
|
+
header = f"[dim]Manifest: {snapshot.manifest_path} · {snapshot.total_entries} runs · {size_text} used[/]"
|
|
678
|
+
self.console.print(header)
|
|
679
|
+
|
|
680
|
+
if snapshot.limit_clamped:
|
|
681
|
+
self.console.print(
|
|
682
|
+
f"[{WARNING_STYLE}]Requested limit exceeded maximum; showing first {snapshot.limit_applied} runs.[/]"
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
if snapshot.total_entries > len(snapshot.entries):
|
|
686
|
+
subset_message = (
|
|
687
|
+
f"[dim]Showing {len(snapshot.entries)} of {snapshot.total_entries} "
|
|
688
|
+
f"runs (limit={snapshot.limit_applied}).[/]"
|
|
689
|
+
)
|
|
690
|
+
self.console.print(subset_message)
|
|
691
|
+
self.console.print("[dim]Hint: run `/transcripts <limit>` to change how many rows are displayed.[/]")
|
|
692
|
+
|
|
693
|
+
if snapshot.migration_summary:
|
|
694
|
+
self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
695
|
+
|
|
696
|
+
for warning in snapshot.warnings:
|
|
697
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
698
|
+
|
|
699
|
+
table = transcripts_cmd._build_table(snapshot.entries)
|
|
700
|
+
self.console.print(table)
|
|
701
|
+
self.console.print("[dim]! Missing transcript[/]")
|
|
702
|
+
|
|
703
|
+
def _show_transcript_detail(self, run_id: str) -> None:
|
|
704
|
+
"""Render the cached transcript log for a single run."""
|
|
705
|
+
snapshot = load_history_snapshot(ctx=self.ctx)
|
|
706
|
+
entry = snapshot.index.get(run_id)
|
|
707
|
+
if entry is None:
|
|
708
|
+
self.console.print(f"[{WARNING_STYLE}]Run id {run_id} was not found in the cache manifest.[/]")
|
|
709
|
+
return
|
|
710
|
+
|
|
711
|
+
try:
|
|
712
|
+
transcript_path, transcript_text = transcripts_cmd._load_transcript_text(entry)
|
|
713
|
+
except click.ClickException as exc:
|
|
714
|
+
self.console.print(f"[{WARNING_STYLE}]{exc}[/]")
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
meta, events = transcripts_cmd._decode_transcript(transcript_text)
|
|
718
|
+
if transcripts_cmd._maybe_launch_transcript_viewer(
|
|
719
|
+
self.ctx,
|
|
720
|
+
entry,
|
|
721
|
+
meta,
|
|
722
|
+
events,
|
|
723
|
+
console_override=self.console,
|
|
724
|
+
force=True,
|
|
725
|
+
initial_view="transcript",
|
|
726
|
+
):
|
|
727
|
+
if snapshot.migration_summary:
|
|
728
|
+
self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
729
|
+
for warning in snapshot.warnings:
|
|
730
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
731
|
+
return
|
|
732
|
+
|
|
733
|
+
if snapshot.migration_summary:
|
|
734
|
+
self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
735
|
+
for warning in snapshot.warnings:
|
|
736
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
737
|
+
view = transcripts_cmd._render_transcript_display(entry, snapshot.manifest_path, transcript_path, meta, events)
|
|
738
|
+
self.console.print(view, markup=False, highlight=False, soft_wrap=True, end="")
|
|
739
|
+
|
|
740
|
+
def _cmd_runs(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
741
|
+
"""Handle the /runs command for browsing remote agent run history.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
args: Command arguments (optional run_id for detail view).
|
|
745
|
+
_invoked_from_agent: Whether invoked from agent context.
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
True to continue session.
|
|
749
|
+
"""
|
|
750
|
+
controller = RemoteRunsController(self)
|
|
751
|
+
return controller.handle_runs_command(args)
|
|
752
|
+
|
|
753
|
+
def _cmd_accounts(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
754
|
+
"""Handle the /accounts command for listing and switching accounts."""
|
|
755
|
+
controller = AccountsController(self)
|
|
756
|
+
return controller.handle_accounts_command(args)
|
|
757
|
+
|
|
758
|
+
def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
759
|
+
"""Handle the /agents command.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
args: Command arguments (optional agent reference).
|
|
763
|
+
_invoked_from_agent: Whether invoked from agent context (unused).
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
True to continue session.
|
|
767
|
+
"""
|
|
768
|
+
client = self._get_client_or_fail()
|
|
769
|
+
if not client:
|
|
770
|
+
return True
|
|
771
|
+
|
|
772
|
+
agents = self._get_agents_or_fail(client)
|
|
773
|
+
if not agents:
|
|
774
|
+
return True
|
|
775
|
+
|
|
776
|
+
picked_agent = self._resolve_or_pick_agent(client, agents, args)
|
|
777
|
+
|
|
778
|
+
if not picked_agent:
|
|
779
|
+
return True
|
|
780
|
+
|
|
781
|
+
return self._run_agent_session(picked_agent)
|
|
782
|
+
|
|
783
|
+
def _get_client_or_fail(self) -> Any:
|
|
784
|
+
"""Get client or handle failure and return None."""
|
|
785
|
+
try:
|
|
786
|
+
return self._get_client()
|
|
787
|
+
except click.ClickException as exc:
|
|
788
|
+
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
789
|
+
return None
|
|
790
|
+
|
|
791
|
+
def _get_agents_or_fail(self, client: Any) -> list:
|
|
792
|
+
"""Get agents list or handle failure and return empty list."""
|
|
793
|
+
try:
|
|
794
|
+
agents = client.list_agents()
|
|
795
|
+
if not agents:
|
|
796
|
+
self._handle_no_agents()
|
|
797
|
+
return agents
|
|
798
|
+
except Exception as exc: # pragma: no cover - API failures
|
|
799
|
+
self.console.print(f"[{ERROR_STYLE}]Failed to load agents: {exc}[/]")
|
|
800
|
+
return []
|
|
801
|
+
|
|
802
|
+
def _handle_no_agents(self) -> None:
|
|
803
|
+
"""Handle case when no agents are available."""
|
|
804
|
+
hint = command_hint("agents create", slash_command=None, ctx=self.ctx)
|
|
805
|
+
if hint:
|
|
806
|
+
self.console.print(f"[{WARNING_STYLE}]No agents available. Use `{hint}` to add one.[/]")
|
|
807
|
+
else:
|
|
808
|
+
self.console.print(f"[{WARNING_STYLE}]No agents available.[/]")
|
|
809
|
+
|
|
810
|
+
def _resolve_or_pick_agent(self, client: Any, agents: list, args: list[str]) -> Any:
|
|
811
|
+
"""Resolve agent from args or pick interactively."""
|
|
812
|
+
if args:
|
|
813
|
+
picked_agent = self._resolve_agent_from_ref(client, agents, args[0])
|
|
814
|
+
if picked_agent is None:
|
|
815
|
+
self.console.print(
|
|
816
|
+
f"[{WARNING_STYLE}]Could not resolve agent '{args[0]}'. Try `/agents` to browse interactively.[/]"
|
|
817
|
+
)
|
|
818
|
+
return None
|
|
819
|
+
else:
|
|
820
|
+
picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
|
|
821
|
+
|
|
822
|
+
return picked_agent
|
|
823
|
+
|
|
824
|
+
def _run_agent_session(self, picked_agent: Any) -> bool:
|
|
825
|
+
"""Run agent session and show follow-up actions."""
|
|
826
|
+
self._remember_agent(picked_agent)
|
|
827
|
+
AgentRunSession(self, picked_agent).run()
|
|
828
|
+
|
|
829
|
+
# Refresh the main palette header and surface follow-up actions
|
|
830
|
+
self._render_header()
|
|
831
|
+
|
|
832
|
+
self._show_agent_followup_actions(picked_agent)
|
|
833
|
+
return self._continue_session()
|
|
834
|
+
|
|
835
|
+
def _show_agent_followup_actions(self, picked_agent: Any) -> None:
|
|
836
|
+
"""Show follow-up action hints after agent session."""
|
|
837
|
+
agent_id = str(getattr(picked_agent, "id", ""))
|
|
838
|
+
agent_label = getattr(picked_agent, "name", "") or agent_id or "this agent"
|
|
839
|
+
|
|
840
|
+
hints: list[tuple[str, str]] = []
|
|
841
|
+
if agent_id:
|
|
842
|
+
hints.append((f"/agents {agent_id}", f"Reopen {agent_label}"))
|
|
843
|
+
hints.extend(
|
|
844
|
+
[
|
|
845
|
+
("/accounts", "Switch account"),
|
|
846
|
+
(self.AGENTS_COMMAND, "Browse agents"),
|
|
847
|
+
(self.STATUS_COMMAND, "Check connection"),
|
|
848
|
+
]
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
self._show_quick_actions(hints, title="Next actions")
|
|
852
|
+
|
|
853
|
+
def _cmd_exit(self, _args: list[str], invoked_from_agent: bool) -> bool:
|
|
854
|
+
"""Handle the /exit command.
|
|
855
|
+
|
|
856
|
+
Args:
|
|
857
|
+
_args: Command arguments (unused).
|
|
858
|
+
invoked_from_agent: Whether invoked from agent context.
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
False to exit session, True to continue.
|
|
862
|
+
"""
|
|
863
|
+
if invoked_from_agent:
|
|
864
|
+
# Returning False would stop the full session; we only want to exit
|
|
865
|
+
# the agent context. Raising a custom flag keeps the outer loop
|
|
866
|
+
# running.
|
|
867
|
+
return True
|
|
868
|
+
|
|
869
|
+
self.console.print(f"[{ACCENT_STYLE}]Closing the command palette.[/]")
|
|
870
|
+
return False
|
|
871
|
+
|
|
872
|
+
# ------------------------------------------------------------------
|
|
873
|
+
# Utilities
|
|
874
|
+
# ------------------------------------------------------------------
|
|
875
|
+
def _register_defaults(self) -> None:
|
|
876
|
+
"""Register default slash commands."""
|
|
877
|
+
self._register(
|
|
878
|
+
SlashCommand(
|
|
879
|
+
name="help",
|
|
880
|
+
help="Show available command palette commands.",
|
|
881
|
+
handler=SlashSession._cmd_help,
|
|
882
|
+
aliases=("?",),
|
|
883
|
+
)
|
|
884
|
+
)
|
|
885
|
+
self._register(
|
|
886
|
+
SlashCommand(
|
|
887
|
+
name="login",
|
|
888
|
+
help="Configure API credentials (alias `/configure`).",
|
|
889
|
+
handler=SlashSession._cmd_login,
|
|
890
|
+
aliases=("configure",),
|
|
891
|
+
)
|
|
892
|
+
)
|
|
893
|
+
self._register(
|
|
894
|
+
SlashCommand(
|
|
895
|
+
name="status",
|
|
896
|
+
help="Display connection status summary.",
|
|
897
|
+
handler=SlashSession._cmd_status,
|
|
898
|
+
)
|
|
899
|
+
)
|
|
900
|
+
self._register(
|
|
901
|
+
SlashCommand(
|
|
902
|
+
name="accounts",
|
|
903
|
+
help="✨ NEW · Browse and switch stored accounts (Textual with Rich fallback).",
|
|
904
|
+
handler=SlashSession._cmd_accounts,
|
|
905
|
+
)
|
|
906
|
+
)
|
|
907
|
+
self._register(
|
|
908
|
+
SlashCommand(
|
|
909
|
+
name="transcripts",
|
|
910
|
+
help=(
|
|
911
|
+
"✨ NEW · Review cached transcript history. "
|
|
912
|
+
"Add a number (e.g. `/transcripts 5`) to change the row limit."
|
|
913
|
+
),
|
|
914
|
+
handler=SlashSession._cmd_transcripts,
|
|
915
|
+
)
|
|
916
|
+
)
|
|
917
|
+
self._register(
|
|
918
|
+
SlashCommand(
|
|
919
|
+
name="agents",
|
|
920
|
+
help="Pick an agent and enter a focused run prompt.",
|
|
921
|
+
handler=SlashSession._cmd_agents,
|
|
922
|
+
)
|
|
923
|
+
)
|
|
924
|
+
self._register(
|
|
925
|
+
SlashCommand(
|
|
926
|
+
name="exit",
|
|
927
|
+
help="Exit the command palette.",
|
|
928
|
+
handler=SlashSession._cmd_exit,
|
|
929
|
+
aliases=("q",),
|
|
930
|
+
)
|
|
931
|
+
)
|
|
932
|
+
self._register(
|
|
933
|
+
SlashCommand(
|
|
934
|
+
name="export",
|
|
935
|
+
help="Export the most recent agent transcript.",
|
|
936
|
+
handler=SlashSession._cmd_export,
|
|
937
|
+
)
|
|
938
|
+
)
|
|
939
|
+
self._register(
|
|
940
|
+
SlashCommand(
|
|
941
|
+
name="update",
|
|
942
|
+
help="Upgrade the glaip-sdk package to the latest version.",
|
|
943
|
+
handler=SlashSession._cmd_update,
|
|
944
|
+
)
|
|
945
|
+
)
|
|
946
|
+
self._register(
|
|
947
|
+
SlashCommand(
|
|
948
|
+
name="runs",
|
|
949
|
+
help="✨ NEW · Browse remote agent run history (requires active agent session).",
|
|
950
|
+
handler=SlashSession._cmd_runs,
|
|
951
|
+
agent_only=True,
|
|
952
|
+
)
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
def _register(self, command: SlashCommand) -> None:
|
|
956
|
+
"""Register a slash command.
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
command: SlashCommand to register.
|
|
960
|
+
"""
|
|
961
|
+
self._unique_commands[command.name] = command
|
|
962
|
+
for key in (command.name, *command.aliases):
|
|
963
|
+
self._commands[key] = command
|
|
964
|
+
|
|
965
|
+
def _visible_commands(self, *, include_agent_only: bool) -> list[SlashCommand]:
|
|
966
|
+
"""Return the list of commands that should be shown in global listings."""
|
|
967
|
+
commands = sorted(self._unique_commands.values(), key=lambda c: c.name)
|
|
968
|
+
if include_agent_only:
|
|
969
|
+
return commands
|
|
970
|
+
return [cmd for cmd in commands if not cmd.agent_only]
|
|
971
|
+
|
|
972
|
+
def open_transcript_viewer(self, *, announce: bool = True) -> None:
|
|
973
|
+
"""Launch the transcript viewer for the most recent run."""
|
|
974
|
+
payload, manifest = self._get_last_transcript()
|
|
975
|
+
if payload is None or manifest is None:
|
|
976
|
+
if announce:
|
|
977
|
+
self.console.print(f"[{WARNING_STYLE}]No transcript is available yet. Run an agent first.[/]")
|
|
978
|
+
return
|
|
979
|
+
|
|
980
|
+
run_id = manifest.get("run_id")
|
|
981
|
+
if not run_id:
|
|
982
|
+
if announce:
|
|
983
|
+
self.console.print(f"[{WARNING_STYLE}]Latest transcript is missing run metadata.[/]")
|
|
984
|
+
return
|
|
985
|
+
|
|
986
|
+
viewer_ctx = ViewerContext(
|
|
987
|
+
manifest_entry=manifest,
|
|
988
|
+
events=list(getattr(payload, "events", []) or []),
|
|
989
|
+
default_output=getattr(payload, "default_output", ""),
|
|
990
|
+
final_output=getattr(payload, "final_output", ""),
|
|
991
|
+
stream_started_at=getattr(payload, "started_at", None),
|
|
992
|
+
meta=getattr(payload, "meta", {}) or {},
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
def _export(destination: Path) -> Path:
|
|
996
|
+
"""Export cached transcript to destination.
|
|
997
|
+
|
|
998
|
+
Args:
|
|
999
|
+
destination: Path to export transcript to.
|
|
1000
|
+
|
|
1001
|
+
Returns:
|
|
1002
|
+
Path to exported transcript file.
|
|
1003
|
+
"""
|
|
1004
|
+
return export_cached_transcript(destination=destination, run_id=run_id)
|
|
1005
|
+
|
|
1006
|
+
try:
|
|
1007
|
+
run_viewer_session(self.console, viewer_ctx, _export)
|
|
1008
|
+
except Exception as exc: # pragma: no cover - interactive failures
|
|
1009
|
+
if announce:
|
|
1010
|
+
self.console.print(f"[{ERROR_STYLE}]Failed to launch transcript viewer: {exc}[/]")
|
|
1011
|
+
|
|
1012
|
+
def _get_last_transcript(self) -> tuple[Any | None, dict[str, Any] | None]:
|
|
1013
|
+
"""Fetch the most recently stored transcript payload and manifest."""
|
|
1014
|
+
ctx_obj = getattr(self.ctx, "obj", None)
|
|
1015
|
+
if not isinstance(ctx_obj, dict):
|
|
1016
|
+
return None, None
|
|
1017
|
+
payload = ctx_obj.get("_last_transcript_payload")
|
|
1018
|
+
manifest = ctx_obj.get("_last_transcript_manifest")
|
|
1019
|
+
return payload, manifest
|
|
1020
|
+
|
|
1021
|
+
def _cmd_export(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
1022
|
+
"""Slash handler for `/export` command."""
|
|
1023
|
+
self.console.print(
|
|
1024
|
+
f"[{WARNING_STYLE}]`/export` is deprecated. Use `/transcripts`, select a run, "
|
|
1025
|
+
"and open the transcript viewer to export.[/]"
|
|
1026
|
+
)
|
|
1027
|
+
return True
|
|
1028
|
+
|
|
1029
|
+
def _cmd_update(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
1030
|
+
"""Slash handler for `/update` command."""
|
|
1031
|
+
if args:
|
|
1032
|
+
self.console.print("Usage: `/update` upgrades glaip-sdk to the latest published version.")
|
|
1033
|
+
return True
|
|
1034
|
+
|
|
1035
|
+
try:
|
|
1036
|
+
self.ctx.invoke(update_command)
|
|
1037
|
+
return True
|
|
1038
|
+
except click.ClickException as exc:
|
|
1039
|
+
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
1040
|
+
# Return False for update command failures to indicate the command didn't complete successfully
|
|
1041
|
+
return False
|
|
1042
|
+
|
|
1043
|
+
# ------------------------------------------------------------------
|
|
1044
|
+
# Agent run coordination helpers
|
|
1045
|
+
# ------------------------------------------------------------------
|
|
1046
|
+
def register_active_renderer(self, renderer: Any) -> None:
|
|
1047
|
+
"""Register the renderer currently streaming an agent run."""
|
|
1048
|
+
self._active_renderer = renderer
|
|
1049
|
+
self._sync_active_renderer()
|
|
1050
|
+
|
|
1051
|
+
def clear_active_renderer(self, renderer: Any | None = None) -> None:
|
|
1052
|
+
"""Clear the active renderer if it matches the provided instance."""
|
|
1053
|
+
if renderer is not None and renderer is not self._active_renderer:
|
|
1054
|
+
return
|
|
1055
|
+
self._active_renderer = None
|
|
1056
|
+
|
|
1057
|
+
def mark_agent_transcript_ready(self, agent_id: str, run_id: str | None) -> None:
|
|
1058
|
+
"""Record that an agent has a transcript ready for the current session."""
|
|
1059
|
+
if not agent_id or not run_id:
|
|
1060
|
+
return
|
|
1061
|
+
self._agent_transcript_ready[agent_id] = run_id
|
|
1062
|
+
|
|
1063
|
+
def clear_agent_transcript_ready(self, agent_id: str | None = None) -> None:
|
|
1064
|
+
"""Reset transcript-ready state for an agent or for all agents."""
|
|
1065
|
+
if agent_id:
|
|
1066
|
+
self._agent_transcript_ready.pop(agent_id, None)
|
|
1067
|
+
return
|
|
1068
|
+
self._agent_transcript_ready.clear()
|
|
1069
|
+
|
|
1070
|
+
def notify_agent_run_started(self) -> None:
|
|
1071
|
+
"""Mark that an agent run is in progress."""
|
|
1072
|
+
self.clear_active_renderer()
|
|
1073
|
+
|
|
1074
|
+
def notify_agent_run_finished(self) -> None:
|
|
1075
|
+
"""Mark that the active agent run has completed."""
|
|
1076
|
+
self.clear_active_renderer()
|
|
1077
|
+
|
|
1078
|
+
def _sync_active_renderer(self) -> None:
|
|
1079
|
+
"""Ensure the active renderer stays in standard (non-verbose) mode."""
|
|
1080
|
+
renderer = self._active_renderer
|
|
1081
|
+
if renderer is None:
|
|
1082
|
+
return
|
|
1083
|
+
|
|
1084
|
+
applied = False
|
|
1085
|
+
apply_verbose = getattr(renderer, "apply_verbosity", None)
|
|
1086
|
+
if callable(apply_verbose):
|
|
1087
|
+
try:
|
|
1088
|
+
apply_verbose(False)
|
|
1089
|
+
applied = True
|
|
1090
|
+
except Exception:
|
|
1091
|
+
pass
|
|
1092
|
+
|
|
1093
|
+
if not applied and hasattr(renderer, "verbose"):
|
|
1094
|
+
try:
|
|
1095
|
+
renderer.verbose = False
|
|
1096
|
+
except Exception:
|
|
1097
|
+
pass
|
|
1098
|
+
|
|
1099
|
+
def _parse(self, raw: str) -> tuple[str, list[str]]:
|
|
1100
|
+
"""Parse a raw command string into verb and arguments.
|
|
1101
|
+
|
|
1102
|
+
Args:
|
|
1103
|
+
raw: Raw command string.
|
|
1104
|
+
|
|
1105
|
+
Returns:
|
|
1106
|
+
Tuple of (verb, args).
|
|
1107
|
+
"""
|
|
1108
|
+
try:
|
|
1109
|
+
tokens = shlex.split(raw)
|
|
1110
|
+
except ValueError:
|
|
1111
|
+
return "", []
|
|
1112
|
+
|
|
1113
|
+
if not tokens:
|
|
1114
|
+
return "", []
|
|
1115
|
+
|
|
1116
|
+
head = tokens[0]
|
|
1117
|
+
if head.startswith("/"):
|
|
1118
|
+
head = head[1:]
|
|
1119
|
+
|
|
1120
|
+
return head, tokens[1:]
|
|
1121
|
+
|
|
1122
|
+
def _suggest(self, verb: str) -> str | None:
|
|
1123
|
+
"""Suggest a similar command name for an unknown verb.
|
|
1124
|
+
|
|
1125
|
+
Args:
|
|
1126
|
+
verb: Unknown command verb.
|
|
1127
|
+
|
|
1128
|
+
Returns:
|
|
1129
|
+
Suggested command name or None.
|
|
1130
|
+
"""
|
|
1131
|
+
keys = [cmd.name for cmd in self._unique_commands.values()]
|
|
1132
|
+
match = get_close_matches(verb, keys, n=1)
|
|
1133
|
+
return match[0] if match else None
|
|
1134
|
+
|
|
1135
|
+
def _convert_message(self, value: Any) -> Any:
|
|
1136
|
+
"""Convert a message value to the appropriate format for display."""
|
|
1137
|
+
if FormattedText is not None and to_formatted_text is not None:
|
|
1138
|
+
return to_formatted_text(value)
|
|
1139
|
+
if FormattedText is not None:
|
|
1140
|
+
return FormattedText([("class:prompt", str(value))])
|
|
1141
|
+
return str(value)
|
|
1142
|
+
|
|
1143
|
+
def _get_prompt_kwargs(self, placeholder: str | None) -> dict[str, Any]:
|
|
1144
|
+
"""Get prompt kwargs with optional placeholder styling."""
|
|
1145
|
+
prompt_kwargs: dict[str, Any] = {"style": self._ptk_style}
|
|
1146
|
+
if placeholder:
|
|
1147
|
+
placeholder_text = (
|
|
1148
|
+
FormattedText([("class:placeholder", placeholder)]) if FormattedText is not None else placeholder
|
|
1149
|
+
)
|
|
1150
|
+
prompt_kwargs["placeholder"] = placeholder_text
|
|
1151
|
+
return prompt_kwargs
|
|
1152
|
+
|
|
1153
|
+
def _prompt_with_prompt_toolkit(self, message: str | Callable[[], Any], placeholder: str | None) -> str:
|
|
1154
|
+
"""Handle prompting with prompt_toolkit."""
|
|
1155
|
+
with patch_stdout(): # pragma: no cover - UI specific
|
|
1156
|
+
if callable(message):
|
|
1157
|
+
|
|
1158
|
+
def prompt_text() -> Any:
|
|
1159
|
+
"""Get formatted prompt text from callable message."""
|
|
1160
|
+
return self._convert_message(message())
|
|
1161
|
+
else:
|
|
1162
|
+
prompt_text = self._convert_message(message)
|
|
1163
|
+
|
|
1164
|
+
prompt_kwargs = self._get_prompt_kwargs(placeholder)
|
|
1165
|
+
|
|
1166
|
+
try:
|
|
1167
|
+
return self._ptk_session.prompt(prompt_text, **prompt_kwargs)
|
|
1168
|
+
except TypeError: # pragma: no cover - compatibility with older prompt_toolkit
|
|
1169
|
+
prompt_kwargs.pop("placeholder", None)
|
|
1170
|
+
return self._ptk_session.prompt(prompt_text, **prompt_kwargs)
|
|
1171
|
+
|
|
1172
|
+
def _extract_message_text(self, raw_value: Any) -> str:
|
|
1173
|
+
"""Extract text content from various message formats."""
|
|
1174
|
+
if isinstance(raw_value, str):
|
|
1175
|
+
return raw_value
|
|
1176
|
+
|
|
1177
|
+
try:
|
|
1178
|
+
if FormattedText is not None and isinstance(raw_value, FormattedText):
|
|
1179
|
+
return "".join(text for _style, text in raw_value)
|
|
1180
|
+
elif isinstance(raw_value, list):
|
|
1181
|
+
return "".join(segment[1] for segment in raw_value)
|
|
1182
|
+
else:
|
|
1183
|
+
return str(raw_value)
|
|
1184
|
+
except Exception:
|
|
1185
|
+
return str(raw_value)
|
|
1186
|
+
|
|
1187
|
+
def _prompt_with_basic_input(self, message: str | Callable[[], Any], placeholder: str | None) -> str:
|
|
1188
|
+
"""Handle prompting with basic input."""
|
|
1189
|
+
if placeholder:
|
|
1190
|
+
self.console.print(f"[dim]{placeholder}[/dim]")
|
|
1191
|
+
|
|
1192
|
+
raw_value = message() if callable(message) else message
|
|
1193
|
+
actual_message = self._extract_message_text(raw_value)
|
|
1194
|
+
|
|
1195
|
+
return input(actual_message)
|
|
1196
|
+
|
|
1197
|
+
def _prompt(self, message: str | Callable[[], Any], *, placeholder: str | None = None) -> str:
|
|
1198
|
+
"""Main prompt function with reduced complexity."""
|
|
1199
|
+
if self._ptk_session and self._ptk_style and patch_stdout:
|
|
1200
|
+
return self._prompt_with_prompt_toolkit(message, placeholder)
|
|
1201
|
+
|
|
1202
|
+
return self._prompt_with_basic_input(message, placeholder)
|
|
1203
|
+
|
|
1204
|
+
def _get_client(self) -> Any: # type: ignore[no-any-return]
|
|
1205
|
+
"""Get or create the API client instance.
|
|
1206
|
+
|
|
1207
|
+
Returns:
|
|
1208
|
+
API client instance.
|
|
1209
|
+
"""
|
|
1210
|
+
if self._client is None:
|
|
1211
|
+
self._client = get_client(self.ctx)
|
|
1212
|
+
return self._client
|
|
1213
|
+
|
|
1214
|
+
def on_account_switched(self, _account_name: str | None = None) -> None:
|
|
1215
|
+
"""Reset any state that depends on the active account.
|
|
1216
|
+
|
|
1217
|
+
The active account can change via `/accounts` (or other flows that call
|
|
1218
|
+
AccountStore.set_active_account). The slash session caches a configured
|
|
1219
|
+
client instance, so we must invalidate it to avoid leaking the previous
|
|
1220
|
+
account's API URL/key into subsequent commands like `/agents` or `/runs`.
|
|
1221
|
+
|
|
1222
|
+
This method clears:
|
|
1223
|
+
- Client and config cache (account-specific credentials)
|
|
1224
|
+
- Current agent and recent agents (agent data is account-scoped)
|
|
1225
|
+
- Runs pagination state (runs are account-scoped)
|
|
1226
|
+
- Active renderer and transcript ready state (UI state tied to account context)
|
|
1227
|
+
- Contextual commands (may be account-specific)
|
|
1228
|
+
|
|
1229
|
+
These broader resets ensure a clean slate when switching accounts, preventing
|
|
1230
|
+
stale data from the previous account from appearing in the new account's context.
|
|
1231
|
+
"""
|
|
1232
|
+
self._client = None
|
|
1233
|
+
self._config_cache = None
|
|
1234
|
+
self._current_agent = None
|
|
1235
|
+
self.recent_agents = []
|
|
1236
|
+
self._runs_pagination_state.clear()
|
|
1237
|
+
self.clear_active_renderer()
|
|
1238
|
+
self.clear_agent_transcript_ready()
|
|
1239
|
+
self.set_contextual_commands(None)
|
|
1240
|
+
|
|
1241
|
+
def set_contextual_commands(self, commands: dict[str, str] | None, *, include_global: bool = True) -> None:
|
|
1242
|
+
"""Set context-specific commands that should appear in completions."""
|
|
1243
|
+
self._contextual_commands = dict(commands or {})
|
|
1244
|
+
self._contextual_include_global = include_global if commands else True
|
|
1245
|
+
|
|
1246
|
+
def get_contextual_commands(self) -> dict[str, str]: # type: ignore[no-any-return]
|
|
1247
|
+
"""Return a copy of the currently active contextual commands."""
|
|
1248
|
+
return dict(self._contextual_commands)
|
|
1249
|
+
|
|
1250
|
+
def should_include_global_commands(self) -> bool:
|
|
1251
|
+
"""Return whether global slash commands should appear in completions."""
|
|
1252
|
+
return self._contextual_include_global
|
|
1253
|
+
|
|
1254
|
+
def _remember_agent(self, agent: Any) -> None: # type: ignore[no-any-return]
|
|
1255
|
+
"""Remember an agent in recent agents list.
|
|
1256
|
+
|
|
1257
|
+
Args:
|
|
1258
|
+
agent: Agent object to remember.
|
|
1259
|
+
"""
|
|
1260
|
+
agent_data = {
|
|
1261
|
+
"id": str(getattr(agent, "id", "")),
|
|
1262
|
+
"name": getattr(agent, "name", "") or "",
|
|
1263
|
+
"type": getattr(agent, "type", "") or "",
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
self.recent_agents = [a for a in self.recent_agents if a.get("id") != agent_data["id"]]
|
|
1267
|
+
self.recent_agents.insert(0, agent_data)
|
|
1268
|
+
self.recent_agents = self.recent_agents[:5]
|
|
1269
|
+
|
|
1270
|
+
def _render_header(
|
|
1271
|
+
self,
|
|
1272
|
+
active_agent: Any | None = None,
|
|
1273
|
+
*,
|
|
1274
|
+
focus_agent: bool = False,
|
|
1275
|
+
initial: bool = False,
|
|
1276
|
+
) -> None:
|
|
1277
|
+
"""Render the session header with branding and status.
|
|
1278
|
+
|
|
1279
|
+
Args:
|
|
1280
|
+
active_agent: Optional active agent to display.
|
|
1281
|
+
focus_agent: Whether to focus on agent display.
|
|
1282
|
+
initial: Whether this is the initial render.
|
|
1283
|
+
"""
|
|
1284
|
+
if focus_agent and active_agent is not None:
|
|
1285
|
+
self._render_focused_agent_header(active_agent)
|
|
1286
|
+
return
|
|
1287
|
+
|
|
1288
|
+
full_header = initial or not self._welcome_rendered
|
|
1289
|
+
if full_header:
|
|
1290
|
+
self._render_branding_banner()
|
|
1291
|
+
self.console.rule(style=PRIMARY)
|
|
1292
|
+
self._render_main_header(active_agent, full=full_header)
|
|
1293
|
+
if full_header:
|
|
1294
|
+
self._welcome_rendered = True
|
|
1295
|
+
self.console.print()
|
|
1296
|
+
|
|
1297
|
+
def _render_branding_banner(self) -> None:
|
|
1298
|
+
"""Render the GL AIP branding banner."""
|
|
1299
|
+
banner = self._branding.get_welcome_banner()
|
|
1300
|
+
heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
|
|
1301
|
+
self.console.print(heading)
|
|
1302
|
+
self.console.print()
|
|
1303
|
+
self.console.print(banner)
|
|
1304
|
+
|
|
1305
|
+
def _maybe_show_update_prompt(self) -> None:
|
|
1306
|
+
"""Display update prompt once per session when applicable."""
|
|
1307
|
+
if self._update_prompt_shown:
|
|
1308
|
+
return
|
|
1309
|
+
|
|
1310
|
+
self._update_notifier(
|
|
1311
|
+
self._branding.version,
|
|
1312
|
+
console=self.console,
|
|
1313
|
+
ctx=self.ctx,
|
|
1314
|
+
slash_command="update",
|
|
1315
|
+
style="panel",
|
|
1316
|
+
)
|
|
1317
|
+
self._update_prompt_shown = True
|
|
1318
|
+
|
|
1319
|
+
def _render_focused_agent_header(self, active_agent: Any) -> None:
|
|
1320
|
+
"""Render header when focusing on a specific agent."""
|
|
1321
|
+
agent_info = self._get_agent_info(active_agent)
|
|
1322
|
+
transcript_status = self._get_transcript_status(active_agent)
|
|
1323
|
+
|
|
1324
|
+
header_grid = self._build_header_grid(agent_info, transcript_status)
|
|
1325
|
+
keybar = self._build_keybar()
|
|
1326
|
+
header_grid.add_row(keybar, "")
|
|
1327
|
+
|
|
1328
|
+
# Agent-scoped commands like /runs will appear in /help, no need to duplicate here
|
|
1329
|
+
self.console.print(AIPPanel(header_grid, title="Agent Session", border_style=PRIMARY))
|
|
1330
|
+
|
|
1331
|
+
def _get_agent_info(self, active_agent: Any) -> dict[str, str]:
|
|
1332
|
+
"""Extract agent information for display."""
|
|
1333
|
+
agent_id = str(getattr(active_agent, "id", ""))
|
|
1334
|
+
return {
|
|
1335
|
+
"id": agent_id,
|
|
1336
|
+
"name": getattr(active_agent, "name", "") or agent_id,
|
|
1337
|
+
"type": getattr(active_agent, "type", "") or "-",
|
|
1338
|
+
"description": getattr(active_agent, "description", "") or "",
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
def _get_transcript_status(self, active_agent: Any) -> dict[str, Any]:
|
|
1342
|
+
"""Get transcript status for the active agent."""
|
|
1343
|
+
agent_id = str(getattr(active_agent, "id", ""))
|
|
1344
|
+
payload, manifest = self._get_last_transcript()
|
|
1345
|
+
|
|
1346
|
+
latest_agent_id = (manifest or {}).get("agent_id")
|
|
1347
|
+
has_transcript = bool(payload and manifest and manifest.get("run_id"))
|
|
1348
|
+
run_id = (manifest or {}).get("run_id")
|
|
1349
|
+
transcript_ready = (
|
|
1350
|
+
has_transcript and latest_agent_id == agent_id and self._agent_transcript_ready.get(agent_id) == run_id
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
return {
|
|
1354
|
+
"has_transcript": has_transcript,
|
|
1355
|
+
"transcript_ready": transcript_ready,
|
|
1356
|
+
"run_id": run_id,
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
def _build_header_grid(self, agent_info: dict[str, str], transcript_status: dict[str, Any]) -> AIPGrid:
|
|
1360
|
+
"""Build the main header grid with agent information."""
|
|
1361
|
+
header_grid = AIPGrid(expand=True)
|
|
1362
|
+
header_grid.add_column(ratio=3)
|
|
1363
|
+
header_grid.add_column(ratio=1, justify="right")
|
|
1364
|
+
|
|
1365
|
+
primary_line = (
|
|
1366
|
+
f"[bold]{agent_info['name']}[/bold] · [dim]{agent_info['type']}[/dim] · "
|
|
1367
|
+
f"[{ACCENT_STYLE}]{agent_info['id']}[/]"
|
|
1368
|
+
)
|
|
1369
|
+
status_line = f"[{SUCCESS_STYLE}]ready[/]"
|
|
1370
|
+
if not transcript_status["has_transcript"]:
|
|
1371
|
+
status_line += " · no transcript"
|
|
1372
|
+
elif transcript_status["transcript_ready"]:
|
|
1373
|
+
status_line += " · transcript ready"
|
|
1374
|
+
else:
|
|
1375
|
+
status_line += " · transcript pending"
|
|
1376
|
+
header_grid.add_row(primary_line, status_line)
|
|
1377
|
+
|
|
1378
|
+
if agent_info["description"]:
|
|
1379
|
+
header_grid.add_row(f"[dim]{agent_info['description']}[/dim]", "")
|
|
1380
|
+
|
|
1381
|
+
return header_grid
|
|
1382
|
+
|
|
1383
|
+
def _build_keybar(self) -> AIPGrid:
|
|
1384
|
+
"""Build the keybar with command hints."""
|
|
1385
|
+
keybar = AIPGrid(expand=True)
|
|
1386
|
+
keybar.add_column(justify="left", ratio=1)
|
|
1387
|
+
keybar.add_column(justify="left", ratio=1)
|
|
1388
|
+
keybar.add_column(justify="left", ratio=1)
|
|
1389
|
+
|
|
1390
|
+
keybar.add_row(
|
|
1391
|
+
format_command_hint(HELP_COMMAND, "Show commands") or "",
|
|
1392
|
+
format_command_hint("/details", "Agent config (expand prompt)") or "",
|
|
1393
|
+
format_command_hint("/exit", "Back") or "",
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
return keybar
|
|
1397
|
+
|
|
1398
|
+
def _render_main_header(self, active_agent: Any | None = None, *, full: bool = False) -> None:
|
|
1399
|
+
"""Render the main AIP environment header."""
|
|
1400
|
+
config = self._load_config()
|
|
1401
|
+
|
|
1402
|
+
account_name, account_host, env_lock = self._get_account_context()
|
|
1403
|
+
api_url = self._get_api_url(config)
|
|
1404
|
+
|
|
1405
|
+
host_display = account_host or "Not configured"
|
|
1406
|
+
account_segment = f"[dim]Account[/dim] • {account_name} ({host_display})"
|
|
1407
|
+
if env_lock:
|
|
1408
|
+
account_segment += " 🔒"
|
|
1409
|
+
|
|
1410
|
+
segments = [account_segment]
|
|
1411
|
+
|
|
1412
|
+
if api_url:
|
|
1413
|
+
base_label = "[dim]Base URL[/dim]"
|
|
1414
|
+
if env_lock:
|
|
1415
|
+
base_label = "[dim]Base URL (env)[/dim]"
|
|
1416
|
+
# Always show Base URL when env-lock is active to reveal overrides
|
|
1417
|
+
if env_lock or api_url != account_host:
|
|
1418
|
+
segments.append(f"{base_label} • {api_url}")
|
|
1419
|
+
elif not api_url:
|
|
1420
|
+
segments.append("[dim]Base URL[/dim] • Not configured")
|
|
1421
|
+
|
|
1422
|
+
agent_info = self._build_agent_status_line(active_agent)
|
|
1423
|
+
if agent_info:
|
|
1424
|
+
segments.append(agent_info)
|
|
1425
|
+
|
|
1426
|
+
rendered_line = " ".join(segments)
|
|
1427
|
+
|
|
1428
|
+
if full:
|
|
1429
|
+
self.console.print(rendered_line, soft_wrap=False)
|
|
1430
|
+
return
|
|
1431
|
+
|
|
1432
|
+
status_bar = AIPGrid(expand=True)
|
|
1433
|
+
status_bar.add_column(ratio=1)
|
|
1434
|
+
status_bar.add_row(rendered_line)
|
|
1435
|
+
self.console.print(
|
|
1436
|
+
AIPPanel(
|
|
1437
|
+
status_bar,
|
|
1438
|
+
border_style=PRIMARY,
|
|
1439
|
+
padding=(0, 1),
|
|
1440
|
+
expand=False,
|
|
1441
|
+
)
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
def _get_api_url(self, _config: dict[str, Any] | None = None) -> str | None:
|
|
1445
|
+
"""Get the API URL from context or account store (CLI/palette ignores env credentials)."""
|
|
1446
|
+
return resolve_api_url_from_context(self.ctx)
|
|
1447
|
+
|
|
1448
|
+
def _get_account_context(self) -> tuple[str, str, bool]:
|
|
1449
|
+
"""Return active account name, host, and env-lock flag."""
|
|
1450
|
+
try:
|
|
1451
|
+
store = get_account_store()
|
|
1452
|
+
active = store.get_active_account() or "default"
|
|
1453
|
+
account = store.get_account(active) if hasattr(store, "get_account") else None
|
|
1454
|
+
host = ""
|
|
1455
|
+
if account:
|
|
1456
|
+
host = account.get("api_url", "")
|
|
1457
|
+
env_lock = env_credentials_present()
|
|
1458
|
+
return active, host, env_lock
|
|
1459
|
+
except Exception:
|
|
1460
|
+
return "default", "", env_credentials_present()
|
|
1461
|
+
|
|
1462
|
+
def _build_agent_status_line(self, active_agent: Any | None) -> str | None:
|
|
1463
|
+
"""Return a short status line about the active or recent agent."""
|
|
1464
|
+
if active_agent is not None:
|
|
1465
|
+
agent_id = str(getattr(active_agent, "id", ""))
|
|
1466
|
+
agent_name = getattr(active_agent, "name", "") or agent_id
|
|
1467
|
+
return f"[dim]Active[/dim]: {agent_name} ({agent_id})"
|
|
1468
|
+
if self.recent_agents:
|
|
1469
|
+
recent = self.recent_agents[0]
|
|
1470
|
+
label = recent.get("name") or recent.get("id") or "-"
|
|
1471
|
+
return f"[dim]Recent[/dim]: {label} ({recent.get('id', '-')})"
|
|
1472
|
+
return None
|
|
1473
|
+
|
|
1474
|
+
def _show_default_quick_actions(self) -> None:
|
|
1475
|
+
"""Show simplified help hint to discover commands."""
|
|
1476
|
+
self.console.print(f"[dim]{'─' * 40}[/]")
|
|
1477
|
+
help_hint = format_command_hint(HELP_COMMAND, "Show all commands") or HELP_COMMAND
|
|
1478
|
+
self.console.print(f"• {help_hint}")
|
|
1479
|
+
self._default_actions_shown = True
|
|
1480
|
+
|
|
1481
|
+
def _collect_scoped_new_action_hints(self, scope: str) -> list[tuple[str, str]]:
|
|
1482
|
+
"""Return new quick action hints filtered by scope."""
|
|
1483
|
+
scoped_actions = [action for action in NEW_QUICK_ACTIONS if _quick_action_scope(action) == scope]
|
|
1484
|
+
# Don't highlight with sparkle emoji in quick actions display - it will show in command palette instead
|
|
1485
|
+
return self._collect_quick_action_hints(scoped_actions)
|
|
1486
|
+
|
|
1487
|
+
def _collect_quick_action_hints(
|
|
1488
|
+
self,
|
|
1489
|
+
actions: Iterable[dict[str, Any]],
|
|
1490
|
+
) -> list[tuple[str, str]]:
|
|
1491
|
+
"""Collect quick action hints from action definitions.
|
|
1492
|
+
|
|
1493
|
+
Args:
|
|
1494
|
+
actions: Iterable of action dictionaries.
|
|
1495
|
+
|
|
1496
|
+
Returns:
|
|
1497
|
+
List of (command, description) tuples.
|
|
1498
|
+
"""
|
|
1499
|
+
collected: list[tuple[str, str]] = []
|
|
1500
|
+
|
|
1501
|
+
def sort_key(payload: dict[str, Any]) -> tuple[int, str]:
|
|
1502
|
+
priority = int(payload.get("priority", 0))
|
|
1503
|
+
label = str(payload.get("slash") or payload.get("cli") or "")
|
|
1504
|
+
return (-priority, label.lower())
|
|
1505
|
+
|
|
1506
|
+
for action in sorted(actions, key=sort_key):
|
|
1507
|
+
hint = self._build_quick_action_hint(action)
|
|
1508
|
+
if hint:
|
|
1509
|
+
collected.append(hint)
|
|
1510
|
+
return collected
|
|
1511
|
+
|
|
1512
|
+
def _build_quick_action_hint(
|
|
1513
|
+
self,
|
|
1514
|
+
action: dict[str, Any],
|
|
1515
|
+
) -> tuple[str, str] | None:
|
|
1516
|
+
"""Build a quick action hint from an action definition.
|
|
1517
|
+
|
|
1518
|
+
Args:
|
|
1519
|
+
action: Action dictionary.
|
|
1520
|
+
|
|
1521
|
+
Returns:
|
|
1522
|
+
Tuple of (command, description) or None.
|
|
1523
|
+
"""
|
|
1524
|
+
command = command_hint(action.get("cli"), slash_command=action.get("slash"), ctx=self.ctx)
|
|
1525
|
+
if not command:
|
|
1526
|
+
return None
|
|
1527
|
+
description = action.get("description", "")
|
|
1528
|
+
# Don't include tag or sparkle emoji in quick actions display
|
|
1529
|
+
# The NEW tag will only show in the command dropdown (help text)
|
|
1530
|
+
return command, description
|
|
1531
|
+
|
|
1532
|
+
def _render_quick_action_group(self, hints: list[tuple[str, str]], title: str) -> None:
|
|
1533
|
+
"""Render a group of quick action hints.
|
|
1534
|
+
|
|
1535
|
+
Args:
|
|
1536
|
+
hints: List of (command, description) tuples.
|
|
1537
|
+
title: Group title.
|
|
1538
|
+
"""
|
|
1539
|
+
for line in self._format_quick_action_lines(hints, title):
|
|
1540
|
+
self.console.print(line)
|
|
1541
|
+
|
|
1542
|
+
def _chunk_tokens(self, tokens: list[str], *, size: int) -> Iterable[list[str]]:
|
|
1543
|
+
"""Chunk tokens into groups of specified size.
|
|
1544
|
+
|
|
1545
|
+
Args:
|
|
1546
|
+
tokens: List of tokens to chunk.
|
|
1547
|
+
size: Size of each chunk.
|
|
1548
|
+
|
|
1549
|
+
Yields:
|
|
1550
|
+
Lists of tokens.
|
|
1551
|
+
"""
|
|
1552
|
+
for index in range(0, len(tokens), size):
|
|
1553
|
+
yield tokens[index : index + size]
|
|
1554
|
+
|
|
1555
|
+
def _render_home_hint(self) -> None:
|
|
1556
|
+
"""Render hint text for home screen."""
|
|
1557
|
+
if self._home_hint_shown:
|
|
1558
|
+
return
|
|
1559
|
+
hint_text = (
|
|
1560
|
+
f"[{HINT_PREFIX_STYLE}]Hint:[/] "
|
|
1561
|
+
f"Type {format_command_hint('/') or '/'} to explore commands · "
|
|
1562
|
+
"Press [dim]Ctrl+D[/] to quit"
|
|
1563
|
+
)
|
|
1564
|
+
self.console.print(hint_text)
|
|
1565
|
+
self._home_hint_shown = True
|
|
1566
|
+
|
|
1567
|
+
def _show_quick_actions(
|
|
1568
|
+
self,
|
|
1569
|
+
hints: Iterable[tuple[str, str]],
|
|
1570
|
+
*,
|
|
1571
|
+
title: str = "Quick actions",
|
|
1572
|
+
inline: bool = False,
|
|
1573
|
+
) -> None:
|
|
1574
|
+
"""Show quick action hints.
|
|
1575
|
+
|
|
1576
|
+
Args:
|
|
1577
|
+
hints: Iterable of (command, description) tuples.
|
|
1578
|
+
title: Title for the hints.
|
|
1579
|
+
inline: Whether to render inline or in a panel.
|
|
1580
|
+
"""
|
|
1581
|
+
hint_list = self._normalize_quick_action_hints(hints)
|
|
1582
|
+
if not hint_list:
|
|
1583
|
+
return
|
|
1584
|
+
|
|
1585
|
+
if inline:
|
|
1586
|
+
self._render_inline_quick_actions(hint_list, title)
|
|
1587
|
+
return
|
|
1588
|
+
|
|
1589
|
+
self._render_panel_quick_actions(hint_list, title)
|
|
1590
|
+
|
|
1591
|
+
def _normalize_quick_action_hints(self, hints: Iterable[tuple[str, str]]) -> list[tuple[str, str]]:
|
|
1592
|
+
"""Normalize quick action hints by filtering out empty commands.
|
|
1593
|
+
|
|
1594
|
+
Args:
|
|
1595
|
+
hints: Iterable of (command, description) tuples.
|
|
1596
|
+
|
|
1597
|
+
Returns:
|
|
1598
|
+
List of normalized hints.
|
|
1599
|
+
"""
|
|
1600
|
+
return [(command, description) for command, description in hints if command]
|
|
1601
|
+
|
|
1602
|
+
def _render_inline_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
|
|
1603
|
+
"""Render quick actions inline.
|
|
1604
|
+
|
|
1605
|
+
Args:
|
|
1606
|
+
hint_list: List of (command, description) tuples.
|
|
1607
|
+
title: Title for the hints.
|
|
1608
|
+
"""
|
|
1609
|
+
tokens: list[str] = []
|
|
1610
|
+
for command, description in hint_list:
|
|
1611
|
+
formatted = format_command_hint(command, description)
|
|
1612
|
+
if formatted:
|
|
1613
|
+
tokens.append(formatted)
|
|
1614
|
+
if not tokens:
|
|
1615
|
+
return
|
|
1616
|
+
prefix = f"[dim]{title}:[/]" if title else ""
|
|
1617
|
+
body = " ".join(tokens)
|
|
1618
|
+
text = f"{prefix} {body}" if prefix else body
|
|
1619
|
+
self.console.print(text.strip())
|
|
1620
|
+
|
|
1621
|
+
def _render_panel_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
|
|
1622
|
+
"""Render quick actions in a panel.
|
|
1623
|
+
|
|
1624
|
+
Args:
|
|
1625
|
+
hint_list: List of (command, description) tuples.
|
|
1626
|
+
title: Panel title.
|
|
1627
|
+
"""
|
|
1628
|
+
body_lines: list[Text] = []
|
|
1629
|
+
for command, description in hint_list:
|
|
1630
|
+
formatted = format_command_hint(command, description)
|
|
1631
|
+
if formatted:
|
|
1632
|
+
body_lines.append(Text.from_markup(formatted))
|
|
1633
|
+
if not body_lines:
|
|
1634
|
+
return
|
|
1635
|
+
panel_content = Group(*body_lines)
|
|
1636
|
+
self.console.print(AIPPanel(panel_content, title=title, border_style=SECONDARY_LIGHT, expand=False))
|
|
1637
|
+
|
|
1638
|
+
def _format_quick_action_lines(self, hints: list[tuple[str, str]], title: str) -> list[str]:
|
|
1639
|
+
"""Return formatted lines for quick action hints."""
|
|
1640
|
+
if not hints:
|
|
1641
|
+
return []
|
|
1642
|
+
formatted_tokens: list[str] = []
|
|
1643
|
+
for command, description in hints:
|
|
1644
|
+
formatted = format_command_hint(command, description)
|
|
1645
|
+
if formatted:
|
|
1646
|
+
formatted_tokens.append(f"• {formatted}")
|
|
1647
|
+
if not formatted_tokens:
|
|
1648
|
+
return []
|
|
1649
|
+
lines: list[str] = []
|
|
1650
|
+
# Use vertical layout (1 per line) for better readability
|
|
1651
|
+
chunks = list(self._chunk_tokens(formatted_tokens, size=1))
|
|
1652
|
+
prefix = f"[dim]{title}[/dim]\n " if title else ""
|
|
1653
|
+
for idx, chunk in enumerate(chunks):
|
|
1654
|
+
row = " ".join(chunk)
|
|
1655
|
+
if idx == 0:
|
|
1656
|
+
lines.append(f"{prefix}{row}" if prefix else row)
|
|
1657
|
+
else:
|
|
1658
|
+
lines.append(f" {row}")
|
|
1659
|
+
return lines
|
|
1660
|
+
|
|
1661
|
+
def _load_config(self) -> dict[str, Any]:
|
|
1662
|
+
"""Load configuration with caching.
|
|
1663
|
+
|
|
1664
|
+
Returns:
|
|
1665
|
+
Configuration dictionary.
|
|
1666
|
+
"""
|
|
1667
|
+
if self._config_cache is None:
|
|
1668
|
+
try:
|
|
1669
|
+
self._config_cache = load_config() or {}
|
|
1670
|
+
except Exception:
|
|
1671
|
+
self._config_cache = {}
|
|
1672
|
+
return self._config_cache
|
|
1673
|
+
|
|
1674
|
+
def _resolve_agent_from_ref(self, client: Any, available_agents: list[Any], ref: str) -> Any | None:
|
|
1675
|
+
"""Resolve an agent from a reference string.
|
|
1676
|
+
|
|
1677
|
+
Args:
|
|
1678
|
+
client: API client instance.
|
|
1679
|
+
available_agents: List of available agents.
|
|
1680
|
+
ref: Reference string (ID or name).
|
|
1681
|
+
|
|
1682
|
+
Returns:
|
|
1683
|
+
Resolved agent or None.
|
|
1684
|
+
"""
|
|
1685
|
+
ref = ref.strip()
|
|
1686
|
+
if not ref:
|
|
1687
|
+
return None
|
|
1688
|
+
|
|
1689
|
+
try:
|
|
1690
|
+
agent = client.get_agent_by_id(ref)
|
|
1691
|
+
if agent:
|
|
1692
|
+
return agent
|
|
1693
|
+
except Exception: # pragma: no cover - passthrough
|
|
1694
|
+
pass
|
|
1695
|
+
|
|
1696
|
+
matches = [a for a in available_agents if str(getattr(a, "id", "")) == ref]
|
|
1697
|
+
if matches:
|
|
1698
|
+
return matches[0]
|
|
1699
|
+
|
|
1700
|
+
try:
|
|
1701
|
+
found = client.find_agents(name=ref)
|
|
1702
|
+
except Exception: # pragma: no cover - passthrough
|
|
1703
|
+
found = []
|
|
1704
|
+
|
|
1705
|
+
if len(found) == 1:
|
|
1706
|
+
return found[0]
|
|
1707
|
+
|
|
1708
|
+
return None
|