glaip-sdk 0.1.3__py3-none-any.whl → 0.6.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/__init__.py +5 -2
- glaip_sdk/_version.py +9 -0
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1191 -0
- glaip_sdk/branding.py +13 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/auth.py +254 -15
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +213 -73
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +729 -113
- glaip_sdk/cli/commands/mcps.py +241 -72
- glaip_sdk/cli/commands/models.py +11 -5
- glaip_sdk/cli/commands/tools.py +49 -57
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/config.py +48 -4
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +35 -19
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +228 -119
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/pager.py +9 -10
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- 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 +62 -21
- glaip_sdk/cli/slash/prompt.py +21 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +771 -140
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -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 +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +27 -1
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +72 -499
- glaip_sdk/cli/update_notifier.py +14 -5
- glaip_sdk/cli/utils.py +243 -1252
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +45 -9
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +287 -29
- glaip_sdk/client/base.py +1 -0
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +122 -12
- glaip_sdk/client/run_rendering.py +133 -88
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +155 -10
- glaip_sdk/config/constants.py +11 -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 +1 -13
- 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/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +706 -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 +58 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +39 -7
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +0 -33
- glaip_sdk/utils/import_export.py +12 -7
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
- glaip_sdk/utils/rendering/renderer/base.py +217 -1476
- glaip_sdk/utils/rendering/renderer/debug.py +26 -20
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -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/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
- 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 +25 -13
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +18 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +16 -24
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
- glaip_sdk-0.6.10.dist-info/RECORD +159 -0
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -240
- glaip_sdk-0.1.3.dist-info/RECORD +0 -83
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -33,9 +33,15 @@ from glaip_sdk.branding import (
|
|
|
33
33
|
WARNING_STYLE,
|
|
34
34
|
AIPBranding,
|
|
35
35
|
)
|
|
36
|
-
from glaip_sdk.cli.
|
|
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
|
|
37
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
|
|
38
43
|
from glaip_sdk.cli.slash.agent_session import AgentRunSession
|
|
44
|
+
from glaip_sdk.cli.slash.accounts_shared import env_credentials_present
|
|
39
45
|
from glaip_sdk.cli.slash.prompt import (
|
|
40
46
|
FormattedText,
|
|
41
47
|
PromptSession,
|
|
@@ -44,19 +50,19 @@ from glaip_sdk.cli.slash.prompt import (
|
|
|
44
50
|
setup_prompt_toolkit,
|
|
45
51
|
to_formatted_text,
|
|
46
52
|
)
|
|
53
|
+
from glaip_sdk.cli.slash.remote_runs_controller import RemoteRunsController
|
|
47
54
|
from glaip_sdk.cli.transcript import (
|
|
48
55
|
export_cached_transcript,
|
|
49
|
-
|
|
50
|
-
resolve_manifest_for_export,
|
|
51
|
-
suggest_filename,
|
|
56
|
+
load_history_snapshot,
|
|
52
57
|
)
|
|
53
58
|
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
54
59
|
from glaip_sdk.cli.update_notifier import maybe_notify_update
|
|
55
60
|
from glaip_sdk.cli.utils import (
|
|
56
61
|
_fuzzy_pick_for_resources,
|
|
57
62
|
command_hint,
|
|
58
|
-
|
|
63
|
+
format_size,
|
|
59
64
|
get_client,
|
|
65
|
+
restore_slash_session_context,
|
|
60
66
|
)
|
|
61
67
|
from glaip_sdk.rich_components import AIPGrid, AIPPanel, AIPTable
|
|
62
68
|
|
|
@@ -71,6 +77,72 @@ class SlashCommand:
|
|
|
71
77
|
help: str
|
|
72
78
|
handler: SlashHandler
|
|
73
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"
|
|
74
146
|
|
|
75
147
|
|
|
76
148
|
class SlashSession:
|
|
@@ -98,8 +170,9 @@ class SlashSession:
|
|
|
98
170
|
self._welcome_rendered = False
|
|
99
171
|
self._active_renderer: Any | None = None
|
|
100
172
|
self._current_agent: Any | None = None
|
|
173
|
+
self._runs_pagination_state: dict[str, dict[str, Any]] = {} # agent_id -> {page, limit, cursor}
|
|
101
174
|
|
|
102
|
-
self._home_placeholder = "
|
|
175
|
+
self._home_placeholder = "Hint: type / to explore commands · Ctrl+D exits"
|
|
103
176
|
|
|
104
177
|
# Command string constants to avoid duplication
|
|
105
178
|
self.STATUS_COMMAND = "/status"
|
|
@@ -120,9 +193,15 @@ class SlashSession:
|
|
|
120
193
|
# ------------------------------------------------------------------
|
|
121
194
|
# Session orchestration
|
|
122
195
|
# ------------------------------------------------------------------
|
|
123
|
-
def refresh_branding(
|
|
196
|
+
def refresh_branding(
|
|
197
|
+
self,
|
|
198
|
+
sdk_version: str | None = None,
|
|
199
|
+
*,
|
|
200
|
+
branding_cls: type[AIPBranding] | None = None,
|
|
201
|
+
) -> None:
|
|
124
202
|
"""Refresh branding assets after an in-session SDK upgrade."""
|
|
125
|
-
|
|
203
|
+
branding_type = branding_cls or AIPBranding
|
|
204
|
+
self._branding = branding_type.create_from_sdk(
|
|
126
205
|
sdk_version=sdk_version,
|
|
127
206
|
package_name="glaip-sdk",
|
|
128
207
|
)
|
|
@@ -132,6 +211,7 @@ class SlashSession:
|
|
|
132
211
|
self._render_header(initial=True)
|
|
133
212
|
|
|
134
213
|
def _setup_prompt_toolkit(self) -> None:
|
|
214
|
+
"""Initialize prompt_toolkit session and style."""
|
|
135
215
|
session, style = setup_prompt_toolkit(self, interactive=self._interactive)
|
|
136
216
|
self._ptk_session = session
|
|
137
217
|
self._ptk_style = style
|
|
@@ -159,10 +239,7 @@ class SlashSession:
|
|
|
159
239
|
self._run_interactive_loop()
|
|
160
240
|
finally:
|
|
161
241
|
if ctx_obj is not None:
|
|
162
|
-
|
|
163
|
-
ctx_obj.pop("_slash_session", None)
|
|
164
|
-
else:
|
|
165
|
-
ctx_obj["_slash_session"] = previous_session
|
|
242
|
+
restore_slash_session_context(ctx_obj, previous_session)
|
|
166
243
|
|
|
167
244
|
def _run_interactive_loop(self) -> None:
|
|
168
245
|
"""Run the main interactive command loop."""
|
|
@@ -208,34 +285,124 @@ class SlashSession:
|
|
|
208
285
|
if not self.handle_command(raw):
|
|
209
286
|
break
|
|
210
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
|
+
|
|
211
333
|
def _ensure_configuration(self) -> bool:
|
|
212
334
|
"""Ensure the CLI has both API URL and credentials before continuing."""
|
|
213
335
|
while not self._configuration_ready():
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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():
|
|
220
348
|
return False
|
|
221
|
-
finally:
|
|
222
|
-
self._suppress_login_layout = False
|
|
223
349
|
|
|
224
350
|
return True
|
|
225
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
|
+
|
|
226
387
|
def _configuration_ready(self) -> bool:
|
|
227
388
|
"""Check whether API URL and credentials are available."""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if
|
|
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:
|
|
231
397
|
return False
|
|
232
398
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
236
404
|
|
|
237
|
-
|
|
238
|
-
return bool(api_key)
|
|
405
|
+
return bool(api_url and api_key)
|
|
239
406
|
|
|
240
407
|
def handle_command(self, raw: str, *, invoked_from_agent: bool = False) -> bool:
|
|
241
408
|
"""Parse and execute a single slash command string."""
|
|
@@ -250,7 +417,7 @@ class SlashSession:
|
|
|
250
417
|
if suggestion:
|
|
251
418
|
self.console.print(f"[{WARNING_STYLE}]Unknown command '{verb}'. Did you mean '/{suggestion}'?[/]")
|
|
252
419
|
else:
|
|
253
|
-
help_command =
|
|
420
|
+
help_command = HELP_COMMAND
|
|
254
421
|
help_hint = format_command_hint(help_command) or help_command
|
|
255
422
|
self.console.print(
|
|
256
423
|
f"[{WARNING_STYLE}]Unknown command '{verb}'. Type {help_hint} for a list of options.[/]"
|
|
@@ -263,15 +430,28 @@ class SlashSession:
|
|
|
263
430
|
return False
|
|
264
431
|
return True
|
|
265
432
|
|
|
433
|
+
def _continue_session(self) -> bool:
|
|
434
|
+
"""Signal that the slash session should remain active."""
|
|
435
|
+
return not self._should_exit
|
|
436
|
+
|
|
266
437
|
# ------------------------------------------------------------------
|
|
267
438
|
# Command handlers
|
|
268
439
|
# ------------------------------------------------------------------
|
|
269
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
|
+
"""
|
|
270
450
|
try:
|
|
271
451
|
if invoked_from_agent:
|
|
272
452
|
self._render_agent_help()
|
|
273
453
|
else:
|
|
274
|
-
self._render_global_help()
|
|
454
|
+
self._render_global_help(include_agent_hint=True)
|
|
275
455
|
except Exception as exc: # pragma: no cover - UI/display errors
|
|
276
456
|
self.console.print(f"[{ERROR_STYLE}]Error displaying help: {exc}[/]")
|
|
277
457
|
return False
|
|
@@ -279,15 +459,17 @@ class SlashSession:
|
|
|
279
459
|
return True
|
|
280
460
|
|
|
281
461
|
def _render_agent_help(self) -> None:
|
|
462
|
+
"""Render help text for agent context commands."""
|
|
282
463
|
table = AIPTable()
|
|
283
464
|
table.add_column("Input", style=HINT_COMMAND_STYLE, no_wrap=True)
|
|
284
465
|
table.add_column("What happens", style=HINT_DESCRIPTION_COLOR)
|
|
285
466
|
table.add_row("<message>", "Run the active agent once with that prompt.")
|
|
286
|
-
table.add_row("/details", "Show the
|
|
467
|
+
table.add_row("/details", "Show the agent export (prompts to expand instructions).")
|
|
287
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.")
|
|
288
470
|
table.add_row("/export [path]", "Export the latest agent transcript as JSONL.")
|
|
289
471
|
table.add_row("/exit (/back)", "Return to the slash home screen.")
|
|
290
|
-
table.add_row("
|
|
472
|
+
table.add_row(f"{HELP_COMMAND} (/?)", "Display this context-aware menu.")
|
|
291
473
|
|
|
292
474
|
panel_items = [table]
|
|
293
475
|
if self.last_run_input:
|
|
@@ -305,13 +487,28 @@ class SlashSession:
|
|
|
305
487
|
border_style=PRIMARY,
|
|
306
488
|
)
|
|
307
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
|
+
)
|
|
308
504
|
|
|
309
|
-
def _render_global_help(self) -> None:
|
|
505
|
+
def _render_global_help(self, *, include_agent_hint: bool = False) -> None:
|
|
506
|
+
"""Render help text for global slash commands."""
|
|
310
507
|
table = AIPTable()
|
|
311
508
|
table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
|
|
312
509
|
table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
|
|
313
510
|
|
|
314
|
-
for cmd in
|
|
511
|
+
for cmd in self._visible_commands(include_agent_only=False):
|
|
315
512
|
aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
|
|
316
513
|
verb = f"/{cmd.name}"
|
|
317
514
|
if aliases:
|
|
@@ -331,12 +528,27 @@ class SlashSession:
|
|
|
331
528
|
border_style=PRIMARY,
|
|
332
529
|
)
|
|
333
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
|
+
)
|
|
334
536
|
|
|
335
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
|
+
"""
|
|
336
547
|
self.console.print(f"[{ACCENT_STYLE}]Launching configuration wizard...[/]")
|
|
337
548
|
try:
|
|
338
|
-
|
|
339
|
-
|
|
549
|
+
# Use the modern account-aware wizard directly (bypasses legacy config gating)
|
|
550
|
+
_configure_interactive(account_name=None)
|
|
551
|
+
self.on_account_switched()
|
|
340
552
|
if self._suppress_login_layout:
|
|
341
553
|
self._welcome_rendered = False
|
|
342
554
|
self._default_actions_shown = False
|
|
@@ -345,9 +557,18 @@ class SlashSession:
|
|
|
345
557
|
self._show_default_quick_actions()
|
|
346
558
|
except click.ClickException as exc:
|
|
347
559
|
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
348
|
-
return
|
|
560
|
+
return self._continue_session()
|
|
349
561
|
|
|
350
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
|
+
"""
|
|
351
572
|
ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
|
|
352
573
|
previous_console = None
|
|
353
574
|
try:
|
|
@@ -374,9 +595,176 @@ class SlashSession:
|
|
|
374
595
|
ctx_obj.pop("_slash_console", None)
|
|
375
596
|
else:
|
|
376
597
|
ctx_obj["_slash_console"] = previous_console
|
|
377
|
-
return
|
|
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)
|
|
378
757
|
|
|
379
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
|
+
"""
|
|
380
768
|
client = self._get_client_or_fail()
|
|
381
769
|
if not client:
|
|
382
770
|
return True
|
|
@@ -442,7 +830,7 @@ class SlashSession:
|
|
|
442
830
|
self._render_header()
|
|
443
831
|
|
|
444
832
|
self._show_agent_followup_actions(picked_agent)
|
|
445
|
-
return
|
|
833
|
+
return self._continue_session()
|
|
446
834
|
|
|
447
835
|
def _show_agent_followup_actions(self, picked_agent: Any) -> None:
|
|
448
836
|
"""Show follow-up action hints after agent session."""
|
|
@@ -454,6 +842,7 @@ class SlashSession:
|
|
|
454
842
|
hints.append((f"/agents {agent_id}", f"Reopen {agent_label}"))
|
|
455
843
|
hints.extend(
|
|
456
844
|
[
|
|
845
|
+
("/accounts", "Switch account"),
|
|
457
846
|
(self.AGENTS_COMMAND, "Browse agents"),
|
|
458
847
|
(self.STATUS_COMMAND, "Check connection"),
|
|
459
848
|
]
|
|
@@ -462,6 +851,15 @@ class SlashSession:
|
|
|
462
851
|
self._show_quick_actions(hints, title="Next actions")
|
|
463
852
|
|
|
464
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
|
+
"""
|
|
465
863
|
if invoked_from_agent:
|
|
466
864
|
# Returning False would stop the full session; we only want to exit
|
|
467
865
|
# the agent context. Raising a custom flag keeps the outer loop
|
|
@@ -475,6 +873,7 @@ class SlashSession:
|
|
|
475
873
|
# Utilities
|
|
476
874
|
# ------------------------------------------------------------------
|
|
477
875
|
def _register_defaults(self) -> None:
|
|
876
|
+
"""Register default slash commands."""
|
|
478
877
|
self._register(
|
|
479
878
|
SlashCommand(
|
|
480
879
|
name="help",
|
|
@@ -486,7 +885,7 @@ class SlashSession:
|
|
|
486
885
|
self._register(
|
|
487
886
|
SlashCommand(
|
|
488
887
|
name="login",
|
|
489
|
-
help="
|
|
888
|
+
help="Configure API credentials (alias `/configure`).",
|
|
490
889
|
handler=SlashSession._cmd_login,
|
|
491
890
|
aliases=("configure",),
|
|
492
891
|
)
|
|
@@ -498,6 +897,23 @@ class SlashSession:
|
|
|
498
897
|
handler=SlashSession._cmd_status,
|
|
499
898
|
)
|
|
500
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
|
+
)
|
|
501
917
|
self._register(
|
|
502
918
|
SlashCommand(
|
|
503
919
|
name="agents",
|
|
@@ -527,12 +943,32 @@ class SlashSession:
|
|
|
527
943
|
handler=SlashSession._cmd_update,
|
|
528
944
|
)
|
|
529
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
|
+
)
|
|
530
954
|
|
|
531
955
|
def _register(self, command: SlashCommand) -> None:
|
|
956
|
+
"""Register a slash command.
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
command: SlashCommand to register.
|
|
960
|
+
"""
|
|
532
961
|
self._unique_commands[command.name] = command
|
|
533
962
|
for key in (command.name, *command.aliases):
|
|
534
963
|
self._commands[key] = command
|
|
535
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
|
+
|
|
536
972
|
def open_transcript_viewer(self, *, announce: bool = True) -> None:
|
|
537
973
|
"""Launch the transcript viewer for the most recent run."""
|
|
538
974
|
payload, manifest = self._get_last_transcript()
|
|
@@ -557,6 +993,14 @@ class SlashSession:
|
|
|
557
993
|
)
|
|
558
994
|
|
|
559
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
|
+
"""
|
|
560
1004
|
return export_cached_transcript(destination=destination, run_id=run_id)
|
|
561
1005
|
|
|
562
1006
|
try:
|
|
@@ -574,55 +1018,13 @@ class SlashSession:
|
|
|
574
1018
|
manifest = ctx_obj.get("_last_transcript_manifest")
|
|
575
1019
|
return payload, manifest
|
|
576
1020
|
|
|
577
|
-
def _cmd_export(self,
|
|
1021
|
+
def _cmd_export(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
578
1022
|
"""Slash handler for `/export` command."""
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if run_id:
|
|
585
|
-
self.console.print(
|
|
586
|
-
f"[{WARNING_STYLE}]No cached transcript found with run id {run_id!r}. "
|
|
587
|
-
"Omit the run id to export the most recent run.[/]"
|
|
588
|
-
)
|
|
589
|
-
else:
|
|
590
|
-
self.console.print(f"[{WARNING_STYLE}]No cached transcripts available yet. Run an agent first.[/]")
|
|
591
|
-
return False
|
|
592
|
-
|
|
593
|
-
destination = self._resolve_export_destination(path_arg, manifest_entry)
|
|
594
|
-
if destination is None:
|
|
595
|
-
return False
|
|
596
|
-
|
|
597
|
-
try:
|
|
598
|
-
exported = export_cached_transcript(
|
|
599
|
-
destination=destination,
|
|
600
|
-
run_id=manifest_entry.get("run_id"),
|
|
601
|
-
)
|
|
602
|
-
except FileNotFoundError as exc:
|
|
603
|
-
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
604
|
-
return False
|
|
605
|
-
except Exception as exc: # pragma: no cover - unexpected IO failures
|
|
606
|
-
self.console.print(f"[{ERROR_STYLE}]Failed to export transcript: {exc}[/]")
|
|
607
|
-
return False
|
|
608
|
-
else:
|
|
609
|
-
self.console.print(f"[{SUCCESS_STYLE}]Transcript exported to[/] {exported}")
|
|
610
|
-
return True
|
|
611
|
-
|
|
612
|
-
def _resolve_export_destination(self, path_arg: str | None, manifest_entry: dict[str, Any]) -> Path | None:
|
|
613
|
-
if path_arg:
|
|
614
|
-
return normalise_export_destination(Path(path_arg))
|
|
615
|
-
|
|
616
|
-
default_name = suggest_filename(manifest_entry)
|
|
617
|
-
prompt = f"Save transcript to [{default_name}]: "
|
|
618
|
-
try:
|
|
619
|
-
response = self.console.input(prompt)
|
|
620
|
-
except EOFError:
|
|
621
|
-
self.console.print("[dim]Export cancelled.[/dim]")
|
|
622
|
-
return None
|
|
623
|
-
|
|
624
|
-
chosen = response.strip() or default_name
|
|
625
|
-
return normalise_export_destination(Path(chosen))
|
|
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
|
|
626
1028
|
|
|
627
1029
|
def _cmd_update(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
628
1030
|
"""Slash handler for `/update` command."""
|
|
@@ -695,6 +1097,14 @@ class SlashSession:
|
|
|
695
1097
|
pass
|
|
696
1098
|
|
|
697
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
|
+
"""
|
|
698
1108
|
try:
|
|
699
1109
|
tokens = shlex.split(raw)
|
|
700
1110
|
except ValueError:
|
|
@@ -710,6 +1120,14 @@ class SlashSession:
|
|
|
710
1120
|
return head, tokens[1:]
|
|
711
1121
|
|
|
712
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
|
+
"""
|
|
713
1131
|
keys = [cmd.name for cmd in self._unique_commands.values()]
|
|
714
1132
|
match = get_close_matches(verb, keys, n=1)
|
|
715
1133
|
return match[0] if match else None
|
|
@@ -738,6 +1156,7 @@ class SlashSession:
|
|
|
738
1156
|
if callable(message):
|
|
739
1157
|
|
|
740
1158
|
def prompt_text() -> Any:
|
|
1159
|
+
"""Get formatted prompt text from callable message."""
|
|
741
1160
|
return self._convert_message(message())
|
|
742
1161
|
else:
|
|
743
1162
|
prompt_text = self._convert_message(message)
|
|
@@ -783,10 +1202,42 @@ class SlashSession:
|
|
|
783
1202
|
return self._prompt_with_basic_input(message, placeholder)
|
|
784
1203
|
|
|
785
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
|
+
"""
|
|
786
1210
|
if self._client is None:
|
|
787
1211
|
self._client = get_client(self.ctx)
|
|
788
1212
|
return self._client
|
|
789
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
|
+
|
|
790
1241
|
def set_contextual_commands(self, commands: dict[str, str] | None, *, include_global: bool = True) -> None:
|
|
791
1242
|
"""Set context-specific commands that should appear in completions."""
|
|
792
1243
|
self._contextual_commands = dict(commands or {})
|
|
@@ -801,6 +1252,11 @@ class SlashSession:
|
|
|
801
1252
|
return self._contextual_include_global
|
|
802
1253
|
|
|
803
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
|
+
"""
|
|
804
1260
|
agent_data = {
|
|
805
1261
|
"id": str(getattr(agent, "id", "")),
|
|
806
1262
|
"name": getattr(agent, "name", "") or "",
|
|
@@ -818,6 +1274,13 @@ class SlashSession:
|
|
|
818
1274
|
focus_agent: bool = False,
|
|
819
1275
|
initial: bool = False,
|
|
820
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
|
+
"""
|
|
821
1284
|
if focus_agent and active_agent is not None:
|
|
822
1285
|
self._render_focused_agent_header(active_agent)
|
|
823
1286
|
return
|
|
@@ -860,8 +1323,9 @@ class SlashSession:
|
|
|
860
1323
|
|
|
861
1324
|
header_grid = self._build_header_grid(agent_info, transcript_status)
|
|
862
1325
|
keybar = self._build_keybar()
|
|
863
|
-
|
|
864
1326
|
header_grid.add_row(keybar, "")
|
|
1327
|
+
|
|
1328
|
+
# Agent-scoped commands like /runs will appear in /help, no need to duplicate here
|
|
865
1329
|
self.console.print(AIPPanel(header_grid, title="Agent Session", border_style=PRIMARY))
|
|
866
1330
|
|
|
867
1331
|
def _get_agent_info(self, active_agent: Any) -> dict[str, str]:
|
|
@@ -903,14 +1367,16 @@ class SlashSession:
|
|
|
903
1367
|
f"[{ACCENT_STYLE}]{agent_info['id']}[/]"
|
|
904
1368
|
)
|
|
905
1369
|
status_line = f"[{SUCCESS_STYLE}]ready[/]"
|
|
906
|
-
|
|
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"
|
|
907
1376
|
header_grid.add_row(primary_line, status_line)
|
|
908
1377
|
|
|
909
1378
|
if agent_info["description"]:
|
|
910
|
-
|
|
911
|
-
if not transcript_status["transcript_ready"]:
|
|
912
|
-
description = f"{description} (transcript pending)"
|
|
913
|
-
header_grid.add_row(f"[dim]{description}[/dim]", "")
|
|
1379
|
+
header_grid.add_row(f"[dim]{agent_info['description']}[/dim]", "")
|
|
914
1380
|
|
|
915
1381
|
return header_grid
|
|
916
1382
|
|
|
@@ -919,10 +1385,11 @@ class SlashSession:
|
|
|
919
1385
|
keybar = AIPGrid(expand=True)
|
|
920
1386
|
keybar.add_column(justify="left", ratio=1)
|
|
921
1387
|
keybar.add_column(justify="left", ratio=1)
|
|
1388
|
+
keybar.add_column(justify="left", ratio=1)
|
|
922
1389
|
|
|
923
1390
|
keybar.add_row(
|
|
924
|
-
format_command_hint(
|
|
925
|
-
format_command_hint("/details", "Agent config") or "",
|
|
1391
|
+
format_command_hint(HELP_COMMAND, "Show commands") or "",
|
|
1392
|
+
format_command_hint("/details", "Agent config (expand prompt)") or "",
|
|
926
1393
|
format_command_hint("/exit", "Back") or "",
|
|
927
1394
|
)
|
|
928
1395
|
|
|
@@ -932,13 +1399,26 @@ class SlashSession:
|
|
|
932
1399
|
"""Render the main AIP environment header."""
|
|
933
1400
|
config = self._load_config()
|
|
934
1401
|
|
|
1402
|
+
account_name, account_host, env_lock = self._get_account_context()
|
|
935
1403
|
api_url = self._get_api_url(config)
|
|
936
|
-
status = "Configured" if config.get("api_key") else "Not configured"
|
|
937
1404
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
+
|
|
942
1422
|
agent_info = self._build_agent_status_line(active_agent)
|
|
943
1423
|
if agent_info:
|
|
944
1424
|
segments.append(agent_info)
|
|
@@ -961,12 +1441,23 @@ class SlashSession:
|
|
|
961
1441
|
)
|
|
962
1442
|
)
|
|
963
1443
|
|
|
964
|
-
def _get_api_url(self,
|
|
965
|
-
"""Get the API URL from
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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()
|
|
970
1461
|
|
|
971
1462
|
def _build_agent_status_line(self, active_agent: Any | None) -> str | None:
|
|
972
1463
|
"""Return a short status line about the active or recent agent."""
|
|
@@ -981,35 +1472,96 @@ class SlashSession:
|
|
|
981
1472
|
return None
|
|
982
1473
|
|
|
983
1474
|
def _show_default_quick_actions(self) -> None:
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
),
|
|
989
|
-
(
|
|
990
|
-
command_hint("agents list", slash_command="agents", ctx=self.ctx),
|
|
991
|
-
"Browse agents",
|
|
992
|
-
),
|
|
993
|
-
(
|
|
994
|
-
command_hint("help", slash_command="help", ctx=self.ctx),
|
|
995
|
-
"Show all commands",
|
|
996
|
-
),
|
|
997
|
-
]
|
|
998
|
-
filtered = [(cmd, desc) for cmd, desc in hints if cmd]
|
|
999
|
-
if filtered:
|
|
1000
|
-
self._show_quick_actions(filtered, title="Quick actions")
|
|
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}")
|
|
1001
1479
|
self._default_actions_shown = True
|
|
1002
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
|
+
|
|
1003
1555
|
def _render_home_hint(self) -> None:
|
|
1556
|
+
"""Render hint text for home screen."""
|
|
1004
1557
|
if self._home_hint_shown:
|
|
1005
1558
|
return
|
|
1006
|
-
|
|
1007
|
-
f"[{HINT_PREFIX_STYLE}]Hint:[/]"
|
|
1008
|
-
f"
|
|
1009
|
-
"
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
self.console.print("\n".join(hint_lines))
|
|
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)
|
|
1013
1565
|
self._home_hint_shown = True
|
|
1014
1566
|
|
|
1015
1567
|
def _show_quick_actions(
|
|
@@ -1019,30 +1571,99 @@ class SlashSession:
|
|
|
1019
1571
|
title: str = "Quick actions",
|
|
1020
1572
|
inline: bool = False,
|
|
1021
1573
|
) -> None:
|
|
1022
|
-
|
|
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)
|
|
1023
1582
|
if not hint_list:
|
|
1024
1583
|
return
|
|
1025
1584
|
|
|
1026
1585
|
if inline:
|
|
1027
|
-
|
|
1028
|
-
for command, description in hint_list:
|
|
1029
|
-
formatted = format_command_hint(command, description)
|
|
1030
|
-
if formatted:
|
|
1031
|
-
lines.append(formatted)
|
|
1032
|
-
if lines:
|
|
1033
|
-
self.console.print("\n".join(lines))
|
|
1586
|
+
self._render_inline_quick_actions(hint_list, title)
|
|
1034
1587
|
return
|
|
1035
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
|
+
"""
|
|
1036
1628
|
body_lines: list[Text] = []
|
|
1037
1629
|
for command, description in hint_list:
|
|
1038
1630
|
formatted = format_command_hint(command, description)
|
|
1039
1631
|
if formatted:
|
|
1040
1632
|
body_lines.append(Text.from_markup(formatted))
|
|
1041
|
-
|
|
1633
|
+
if not body_lines:
|
|
1634
|
+
return
|
|
1042
1635
|
panel_content = Group(*body_lines)
|
|
1043
1636
|
self.console.print(AIPPanel(panel_content, title=title, border_style=SECONDARY_LIGHT, expand=False))
|
|
1044
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
|
+
|
|
1045
1661
|
def _load_config(self) -> dict[str, Any]:
|
|
1662
|
+
"""Load configuration with caching.
|
|
1663
|
+
|
|
1664
|
+
Returns:
|
|
1665
|
+
Configuration dictionary.
|
|
1666
|
+
"""
|
|
1046
1667
|
if self._config_cache is None:
|
|
1047
1668
|
try:
|
|
1048
1669
|
self._config_cache = load_config() or {}
|
|
@@ -1051,6 +1672,16 @@ class SlashSession:
|
|
|
1051
1672
|
return self._config_cache
|
|
1052
1673
|
|
|
1053
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
|
+
"""
|
|
1054
1685
|
ref = ref.strip()
|
|
1055
1686
|
if not ref:
|
|
1056
1687
|
return None
|