glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.7__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 +42 -5
- glaip_sdk/agents/base.py +156 -32
- glaip_sdk/cli/auth.py +14 -8
- glaip_sdk/cli/commands/accounts.py +1 -1
- glaip_sdk/cli/commands/agents/__init__.py +119 -0
- glaip_sdk/cli/commands/agents/_common.py +561 -0
- glaip_sdk/cli/commands/agents/create.py +151 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +15 -12
- glaip_sdk/cli/commands/configure.py +2 -3
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/core/output.py +12 -7
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/main.py +127 -39
- glaip_sdk/cli/pager.py +3 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/accounts_controller.py +112 -32
- glaip_sdk/cli/slash/agent_session.py +5 -2
- glaip_sdk/cli/slash/prompt.py +11 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
- glaip_sdk/cli/slash/session.py +58 -13
- glaip_sdk/cli/slash/tui/__init__.py +26 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +7 -5
- glaip_sdk/cli/slash/tui/accounts_app.py +70 -9
- glaip_sdk/cli/slash/tui/clipboard.py +147 -0
- glaip_sdk/cli/slash/tui/context.py +59 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/terminal.py +402 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +123 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +5 -3
- glaip_sdk/cli/update_notifier.py +215 -7
- glaip_sdk/cli/validators.py +1 -1
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agents.py +50 -8
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +7 -1
- glaip_sdk/client/mcps.py +44 -13
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +367 -3
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +57 -26
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/models/__init__.py +17 -0
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/registry/tool.py +273 -59
- glaip_sdk/runner/__init__.py +20 -3
- glaip_sdk/runner/deps.py +5 -8
- glaip_sdk/runner/langgraph.py +317 -42
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/base.py +44 -11
- glaip_sdk/utils/__init__.py +1 -0
- glaip_sdk/utils/bundler.py +138 -2
- glaip_sdk/utils/import_resolver.py +43 -11
- glaip_sdk/utils/rendering/renderer/base.py +58 -0
- glaip_sdk/utils/runtime_config.py +15 -12
- glaip_sdk/utils/sync.py +31 -11
- glaip_sdk/utils/tool_detection.py +274 -6
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +47 -37
- glaip_sdk-0.7.7.dist-info/RECORD +213 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
- glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
|
@@ -151,79 +151,149 @@ class AccountsController:
|
|
|
151
151
|
|
|
152
152
|
def _render_textual(self, rows: list[dict[str, str | bool]], store: AccountStore, env_lock: bool) -> None:
|
|
153
153
|
"""Launch the Textual accounts browser."""
|
|
154
|
-
|
|
154
|
+
active_before = store.get_active_account()
|
|
155
|
+
notified = False
|
|
156
|
+
|
|
157
|
+
def _switch_in_textual(name: str) -> tuple[bool, str]:
|
|
158
|
+
nonlocal notified
|
|
159
|
+
switched, message = self._switch_account(
|
|
160
|
+
store,
|
|
161
|
+
name,
|
|
162
|
+
env_lock,
|
|
163
|
+
emit_console=False,
|
|
164
|
+
invalidate_session=True,
|
|
165
|
+
)
|
|
166
|
+
if switched:
|
|
167
|
+
notified = True
|
|
168
|
+
return switched, message
|
|
169
|
+
|
|
170
|
+
callbacks = AccountsTUICallbacks(switch_account=_switch_in_textual)
|
|
155
171
|
active = next((row["name"] for row in rows if row.get("active")), None)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
try:
|
|
173
|
+
# Inject TUI context for theme support
|
|
174
|
+
tui_ctx = getattr(self.session, "tui_ctx", None)
|
|
175
|
+
run_accounts_textual(rows, active_account=active, env_lock=env_lock, callbacks=callbacks, ctx=tui_ctx)
|
|
176
|
+
except Exception as exc: # pragma: no cover - defensive around Textual failures
|
|
177
|
+
self.console.print(f"[{WARNING_STYLE}]Accounts browser exited unexpectedly: {exc}[/]")
|
|
178
|
+
|
|
179
|
+
# Exit snapshot: surface a success banner when a switch occurred inside the TUI.
|
|
180
|
+
# Always notify when the active account changed, even if Textual raised.
|
|
181
|
+
active_after = store.get_active_account()
|
|
182
|
+
if active_after != active_before and not notified:
|
|
183
|
+
self._notify_account_switched(active_after)
|
|
159
184
|
if active_after != active:
|
|
160
185
|
host_after = ""
|
|
161
|
-
|
|
186
|
+
display_account = active_after or "default"
|
|
187
|
+
account_after = store.get_account(display_account) if hasattr(store, "get_account") else None
|
|
162
188
|
if account_after:
|
|
163
189
|
host_after = account_after.get("api_url", "")
|
|
164
190
|
host_suffix = f" • {host_after}" if host_after else ""
|
|
165
191
|
self.console.print(
|
|
166
192
|
AIPPanel(
|
|
167
|
-
f"[{SUCCESS_STYLE}]Active account ➜ {
|
|
193
|
+
f"[{SUCCESS_STYLE}]Active account ➜ {display_account}[/]{host_suffix}",
|
|
168
194
|
title="✅ Account Switched",
|
|
169
195
|
border_style=SUCCESS_STYLE,
|
|
170
196
|
)
|
|
171
197
|
)
|
|
172
198
|
|
|
173
|
-
def
|
|
174
|
-
"""
|
|
199
|
+
def _format_connection_error_message(self, error_reason: str, account_name: str, api_url: str) -> str:
|
|
200
|
+
"""Format error message for connection validation failures."""
|
|
201
|
+
code, detail = self._parse_error_reason(error_reason)
|
|
202
|
+
if code == "connection_failed":
|
|
203
|
+
return f"Switch aborted: cannot reach {api_url}. Check URL or network."
|
|
204
|
+
if code == "api_failed":
|
|
205
|
+
return f"Switch aborted: API error for '{account_name}'. Check credentials."
|
|
206
|
+
detail_suffix = f": {detail}" if detail else ""
|
|
207
|
+
return f"Switch aborted: {code or 'Validation failed'}{detail_suffix}"
|
|
208
|
+
|
|
209
|
+
def _emit_error_message(self, msg: str, style: str = ERROR_STYLE) -> None:
|
|
210
|
+
"""Emit an error or warning message to the console."""
|
|
211
|
+
self.console.print(f"[{style}]{msg}[/]")
|
|
212
|
+
|
|
213
|
+
def _validate_account_switch(
|
|
214
|
+
self, store: AccountStore, name: str, env_lock: bool, emit_console: bool
|
|
215
|
+
) -> tuple[bool, str, dict[str, str] | None]:
|
|
216
|
+
"""Validate account switch prerequisites; returns (is_valid, error_msg, account_dict)."""
|
|
175
217
|
if env_lock:
|
|
176
218
|
msg = "Env credentials detected (AIP_API_URL/AIP_API_KEY); switching is disabled."
|
|
177
|
-
|
|
178
|
-
|
|
219
|
+
if emit_console:
|
|
220
|
+
self._emit_error_message(msg, WARNING_STYLE)
|
|
221
|
+
return False, msg, None
|
|
179
222
|
|
|
180
223
|
account = store.get_account(name)
|
|
181
224
|
if not account:
|
|
182
225
|
msg = f"Account '{name}' not found."
|
|
183
|
-
|
|
184
|
-
|
|
226
|
+
if emit_console:
|
|
227
|
+
self._emit_error_message(msg)
|
|
228
|
+
return False, msg, None
|
|
185
229
|
|
|
186
230
|
api_url = account.get("api_url", "")
|
|
187
231
|
api_key = account.get("api_key", "")
|
|
188
232
|
if not api_url or not api_key:
|
|
189
233
|
edit_cmd = f"aip accounts edit {name}"
|
|
190
234
|
msg = f"Account '{name}' is missing credentials. Use `/login` or `{edit_cmd}`."
|
|
191
|
-
|
|
192
|
-
|
|
235
|
+
if emit_console:
|
|
236
|
+
self._emit_error_message(msg)
|
|
237
|
+
return False, msg, None
|
|
193
238
|
|
|
194
239
|
ok, error_reason = check_connection_with_reason(api_url, api_key, abort_on_error=False)
|
|
195
240
|
if not ok:
|
|
196
|
-
|
|
197
|
-
if
|
|
198
|
-
msg
|
|
199
|
-
|
|
200
|
-
msg = f"Switch aborted: API error for '{name}'. Check credentials."
|
|
201
|
-
else:
|
|
202
|
-
detail_suffix = f": {detail}" if detail else ""
|
|
203
|
-
msg = f"Switch aborted: {code or 'Validation failed'}{detail_suffix}"
|
|
204
|
-
self.console.print(f"[{WARNING_STYLE}]{msg}[/]")
|
|
205
|
-
return False, msg
|
|
241
|
+
msg = self._format_connection_error_message(error_reason, name, api_url)
|
|
242
|
+
if emit_console:
|
|
243
|
+
self._emit_error_message(msg, WARNING_STYLE)
|
|
244
|
+
return False, msg, None
|
|
206
245
|
|
|
246
|
+
return True, "", account
|
|
247
|
+
|
|
248
|
+
def _execute_account_switch(
|
|
249
|
+
self, store: AccountStore, name: str, account: dict[str, str], invalidate_session: bool, emit_console: bool
|
|
250
|
+
) -> tuple[bool, str]:
|
|
251
|
+
"""Execute the account switch and emit success message."""
|
|
207
252
|
try:
|
|
208
253
|
store.set_active_account(name)
|
|
254
|
+
api_url = account.get("api_url", "")
|
|
255
|
+
api_key = account.get("api_key", "")
|
|
209
256
|
masked_key = mask_api_key_display(api_key)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
257
|
+
if invalidate_session:
|
|
258
|
+
self._notify_account_switched(name)
|
|
259
|
+
if emit_console:
|
|
260
|
+
self.console.print(
|
|
261
|
+
AIPPanel(
|
|
262
|
+
f"[{SUCCESS_STYLE}]Active account ➜ {name}[/]\nAPI URL: {api_url}\nKey: {masked_key}",
|
|
263
|
+
title="✅ Account Switched",
|
|
264
|
+
border_style=SUCCESS_STYLE,
|
|
265
|
+
)
|
|
215
266
|
)
|
|
216
|
-
)
|
|
217
267
|
return True, f"Switched to '{name}'."
|
|
218
268
|
except AccountStoreError as exc:
|
|
219
269
|
msg = f"Failed to set active account: {exc}"
|
|
220
|
-
|
|
270
|
+
if emit_console:
|
|
271
|
+
self._emit_error_message(msg)
|
|
221
272
|
return False, msg
|
|
222
273
|
except Exception as exc: # NOSONAR(S1045) - catch-all needed for unexpected errors
|
|
223
274
|
msg = f"Unexpected error while switching to '{name}': {exc}"
|
|
224
|
-
|
|
275
|
+
if emit_console:
|
|
276
|
+
self._emit_error_message(msg)
|
|
225
277
|
return False, msg
|
|
226
278
|
|
|
279
|
+
def _switch_account(
|
|
280
|
+
self,
|
|
281
|
+
store: AccountStore,
|
|
282
|
+
name: str,
|
|
283
|
+
env_lock: bool,
|
|
284
|
+
*,
|
|
285
|
+
emit_console: bool = True,
|
|
286
|
+
invalidate_session: bool = True,
|
|
287
|
+
) -> tuple[bool, str]:
|
|
288
|
+
"""Validate and switch active account; returns (success, message)."""
|
|
289
|
+
is_valid, error_msg, account = self._validate_account_switch(store, name, env_lock, emit_console)
|
|
290
|
+
if not is_valid:
|
|
291
|
+
return False, error_msg
|
|
292
|
+
|
|
293
|
+
if account is None: # Defensive – should never happen, but avoid crashing in production
|
|
294
|
+
return False, "Unable to locate account after validation."
|
|
295
|
+
return self._execute_account_switch(store, name, account, invalidate_session, emit_console)
|
|
296
|
+
|
|
227
297
|
@staticmethod
|
|
228
298
|
def _parse_error_reason(reason: str | None) -> tuple[str, str]:
|
|
229
299
|
"""Parse error reason into (code, detail) to avoid fragile substring checks."""
|
|
@@ -404,8 +474,18 @@ class AccountsController:
|
|
|
404
474
|
except Exception as exc:
|
|
405
475
|
self.console.print(f"[{WARNING_STYLE}]Account saved but could not set active: {exc}[/]")
|
|
406
476
|
else:
|
|
477
|
+
self._notify_account_switched(name)
|
|
407
478
|
self._announce_active_change(store, name)
|
|
408
479
|
|
|
480
|
+
def _notify_account_switched(self, name: str | None) -> None:
|
|
481
|
+
"""Best-effort notify the hosting session that the active account changed."""
|
|
482
|
+
notify = getattr(self.session, "on_account_switched", None)
|
|
483
|
+
if callable(notify):
|
|
484
|
+
try:
|
|
485
|
+
notify(name)
|
|
486
|
+
except Exception: # pragma: no cover - best-effort callback
|
|
487
|
+
pass
|
|
488
|
+
|
|
409
489
|
def _confirm_delete_prompt(self, name: str) -> bool:
|
|
410
490
|
"""Ask for delete confirmation; return True when confirmed."""
|
|
411
491
|
self.console.print(f"[{WARNING_STYLE}]Type '{name}' to confirm deletion. This cannot be undone.[/]")
|
|
@@ -17,7 +17,7 @@ from glaip_sdk.cli.commands.agents import run as agents_run_command
|
|
|
17
17
|
from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
|
|
18
18
|
from glaip_sdk.cli.hints import format_command_hint
|
|
19
19
|
from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
|
|
20
|
-
from glaip_sdk.cli.
|
|
20
|
+
from glaip_sdk.cli.core.context import bind_slash_session_context
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
23
23
|
from glaip_sdk.cli.slash.session import SlashSession
|
|
@@ -38,7 +38,10 @@ class AgentRunSession:
|
|
|
38
38
|
self.console = session.console
|
|
39
39
|
self._agent_id = str(getattr(agent, "id", ""))
|
|
40
40
|
self._agent_name = getattr(agent, "name", "") or self._agent_id
|
|
41
|
-
self._prompt_placeholder: str =
|
|
41
|
+
self._prompt_placeholder: str = (
|
|
42
|
+
"Chat with this agent here; use / for shortcuts. "
|
|
43
|
+
"Alt+Enter inserts a newline. Ctrl+T opens the last transcript."
|
|
44
|
+
)
|
|
42
45
|
self._contextual_completion_help: dict[str, str] = {
|
|
43
46
|
"details": "Show this agent's configuration (+ expands prompt).",
|
|
44
47
|
"help": "Display this context-aware menu.",
|
glaip_sdk/cli/slash/prompt.py
CHANGED
|
@@ -162,6 +162,17 @@ def _create_key_bindings(_session: SlashSession) -> Any:
|
|
|
162
162
|
if buffer.complete_state is not None:
|
|
163
163
|
buffer.cancel_completion()
|
|
164
164
|
|
|
165
|
+
@bindings.add("c-t") # type: ignore[misc]
|
|
166
|
+
def _handle_ctrl_t_key(event: Any) -> None: # vulture: ignore
|
|
167
|
+
"""Handle Ctrl+T key - open the transcript viewer (when available)."""
|
|
168
|
+
buffer = event.app.current_buffer
|
|
169
|
+
if buffer.complete_state is not None:
|
|
170
|
+
buffer.cancel_completion()
|
|
171
|
+
|
|
172
|
+
open_viewer = getattr(_session, "open_transcript_viewer", None)
|
|
173
|
+
if callable(open_viewer):
|
|
174
|
+
open_viewer(announce=True)
|
|
175
|
+
|
|
165
176
|
return bindings
|
|
166
177
|
|
|
167
178
|
|
|
@@ -30,7 +30,7 @@ from glaip_sdk.branding import (
|
|
|
30
30
|
)
|
|
31
31
|
from glaip_sdk.cli.constants import DEFAULT_REMOTE_RUNS_PAGE_LIMIT
|
|
32
32
|
from glaip_sdk.cli.slash.tui.remote_runs_app import RemoteRunsTUICallbacks, run_remote_runs_textual
|
|
33
|
-
from glaip_sdk.cli.
|
|
33
|
+
from glaip_sdk.cli.core.prompting import prompt_export_choice_questionary, questionary_safe_ask
|
|
34
34
|
from glaip_sdk.exceptions import (
|
|
35
35
|
AuthenticationError,
|
|
36
36
|
ForbiddenError,
|
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -6,6 +6,7 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import asyncio
|
|
9
10
|
import importlib
|
|
10
11
|
import os
|
|
11
12
|
import shlex
|
|
@@ -51,19 +52,17 @@ from glaip_sdk.cli.slash.prompt import (
|
|
|
51
52
|
to_formatted_text,
|
|
52
53
|
)
|
|
53
54
|
from glaip_sdk.cli.slash.remote_runs_controller import RemoteRunsController
|
|
55
|
+
from glaip_sdk.cli.slash.tui.context import TUIContext
|
|
54
56
|
from glaip_sdk.cli.transcript import (
|
|
55
57
|
export_cached_transcript,
|
|
56
58
|
load_history_snapshot,
|
|
57
59
|
)
|
|
58
60
|
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
59
61
|
from glaip_sdk.cli.update_notifier import maybe_notify_update
|
|
60
|
-
from glaip_sdk.cli.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
get_client,
|
|
65
|
-
restore_slash_session_context,
|
|
66
|
-
)
|
|
62
|
+
from glaip_sdk.cli.core.context import get_client, restore_slash_session_context
|
|
63
|
+
from glaip_sdk.cli.core.output import format_size
|
|
64
|
+
from glaip_sdk.cli.core.prompting import _fuzzy_pick_for_resources
|
|
65
|
+
from glaip_sdk.cli.hints import command_hint
|
|
67
66
|
from glaip_sdk.rich_components import AIPGrid, AIPPanel, AIPTable
|
|
68
67
|
|
|
69
68
|
SlashHandler = Callable[["SlashSession", list[str], bool], bool]
|
|
@@ -189,6 +188,7 @@ class SlashSession:
|
|
|
189
188
|
self._update_notifier = maybe_notify_update
|
|
190
189
|
self._home_hint_shown = False
|
|
191
190
|
self._agent_transcript_ready: dict[str, str] = {}
|
|
191
|
+
self.tui_ctx: TUIContext | None = None
|
|
192
192
|
|
|
193
193
|
# ------------------------------------------------------------------
|
|
194
194
|
# Session orchestration
|
|
@@ -218,6 +218,22 @@ class SlashSession:
|
|
|
218
218
|
|
|
219
219
|
def run(self, initial_commands: Iterable[str] | None = None) -> None:
|
|
220
220
|
"""Start the command palette session loop."""
|
|
221
|
+
# Initialize TUI context asynchronously
|
|
222
|
+
try:
|
|
223
|
+
self.tui_ctx = asyncio.run(TUIContext.create())
|
|
224
|
+
except RuntimeError:
|
|
225
|
+
try:
|
|
226
|
+
loop = asyncio.get_event_loop()
|
|
227
|
+
except RuntimeError:
|
|
228
|
+
self.tui_ctx = None
|
|
229
|
+
else:
|
|
230
|
+
if loop.is_running():
|
|
231
|
+
self.tui_ctx = None
|
|
232
|
+
else:
|
|
233
|
+
self.tui_ctx = loop.run_until_complete(TUIContext.create())
|
|
234
|
+
except Exception:
|
|
235
|
+
self.tui_ctx = None
|
|
236
|
+
|
|
221
237
|
ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
|
|
222
238
|
previous_session = None
|
|
223
239
|
if ctx_obj is not None:
|
|
@@ -548,7 +564,7 @@ class SlashSession:
|
|
|
548
564
|
try:
|
|
549
565
|
# Use the modern account-aware wizard directly (bypasses legacy config gating)
|
|
550
566
|
_configure_interactive(account_name=None)
|
|
551
|
-
self.
|
|
567
|
+
self.on_account_switched()
|
|
552
568
|
if self._suppress_login_layout:
|
|
553
569
|
self._welcome_rendered = False
|
|
554
570
|
self._default_actions_shown = False
|
|
@@ -1211,6 +1227,33 @@ class SlashSession:
|
|
|
1211
1227
|
self._client = get_client(self.ctx)
|
|
1212
1228
|
return self._client
|
|
1213
1229
|
|
|
1230
|
+
def on_account_switched(self, _account_name: str | None = None) -> None:
|
|
1231
|
+
"""Reset any state that depends on the active account.
|
|
1232
|
+
|
|
1233
|
+
The active account can change via `/accounts` (or other flows that call
|
|
1234
|
+
AccountStore.set_active_account). The slash session caches a configured
|
|
1235
|
+
client instance, so we must invalidate it to avoid leaking the previous
|
|
1236
|
+
account's API URL/key into subsequent commands like `/agents` or `/runs`.
|
|
1237
|
+
|
|
1238
|
+
This method clears:
|
|
1239
|
+
- Client and config cache (account-specific credentials)
|
|
1240
|
+
- Current agent and recent agents (agent data is account-scoped)
|
|
1241
|
+
- Runs pagination state (runs are account-scoped)
|
|
1242
|
+
- Active renderer and transcript ready state (UI state tied to account context)
|
|
1243
|
+
- Contextual commands (may be account-specific)
|
|
1244
|
+
|
|
1245
|
+
These broader resets ensure a clean slate when switching accounts, preventing
|
|
1246
|
+
stale data from the previous account from appearing in the new account's context.
|
|
1247
|
+
"""
|
|
1248
|
+
self._client = None
|
|
1249
|
+
self._config_cache = None
|
|
1250
|
+
self._current_agent = None
|
|
1251
|
+
self.recent_agents = []
|
|
1252
|
+
self._runs_pagination_state.clear()
|
|
1253
|
+
self.clear_active_renderer()
|
|
1254
|
+
self.clear_agent_transcript_ready()
|
|
1255
|
+
self.set_contextual_commands(None)
|
|
1256
|
+
|
|
1214
1257
|
def set_contextual_commands(self, commands: dict[str, str] | None, *, include_global: bool = True) -> None:
|
|
1215
1258
|
"""Set context-specific commands that should appear in completions."""
|
|
1216
1259
|
self._contextual_commands = dict(commands or {})
|
|
@@ -1340,14 +1383,16 @@ class SlashSession:
|
|
|
1340
1383
|
f"[{ACCENT_STYLE}]{agent_info['id']}[/]"
|
|
1341
1384
|
)
|
|
1342
1385
|
status_line = f"[{SUCCESS_STYLE}]ready[/]"
|
|
1343
|
-
|
|
1386
|
+
if not transcript_status["has_transcript"]:
|
|
1387
|
+
status_line += " · no transcript"
|
|
1388
|
+
elif transcript_status["transcript_ready"]:
|
|
1389
|
+
status_line += " · transcript ready"
|
|
1390
|
+
else:
|
|
1391
|
+
status_line += " · transcript pending"
|
|
1344
1392
|
header_grid.add_row(primary_line, status_line)
|
|
1345
1393
|
|
|
1346
1394
|
if agent_info["description"]:
|
|
1347
|
-
|
|
1348
|
-
if not transcript_status["transcript_ready"]:
|
|
1349
|
-
description = f"{description} (transcript pending)"
|
|
1350
|
-
header_grid.add_row(f"[dim]{description}[/dim]", "")
|
|
1395
|
+
header_grid.add_row(f"[dim]{agent_info['description']}[/dim]", "")
|
|
1351
1396
|
|
|
1352
1397
|
return header_grid
|
|
1353
1398
|
|
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
"""Textual UI helpers for slash commands."""
|
|
2
2
|
|
|
3
|
+
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter, ClipboardResult
|
|
4
|
+
from glaip_sdk.cli.slash.tui.context import TUIContext
|
|
5
|
+
from glaip_sdk.cli.slash.tui.keybind_registry import (
|
|
6
|
+
Keybind,
|
|
7
|
+
KeybindRegistry,
|
|
8
|
+
format_key_sequence,
|
|
9
|
+
parse_key_sequence,
|
|
10
|
+
)
|
|
11
|
+
from glaip_sdk.cli.slash.tui.toast import ToastBus, ToastVariant
|
|
3
12
|
from glaip_sdk.cli.slash.tui.remote_runs_app import (
|
|
4
13
|
RemoteRunsTextualApp,
|
|
5
14
|
RemoteRunsTUICallbacks,
|
|
6
15
|
run_remote_runs_textual,
|
|
7
16
|
)
|
|
17
|
+
from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities, detect_terminal_background
|
|
8
18
|
|
|
9
|
-
__all__ = [
|
|
19
|
+
__all__ = [
|
|
20
|
+
"TUIContext",
|
|
21
|
+
"ToastBus",
|
|
22
|
+
"ToastVariant",
|
|
23
|
+
"TerminalCapabilities",
|
|
24
|
+
"detect_terminal_background",
|
|
25
|
+
"RemoteRunsTextualApp",
|
|
26
|
+
"RemoteRunsTUICallbacks",
|
|
27
|
+
"run_remote_runs_textual",
|
|
28
|
+
"KeybindRegistry",
|
|
29
|
+
"Keybind",
|
|
30
|
+
"parse_key_sequence",
|
|
31
|
+
"format_key_sequence",
|
|
32
|
+
"ClipboardAdapter",
|
|
33
|
+
"ClipboardResult",
|
|
34
|
+
]
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
#env-lock {
|
|
13
13
|
padding: 0 1 0 1;
|
|
14
|
-
color:
|
|
14
|
+
color: $warning;
|
|
15
15
|
height: 1;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
padding: 0 1 0 1;
|
|
46
46
|
margin: 0 0 0 0;
|
|
47
47
|
height: 1fr;
|
|
48
|
+
border: tall $primary;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
#status-bar {
|
|
@@ -58,11 +59,12 @@
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
#status {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
color:
|
|
62
|
+
height: 3;
|
|
63
|
+
padding: 0 1;
|
|
64
|
+
color: $secondary;
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
|
|
66
68
|
.form-label {
|
|
67
69
|
padding: 0 1 0 1;
|
|
68
70
|
}
|
|
@@ -73,7 +75,7 @@
|
|
|
73
75
|
|
|
74
76
|
#form-status, #confirm-status {
|
|
75
77
|
padding: 0 1;
|
|
76
|
-
color:
|
|
78
|
+
color: $warning;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
#form-test {
|
|
@@ -23,7 +23,10 @@ from glaip_sdk.cli.slash.accounts_shared import (
|
|
|
23
23
|
env_credentials_present,
|
|
24
24
|
)
|
|
25
25
|
from glaip_sdk.cli.slash.tui.background_tasks import BackgroundTaskMixin
|
|
26
|
+
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
|
|
27
|
+
from glaip_sdk.cli.slash.tui.context import TUIContext
|
|
26
28
|
from glaip_sdk.cli.slash.tui.loading import hide_loading_indicator, show_loading_indicator
|
|
29
|
+
from glaip_sdk.cli.slash.tui.theme.catalog import _BUILTIN_THEMES
|
|
27
30
|
from glaip_sdk.cli.validators import validate_api_key
|
|
28
31
|
from glaip_sdk.utils.validation import validate_url
|
|
29
32
|
|
|
@@ -51,6 +54,13 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
51
54
|
LoadingIndicator = None # type: ignore[assignment]
|
|
52
55
|
ModalScreen = None # type: ignore[assignment]
|
|
53
56
|
Static = None # type: ignore[assignment]
|
|
57
|
+
Theme = None # type: ignore[assignment]
|
|
58
|
+
|
|
59
|
+
if App is not None:
|
|
60
|
+
try: # pragma: no cover - optional dependency
|
|
61
|
+
from textual.theme import Theme
|
|
62
|
+
except Exception: # pragma: no cover - optional dependency
|
|
63
|
+
Theme = None # type: ignore[assignment]
|
|
54
64
|
|
|
55
65
|
TEXTUAL_SUPPORTED = App is not None and DataTable is not None
|
|
56
66
|
|
|
@@ -201,11 +211,12 @@ def run_accounts_textual(
|
|
|
201
211
|
active_account: str | None,
|
|
202
212
|
env_lock: bool,
|
|
203
213
|
callbacks: AccountsTUICallbacks,
|
|
214
|
+
ctx: TUIContext | None = None,
|
|
204
215
|
) -> None:
|
|
205
216
|
"""Launch the Textual accounts browser if dependencies are available."""
|
|
206
217
|
if not TEXTUAL_SUPPORTED:
|
|
207
218
|
return
|
|
208
|
-
app = AccountsTextualApp(rows, active_account, env_lock, callbacks)
|
|
219
|
+
app = AccountsTextualApp(rows, active_account, env_lock, callbacks, ctx=ctx)
|
|
209
220
|
app.run()
|
|
210
221
|
|
|
211
222
|
|
|
@@ -379,16 +390,18 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
379
390
|
|
|
380
391
|
CSS_PATH = CSS_FILE_NAME
|
|
381
392
|
BINDINGS = [
|
|
382
|
-
Binding("enter", "switch_row", "Switch", show=True),
|
|
383
|
-
Binding("return", "switch_row", "Switch", show=False),
|
|
384
|
-
Binding("/", "focus_filter", "Filter", show=True),
|
|
385
|
-
Binding("a", "add_account", "Add", show=True),
|
|
386
|
-
Binding("e", "edit_account", "Edit", show=True),
|
|
387
|
-
Binding("d", "delete_account", "Delete", show=True),
|
|
393
|
+
Binding("enter", "switch_row", "Switch", show=True) if Binding else None,
|
|
394
|
+
Binding("return", "switch_row", "Switch", show=False) if Binding else None,
|
|
395
|
+
Binding("/", "focus_filter", "Filter", show=True) if Binding else None,
|
|
396
|
+
Binding("a", "add_account", "Add", show=True) if Binding else None,
|
|
397
|
+
Binding("e", "edit_account", "Edit", show=True) if Binding else None,
|
|
398
|
+
Binding("d", "delete_account", "Delete", show=True) if Binding else None,
|
|
399
|
+
Binding("c", "copy_account", "Copy", show=True) if Binding else None,
|
|
388
400
|
# Esc clears filter when focused/non-empty; otherwise exits
|
|
389
|
-
Binding("escape", "clear_or_exit", "Close", priority=True),
|
|
390
|
-
Binding("q", "app_exit", "Close", priority=True),
|
|
401
|
+
Binding("escape", "clear_or_exit", "Close", priority=True) if Binding else None,
|
|
402
|
+
Binding("q", "app_exit", "Close", priority=True) if Binding else None,
|
|
391
403
|
]
|
|
404
|
+
BINDINGS = [b for b in BINDINGS if b is not None]
|
|
392
405
|
|
|
393
406
|
def __init__(
|
|
394
407
|
self,
|
|
@@ -396,6 +409,7 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
396
409
|
active_account: str | None,
|
|
397
410
|
env_lock: bool,
|
|
398
411
|
callbacks: AccountsTUICallbacks,
|
|
412
|
+
ctx: TUIContext | None = None,
|
|
399
413
|
) -> None:
|
|
400
414
|
"""Initialize the Textual accounts app.
|
|
401
415
|
|
|
@@ -404,6 +418,7 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
404
418
|
active_account: Name of the currently active account.
|
|
405
419
|
env_lock: Whether environment credentials are locking account switching.
|
|
406
420
|
callbacks: Callbacks for account switching operations.
|
|
421
|
+
ctx: Shared TUI context.
|
|
407
422
|
"""
|
|
408
423
|
super().__init__()
|
|
409
424
|
self._store = get_account_store()
|
|
@@ -411,6 +426,7 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
411
426
|
self._active_account = active_account
|
|
412
427
|
self._env_lock = env_lock
|
|
413
428
|
self._callbacks = callbacks
|
|
429
|
+
self._ctx = ctx
|
|
414
430
|
self._filter_text: str = ""
|
|
415
431
|
self._is_switching = False
|
|
416
432
|
|
|
@@ -449,6 +465,7 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
449
465
|
|
|
450
466
|
def on_mount(self) -> None:
|
|
451
467
|
"""Configure table columns and load rows."""
|
|
468
|
+
self._apply_theme()
|
|
452
469
|
table = self.query_one(ACCOUNTS_TABLE_ID, DataTable)
|
|
453
470
|
table.add_column("Name", width=20)
|
|
454
471
|
table.add_column("API URL", width=40)
|
|
@@ -684,6 +701,10 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
684
701
|
return
|
|
685
702
|
self.exit()
|
|
686
703
|
|
|
704
|
+
def action_app_exit(self) -> None:
|
|
705
|
+
"""Exit the application regardless of focus state."""
|
|
706
|
+
self.exit()
|
|
707
|
+
|
|
687
708
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
688
709
|
"""Handle filter bar buttons."""
|
|
689
710
|
if event.button.id == "filter-clear":
|
|
@@ -748,6 +769,26 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
748
769
|
return
|
|
749
770
|
self.push_screen(ConfirmDeleteModal(name), self._on_delete_result)
|
|
750
771
|
|
|
772
|
+
def action_copy_account(self) -> None:
|
|
773
|
+
"""Copy selected account name and URL to clipboard."""
|
|
774
|
+
name = self._get_selected_name()
|
|
775
|
+
if not name:
|
|
776
|
+
self._set_status("Select an account to copy.", "yellow")
|
|
777
|
+
return
|
|
778
|
+
|
|
779
|
+
account = self._store.get_account(name)
|
|
780
|
+
if not account:
|
|
781
|
+
return
|
|
782
|
+
|
|
783
|
+
text = f"Account: {name}\nURL: {account.get('api_url', '')}"
|
|
784
|
+
adapter = ClipboardAdapter()
|
|
785
|
+
result = adapter.copy(text)
|
|
786
|
+
|
|
787
|
+
if result.success:
|
|
788
|
+
self._set_status(f"Copied '{name}' to clipboard.", "green")
|
|
789
|
+
else:
|
|
790
|
+
self._set_status(f"Copy failed: {result.message}", "red")
|
|
791
|
+
|
|
751
792
|
def _check_env_lock_hotkey(self) -> bool:
|
|
752
793
|
"""Prevent mutations when env credentials are present."""
|
|
753
794
|
if not self._is_env_locked():
|
|
@@ -870,3 +911,23 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
870
911
|
filter_input = self.query_one(FILTER_INPUT_ID, Input)
|
|
871
912
|
clear_btn = self.query_one("#filter-clear", Button)
|
|
872
913
|
clear_btn.display = bool(filter_input.value or self._filter_text)
|
|
914
|
+
|
|
915
|
+
def _apply_theme(self) -> None:
|
|
916
|
+
"""Register built-in themes and set the active one from context."""
|
|
917
|
+
if not self._ctx or not self._ctx.theme or Theme is None:
|
|
918
|
+
return
|
|
919
|
+
|
|
920
|
+
for name, tokens in _BUILTIN_THEMES.items():
|
|
921
|
+
self.register_theme(
|
|
922
|
+
Theme(
|
|
923
|
+
name=name,
|
|
924
|
+
primary=tokens.primary,
|
|
925
|
+
secondary=tokens.secondary,
|
|
926
|
+
accent=tokens.accent,
|
|
927
|
+
warning=tokens.warning,
|
|
928
|
+
error=tokens.error,
|
|
929
|
+
success=tokens.success,
|
|
930
|
+
)
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
self.theme = self._ctx.theme.theme_name
|