glaip-sdk 0.6.26__py3-none-any.whl → 0.7.1__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/cli/commands/mcps/__init__.py +0 -4
- glaip_sdk/cli/commands/mcps/_common.py +11 -42
- glaip_sdk/cli/commands/mcps/create.py +8 -9
- glaip_sdk/cli/commands/mcps/update.py +56 -12
- glaip_sdk/cli/commands/tools/create.py +2 -2
- glaip_sdk/cli/slash/accounts_controller.py +3 -1
- glaip_sdk/cli/slash/session.py +19 -0
- 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 +66 -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/client/tools.py +14 -20
- glaip_sdk/registry/tool.py +193 -81
- {glaip_sdk-0.6.26.dist-info → glaip_sdk-0.7.1.dist-info}/METADATA +2 -2
- {glaip_sdk-0.6.26.dist-info → glaip_sdk-0.7.1.dist-info}/RECORD +26 -18
- glaip_sdk/client/_agent_payloads.py +0 -52
- {glaip_sdk-0.6.26.dist-info → glaip_sdk-0.7.1.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.6.26.dist-info → glaip_sdk-0.7.1.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.6.26.dist-info → glaip_sdk-0.7.1.dist-info}/top_level.txt +0 -0
|
@@ -14,8 +14,6 @@ from glaip_sdk.cli.commands.mcps._common import ( # noqa: E402
|
|
|
14
14
|
console,
|
|
15
15
|
_load_import_ready_payload,
|
|
16
16
|
_merge_import_payload,
|
|
17
|
-
_assemble_update_data_from_import_payload,
|
|
18
|
-
_assemble_update_data_from_cli_options,
|
|
19
17
|
_strip_server_only_fields,
|
|
20
18
|
_coerce_cli_string,
|
|
21
19
|
_collect_cli_overrides,
|
|
@@ -73,8 +71,6 @@ __all__ = [
|
|
|
73
71
|
"console",
|
|
74
72
|
"_load_import_ready_payload",
|
|
75
73
|
"_merge_import_payload",
|
|
76
|
-
"_assemble_update_data_from_import_payload",
|
|
77
|
-
"_assemble_update_data_from_cli_options",
|
|
78
74
|
"_strip_server_only_fields",
|
|
79
75
|
"_coerce_cli_string",
|
|
80
76
|
"_collect_cli_overrides",
|
|
@@ -328,66 +328,35 @@ def _handle_cli_error(ctx: Any, error: Exception, operation: str) -> None:
|
|
|
328
328
|
ctx.exit(1)
|
|
329
329
|
|
|
330
330
|
|
|
331
|
-
def
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
Args:
|
|
335
|
-
import_payload: Import payload dictionary or None
|
|
336
|
-
|
|
337
|
-
Returns:
|
|
338
|
-
Dictionary with update fields from import payload
|
|
339
|
-
"""
|
|
340
|
-
update_data: dict[str, Any] = {}
|
|
341
|
-
if import_payload:
|
|
342
|
-
for field in ("name", "transport", "description", "config", "authentication"):
|
|
343
|
-
if field in import_payload:
|
|
344
|
-
update_data[field] = import_payload[field]
|
|
345
|
-
return update_data
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
def _assemble_update_data_from_cli_options(
|
|
349
|
-
update_data: dict[str, Any],
|
|
350
|
-
name: str | None,
|
|
351
|
-
transport: str | None,
|
|
352
|
-
description: str | None,
|
|
331
|
+
def _parse_and_validate_config_auth(
|
|
332
|
+
update_dict: dict[str, Any],
|
|
353
333
|
config: str | None,
|
|
354
334
|
auth: str | None,
|
|
335
|
+
transport: str | None,
|
|
355
336
|
import_payload: dict[str, Any] | None,
|
|
356
337
|
mcp: Any,
|
|
357
|
-
) ->
|
|
358
|
-
"""
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Parse and validate config and auth CLI options, updating dict in-place.
|
|
359
340
|
|
|
360
341
|
Args:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
config: Config option
|
|
366
|
-
auth: Auth option
|
|
342
|
+
update_dict: Dictionary to update with parsed config/auth
|
|
343
|
+
config: Config option string
|
|
344
|
+
auth: Auth option string
|
|
345
|
+
transport: Transport option for config validation
|
|
367
346
|
import_payload: Import payload dictionary or None
|
|
368
347
|
mcp: Current MCP object
|
|
369
|
-
|
|
370
|
-
Returns:
|
|
371
|
-
Updated dictionary with CLI option values
|
|
372
348
|
"""
|
|
373
|
-
if name is not None:
|
|
374
|
-
update_data["name"] = name
|
|
375
|
-
if transport is not None:
|
|
376
|
-
update_data["transport"] = transport
|
|
377
|
-
if description is not None:
|
|
378
|
-
update_data["description"] = description
|
|
379
349
|
if config is not None:
|
|
380
350
|
parsed_config = parse_json_input(config)
|
|
381
351
|
config_transport = _get_config_transport(transport, import_payload, mcp)
|
|
382
|
-
|
|
352
|
+
update_dict["config"] = validate_mcp_config_structure(
|
|
383
353
|
parsed_config,
|
|
384
354
|
transport=config_transport,
|
|
385
355
|
source="--config",
|
|
386
356
|
)
|
|
387
357
|
if auth is not None:
|
|
388
358
|
parsed_auth = parse_json_input(auth)
|
|
389
|
-
|
|
390
|
-
return update_data
|
|
359
|
+
update_dict["authentication"] = validate_mcp_auth_structure(parsed_auth, source="--auth")
|
|
391
360
|
|
|
392
361
|
|
|
393
362
|
def _generate_update_preview(mcp: Any, update_data: dict[str, Any], cli_overrides: dict[str, Any]) -> str:
|
|
@@ -111,29 +111,28 @@ def create(
|
|
|
111
111
|
effective_description = merged_payload.get("description")
|
|
112
112
|
effective_config = merged_payload.get("config") or {}
|
|
113
113
|
effective_auth = merged_payload.get("authentication")
|
|
114
|
+
mcp_metadata = merged_payload.get("mcp_metadata")
|
|
114
115
|
|
|
115
116
|
with spinner_context(
|
|
116
117
|
ctx,
|
|
117
118
|
"[bold blue]Creating MCP…[/bold blue]",
|
|
118
119
|
console_override=console,
|
|
119
120
|
):
|
|
121
|
+
# Use SDK client method to create MCP
|
|
120
122
|
create_kwargs: dict[str, Any] = {
|
|
121
|
-
"name": effective_name,
|
|
122
|
-
"config": effective_config,
|
|
123
123
|
"transport": effective_transport,
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
if effective_description is not None:
|
|
127
|
-
create_kwargs["description"] = effective_description
|
|
128
|
-
|
|
129
125
|
if effective_auth:
|
|
130
126
|
create_kwargs["authentication"] = effective_auth
|
|
131
|
-
|
|
132
|
-
mcp_metadata = merged_payload.get("mcp_metadata")
|
|
133
127
|
if mcp_metadata is not None:
|
|
134
128
|
create_kwargs["mcp_metadata"] = mcp_metadata
|
|
135
129
|
|
|
136
|
-
mcp = api_client.mcps.create_mcp(
|
|
130
|
+
mcp = api_client.mcps.create_mcp(
|
|
131
|
+
name=effective_name,
|
|
132
|
+
description=effective_description,
|
|
133
|
+
config=effective_config,
|
|
134
|
+
**create_kwargs,
|
|
135
|
+
)
|
|
137
136
|
|
|
138
137
|
# Handle JSON output
|
|
139
138
|
handle_json_output(ctx, mcp.model_dump())
|
|
@@ -16,11 +16,10 @@ from glaip_sdk.cli.core.context import get_client
|
|
|
16
16
|
from glaip_sdk.cli.core.rendering import spinner_context
|
|
17
17
|
|
|
18
18
|
from ._common import (
|
|
19
|
-
_assemble_update_data_from_cli_options,
|
|
20
|
-
_assemble_update_data_from_import_payload,
|
|
21
19
|
_handle_cli_error,
|
|
22
20
|
_handle_update_preview_and_confirmation,
|
|
23
21
|
_load_import_ready_payload,
|
|
22
|
+
_parse_and_validate_config_auth,
|
|
24
23
|
_resolve_mcp,
|
|
25
24
|
_validate_import_payload_fields,
|
|
26
25
|
_validate_update_inputs,
|
|
@@ -29,6 +28,49 @@ from ._common import (
|
|
|
29
28
|
)
|
|
30
29
|
|
|
31
30
|
|
|
31
|
+
def _merge_update_kwargs(
|
|
32
|
+
import_payload: dict[str, Any] | None,
|
|
33
|
+
name: str | None,
|
|
34
|
+
transport: str | None,
|
|
35
|
+
description: str | None,
|
|
36
|
+
config: str | None,
|
|
37
|
+
auth: str | None,
|
|
38
|
+
mcp: Any,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""Merge import payload and CLI options into kwargs for SDK builder.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
import_payload: Import payload dictionary or None
|
|
44
|
+
name: MCP name option
|
|
45
|
+
transport: Transport option
|
|
46
|
+
description: Description option
|
|
47
|
+
config: Config option
|
|
48
|
+
auth: Auth option
|
|
49
|
+
mcp: Current MCP object
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dictionary with merged update kwargs
|
|
53
|
+
"""
|
|
54
|
+
update_kwargs: dict[str, Any] = {}
|
|
55
|
+
|
|
56
|
+
# Start with import payload fields
|
|
57
|
+
if import_payload:
|
|
58
|
+
for field in ("name", "transport", "description", "config", "authentication"):
|
|
59
|
+
if field in import_payload:
|
|
60
|
+
update_kwargs[field] = import_payload[field]
|
|
61
|
+
|
|
62
|
+
# Override with CLI options (CLI takes precedence)
|
|
63
|
+
if name is not None:
|
|
64
|
+
update_kwargs["name"] = name
|
|
65
|
+
if transport is not None:
|
|
66
|
+
update_kwargs["transport"] = transport
|
|
67
|
+
if description is not None:
|
|
68
|
+
update_kwargs["description"] = description
|
|
69
|
+
_parse_and_validate_config_auth(update_kwargs, config, auth, transport, import_payload, mcp)
|
|
70
|
+
|
|
71
|
+
return update_kwargs
|
|
72
|
+
|
|
73
|
+
|
|
32
74
|
@mcps_group.command()
|
|
33
75
|
@click.argument("mcp_ref")
|
|
34
76
|
@click.option("--name", help="New MCP name")
|
|
@@ -87,7 +129,8 @@ def update(
|
|
|
87
129
|
Note:
|
|
88
130
|
Must specify either --import OR at least one CLI field.
|
|
89
131
|
CLI options override imported values when both are specified.
|
|
90
|
-
|
|
132
|
+
Method selection (PATCH vs PUT) is handled automatically by the SDK client
|
|
133
|
+
based on the fields provided.
|
|
91
134
|
|
|
92
135
|
\b
|
|
93
136
|
Examples:
|
|
@@ -116,28 +159,29 @@ def update(
|
|
|
116
159
|
if not _validate_import_payload_fields(import_payload):
|
|
117
160
|
return
|
|
118
161
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
update_data = _assemble_update_data_from_cli_options(
|
|
122
|
-
update_data, name, transport, description, config, auth, import_payload, mcp
|
|
123
|
-
)
|
|
162
|
+
# Merge import payload and CLI options into kwargs for SDK builder
|
|
163
|
+
update_kwargs = _merge_update_kwargs(import_payload, name, transport, description, config, auth, mcp)
|
|
124
164
|
|
|
125
|
-
if not
|
|
165
|
+
if not update_kwargs:
|
|
126
166
|
raise click.ClickException("No update fields specified")
|
|
127
167
|
|
|
168
|
+
# Build preview data for confirmation (using the same structure as before)
|
|
169
|
+
preview_data = update_kwargs.copy()
|
|
170
|
+
|
|
128
171
|
# Show confirmation preview for import-based updates (unless -y flag)
|
|
129
172
|
if not _handle_update_preview_and_confirmation(
|
|
130
|
-
import_payload, y, mcp,
|
|
173
|
+
import_payload, y, mcp, preview_data, name, transport, description, config, auth
|
|
131
174
|
):
|
|
132
175
|
return
|
|
133
176
|
|
|
134
|
-
#
|
|
177
|
+
# Use SDK client method to update MCP
|
|
178
|
+
# Pass mcp object (not mcp.id) to avoid extra fetch; SDK accepts str | MCP
|
|
135
179
|
with spinner_context(
|
|
136
180
|
ctx,
|
|
137
181
|
"[bold blue]Updating MCP…[/bold blue]",
|
|
138
182
|
console_override=console,
|
|
139
183
|
):
|
|
140
|
-
updated_mcp = client.mcps.update_mcp(mcp, **
|
|
184
|
+
updated_mcp = client.mcps.update_mcp(mcp, **update_kwargs)
|
|
141
185
|
|
|
142
186
|
handle_json_output(ctx, updated_mcp.model_dump())
|
|
143
187
|
handle_rich_output(ctx, display_update_success("MCP", updated_mcp.name))
|
|
@@ -13,14 +13,14 @@ from typing import Any
|
|
|
13
13
|
import click
|
|
14
14
|
|
|
15
15
|
from glaip_sdk.cli.context import get_ctx_value, output_flags
|
|
16
|
+
from glaip_sdk.cli.core.context import get_client, handle_best_effort_check
|
|
17
|
+
from glaip_sdk.cli.core.rendering import spinner_context
|
|
16
18
|
from glaip_sdk.cli.display import (
|
|
17
19
|
display_api_error,
|
|
18
20
|
display_creation_success,
|
|
19
21
|
handle_json_output,
|
|
20
22
|
handle_rich_output,
|
|
21
23
|
)
|
|
22
|
-
from glaip_sdk.cli.core.context import get_client, handle_best_effort_check
|
|
23
|
-
from glaip_sdk.cli.core.rendering import spinner_context
|
|
24
24
|
from glaip_sdk.cli.io import load_resource_from_file_with_validation as load_resource_from_file
|
|
25
25
|
from glaip_sdk.utils.import_export import merge_import_with_cli_args
|
|
26
26
|
|
|
@@ -170,7 +170,9 @@ class AccountsController:
|
|
|
170
170
|
callbacks = AccountsTUICallbacks(switch_account=_switch_in_textual)
|
|
171
171
|
active = next((row["name"] for row in rows if row.get("active")), None)
|
|
172
172
|
try:
|
|
173
|
-
|
|
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)
|
|
174
176
|
except Exception as exc: # pragma: no cover - defensive around Textual failures
|
|
175
177
|
self.console.print(f"[{WARNING_STYLE}]Accounts browser exited unexpectedly: {exc}[/]")
|
|
176
178
|
|
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,6 +52,7 @@ 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,
|
|
@@ -186,6 +188,7 @@ class SlashSession:
|
|
|
186
188
|
self._update_notifier = maybe_notify_update
|
|
187
189
|
self._home_hint_shown = False
|
|
188
190
|
self._agent_transcript_ready: dict[str, str] = {}
|
|
191
|
+
self.tui_ctx: TUIContext | None = None
|
|
189
192
|
|
|
190
193
|
# ------------------------------------------------------------------
|
|
191
194
|
# Session orchestration
|
|
@@ -215,6 +218,22 @@ class SlashSession:
|
|
|
215
218
|
|
|
216
219
|
def run(self, initial_commands: Iterable[str] | None = None) -> None:
|
|
217
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
|
+
|
|
218
237
|
ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
|
|
219
238
|
previous_session = None
|
|
220
239
|
if ctx_obj is not None:
|
|
@@ -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)
|
|
@@ -752,6 +769,26 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
752
769
|
return
|
|
753
770
|
self.push_screen(ConfirmDeleteModal(name), self._on_delete_result)
|
|
754
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
|
+
|
|
755
792
|
def _check_env_lock_hotkey(self) -> bool:
|
|
756
793
|
"""Prevent mutations when env credentials are present."""
|
|
757
794
|
if not self._is_env_locked():
|
|
@@ -874,3 +911,23 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
874
911
|
filter_input = self.query_one(FILTER_INPUT_ID, Input)
|
|
875
912
|
clear_btn = self.query_one("#filter-clear", Button)
|
|
876
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
|