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