glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__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 +44 -4
- glaip_sdk/_version.py +9 -0
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1413 -0
- glaip_sdk/branding.py +126 -2
- glaip_sdk/cli/account_store.py +555 -0
- glaip_sdk/cli/auth.py +260 -15
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents/__init__.py +116 -0
- glaip_sdk/cli/commands/agents/_common.py +562 -0
- glaip_sdk/cli/commands/agents/create.py +155 -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 +104 -0
- glaip_sdk/cli/commands/configure.py +728 -113
- 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 +12 -8
- 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_original.py +756 -0
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/config.py +49 -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 +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +41 -20
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +340 -143
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/pager.py +12 -13
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +580 -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 +568 -0
- glaip_sdk/cli/slash/session.py +1105 -153
- glaip_sdk/cli/slash/tui/__init__.py +36 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/clipboard.py +195 -0
- glaip_sdk/cli/slash/tui/context.py +92 -0
- glaip_sdk/cli/slash/tui/indicators.py +341 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
- glaip_sdk/cli/slash/tui/loading.py +80 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
- glaip_sdk/cli/slash/tui/terminal.py +407 -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 +112 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +388 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +66 -1
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +72 -463
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/cli/update_notifier.py +227 -10
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +3 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +576 -44
- glaip_sdk/client/base.py +26 -0
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +25 -14
- glaip_sdk/client/mcps.py +165 -24
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +546 -92
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +206 -32
- glaip_sdk/config/constants.py +33 -2
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +89 -0
- 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/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +136 -0
- glaip_sdk/models/_provider_mappings.py +101 -0
- glaip_sdk/models/_validation.py +97 -0
- glaip_sdk/models/agent.py +48 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/constants.py +141 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/model.py +170 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -0
- glaip_sdk/payload_schemas/guardrails.py +34 -0
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +445 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +76 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +1055 -0
- glaip_sdk/runner/logging_config.py +77 -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 +116 -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 +242 -0
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +488 -0
- glaip_sdk/utils/__init__.py +59 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +8 -2
- glaip_sdk/utils/bundler.py +403 -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 +524 -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 +299 -1434
- glaip_sdk/utils/rendering/renderer/config.py +1 -5
- 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 -33
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- 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 +426 -0
- glaip_sdk/utils/serialization.py +18 -0
- glaip_sdk/utils/sync.py +162 -0
- glaip_sdk/utils/tool_detection.py +301 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- glaip_sdk/utils/validation.py +16 -24
- {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
- glaip_sdk-0.7.17.dist-info/RECORD +224 -0
- {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1369
- glaip_sdk/cli/commands/mcps.py +0 -1187
- glaip_sdk/cli/commands/tools.py +0 -584
- glaip_sdk/cli/utils.py +0 -1278
- glaip_sdk/models.py +0 -240
- glaip_sdk-0.1.2.dist-info/RECORD +0 -82
- glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -6,10 +6,13 @@ 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
|
|
12
13
|
import sys
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
13
16
|
from collections.abc import Callable, Iterable
|
|
14
17
|
from dataclasses import dataclass
|
|
15
18
|
from difflib import get_close_matches
|
|
@@ -18,6 +21,7 @@ from typing import Any
|
|
|
18
21
|
|
|
19
22
|
import click
|
|
20
23
|
from rich.console import Console, Group
|
|
24
|
+
from rich.live import Live
|
|
21
25
|
from rich.text import Text
|
|
22
26
|
|
|
23
27
|
from glaip_sdk.branding import (
|
|
@@ -32,9 +36,19 @@ from glaip_sdk.branding import (
|
|
|
32
36
|
SUCCESS_STYLE,
|
|
33
37
|
WARNING_STYLE,
|
|
34
38
|
AIPBranding,
|
|
39
|
+
LogoAnimator,
|
|
35
40
|
)
|
|
36
|
-
from glaip_sdk.cli.
|
|
41
|
+
from glaip_sdk.cli.account_store import get_account_store
|
|
42
|
+
from glaip_sdk.cli.auth import resolve_api_url_from_context
|
|
43
|
+
from glaip_sdk.cli.commands import transcripts as transcripts_cmd
|
|
44
|
+
from glaip_sdk.cli.commands.configure import _configure_interactive, load_config
|
|
37
45
|
from glaip_sdk.cli.commands.update import update_command
|
|
46
|
+
from glaip_sdk.cli.core.context import get_client, restore_slash_session_context
|
|
47
|
+
from glaip_sdk.cli.core.output import format_size
|
|
48
|
+
from glaip_sdk.cli.core.prompting import _fuzzy_pick_for_resources
|
|
49
|
+
from glaip_sdk.cli.hints import command_hint, format_command_hint
|
|
50
|
+
from glaip_sdk.cli.slash.accounts_controller import AccountsController
|
|
51
|
+
from glaip_sdk.cli.slash.accounts_shared import env_credentials_present
|
|
38
52
|
from glaip_sdk.cli.slash.agent_session import AgentRunSession
|
|
39
53
|
from glaip_sdk.cli.slash.prompt import (
|
|
40
54
|
FormattedText,
|
|
@@ -44,20 +58,14 @@ from glaip_sdk.cli.slash.prompt import (
|
|
|
44
58
|
setup_prompt_toolkit,
|
|
45
59
|
to_formatted_text,
|
|
46
60
|
)
|
|
61
|
+
from glaip_sdk.cli.slash.remote_runs_controller import RemoteRunsController
|
|
62
|
+
from glaip_sdk.cli.slash.tui.context import TUIContext
|
|
47
63
|
from glaip_sdk.cli.transcript import (
|
|
48
64
|
export_cached_transcript,
|
|
49
|
-
|
|
50
|
-
resolve_manifest_for_export,
|
|
51
|
-
suggest_filename,
|
|
65
|
+
load_history_snapshot,
|
|
52
66
|
)
|
|
53
67
|
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
54
68
|
from glaip_sdk.cli.update_notifier import maybe_notify_update
|
|
55
|
-
from glaip_sdk.cli.utils import (
|
|
56
|
-
_fuzzy_pick_for_resources,
|
|
57
|
-
command_hint,
|
|
58
|
-
format_command_hint,
|
|
59
|
-
get_client,
|
|
60
|
-
)
|
|
61
69
|
from glaip_sdk.rich_components import AIPGrid, AIPPanel, AIPTable
|
|
62
70
|
|
|
63
71
|
SlashHandler = Callable[["SlashSession", list[str], bool], bool]
|
|
@@ -71,6 +79,88 @@ class SlashCommand:
|
|
|
71
79
|
help: str
|
|
72
80
|
handler: SlashHandler
|
|
73
81
|
aliases: tuple[str, ...] = ()
|
|
82
|
+
agent_only: bool = False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
NEW_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
|
|
86
|
+
{
|
|
87
|
+
"cli": "transcripts",
|
|
88
|
+
"slash": "transcripts",
|
|
89
|
+
"description": "Review transcript cache",
|
|
90
|
+
"tag": "NEW",
|
|
91
|
+
"priority": 10,
|
|
92
|
+
"scope": "global",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"cli": None,
|
|
96
|
+
"slash": "runs",
|
|
97
|
+
"description": "View remote run history for the active agent",
|
|
98
|
+
"tag": "NEW",
|
|
99
|
+
"priority": 8,
|
|
100
|
+
"scope": "agent",
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
DEFAULT_QUICK_ACTIONS: tuple[dict[str, Any], ...] = (
|
|
106
|
+
{
|
|
107
|
+
"cli": None,
|
|
108
|
+
"slash": "accounts",
|
|
109
|
+
"description": "Switch account profile",
|
|
110
|
+
"priority": 5,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"cli": "status",
|
|
114
|
+
"slash": "status",
|
|
115
|
+
"description": "Connection check",
|
|
116
|
+
"priority": 0,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"cli": "agents list",
|
|
120
|
+
"slash": "agents",
|
|
121
|
+
"description": "Browse agents",
|
|
122
|
+
"priority": 0,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"cli": "help",
|
|
126
|
+
"slash": "help",
|
|
127
|
+
"description": "Show all commands",
|
|
128
|
+
"priority": 0,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"cli": "configure",
|
|
132
|
+
"slash": "login",
|
|
133
|
+
"description": f"Configure credentials (alias [{HINT_COMMAND_STYLE}]/configure[/])",
|
|
134
|
+
"priority": -1,
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
HELP_COMMAND = "/help"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _quick_action_scope(action: dict[str, Any]) -> str:
|
|
143
|
+
"""Return the scope for a quick action definition."""
|
|
144
|
+
scope = action.get("scope") or "global"
|
|
145
|
+
if isinstance(scope, str):
|
|
146
|
+
return scope.lower()
|
|
147
|
+
return "global"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class AnimationState:
|
|
152
|
+
"""State for logo animation shared between threads.
|
|
153
|
+
|
|
154
|
+
Uses mutable lists for integer values to allow thread-safe updates
|
|
155
|
+
without requiring locks or atomic operations.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
pulse_step: list[int] # Current animation step position
|
|
159
|
+
pulse_direction: list[int] # Direction of pulse (1 or -1)
|
|
160
|
+
step_size: list[int] # Step size for animation
|
|
161
|
+
current_status: list[str] # Current status message
|
|
162
|
+
animation_running: threading.Event # Event signaling animation is running
|
|
163
|
+
stop_requested: threading.Event # Event signaling stop was requested
|
|
74
164
|
|
|
75
165
|
|
|
76
166
|
class SlashSession:
|
|
@@ -84,7 +174,11 @@ class SlashSession:
|
|
|
84
174
|
console: Optional console instance, creates default if None
|
|
85
175
|
"""
|
|
86
176
|
self.ctx = ctx
|
|
87
|
-
self.
|
|
177
|
+
self._interactive = bool(sys.stdin.isatty() and sys.stdout.isatty())
|
|
178
|
+
if console is None:
|
|
179
|
+
self.console = AIPBranding._make_console(force_terminal=self._interactive, soft_wrap=False)
|
|
180
|
+
else:
|
|
181
|
+
self.console = console
|
|
88
182
|
self._commands: dict[str, SlashCommand] = {}
|
|
89
183
|
self._unique_commands: dict[str, SlashCommand] = {}
|
|
90
184
|
self._contextual_commands: dict[str, str] = {}
|
|
@@ -93,13 +187,13 @@ class SlashSession:
|
|
|
93
187
|
self.recent_agents: list[dict[str, str]] = []
|
|
94
188
|
self.last_run_input: str | None = None
|
|
95
189
|
self._should_exit = False
|
|
96
|
-
self._interactive = bool(sys.stdin.isatty() and sys.stdout.isatty())
|
|
97
190
|
self._config_cache: dict[str, Any] | None = None
|
|
98
191
|
self._welcome_rendered = False
|
|
99
192
|
self._active_renderer: Any | None = None
|
|
100
193
|
self._current_agent: Any | None = None
|
|
194
|
+
self._runs_pagination_state: dict[str, dict[str, Any]] = {} # agent_id -> {page, limit, cursor}
|
|
101
195
|
|
|
102
|
-
self._home_placeholder = "
|
|
196
|
+
self._home_placeholder = "Hint: type / to explore commands · Ctrl+D exits"
|
|
103
197
|
|
|
104
198
|
# Command string constants to avoid duplication
|
|
105
199
|
self.STATUS_COMMAND = "/status"
|
|
@@ -116,13 +210,29 @@ class SlashSession:
|
|
|
116
210
|
self._update_notifier = maybe_notify_update
|
|
117
211
|
self._home_hint_shown = False
|
|
118
212
|
self._agent_transcript_ready: dict[str, str] = {}
|
|
213
|
+
self.tui_ctx: TUIContext | None = None
|
|
214
|
+
|
|
215
|
+
# Animation configuration constants
|
|
216
|
+
ANIMATION_FPS = 20
|
|
217
|
+
ANIMATION_FRAME_DURATION = 1.0 / ANIMATION_FPS # 0.05 seconds
|
|
218
|
+
ANIMATION_STARTUP_DELAY = 0.1 # Delay to ensure animation starts
|
|
219
|
+
|
|
220
|
+
# Startup UI constants
|
|
221
|
+
INITIALIZING_STATUS = "Initializing..."
|
|
222
|
+
CLI_HEADING_MARKUP = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
|
|
119
223
|
|
|
120
224
|
# ------------------------------------------------------------------
|
|
121
225
|
# Session orchestration
|
|
122
226
|
# ------------------------------------------------------------------
|
|
123
|
-
def refresh_branding(
|
|
227
|
+
def refresh_branding(
|
|
228
|
+
self,
|
|
229
|
+
sdk_version: str | None = None,
|
|
230
|
+
*,
|
|
231
|
+
branding_cls: type[AIPBranding] | None = None,
|
|
232
|
+
) -> None:
|
|
124
233
|
"""Refresh branding assets after an in-session SDK upgrade."""
|
|
125
|
-
|
|
234
|
+
branding_type = branding_cls or AIPBranding
|
|
235
|
+
self._branding = branding_type.create_from_sdk(
|
|
126
236
|
sdk_version=sdk_version,
|
|
127
237
|
package_name="glaip-sdk",
|
|
128
238
|
)
|
|
@@ -132,6 +242,7 @@ class SlashSession:
|
|
|
132
242
|
self._render_header(initial=True)
|
|
133
243
|
|
|
134
244
|
def _setup_prompt_toolkit(self) -> None:
|
|
245
|
+
"""Initialize prompt_toolkit session and style."""
|
|
135
246
|
session, style = setup_prompt_toolkit(self, interactive=self._interactive)
|
|
136
247
|
self._ptk_session = session
|
|
137
248
|
self._ptk_style = style
|
|
@@ -149,20 +260,301 @@ class SlashSession:
|
|
|
149
260
|
self._run_non_interactive(initial_commands)
|
|
150
261
|
return
|
|
151
262
|
|
|
152
|
-
if
|
|
263
|
+
# Use animated logo during initialization if supported
|
|
264
|
+
animator = LogoAnimator(console=self.console)
|
|
265
|
+
if animator.should_animate() and self._interactive:
|
|
266
|
+
config_available = self._run_with_animated_logo(animator)
|
|
267
|
+
else:
|
|
268
|
+
# Fallback to static logo for non-TTY or NO_COLOR
|
|
269
|
+
config_available = self._run_with_static_logo(animator)
|
|
270
|
+
|
|
271
|
+
if not config_available:
|
|
153
272
|
return
|
|
154
273
|
|
|
155
|
-
self.
|
|
156
|
-
self._render_header(initial=not self._welcome_rendered)
|
|
274
|
+
self._render_header(initial=not self._welcome_rendered, show_branding=False)
|
|
157
275
|
if not self._default_actions_shown:
|
|
158
276
|
self._show_default_quick_actions()
|
|
159
277
|
self._run_interactive_loop()
|
|
160
278
|
finally:
|
|
161
279
|
if ctx_obj is not None:
|
|
162
|
-
|
|
163
|
-
|
|
280
|
+
restore_slash_session_context(ctx_obj, previous_session)
|
|
281
|
+
|
|
282
|
+
def _initialize_tui_context(self) -> None:
|
|
283
|
+
"""Initialize TUI context with error handling.
|
|
284
|
+
|
|
285
|
+
Sets self.tui_ctx to None if initialization fails.
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
self.tui_ctx = asyncio.run(TUIContext.create(detect_osc11=False))
|
|
289
|
+
except RuntimeError:
|
|
290
|
+
try:
|
|
291
|
+
loop = asyncio.get_event_loop()
|
|
292
|
+
except RuntimeError:
|
|
293
|
+
self.tui_ctx = None
|
|
294
|
+
else:
|
|
295
|
+
if loop.is_running():
|
|
296
|
+
self.tui_ctx = None
|
|
164
297
|
else:
|
|
165
|
-
|
|
298
|
+
self.tui_ctx = loop.run_until_complete(TUIContext.create(detect_osc11=False))
|
|
299
|
+
except Exception:
|
|
300
|
+
self.tui_ctx = None
|
|
301
|
+
|
|
302
|
+
def _run_initialization_tasks(
|
|
303
|
+
self,
|
|
304
|
+
current_status: list[str],
|
|
305
|
+
animation_running: threading.Event,
|
|
306
|
+
status_callback: Callable[[str], None] | None = None,
|
|
307
|
+
) -> bool:
|
|
308
|
+
"""Run initialization tasks with status updates.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
current_status: Mutable list with current status message.
|
|
312
|
+
animation_running: Event to signal animation state.
|
|
313
|
+
status_callback: Optional callback to invoke when status changes.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
True if configuration is available, False otherwise.
|
|
317
|
+
"""
|
|
318
|
+
# Task 1: TUI Context.
|
|
319
|
+
current_status[0] = "Detecting terminal..."
|
|
320
|
+
if status_callback:
|
|
321
|
+
status_callback(current_status[0])
|
|
322
|
+
self._initialize_tui_context()
|
|
323
|
+
|
|
324
|
+
# Task 2: Configuration.
|
|
325
|
+
current_status[0] = "Connecting to API..."
|
|
326
|
+
if status_callback:
|
|
327
|
+
status_callback(current_status[0])
|
|
328
|
+
if not self._ensure_configuration():
|
|
329
|
+
animation_running.clear()
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
# Task 3: Updates.
|
|
333
|
+
current_status[0] = "Checking for updates..."
|
|
334
|
+
if status_callback:
|
|
335
|
+
status_callback(current_status[0])
|
|
336
|
+
# Defer update prompt if we are in animated initialization to avoid blocking/cluttering
|
|
337
|
+
self._maybe_show_update_prompt(defer=bool(status_callback is None))
|
|
338
|
+
return True
|
|
339
|
+
|
|
340
|
+
def _update_pulse_step(
|
|
341
|
+
self,
|
|
342
|
+
state: AnimationState,
|
|
343
|
+
animator: LogoAnimator,
|
|
344
|
+
) -> bool:
|
|
345
|
+
"""Update pulse step and direction.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
state: Animation state container.
|
|
349
|
+
animator: LogoAnimator instance for animation.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
True if animation should continue, False if should stop.
|
|
353
|
+
"""
|
|
354
|
+
state.pulse_step[0] += state.pulse_direction[0] * state.step_size[0]
|
|
355
|
+
if state.pulse_step[0] >= animator.max_width + 5:
|
|
356
|
+
state.pulse_step[0] = animator.max_width + 5
|
|
357
|
+
state.pulse_direction[0] = -1
|
|
358
|
+
return not state.stop_requested.is_set()
|
|
359
|
+
if state.pulse_step[0] <= -5:
|
|
360
|
+
state.pulse_step[0] = -5
|
|
361
|
+
state.pulse_direction[0] = 1
|
|
362
|
+
return not state.stop_requested.is_set()
|
|
363
|
+
return True
|
|
364
|
+
|
|
365
|
+
def _create_animation_updater(
|
|
366
|
+
self,
|
|
367
|
+
animator: LogoAnimator,
|
|
368
|
+
state: AnimationState,
|
|
369
|
+
heading: Text,
|
|
370
|
+
) -> Callable[[Live], None]:
|
|
371
|
+
"""Create animation update function for background thread.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
animator: LogoAnimator instance for animation.
|
|
375
|
+
state: Animation state container.
|
|
376
|
+
heading: Text heading for frames.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Function to update animation in background thread.
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
def build_frame(step: int, status_text: str) -> Group:
|
|
383
|
+
return Group(heading, Text(""), animator.generate_frame(step, status_text))
|
|
384
|
+
|
|
385
|
+
def update_animation(live: Live) -> None:
|
|
386
|
+
"""Update animation in background thread."""
|
|
387
|
+
while state.animation_running.is_set():
|
|
388
|
+
# Calculate next step
|
|
389
|
+
if not self._update_pulse_step(state, animator):
|
|
390
|
+
break
|
|
391
|
+
|
|
392
|
+
# Update frame with current status
|
|
393
|
+
try:
|
|
394
|
+
live.update(build_frame(state.pulse_step[0], state.current_status[0]))
|
|
395
|
+
except Exception:
|
|
396
|
+
# Animation may be stopped, ignore errors
|
|
397
|
+
break
|
|
398
|
+
time.sleep(self.ANIMATION_FRAME_DURATION)
|
|
399
|
+
state.animation_running.clear()
|
|
400
|
+
|
|
401
|
+
return update_animation
|
|
402
|
+
|
|
403
|
+
def _stop_animation_thread(
|
|
404
|
+
self,
|
|
405
|
+
animation_thread: threading.Thread,
|
|
406
|
+
state: AnimationState,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Stop animation thread gracefully.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
animation_thread: Thread running animation.
|
|
412
|
+
state: Animation state container.
|
|
413
|
+
"""
|
|
414
|
+
state.stop_requested.set()
|
|
415
|
+
state.step_size[0] = 3
|
|
416
|
+
animation_thread.join(timeout=1.5)
|
|
417
|
+
if animation_thread.is_alive():
|
|
418
|
+
state.animation_running.clear()
|
|
419
|
+
animation_thread.join(timeout=0.2)
|
|
420
|
+
|
|
421
|
+
def _run_animated_initialization(
|
|
422
|
+
self,
|
|
423
|
+
live: Live,
|
|
424
|
+
animator: LogoAnimator,
|
|
425
|
+
state: AnimationState,
|
|
426
|
+
heading: Text,
|
|
427
|
+
banner: Text,
|
|
428
|
+
) -> bool:
|
|
429
|
+
"""Run initialization tasks with animated logo.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
live: Live context for animation updates.
|
|
433
|
+
animator: LogoAnimator instance for animation.
|
|
434
|
+
state: Animation state container.
|
|
435
|
+
heading: Text heading for frames.
|
|
436
|
+
banner: Text banner for final display.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
True if configuration is available, False otherwise.
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
def build_banner() -> Group:
|
|
443
|
+
return Group(heading, Text(""), banner)
|
|
444
|
+
|
|
445
|
+
update_animation = self._create_animation_updater(
|
|
446
|
+
animator,
|
|
447
|
+
state,
|
|
448
|
+
heading,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Start animation thread.
|
|
452
|
+
animation_thread = threading.Thread(target=update_animation, args=(live,), daemon=True)
|
|
453
|
+
animation_thread.start()
|
|
454
|
+
|
|
455
|
+
# Small delay to ensure animation starts.
|
|
456
|
+
time.sleep(self.ANIMATION_STARTUP_DELAY)
|
|
457
|
+
|
|
458
|
+
# Run initialization tasks.
|
|
459
|
+
if not self._run_initialization_tasks(state.current_status, state.animation_running, status_callback=None):
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
# Stop animation and show final banner.
|
|
463
|
+
self._stop_animation_thread(animation_thread, state)
|
|
464
|
+
live.update(build_banner())
|
|
465
|
+
return True
|
|
466
|
+
|
|
467
|
+
def _run_with_animated_logo(self, animator: LogoAnimator) -> bool:
|
|
468
|
+
"""Run initialization with animated logo.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
animator: LogoAnimator instance for animation.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
True if configuration is available, False otherwise.
|
|
475
|
+
"""
|
|
476
|
+
state = AnimationState(
|
|
477
|
+
pulse_step=[0], # Use list for mutable shared state.
|
|
478
|
+
pulse_direction=[1], # Use list for mutable shared state.
|
|
479
|
+
step_size=[1],
|
|
480
|
+
current_status=[self.INITIALIZING_STATUS],
|
|
481
|
+
animation_running=threading.Event(),
|
|
482
|
+
stop_requested=threading.Event(),
|
|
483
|
+
)
|
|
484
|
+
state.animation_running.set()
|
|
485
|
+
heading = Text.from_markup(self.CLI_HEADING_MARKUP)
|
|
486
|
+
banner = Text.from_markup(self._branding.get_welcome_banner())
|
|
487
|
+
|
|
488
|
+
def build_frame(step: int, status_text: str) -> Group:
|
|
489
|
+
return Group(heading, Text(""), animator.generate_frame(step, status_text))
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
with Live(
|
|
493
|
+
build_frame(0, state.current_status[0]),
|
|
494
|
+
console=self.console,
|
|
495
|
+
refresh_per_second=self.ANIMATION_FPS,
|
|
496
|
+
transient=False,
|
|
497
|
+
) as live:
|
|
498
|
+
return self._run_animated_initialization(
|
|
499
|
+
live,
|
|
500
|
+
animator,
|
|
501
|
+
state,
|
|
502
|
+
heading,
|
|
503
|
+
banner,
|
|
504
|
+
)
|
|
505
|
+
except KeyboardInterrupt:
|
|
506
|
+
# Graceful exit on Ctrl+C
|
|
507
|
+
state.animation_running.clear()
|
|
508
|
+
# Align with static path: show heading and cancellation message
|
|
509
|
+
heading = Text.from_markup(self.CLI_HEADING_MARKUP)
|
|
510
|
+
self.console.print(Group(heading, Text(""), animator.static_frame("Initialization cancelled.")))
|
|
511
|
+
return False
|
|
512
|
+
|
|
513
|
+
def _run_with_static_logo(self, animator: LogoAnimator) -> bool:
|
|
514
|
+
"""Run initialization with static logo (non-TTY or NO_COLOR).
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
animator: LogoAnimator instance for static display.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
True if configuration is available, False otherwise.
|
|
521
|
+
"""
|
|
522
|
+
heading = Text.from_markup(self.CLI_HEADING_MARKUP)
|
|
523
|
+
banner = Text.from_markup(self._branding.get_welcome_banner())
|
|
524
|
+
|
|
525
|
+
def build_frame(status_text: str) -> Group:
|
|
526
|
+
return Group(heading, Text(""), animator.static_frame(status_text))
|
|
527
|
+
|
|
528
|
+
def build_banner() -> Group:
|
|
529
|
+
return Group(heading, Text(""), banner)
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
with Live(
|
|
533
|
+
build_frame(self.INITIALIZING_STATUS),
|
|
534
|
+
console=self.console,
|
|
535
|
+
refresh_per_second=4,
|
|
536
|
+
transient=False,
|
|
537
|
+
) as live:
|
|
538
|
+
# Run initialization tasks with status updates, reusing shared logic.
|
|
539
|
+
current_status = [self.INITIALIZING_STATUS]
|
|
540
|
+
animation_running = threading.Event()
|
|
541
|
+
animation_running.set()
|
|
542
|
+
|
|
543
|
+
# Update Live display when status changes via callback.
|
|
544
|
+
def update_display(status: str) -> None:
|
|
545
|
+
"""Update Live display with current status."""
|
|
546
|
+
live.update(build_frame(status))
|
|
547
|
+
|
|
548
|
+
if not self._run_initialization_tasks(
|
|
549
|
+
current_status, animation_running, status_callback=update_display
|
|
550
|
+
):
|
|
551
|
+
return False
|
|
552
|
+
|
|
553
|
+
live.update(build_banner())
|
|
554
|
+
return True
|
|
555
|
+
except KeyboardInterrupt:
|
|
556
|
+
self.console.print(Group(heading, Text(""), animator.static_frame("Initialization cancelled.")))
|
|
557
|
+
return False
|
|
166
558
|
|
|
167
559
|
def _run_interactive_loop(self) -> None:
|
|
168
560
|
"""Run the main interactive command loop."""
|
|
@@ -208,34 +600,124 @@ class SlashSession:
|
|
|
208
600
|
if not self.handle_command(raw):
|
|
209
601
|
break
|
|
210
602
|
|
|
603
|
+
def _handle_account_selection(self) -> bool:
|
|
604
|
+
"""Handle account selection when accounts exist but none are active.
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
True if configuration is ready after selection, False if user aborted.
|
|
608
|
+
"""
|
|
609
|
+
self.console.print(f"[{INFO_STYLE}]No active account selected. Please choose an account:[/]")
|
|
610
|
+
try:
|
|
611
|
+
self._cmd_accounts([], False)
|
|
612
|
+
self._config_cache = None
|
|
613
|
+
return self._check_configuration_after_selection()
|
|
614
|
+
except KeyboardInterrupt:
|
|
615
|
+
self.console.print(f"[{ERROR_STYLE}]Account selection aborted. Closing the command palette.[/]")
|
|
616
|
+
return False
|
|
617
|
+
|
|
618
|
+
def _check_configuration_after_selection(self) -> bool:
|
|
619
|
+
"""Check if configuration is ready after account selection.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
True if configuration is ready, False otherwise.
|
|
623
|
+
"""
|
|
624
|
+
return self._configuration_ready()
|
|
625
|
+
|
|
626
|
+
def _handle_new_account_creation(self) -> bool:
|
|
627
|
+
"""Handle new account creation when no accounts exist.
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
True if configuration succeeded, False if user aborted.
|
|
631
|
+
"""
|
|
632
|
+
previous_tip_env = os.environ.get("AIP_SUPPRESS_CONFIGURE_TIP")
|
|
633
|
+
os.environ["AIP_SUPPRESS_CONFIGURE_TIP"] = "1"
|
|
634
|
+
self._suppress_login_layout = True
|
|
635
|
+
try:
|
|
636
|
+
self._cmd_login([], False)
|
|
637
|
+
return True
|
|
638
|
+
except KeyboardInterrupt:
|
|
639
|
+
self.console.print(f"[{ERROR_STYLE}]Configuration aborted. Closing the command palette.[/]")
|
|
640
|
+
return False
|
|
641
|
+
finally:
|
|
642
|
+
self._suppress_login_layout = False
|
|
643
|
+
if previous_tip_env is None:
|
|
644
|
+
os.environ.pop("AIP_SUPPRESS_CONFIGURE_TIP", None)
|
|
645
|
+
else:
|
|
646
|
+
os.environ["AIP_SUPPRESS_CONFIGURE_TIP"] = previous_tip_env
|
|
647
|
+
|
|
211
648
|
def _ensure_configuration(self) -> bool:
|
|
212
649
|
"""Ensure the CLI has both API URL and credentials before continuing."""
|
|
213
650
|
while not self._configuration_ready():
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
651
|
+
store = get_account_store()
|
|
652
|
+
accounts = store.list_accounts()
|
|
653
|
+
active_account = store.get_active_account()
|
|
654
|
+
|
|
655
|
+
# If accounts exist but none are active, show accounts list first
|
|
656
|
+
if accounts and (not active_account or active_account not in accounts):
|
|
657
|
+
if not self._handle_account_selection():
|
|
658
|
+
return False
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
# No accounts exist - prompt for configuration
|
|
662
|
+
if not self._handle_new_account_creation():
|
|
220
663
|
return False
|
|
221
|
-
finally:
|
|
222
|
-
self._suppress_login_layout = False
|
|
223
664
|
|
|
224
665
|
return True
|
|
225
666
|
|
|
667
|
+
def _get_credentials_from_context_and_env(self) -> tuple[str, str]:
|
|
668
|
+
"""Get credentials from context and environment variables.
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
Tuple of (api_url, api_key) from context/env overrides.
|
|
672
|
+
"""
|
|
673
|
+
api_url = ""
|
|
674
|
+
api_key = ""
|
|
675
|
+
if isinstance(self.ctx.obj, dict):
|
|
676
|
+
api_url = self.ctx.obj.get("api_url", "")
|
|
677
|
+
api_key = self.ctx.obj.get("api_key", "")
|
|
678
|
+
# Environment variables take precedence
|
|
679
|
+
env_url = os.getenv("AIP_API_URL", "")
|
|
680
|
+
env_key = os.getenv("AIP_API_KEY", "")
|
|
681
|
+
return (env_url or api_url, env_key or api_key)
|
|
682
|
+
|
|
683
|
+
def _get_credentials_from_account_store(self) -> tuple[str, str] | None:
|
|
684
|
+
"""Get credentials from the active account in account store.
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
Tuple of (api_url, api_key) if active account exists, None otherwise.
|
|
688
|
+
"""
|
|
689
|
+
store = get_account_store()
|
|
690
|
+
active_account = store.get_active_account()
|
|
691
|
+
if not active_account:
|
|
692
|
+
return None
|
|
693
|
+
|
|
694
|
+
account = store.get_account(active_account)
|
|
695
|
+
if not account:
|
|
696
|
+
return None
|
|
697
|
+
|
|
698
|
+
api_url = account.get("api_url", "")
|
|
699
|
+
api_key = account.get("api_key", "")
|
|
700
|
+
return (api_url, api_key)
|
|
701
|
+
|
|
226
702
|
def _configuration_ready(self) -> bool:
|
|
227
703
|
"""Check whether API URL and credentials are available."""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if
|
|
704
|
+
# Check for explicit overrides in context/env first
|
|
705
|
+
override_url, override_key = self._get_credentials_from_context_and_env()
|
|
706
|
+
if override_url and override_key:
|
|
707
|
+
return True
|
|
708
|
+
|
|
709
|
+
# Read from account store directly to avoid stale cache
|
|
710
|
+
account_creds = self._get_credentials_from_account_store()
|
|
711
|
+
if account_creds is None:
|
|
231
712
|
return False
|
|
232
713
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
714
|
+
store_url, store_key = account_creds
|
|
715
|
+
|
|
716
|
+
# Use override values if available, otherwise use store values
|
|
717
|
+
api_url = override_url or store_url
|
|
718
|
+
api_key = override_key or store_key
|
|
236
719
|
|
|
237
|
-
|
|
238
|
-
return bool(api_key)
|
|
720
|
+
return bool(api_url and api_key)
|
|
239
721
|
|
|
240
722
|
def handle_command(self, raw: str, *, invoked_from_agent: bool = False) -> bool:
|
|
241
723
|
"""Parse and execute a single slash command string."""
|
|
@@ -250,7 +732,7 @@ class SlashSession:
|
|
|
250
732
|
if suggestion:
|
|
251
733
|
self.console.print(f"[{WARNING_STYLE}]Unknown command '{verb}'. Did you mean '/{suggestion}'?[/]")
|
|
252
734
|
else:
|
|
253
|
-
help_command =
|
|
735
|
+
help_command = HELP_COMMAND
|
|
254
736
|
help_hint = format_command_hint(help_command) or help_command
|
|
255
737
|
self.console.print(
|
|
256
738
|
f"[{WARNING_STYLE}]Unknown command '{verb}'. Type {help_hint} for a list of options.[/]"
|
|
@@ -263,15 +745,28 @@ class SlashSession:
|
|
|
263
745
|
return False
|
|
264
746
|
return True
|
|
265
747
|
|
|
748
|
+
def _continue_session(self) -> bool:
|
|
749
|
+
"""Signal that the slash session should remain active."""
|
|
750
|
+
return not self._should_exit
|
|
751
|
+
|
|
266
752
|
# ------------------------------------------------------------------
|
|
267
753
|
# Command handlers
|
|
268
754
|
# ------------------------------------------------------------------
|
|
269
755
|
def _cmd_help(self, _args: list[str], invoked_from_agent: bool) -> bool:
|
|
756
|
+
"""Handle the /help command.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
_args: Command arguments (unused).
|
|
760
|
+
invoked_from_agent: Whether invoked from agent context.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
True to continue session.
|
|
764
|
+
"""
|
|
270
765
|
try:
|
|
271
766
|
if invoked_from_agent:
|
|
272
767
|
self._render_agent_help()
|
|
273
768
|
else:
|
|
274
|
-
self._render_global_help()
|
|
769
|
+
self._render_global_help(include_agent_hint=True)
|
|
275
770
|
except Exception as exc: # pragma: no cover - UI/display errors
|
|
276
771
|
self.console.print(f"[{ERROR_STYLE}]Error displaying help: {exc}[/]")
|
|
277
772
|
return False
|
|
@@ -279,15 +774,17 @@ class SlashSession:
|
|
|
279
774
|
return True
|
|
280
775
|
|
|
281
776
|
def _render_agent_help(self) -> None:
|
|
777
|
+
"""Render help text for agent context commands."""
|
|
282
778
|
table = AIPTable()
|
|
283
779
|
table.add_column("Input", style=HINT_COMMAND_STYLE, no_wrap=True)
|
|
284
780
|
table.add_column("What happens", style=HINT_DESCRIPTION_COLOR)
|
|
285
781
|
table.add_row("<message>", "Run the active agent once with that prompt.")
|
|
286
|
-
table.add_row("/details", "Show the
|
|
782
|
+
table.add_row("/details", "Show the agent export (prompts to expand instructions).")
|
|
287
783
|
table.add_row(self.STATUS_COMMAND, "Display connection status without leaving.")
|
|
784
|
+
table.add_row("/runs", "✨ NEW · Open the remote run browser for this agent.")
|
|
288
785
|
table.add_row("/export [path]", "Export the latest agent transcript as JSONL.")
|
|
289
786
|
table.add_row("/exit (/back)", "Return to the slash home screen.")
|
|
290
|
-
table.add_row("
|
|
787
|
+
table.add_row(f"{HELP_COMMAND} (/?)", "Display this context-aware menu.")
|
|
291
788
|
|
|
292
789
|
panel_items = [table]
|
|
293
790
|
if self.last_run_input:
|
|
@@ -305,13 +802,28 @@ class SlashSession:
|
|
|
305
802
|
border_style=PRIMARY,
|
|
306
803
|
)
|
|
307
804
|
)
|
|
805
|
+
new_commands_table = AIPTable()
|
|
806
|
+
new_commands_table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
|
|
807
|
+
new_commands_table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
|
|
808
|
+
new_commands_table.add_row(
|
|
809
|
+
"/runs",
|
|
810
|
+
"✨ NEW · View remote run history with keyboard navigation and export options.",
|
|
811
|
+
)
|
|
812
|
+
self.console.print(
|
|
813
|
+
AIPPanel(
|
|
814
|
+
new_commands_table,
|
|
815
|
+
title="New commands",
|
|
816
|
+
border_style=SECONDARY_LIGHT,
|
|
817
|
+
)
|
|
818
|
+
)
|
|
308
819
|
|
|
309
|
-
def _render_global_help(self) -> None:
|
|
820
|
+
def _render_global_help(self, *, include_agent_hint: bool = False) -> None:
|
|
821
|
+
"""Render help text for global slash commands."""
|
|
310
822
|
table = AIPTable()
|
|
311
823
|
table.add_column("Command", style=HINT_COMMAND_STYLE, no_wrap=True)
|
|
312
824
|
table.add_column("Description", style=HINT_DESCRIPTION_COLOR)
|
|
313
825
|
|
|
314
|
-
for cmd in
|
|
826
|
+
for cmd in self._visible_commands(include_agent_only=False):
|
|
315
827
|
aliases = ", ".join(f"/{alias}" for alias in cmd.aliases if alias)
|
|
316
828
|
verb = f"/{cmd.name}"
|
|
317
829
|
if aliases:
|
|
@@ -331,12 +843,27 @@ class SlashSession:
|
|
|
331
843
|
border_style=PRIMARY,
|
|
332
844
|
)
|
|
333
845
|
)
|
|
846
|
+
if include_agent_hint:
|
|
847
|
+
self.console.print(
|
|
848
|
+
"[dim]Additional commands (e.g. `/runs`) become available after you pick an agent with `/agents`. "
|
|
849
|
+
"Those agent-only commands stay hidden here to avoid confusion.[/]"
|
|
850
|
+
)
|
|
334
851
|
|
|
335
852
|
def _cmd_login(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
853
|
+
"""Handle the /login command.
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
_args: Command arguments (unused).
|
|
857
|
+
_invoked_from_agent: Whether invoked from agent context (unused).
|
|
858
|
+
|
|
859
|
+
Returns:
|
|
860
|
+
True to continue session.
|
|
861
|
+
"""
|
|
336
862
|
self.console.print(f"[{ACCENT_STYLE}]Launching configuration wizard...[/]")
|
|
337
863
|
try:
|
|
338
|
-
|
|
339
|
-
|
|
864
|
+
# Use the modern account-aware wizard directly (bypasses legacy config gating)
|
|
865
|
+
_configure_interactive(account_name=None)
|
|
866
|
+
self.on_account_switched()
|
|
340
867
|
if self._suppress_login_layout:
|
|
341
868
|
self._welcome_rendered = False
|
|
342
869
|
self._default_actions_shown = False
|
|
@@ -345,9 +872,18 @@ class SlashSession:
|
|
|
345
872
|
self._show_default_quick_actions()
|
|
346
873
|
except click.ClickException as exc:
|
|
347
874
|
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
348
|
-
return
|
|
875
|
+
return self._continue_session()
|
|
349
876
|
|
|
350
877
|
def _cmd_status(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
878
|
+
"""Handle the /status command.
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
_args: Command arguments (unused).
|
|
882
|
+
_invoked_from_agent: Whether invoked from agent context (unused).
|
|
883
|
+
|
|
884
|
+
Returns:
|
|
885
|
+
True to continue session.
|
|
886
|
+
"""
|
|
351
887
|
ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
|
|
352
888
|
previous_console = None
|
|
353
889
|
try:
|
|
@@ -374,9 +910,176 @@ class SlashSession:
|
|
|
374
910
|
ctx_obj.pop("_slash_console", None)
|
|
375
911
|
else:
|
|
376
912
|
ctx_obj["_slash_console"] = previous_console
|
|
377
|
-
return
|
|
913
|
+
return self._continue_session()
|
|
914
|
+
|
|
915
|
+
def _cmd_transcripts(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
916
|
+
"""Handle the /transcripts command.
|
|
917
|
+
|
|
918
|
+
Args:
|
|
919
|
+
args: Command arguments (limit or detail/show with run_id).
|
|
920
|
+
_invoked_from_agent: Whether invoked from agent context (unused).
|
|
921
|
+
|
|
922
|
+
Returns:
|
|
923
|
+
True to continue session.
|
|
924
|
+
"""
|
|
925
|
+
if args and args[0].lower() in {"detail", "show"}:
|
|
926
|
+
if len(args) < 2:
|
|
927
|
+
self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts detail <run_id>[/]")
|
|
928
|
+
return self._continue_session()
|
|
929
|
+
self._show_transcript_detail(args[1])
|
|
930
|
+
return self._continue_session()
|
|
931
|
+
|
|
932
|
+
limit, ok = self._parse_transcripts_limit(args)
|
|
933
|
+
if not ok:
|
|
934
|
+
return self._continue_session()
|
|
935
|
+
|
|
936
|
+
snapshot = load_history_snapshot(limit=limit, ctx=self.ctx)
|
|
937
|
+
|
|
938
|
+
if self._handle_transcripts_empty(snapshot, limit):
|
|
939
|
+
return self._continue_session()
|
|
940
|
+
|
|
941
|
+
self._render_transcripts_snapshot(snapshot)
|
|
942
|
+
return self._continue_session()
|
|
943
|
+
|
|
944
|
+
def _parse_transcripts_limit(self, args: list[str]) -> tuple[int | None, bool]:
|
|
945
|
+
"""Parse limit argument from transcripts command.
|
|
946
|
+
|
|
947
|
+
Args:
|
|
948
|
+
args: Command arguments.
|
|
949
|
+
|
|
950
|
+
Returns:
|
|
951
|
+
Tuple of (limit value or None, success boolean).
|
|
952
|
+
"""
|
|
953
|
+
if not args:
|
|
954
|
+
return None, True
|
|
955
|
+
try:
|
|
956
|
+
limit = int(args[0])
|
|
957
|
+
except ValueError:
|
|
958
|
+
self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts [limit][/]")
|
|
959
|
+
return None, False
|
|
960
|
+
if limit < 0:
|
|
961
|
+
self.console.print(f"[{WARNING_STYLE}]Usage: /transcripts [limit][/]")
|
|
962
|
+
return None, False
|
|
963
|
+
return limit, True
|
|
964
|
+
|
|
965
|
+
def _handle_transcripts_empty(self, snapshot: Any, limit: int | None) -> bool:
|
|
966
|
+
"""Handle empty transcript snapshot cases.
|
|
967
|
+
|
|
968
|
+
Args:
|
|
969
|
+
snapshot: Transcript snapshot object.
|
|
970
|
+
limit: Limit value or None.
|
|
971
|
+
|
|
972
|
+
Returns:
|
|
973
|
+
True if empty case was handled, False otherwise.
|
|
974
|
+
"""
|
|
975
|
+
if snapshot.cached_entries == 0:
|
|
976
|
+
self.console.print(f"[{WARNING_STYLE}]No cached transcripts yet. Run an agent first.[/]")
|
|
977
|
+
for warning in snapshot.warnings:
|
|
978
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
979
|
+
return True
|
|
980
|
+
if limit == 0 and snapshot.cached_entries:
|
|
981
|
+
self.console.print(f"[{WARNING_STYLE}]Limit is 0; nothing to display.[/]")
|
|
982
|
+
return True
|
|
983
|
+
return False
|
|
984
|
+
|
|
985
|
+
def _render_transcripts_snapshot(self, snapshot: Any) -> None:
|
|
986
|
+
"""Render transcript snapshot table and metadata.
|
|
987
|
+
|
|
988
|
+
Args:
|
|
989
|
+
snapshot: Transcript snapshot object to render.
|
|
990
|
+
"""
|
|
991
|
+
size_text = format_size(snapshot.total_size_bytes)
|
|
992
|
+
header = f"[dim]Manifest: {snapshot.manifest_path} · {snapshot.total_entries} runs · {size_text} used[/]"
|
|
993
|
+
self.console.print(header)
|
|
994
|
+
|
|
995
|
+
if snapshot.limit_clamped:
|
|
996
|
+
self.console.print(
|
|
997
|
+
f"[{WARNING_STYLE}]Requested limit exceeded maximum; showing first {snapshot.limit_applied} runs.[/]"
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
if snapshot.total_entries > len(snapshot.entries):
|
|
1001
|
+
subset_message = (
|
|
1002
|
+
f"[dim]Showing {len(snapshot.entries)} of {snapshot.total_entries} "
|
|
1003
|
+
f"runs (limit={snapshot.limit_applied}).[/]"
|
|
1004
|
+
)
|
|
1005
|
+
self.console.print(subset_message)
|
|
1006
|
+
self.console.print("[dim]Hint: run `/transcripts <limit>` to change how many rows are displayed.[/]")
|
|
1007
|
+
|
|
1008
|
+
if snapshot.migration_summary:
|
|
1009
|
+
self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
1010
|
+
|
|
1011
|
+
for warning in snapshot.warnings:
|
|
1012
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
1013
|
+
|
|
1014
|
+
table = transcripts_cmd._build_table(snapshot.entries)
|
|
1015
|
+
self.console.print(table)
|
|
1016
|
+
self.console.print("[dim]! Missing transcript[/]")
|
|
1017
|
+
|
|
1018
|
+
def _show_transcript_detail(self, run_id: str) -> None:
|
|
1019
|
+
"""Render the cached transcript log for a single run."""
|
|
1020
|
+
snapshot = load_history_snapshot(ctx=self.ctx)
|
|
1021
|
+
entry = snapshot.index.get(run_id)
|
|
1022
|
+
if entry is None:
|
|
1023
|
+
self.console.print(f"[{WARNING_STYLE}]Run id {run_id} was not found in the cache manifest.[/]")
|
|
1024
|
+
return
|
|
1025
|
+
|
|
1026
|
+
try:
|
|
1027
|
+
transcript_path, transcript_text = transcripts_cmd._load_transcript_text(entry)
|
|
1028
|
+
except click.ClickException as exc:
|
|
1029
|
+
self.console.print(f"[{WARNING_STYLE}]{exc}[/]")
|
|
1030
|
+
return
|
|
1031
|
+
|
|
1032
|
+
meta, events = transcripts_cmd._decode_transcript(transcript_text)
|
|
1033
|
+
if transcripts_cmd._maybe_launch_transcript_viewer(
|
|
1034
|
+
self.ctx,
|
|
1035
|
+
entry,
|
|
1036
|
+
meta,
|
|
1037
|
+
events,
|
|
1038
|
+
console_override=self.console,
|
|
1039
|
+
force=True,
|
|
1040
|
+
initial_view="transcript",
|
|
1041
|
+
):
|
|
1042
|
+
if snapshot.migration_summary:
|
|
1043
|
+
self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
1044
|
+
for warning in snapshot.warnings:
|
|
1045
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
1046
|
+
return
|
|
1047
|
+
|
|
1048
|
+
if snapshot.migration_summary:
|
|
1049
|
+
self.console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
1050
|
+
for warning in snapshot.warnings:
|
|
1051
|
+
self.console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
1052
|
+
view = transcripts_cmd._render_transcript_display(entry, snapshot.manifest_path, transcript_path, meta, events)
|
|
1053
|
+
self.console.print(view, markup=False, highlight=False, soft_wrap=True, end="")
|
|
1054
|
+
|
|
1055
|
+
def _cmd_runs(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
1056
|
+
"""Handle the /runs command for browsing remote agent run history.
|
|
1057
|
+
|
|
1058
|
+
Args:
|
|
1059
|
+
args: Command arguments (optional run_id for detail view).
|
|
1060
|
+
_invoked_from_agent: Whether invoked from agent context.
|
|
1061
|
+
|
|
1062
|
+
Returns:
|
|
1063
|
+
True to continue session.
|
|
1064
|
+
"""
|
|
1065
|
+
controller = RemoteRunsController(self)
|
|
1066
|
+
return controller.handle_runs_command(args)
|
|
1067
|
+
|
|
1068
|
+
def _cmd_accounts(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
1069
|
+
"""Handle the /accounts command for listing and switching accounts."""
|
|
1070
|
+
controller = AccountsController(self)
|
|
1071
|
+
return controller.handle_accounts_command(args)
|
|
378
1072
|
|
|
379
1073
|
def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
1074
|
+
"""Handle the /agents command.
|
|
1075
|
+
|
|
1076
|
+
Args:
|
|
1077
|
+
args: Command arguments (optional agent reference).
|
|
1078
|
+
_invoked_from_agent: Whether invoked from agent context (unused).
|
|
1079
|
+
|
|
1080
|
+
Returns:
|
|
1081
|
+
True to continue session.
|
|
1082
|
+
"""
|
|
380
1083
|
client = self._get_client_or_fail()
|
|
381
1084
|
if not client:
|
|
382
1085
|
return True
|
|
@@ -442,7 +1145,7 @@ class SlashSession:
|
|
|
442
1145
|
self._render_header()
|
|
443
1146
|
|
|
444
1147
|
self._show_agent_followup_actions(picked_agent)
|
|
445
|
-
return
|
|
1148
|
+
return self._continue_session()
|
|
446
1149
|
|
|
447
1150
|
def _show_agent_followup_actions(self, picked_agent: Any) -> None:
|
|
448
1151
|
"""Show follow-up action hints after agent session."""
|
|
@@ -454,6 +1157,7 @@ class SlashSession:
|
|
|
454
1157
|
hints.append((f"/agents {agent_id}", f"Reopen {agent_label}"))
|
|
455
1158
|
hints.extend(
|
|
456
1159
|
[
|
|
1160
|
+
("/accounts", "Switch account"),
|
|
457
1161
|
(self.AGENTS_COMMAND, "Browse agents"),
|
|
458
1162
|
(self.STATUS_COMMAND, "Check connection"),
|
|
459
1163
|
]
|
|
@@ -462,6 +1166,15 @@ class SlashSession:
|
|
|
462
1166
|
self._show_quick_actions(hints, title="Next actions")
|
|
463
1167
|
|
|
464
1168
|
def _cmd_exit(self, _args: list[str], invoked_from_agent: bool) -> bool:
|
|
1169
|
+
"""Handle the /exit command.
|
|
1170
|
+
|
|
1171
|
+
Args:
|
|
1172
|
+
_args: Command arguments (unused).
|
|
1173
|
+
invoked_from_agent: Whether invoked from agent context.
|
|
1174
|
+
|
|
1175
|
+
Returns:
|
|
1176
|
+
False to exit session, True to continue.
|
|
1177
|
+
"""
|
|
465
1178
|
if invoked_from_agent:
|
|
466
1179
|
# Returning False would stop the full session; we only want to exit
|
|
467
1180
|
# the agent context. Raising a custom flag keeps the outer loop
|
|
@@ -475,6 +1188,7 @@ class SlashSession:
|
|
|
475
1188
|
# Utilities
|
|
476
1189
|
# ------------------------------------------------------------------
|
|
477
1190
|
def _register_defaults(self) -> None:
|
|
1191
|
+
"""Register default slash commands."""
|
|
478
1192
|
self._register(
|
|
479
1193
|
SlashCommand(
|
|
480
1194
|
name="help",
|
|
@@ -486,7 +1200,7 @@ class SlashSession:
|
|
|
486
1200
|
self._register(
|
|
487
1201
|
SlashCommand(
|
|
488
1202
|
name="login",
|
|
489
|
-
help="
|
|
1203
|
+
help="Configure API credentials (alias `/configure`).",
|
|
490
1204
|
handler=SlashSession._cmd_login,
|
|
491
1205
|
aliases=("configure",),
|
|
492
1206
|
)
|
|
@@ -498,6 +1212,23 @@ class SlashSession:
|
|
|
498
1212
|
handler=SlashSession._cmd_status,
|
|
499
1213
|
)
|
|
500
1214
|
)
|
|
1215
|
+
self._register(
|
|
1216
|
+
SlashCommand(
|
|
1217
|
+
name="accounts",
|
|
1218
|
+
help="✨ NEW · Browse and switch stored accounts (Textual with Rich fallback).",
|
|
1219
|
+
handler=SlashSession._cmd_accounts,
|
|
1220
|
+
)
|
|
1221
|
+
)
|
|
1222
|
+
self._register(
|
|
1223
|
+
SlashCommand(
|
|
1224
|
+
name="transcripts",
|
|
1225
|
+
help=(
|
|
1226
|
+
"✨ NEW · Review cached transcript history. "
|
|
1227
|
+
"Add a number (e.g. `/transcripts 5`) to change the row limit."
|
|
1228
|
+
),
|
|
1229
|
+
handler=SlashSession._cmd_transcripts,
|
|
1230
|
+
)
|
|
1231
|
+
)
|
|
501
1232
|
self._register(
|
|
502
1233
|
SlashCommand(
|
|
503
1234
|
name="agents",
|
|
@@ -527,12 +1258,32 @@ class SlashSession:
|
|
|
527
1258
|
handler=SlashSession._cmd_update,
|
|
528
1259
|
)
|
|
529
1260
|
)
|
|
1261
|
+
self._register(
|
|
1262
|
+
SlashCommand(
|
|
1263
|
+
name="runs",
|
|
1264
|
+
help="✨ NEW · Browse remote agent run history (requires active agent session).",
|
|
1265
|
+
handler=SlashSession._cmd_runs,
|
|
1266
|
+
agent_only=True,
|
|
1267
|
+
)
|
|
1268
|
+
)
|
|
530
1269
|
|
|
531
1270
|
def _register(self, command: SlashCommand) -> None:
|
|
1271
|
+
"""Register a slash command.
|
|
1272
|
+
|
|
1273
|
+
Args:
|
|
1274
|
+
command: SlashCommand to register.
|
|
1275
|
+
"""
|
|
532
1276
|
self._unique_commands[command.name] = command
|
|
533
1277
|
for key in (command.name, *command.aliases):
|
|
534
1278
|
self._commands[key] = command
|
|
535
1279
|
|
|
1280
|
+
def _visible_commands(self, *, include_agent_only: bool) -> list[SlashCommand]:
|
|
1281
|
+
"""Return the list of commands that should be shown in global listings."""
|
|
1282
|
+
commands = sorted(self._unique_commands.values(), key=lambda c: c.name)
|
|
1283
|
+
if include_agent_only:
|
|
1284
|
+
return commands
|
|
1285
|
+
return [cmd for cmd in commands if not cmd.agent_only]
|
|
1286
|
+
|
|
536
1287
|
def open_transcript_viewer(self, *, announce: bool = True) -> None:
|
|
537
1288
|
"""Launch the transcript viewer for the most recent run."""
|
|
538
1289
|
payload, manifest = self._get_last_transcript()
|
|
@@ -557,6 +1308,14 @@ class SlashSession:
|
|
|
557
1308
|
)
|
|
558
1309
|
|
|
559
1310
|
def _export(destination: Path) -> Path:
|
|
1311
|
+
"""Export cached transcript to destination.
|
|
1312
|
+
|
|
1313
|
+
Args:
|
|
1314
|
+
destination: Path to export transcript to.
|
|
1315
|
+
|
|
1316
|
+
Returns:
|
|
1317
|
+
Path to exported transcript file.
|
|
1318
|
+
"""
|
|
560
1319
|
return export_cached_transcript(destination=destination, run_id=run_id)
|
|
561
1320
|
|
|
562
1321
|
try:
|
|
@@ -574,55 +1333,13 @@ class SlashSession:
|
|
|
574
1333
|
manifest = ctx_obj.get("_last_transcript_manifest")
|
|
575
1334
|
return payload, manifest
|
|
576
1335
|
|
|
577
|
-
def _cmd_export(self,
|
|
1336
|
+
def _cmd_export(self, _args: list[str], _invoked_from_agent: bool) -> bool:
|
|
578
1337
|
"""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))
|
|
1338
|
+
self.console.print(
|
|
1339
|
+
f"[{WARNING_STYLE}]`/export` is deprecated. Use `/transcripts`, select a run, "
|
|
1340
|
+
"and open the transcript viewer to export.[/]"
|
|
1341
|
+
)
|
|
1342
|
+
return True
|
|
626
1343
|
|
|
627
1344
|
def _cmd_update(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
628
1345
|
"""Slash handler for `/update` command."""
|
|
@@ -695,6 +1412,14 @@ class SlashSession:
|
|
|
695
1412
|
pass
|
|
696
1413
|
|
|
697
1414
|
def _parse(self, raw: str) -> tuple[str, list[str]]:
|
|
1415
|
+
"""Parse a raw command string into verb and arguments.
|
|
1416
|
+
|
|
1417
|
+
Args:
|
|
1418
|
+
raw: Raw command string.
|
|
1419
|
+
|
|
1420
|
+
Returns:
|
|
1421
|
+
Tuple of (verb, args).
|
|
1422
|
+
"""
|
|
698
1423
|
try:
|
|
699
1424
|
tokens = shlex.split(raw)
|
|
700
1425
|
except ValueError:
|
|
@@ -710,6 +1435,14 @@ class SlashSession:
|
|
|
710
1435
|
return head, tokens[1:]
|
|
711
1436
|
|
|
712
1437
|
def _suggest(self, verb: str) -> str | None:
|
|
1438
|
+
"""Suggest a similar command name for an unknown verb.
|
|
1439
|
+
|
|
1440
|
+
Args:
|
|
1441
|
+
verb: Unknown command verb.
|
|
1442
|
+
|
|
1443
|
+
Returns:
|
|
1444
|
+
Suggested command name or None.
|
|
1445
|
+
"""
|
|
713
1446
|
keys = [cmd.name for cmd in self._unique_commands.values()]
|
|
714
1447
|
match = get_close_matches(verb, keys, n=1)
|
|
715
1448
|
return match[0] if match else None
|
|
@@ -738,6 +1471,7 @@ class SlashSession:
|
|
|
738
1471
|
if callable(message):
|
|
739
1472
|
|
|
740
1473
|
def prompt_text() -> Any:
|
|
1474
|
+
"""Get formatted prompt text from callable message."""
|
|
741
1475
|
return self._convert_message(message())
|
|
742
1476
|
else:
|
|
743
1477
|
prompt_text = self._convert_message(message)
|
|
@@ -783,10 +1517,42 @@ class SlashSession:
|
|
|
783
1517
|
return self._prompt_with_basic_input(message, placeholder)
|
|
784
1518
|
|
|
785
1519
|
def _get_client(self) -> Any: # type: ignore[no-any-return]
|
|
1520
|
+
"""Get or create the API client instance.
|
|
1521
|
+
|
|
1522
|
+
Returns:
|
|
1523
|
+
API client instance.
|
|
1524
|
+
"""
|
|
786
1525
|
if self._client is None:
|
|
787
1526
|
self._client = get_client(self.ctx)
|
|
788
1527
|
return self._client
|
|
789
1528
|
|
|
1529
|
+
def on_account_switched(self, _account_name: str | None = None) -> None:
|
|
1530
|
+
"""Reset any state that depends on the active account.
|
|
1531
|
+
|
|
1532
|
+
The active account can change via `/accounts` (or other flows that call
|
|
1533
|
+
AccountStore.set_active_account). The slash session caches a configured
|
|
1534
|
+
client instance, so we must invalidate it to avoid leaking the previous
|
|
1535
|
+
account's API URL/key into subsequent commands like `/agents` or `/runs`.
|
|
1536
|
+
|
|
1537
|
+
This method clears:
|
|
1538
|
+
- Client and config cache (account-specific credentials)
|
|
1539
|
+
- Current agent and recent agents (agent data is account-scoped)
|
|
1540
|
+
- Runs pagination state (runs are account-scoped)
|
|
1541
|
+
- Active renderer and transcript ready state (UI state tied to account context)
|
|
1542
|
+
- Contextual commands (may be account-specific)
|
|
1543
|
+
|
|
1544
|
+
These broader resets ensure a clean slate when switching accounts, preventing
|
|
1545
|
+
stale data from the previous account from appearing in the new account's context.
|
|
1546
|
+
"""
|
|
1547
|
+
self._client = None
|
|
1548
|
+
self._config_cache = None
|
|
1549
|
+
self._current_agent = None
|
|
1550
|
+
self.recent_agents = []
|
|
1551
|
+
self._runs_pagination_state.clear()
|
|
1552
|
+
self.clear_active_renderer()
|
|
1553
|
+
self.clear_agent_transcript_ready()
|
|
1554
|
+
self.set_contextual_commands(None)
|
|
1555
|
+
|
|
790
1556
|
def set_contextual_commands(self, commands: dict[str, str] | None, *, include_global: bool = True) -> None:
|
|
791
1557
|
"""Set context-specific commands that should appear in completions."""
|
|
792
1558
|
self._contextual_commands = dict(commands or {})
|
|
@@ -801,6 +1567,11 @@ class SlashSession:
|
|
|
801
1567
|
return self._contextual_include_global
|
|
802
1568
|
|
|
803
1569
|
def _remember_agent(self, agent: Any) -> None: # type: ignore[no-any-return]
|
|
1570
|
+
"""Remember an agent in recent agents list.
|
|
1571
|
+
|
|
1572
|
+
Args:
|
|
1573
|
+
agent: Agent object to remember.
|
|
1574
|
+
"""
|
|
804
1575
|
agent_data = {
|
|
805
1576
|
"id": str(getattr(agent, "id", "")),
|
|
806
1577
|
"name": getattr(agent, "name", "") or "",
|
|
@@ -817,14 +1588,24 @@ class SlashSession:
|
|
|
817
1588
|
*,
|
|
818
1589
|
focus_agent: bool = False,
|
|
819
1590
|
initial: bool = False,
|
|
1591
|
+
show_branding: bool = True,
|
|
820
1592
|
) -> None:
|
|
1593
|
+
"""Render the session header with branding and status.
|
|
1594
|
+
|
|
1595
|
+
Args:
|
|
1596
|
+
active_agent: Optional active agent to display.
|
|
1597
|
+
focus_agent: Whether to focus on agent display.
|
|
1598
|
+
initial: Whether this is the initial render.
|
|
1599
|
+
show_branding: Whether to render the branding banner.
|
|
1600
|
+
"""
|
|
821
1601
|
if focus_agent and active_agent is not None:
|
|
822
1602
|
self._render_focused_agent_header(active_agent)
|
|
823
1603
|
return
|
|
824
1604
|
|
|
825
1605
|
full_header = initial or not self._welcome_rendered
|
|
826
|
-
if full_header:
|
|
1606
|
+
if full_header and show_branding:
|
|
827
1607
|
self._render_branding_banner()
|
|
1608
|
+
if full_header:
|
|
828
1609
|
self.console.rule(style=PRIMARY)
|
|
829
1610
|
self._render_main_header(active_agent, full=full_header)
|
|
830
1611
|
if full_header:
|
|
@@ -834,14 +1615,17 @@ class SlashSession:
|
|
|
834
1615
|
def _render_branding_banner(self) -> None:
|
|
835
1616
|
"""Render the GL AIP branding banner."""
|
|
836
1617
|
banner = self._branding.get_welcome_banner()
|
|
837
|
-
heading =
|
|
1618
|
+
heading = self.CLI_HEADING_MARKUP
|
|
838
1619
|
self.console.print(heading)
|
|
839
1620
|
self.console.print()
|
|
840
1621
|
self.console.print(banner)
|
|
841
1622
|
|
|
842
|
-
def _maybe_show_update_prompt(self) -> None:
|
|
1623
|
+
def _maybe_show_update_prompt(self, *, defer: bool = False) -> None:
|
|
843
1624
|
"""Display update prompt once per session when applicable."""
|
|
844
|
-
if self._update_prompt_shown:
|
|
1625
|
+
if self._update_prompt_shown or (defer and not self._update_prompt_shown):
|
|
1626
|
+
if defer:
|
|
1627
|
+
# Just mark as ready to show, but don't show yet
|
|
1628
|
+
return
|
|
845
1629
|
return
|
|
846
1630
|
|
|
847
1631
|
self._update_notifier(
|
|
@@ -860,8 +1644,9 @@ class SlashSession:
|
|
|
860
1644
|
|
|
861
1645
|
header_grid = self._build_header_grid(agent_info, transcript_status)
|
|
862
1646
|
keybar = self._build_keybar()
|
|
863
|
-
|
|
864
1647
|
header_grid.add_row(keybar, "")
|
|
1648
|
+
|
|
1649
|
+
# Agent-scoped commands like /runs will appear in /help, no need to duplicate here
|
|
865
1650
|
self.console.print(AIPPanel(header_grid, title="Agent Session", border_style=PRIMARY))
|
|
866
1651
|
|
|
867
1652
|
def _get_agent_info(self, active_agent: Any) -> dict[str, str]:
|
|
@@ -903,14 +1688,16 @@ class SlashSession:
|
|
|
903
1688
|
f"[{ACCENT_STYLE}]{agent_info['id']}[/]"
|
|
904
1689
|
)
|
|
905
1690
|
status_line = f"[{SUCCESS_STYLE}]ready[/]"
|
|
906
|
-
|
|
1691
|
+
if not transcript_status["has_transcript"]:
|
|
1692
|
+
status_line += " · no transcript"
|
|
1693
|
+
elif transcript_status["transcript_ready"]:
|
|
1694
|
+
status_line += " · transcript ready"
|
|
1695
|
+
else:
|
|
1696
|
+
status_line += " · transcript pending"
|
|
907
1697
|
header_grid.add_row(primary_line, status_line)
|
|
908
1698
|
|
|
909
1699
|
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]", "")
|
|
1700
|
+
header_grid.add_row(f"[dim]{agent_info['description']}[/dim]", "")
|
|
914
1701
|
|
|
915
1702
|
return header_grid
|
|
916
1703
|
|
|
@@ -919,10 +1706,11 @@ class SlashSession:
|
|
|
919
1706
|
keybar = AIPGrid(expand=True)
|
|
920
1707
|
keybar.add_column(justify="left", ratio=1)
|
|
921
1708
|
keybar.add_column(justify="left", ratio=1)
|
|
1709
|
+
keybar.add_column(justify="left", ratio=1)
|
|
922
1710
|
|
|
923
1711
|
keybar.add_row(
|
|
924
|
-
format_command_hint(
|
|
925
|
-
format_command_hint("/details", "Agent config") or "",
|
|
1712
|
+
format_command_hint(HELP_COMMAND, "Show commands") or "",
|
|
1713
|
+
format_command_hint("/details", "Agent config (expand prompt)") or "",
|
|
926
1714
|
format_command_hint("/exit", "Back") or "",
|
|
927
1715
|
)
|
|
928
1716
|
|
|
@@ -932,13 +1720,26 @@ class SlashSession:
|
|
|
932
1720
|
"""Render the main AIP environment header."""
|
|
933
1721
|
config = self._load_config()
|
|
934
1722
|
|
|
1723
|
+
account_name, account_host, env_lock = self._get_account_context()
|
|
935
1724
|
api_url = self._get_api_url(config)
|
|
936
|
-
status = "Configured" if config.get("api_key") else "Not configured"
|
|
937
1725
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1726
|
+
host_display = account_host or "Not configured"
|
|
1727
|
+
account_segment = f"[dim]Account[/dim] • {account_name} ({host_display})"
|
|
1728
|
+
if env_lock:
|
|
1729
|
+
account_segment += " 🔒"
|
|
1730
|
+
|
|
1731
|
+
segments = [account_segment]
|
|
1732
|
+
|
|
1733
|
+
if api_url:
|
|
1734
|
+
base_label = "[dim]Base URL[/dim]"
|
|
1735
|
+
if env_lock:
|
|
1736
|
+
base_label = "[dim]Base URL (env)[/dim]"
|
|
1737
|
+
# Always show Base URL when env-lock is active to reveal overrides
|
|
1738
|
+
if env_lock or api_url != account_host:
|
|
1739
|
+
segments.append(f"{base_label} • {api_url}")
|
|
1740
|
+
elif not api_url:
|
|
1741
|
+
segments.append("[dim]Base URL[/dim] • Not configured")
|
|
1742
|
+
|
|
942
1743
|
agent_info = self._build_agent_status_line(active_agent)
|
|
943
1744
|
if agent_info:
|
|
944
1745
|
segments.append(agent_info)
|
|
@@ -961,12 +1762,23 @@ class SlashSession:
|
|
|
961
1762
|
)
|
|
962
1763
|
)
|
|
963
1764
|
|
|
964
|
-
def _get_api_url(self,
|
|
965
|
-
"""Get the API URL from
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1765
|
+
def _get_api_url(self, _config: dict[str, Any] | None = None) -> str | None:
|
|
1766
|
+
"""Get the API URL from context or account store (CLI/palette ignores env credentials)."""
|
|
1767
|
+
return resolve_api_url_from_context(self.ctx)
|
|
1768
|
+
|
|
1769
|
+
def _get_account_context(self) -> tuple[str, str, bool]:
|
|
1770
|
+
"""Return active account name, host, and env-lock flag."""
|
|
1771
|
+
try:
|
|
1772
|
+
store = get_account_store()
|
|
1773
|
+
active = store.get_active_account() or "default"
|
|
1774
|
+
account = store.get_account(active) if hasattr(store, "get_account") else None
|
|
1775
|
+
host = ""
|
|
1776
|
+
if account:
|
|
1777
|
+
host = account.get("api_url", "")
|
|
1778
|
+
env_lock = env_credentials_present()
|
|
1779
|
+
return active, host, env_lock
|
|
1780
|
+
except Exception:
|
|
1781
|
+
return "default", "", env_credentials_present()
|
|
970
1782
|
|
|
971
1783
|
def _build_agent_status_line(self, active_agent: Any | None) -> str | None:
|
|
972
1784
|
"""Return a short status line about the active or recent agent."""
|
|
@@ -981,35 +1793,96 @@ class SlashSession:
|
|
|
981
1793
|
return None
|
|
982
1794
|
|
|
983
1795
|
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")
|
|
1796
|
+
"""Show simplified help hint to discover commands."""
|
|
1797
|
+
self.console.print(f"[dim]{'─' * 40}[/]")
|
|
1798
|
+
help_hint = format_command_hint(HELP_COMMAND, "Show all commands") or HELP_COMMAND
|
|
1799
|
+
self.console.print(f"• {help_hint}")
|
|
1001
1800
|
self._default_actions_shown = True
|
|
1002
1801
|
|
|
1802
|
+
def _collect_scoped_new_action_hints(self, scope: str) -> list[tuple[str, str]]:
|
|
1803
|
+
"""Return new quick action hints filtered by scope."""
|
|
1804
|
+
scoped_actions = [action for action in NEW_QUICK_ACTIONS if _quick_action_scope(action) == scope]
|
|
1805
|
+
# Don't highlight with sparkle emoji in quick actions display - it will show in command palette instead
|
|
1806
|
+
return self._collect_quick_action_hints(scoped_actions)
|
|
1807
|
+
|
|
1808
|
+
def _collect_quick_action_hints(
|
|
1809
|
+
self,
|
|
1810
|
+
actions: Iterable[dict[str, Any]],
|
|
1811
|
+
) -> list[tuple[str, str]]:
|
|
1812
|
+
"""Collect quick action hints from action definitions.
|
|
1813
|
+
|
|
1814
|
+
Args:
|
|
1815
|
+
actions: Iterable of action dictionaries.
|
|
1816
|
+
|
|
1817
|
+
Returns:
|
|
1818
|
+
List of (command, description) tuples.
|
|
1819
|
+
"""
|
|
1820
|
+
collected: list[tuple[str, str]] = []
|
|
1821
|
+
|
|
1822
|
+
def sort_key(payload: dict[str, Any]) -> tuple[int, str]:
|
|
1823
|
+
priority = int(payload.get("priority", 0))
|
|
1824
|
+
label = str(payload.get("slash") or payload.get("cli") or "")
|
|
1825
|
+
return (-priority, label.lower())
|
|
1826
|
+
|
|
1827
|
+
for action in sorted(actions, key=sort_key):
|
|
1828
|
+
hint = self._build_quick_action_hint(action)
|
|
1829
|
+
if hint:
|
|
1830
|
+
collected.append(hint)
|
|
1831
|
+
return collected
|
|
1832
|
+
|
|
1833
|
+
def _build_quick_action_hint(
|
|
1834
|
+
self,
|
|
1835
|
+
action: dict[str, Any],
|
|
1836
|
+
) -> tuple[str, str] | None:
|
|
1837
|
+
"""Build a quick action hint from an action definition.
|
|
1838
|
+
|
|
1839
|
+
Args:
|
|
1840
|
+
action: Action dictionary.
|
|
1841
|
+
|
|
1842
|
+
Returns:
|
|
1843
|
+
Tuple of (command, description) or None.
|
|
1844
|
+
"""
|
|
1845
|
+
command = command_hint(action.get("cli"), slash_command=action.get("slash"), ctx=self.ctx)
|
|
1846
|
+
if not command:
|
|
1847
|
+
return None
|
|
1848
|
+
description = action.get("description", "")
|
|
1849
|
+
# Don't include tag or sparkle emoji in quick actions display
|
|
1850
|
+
# The NEW tag will only show in the command dropdown (help text)
|
|
1851
|
+
return command, description
|
|
1852
|
+
|
|
1853
|
+
def _render_quick_action_group(self, hints: list[tuple[str, str]], title: str) -> None:
|
|
1854
|
+
"""Render a group of quick action hints.
|
|
1855
|
+
|
|
1856
|
+
Args:
|
|
1857
|
+
hints: List of (command, description) tuples.
|
|
1858
|
+
title: Group title.
|
|
1859
|
+
"""
|
|
1860
|
+
for line in self._format_quick_action_lines(hints, title):
|
|
1861
|
+
self.console.print(line)
|
|
1862
|
+
|
|
1863
|
+
def _chunk_tokens(self, tokens: list[str], *, size: int) -> Iterable[list[str]]:
|
|
1864
|
+
"""Chunk tokens into groups of specified size.
|
|
1865
|
+
|
|
1866
|
+
Args:
|
|
1867
|
+
tokens: List of tokens to chunk.
|
|
1868
|
+
size: Size of each chunk.
|
|
1869
|
+
|
|
1870
|
+
Yields:
|
|
1871
|
+
Lists of tokens.
|
|
1872
|
+
"""
|
|
1873
|
+
for index in range(0, len(tokens), size):
|
|
1874
|
+
yield tokens[index : index + size]
|
|
1875
|
+
|
|
1003
1876
|
def _render_home_hint(self) -> None:
|
|
1877
|
+
"""Render hint text for home screen."""
|
|
1004
1878
|
if self._home_hint_shown:
|
|
1005
1879
|
return
|
|
1006
|
-
|
|
1007
|
-
f"[{HINT_PREFIX_STYLE}]Hint:[/]"
|
|
1008
|
-
f"
|
|
1009
|
-
"
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
self.console.print("\n".join(hint_lines))
|
|
1880
|
+
hint_text = (
|
|
1881
|
+
f"[{HINT_PREFIX_STYLE}]Hint:[/] "
|
|
1882
|
+
f"Type {format_command_hint('/') or '/'} to explore commands · "
|
|
1883
|
+
"Press [dim]Ctrl+D[/] to quit"
|
|
1884
|
+
)
|
|
1885
|
+
self.console.print(hint_text)
|
|
1013
1886
|
self._home_hint_shown = True
|
|
1014
1887
|
|
|
1015
1888
|
def _show_quick_actions(
|
|
@@ -1019,30 +1892,99 @@ class SlashSession:
|
|
|
1019
1892
|
title: str = "Quick actions",
|
|
1020
1893
|
inline: bool = False,
|
|
1021
1894
|
) -> None:
|
|
1022
|
-
|
|
1895
|
+
"""Show quick action hints.
|
|
1896
|
+
|
|
1897
|
+
Args:
|
|
1898
|
+
hints: Iterable of (command, description) tuples.
|
|
1899
|
+
title: Title for the hints.
|
|
1900
|
+
inline: Whether to render inline or in a panel.
|
|
1901
|
+
"""
|
|
1902
|
+
hint_list = self._normalize_quick_action_hints(hints)
|
|
1023
1903
|
if not hint_list:
|
|
1024
1904
|
return
|
|
1025
1905
|
|
|
1026
1906
|
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))
|
|
1907
|
+
self._render_inline_quick_actions(hint_list, title)
|
|
1034
1908
|
return
|
|
1035
1909
|
|
|
1910
|
+
self._render_panel_quick_actions(hint_list, title)
|
|
1911
|
+
|
|
1912
|
+
def _normalize_quick_action_hints(self, hints: Iterable[tuple[str, str]]) -> list[tuple[str, str]]:
|
|
1913
|
+
"""Normalize quick action hints by filtering out empty commands.
|
|
1914
|
+
|
|
1915
|
+
Args:
|
|
1916
|
+
hints: Iterable of (command, description) tuples.
|
|
1917
|
+
|
|
1918
|
+
Returns:
|
|
1919
|
+
List of normalized hints.
|
|
1920
|
+
"""
|
|
1921
|
+
return [(command, description) for command, description in hints if command]
|
|
1922
|
+
|
|
1923
|
+
def _render_inline_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
|
|
1924
|
+
"""Render quick actions inline.
|
|
1925
|
+
|
|
1926
|
+
Args:
|
|
1927
|
+
hint_list: List of (command, description) tuples.
|
|
1928
|
+
title: Title for the hints.
|
|
1929
|
+
"""
|
|
1930
|
+
tokens: list[str] = []
|
|
1931
|
+
for command, description in hint_list:
|
|
1932
|
+
formatted = format_command_hint(command, description)
|
|
1933
|
+
if formatted:
|
|
1934
|
+
tokens.append(formatted)
|
|
1935
|
+
if not tokens:
|
|
1936
|
+
return
|
|
1937
|
+
prefix = f"[dim]{title}:[/]" if title else ""
|
|
1938
|
+
body = " ".join(tokens)
|
|
1939
|
+
text = f"{prefix} {body}" if prefix else body
|
|
1940
|
+
self.console.print(text.strip())
|
|
1941
|
+
|
|
1942
|
+
def _render_panel_quick_actions(self, hint_list: list[tuple[str, str]], title: str) -> None:
|
|
1943
|
+
"""Render quick actions in a panel.
|
|
1944
|
+
|
|
1945
|
+
Args:
|
|
1946
|
+
hint_list: List of (command, description) tuples.
|
|
1947
|
+
title: Panel title.
|
|
1948
|
+
"""
|
|
1036
1949
|
body_lines: list[Text] = []
|
|
1037
1950
|
for command, description in hint_list:
|
|
1038
1951
|
formatted = format_command_hint(command, description)
|
|
1039
1952
|
if formatted:
|
|
1040
1953
|
body_lines.append(Text.from_markup(formatted))
|
|
1041
|
-
|
|
1954
|
+
if not body_lines:
|
|
1955
|
+
return
|
|
1042
1956
|
panel_content = Group(*body_lines)
|
|
1043
1957
|
self.console.print(AIPPanel(panel_content, title=title, border_style=SECONDARY_LIGHT, expand=False))
|
|
1044
1958
|
|
|
1959
|
+
def _format_quick_action_lines(self, hints: list[tuple[str, str]], title: str) -> list[str]:
|
|
1960
|
+
"""Return formatted lines for quick action hints."""
|
|
1961
|
+
if not hints:
|
|
1962
|
+
return []
|
|
1963
|
+
formatted_tokens: list[str] = []
|
|
1964
|
+
for command, description in hints:
|
|
1965
|
+
formatted = format_command_hint(command, description)
|
|
1966
|
+
if formatted:
|
|
1967
|
+
formatted_tokens.append(f"• {formatted}")
|
|
1968
|
+
if not formatted_tokens:
|
|
1969
|
+
return []
|
|
1970
|
+
lines: list[str] = []
|
|
1971
|
+
# Use vertical layout (1 per line) for better readability
|
|
1972
|
+
chunks = list(self._chunk_tokens(formatted_tokens, size=1))
|
|
1973
|
+
prefix = f"[dim]{title}[/dim]\n " if title else ""
|
|
1974
|
+
for idx, chunk in enumerate(chunks):
|
|
1975
|
+
row = " ".join(chunk)
|
|
1976
|
+
if idx == 0:
|
|
1977
|
+
lines.append(f"{prefix}{row}" if prefix else row)
|
|
1978
|
+
else:
|
|
1979
|
+
lines.append(f" {row}")
|
|
1980
|
+
return lines
|
|
1981
|
+
|
|
1045
1982
|
def _load_config(self) -> dict[str, Any]:
|
|
1983
|
+
"""Load configuration with caching.
|
|
1984
|
+
|
|
1985
|
+
Returns:
|
|
1986
|
+
Configuration dictionary.
|
|
1987
|
+
"""
|
|
1046
1988
|
if self._config_cache is None:
|
|
1047
1989
|
try:
|
|
1048
1990
|
self._config_cache = load_config() or {}
|
|
@@ -1051,6 +1993,16 @@ class SlashSession:
|
|
|
1051
1993
|
return self._config_cache
|
|
1052
1994
|
|
|
1053
1995
|
def _resolve_agent_from_ref(self, client: Any, available_agents: list[Any], ref: str) -> Any | None:
|
|
1996
|
+
"""Resolve an agent from a reference string.
|
|
1997
|
+
|
|
1998
|
+
Args:
|
|
1999
|
+
client: API client instance.
|
|
2000
|
+
available_agents: List of available agents.
|
|
2001
|
+
ref: Reference string (ID or name).
|
|
2002
|
+
|
|
2003
|
+
Returns:
|
|
2004
|
+
Resolved agent or None.
|
|
2005
|
+
"""
|
|
1054
2006
|
ref = ref.strip()
|
|
1055
2007
|
if not ref:
|
|
1056
2008
|
return None
|