glaip-sdk 0.6.15b2__py3-none-any.whl → 0.6.15b3__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/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1196 -0
- glaip_sdk/cli/__init__.py +9 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +78 -0
- glaip_sdk/cli/auth.py +699 -0
- glaip_sdk/cli/commands/__init__.py +5 -0
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +1509 -0
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +896 -0
- glaip_sdk/cli/commands/mcps.py +1356 -0
- glaip_sdk/cli/commands/models.py +69 -0
- glaip_sdk/cli/commands/tools.py +576 -0
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +61 -0
- glaip_sdk/cli/config.py +95 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +150 -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 +355 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +112 -0
- glaip_sdk/cli/main.py +615 -0
- glaip_sdk/cli/masking.py +136 -0
- glaip_sdk/cli/mcp_validators.py +287 -0
- glaip_sdk/cli/pager.py +266 -0
- glaip_sdk/cli/parsers/__init__.py +7 -0
- glaip_sdk/cli/parsers/json_input.py +177 -0
- glaip_sdk/cli/resolution.py +67 -0
- glaip_sdk/cli/rich_helpers.py +27 -0
- glaip_sdk/cli/slash/__init__.py +15 -0
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +285 -0
- glaip_sdk/cli/slash/prompt.py +256 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +1708 -0
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +31 -0
- glaip_sdk/cli/transcript/cache.py +536 -0
- glaip_sdk/cli/transcript/capture.py +329 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +77 -0
- glaip_sdk/cli/transcript/viewer.py +374 -0
- glaip_sdk/cli/update_notifier.py +290 -0
- glaip_sdk/cli/utils.py +263 -0
- glaip_sdk/cli/validators.py +238 -0
- glaip_sdk/client/__init__.py +11 -0
- glaip_sdk/client/_agent_payloads.py +520 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +1335 -0
- glaip_sdk/client/base.py +502 -0
- glaip_sdk/client/main.py +249 -0
- glaip_sdk/client/mcps.py +370 -0
- glaip_sdk/client/run_rendering.py +700 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +661 -0
- glaip_sdk/client/validators.py +198 -0
- glaip_sdk/config/constants.py +52 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +7 -0
- glaip_sdk/payload_schemas/agent.py +85 -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 +232 -0
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +782 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +86 -0
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +194 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +486 -0
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +135 -0
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +61 -0
- glaip_sdk/utils/import_export.py +168 -0
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -0
- glaip_sdk/utils/rendering/formatting.py +264 -0
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/layout/panels.py +156 -0
- glaip_sdk/utils/rendering/layout/progress.py +202 -0
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +85 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +55 -0
- glaip_sdk/utils/rendering/renderer/base.py +1024 -0
- glaip_sdk/utils/rendering/renderer/config.py +27 -0
- glaip_sdk/utils/rendering/renderer/console.py +55 -0
- glaip_sdk/utils/rendering/renderer/debug.py +178 -0
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +202 -0
- 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/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 +195 -0
- glaip_sdk/utils/run_renderer.py +41 -0
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +424 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +264 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/METADATA +1 -1
- glaip_sdk-0.6.15b3.dist-info/RECORD +160 -0
- glaip_sdk-0.6.15b2.dist-info/RECORD +0 -12
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Utilities for launching the post-run transcript viewer.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from glaip_sdk.cli.context import get_ctx_value
|
|
16
|
+
from glaip_sdk.cli.transcript.cache import (
|
|
17
|
+
export_transcript as export_cached_transcript,
|
|
18
|
+
)
|
|
19
|
+
from glaip_sdk.cli.transcript.capture import StoredTranscriptContext
|
|
20
|
+
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def should_launch_post_run_viewer(ctx: Any, console: Console, *, slash_mode: bool) -> bool:
|
|
24
|
+
"""Return True if the viewer should open automatically."""
|
|
25
|
+
if slash_mode:
|
|
26
|
+
return False
|
|
27
|
+
ctx_obj = getattr(ctx, "obj", None)
|
|
28
|
+
if isinstance(ctx_obj, dict) and ctx_obj.get("_slash_session"):
|
|
29
|
+
return False
|
|
30
|
+
if get_ctx_value(ctx, "view", "rich") != "rich":
|
|
31
|
+
return False
|
|
32
|
+
if not bool(get_ctx_value(ctx, "tty", True)):
|
|
33
|
+
return False
|
|
34
|
+
if not console.is_terminal:
|
|
35
|
+
return False
|
|
36
|
+
try:
|
|
37
|
+
if not sys.stdin.isatty():
|
|
38
|
+
return False
|
|
39
|
+
except Exception:
|
|
40
|
+
return False
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def maybe_launch_post_run_viewer(
|
|
45
|
+
ctx: Any,
|
|
46
|
+
transcript_context: StoredTranscriptContext | None,
|
|
47
|
+
*,
|
|
48
|
+
console: Console,
|
|
49
|
+
slash_mode: bool,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Launch the post-run viewer when context and settings allow it."""
|
|
52
|
+
if transcript_context is None:
|
|
53
|
+
return
|
|
54
|
+
if not should_launch_post_run_viewer(ctx, console, slash_mode=slash_mode):
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
manifest_entry = transcript_context.store_result.manifest_entry
|
|
58
|
+
run_id = manifest_entry.get("run_id")
|
|
59
|
+
if not run_id:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
viewer_ctx = ViewerContext(
|
|
63
|
+
manifest_entry=manifest_entry,
|
|
64
|
+
events=transcript_context.payload.events,
|
|
65
|
+
default_output=transcript_context.payload.default_output,
|
|
66
|
+
final_output=transcript_context.payload.final_output,
|
|
67
|
+
stream_started_at=None,
|
|
68
|
+
meta=transcript_context.payload.meta,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _export(destination: Path) -> Path:
|
|
72
|
+
return export_cached_transcript(destination=destination, run_id=run_id)
|
|
73
|
+
|
|
74
|
+
run_viewer_session(console, viewer_ctx, _export)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = ["should_launch_post_run_viewer", "maybe_launch_post_run_viewer"]
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Interactive viewer for post-run transcript exploration.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
try: # pragma: no cover - optional dependency
|
|
17
|
+
import questionary
|
|
18
|
+
from questionary import Choice
|
|
19
|
+
except Exception: # pragma: no cover - optional dependency
|
|
20
|
+
questionary = None # type: ignore[assignment]
|
|
21
|
+
Choice = None # type: ignore[assignment]
|
|
22
|
+
|
|
23
|
+
from glaip_sdk.cli.transcript.cache import suggest_filename
|
|
24
|
+
from glaip_sdk.cli.utils import prompt_export_choice_questionary, questionary_safe_ask
|
|
25
|
+
from glaip_sdk.utils.rendering.layout.progress import is_delegation_tool
|
|
26
|
+
from glaip_sdk.utils.rendering.layout.transcript import DEFAULT_TRANSCRIPT_THEME
|
|
27
|
+
from glaip_sdk.utils.rendering.viewer import (
|
|
28
|
+
ViewerContext as PresenterViewerContext,
|
|
29
|
+
prepare_viewer_snapshot as presenter_prepare_viewer_snapshot,
|
|
30
|
+
render_post_run_view as presenter_render_post_run_view,
|
|
31
|
+
render_transcript_events as presenter_render_transcript_events,
|
|
32
|
+
render_transcript_view as presenter_render_transcript_view,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
EXPORT_CANCELLED_MESSAGE = "[dim]Export cancelled.[/dim]"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
ViewerContext = PresenterViewerContext
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
42
|
+
"""Simple interactive session for inspecting agent run transcripts."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
console: Console,
|
|
47
|
+
ctx: ViewerContext,
|
|
48
|
+
export_callback: Callable[[Path], Path],
|
|
49
|
+
*,
|
|
50
|
+
initial_view: str = "default",
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Initialize viewer state for a captured transcript."""
|
|
53
|
+
self.console = console
|
|
54
|
+
self.ctx = ctx
|
|
55
|
+
self._export_callback = export_callback
|
|
56
|
+
self._view_mode = initial_view if initial_view in {"default", "transcript"} else "default"
|
|
57
|
+
|
|
58
|
+
def run(self) -> None:
|
|
59
|
+
"""Enter the interactive loop."""
|
|
60
|
+
if not self.ctx.events and not (self.ctx.default_output or self.ctx.final_output):
|
|
61
|
+
return
|
|
62
|
+
if self._view_mode == "transcript":
|
|
63
|
+
self._render()
|
|
64
|
+
self._print_command_hint()
|
|
65
|
+
self._fallback_loop()
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Rendering helpers
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
def _render(self) -> None:
|
|
71
|
+
"""Render the transcript viewer interface."""
|
|
72
|
+
try:
|
|
73
|
+
if self.console.is_terminal:
|
|
74
|
+
self.console.clear()
|
|
75
|
+
except Exception: # pragma: no cover - platform quirks
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
header = f"Agent transcript viewer · run {self.ctx.manifest_entry.get('run_id')}"
|
|
79
|
+
agent_label = self.ctx.manifest_entry.get("agent_name") or "unknown agent"
|
|
80
|
+
model = self.ctx.manifest_entry.get("model") or self.ctx.meta.get("model")
|
|
81
|
+
agent_id = self.ctx.manifest_entry.get("agent_id")
|
|
82
|
+
subtitle_parts = [agent_label]
|
|
83
|
+
if model:
|
|
84
|
+
subtitle_parts.append(str(model))
|
|
85
|
+
if agent_id:
|
|
86
|
+
subtitle_parts.append(agent_id)
|
|
87
|
+
|
|
88
|
+
if self._view_mode == "transcript":
|
|
89
|
+
self.console.rule(header)
|
|
90
|
+
if subtitle_parts:
|
|
91
|
+
self.console.print(f"[dim]{' · '.join(subtitle_parts)}[/]")
|
|
92
|
+
self.console.print()
|
|
93
|
+
|
|
94
|
+
if self._view_mode == "default":
|
|
95
|
+
presenter_render_post_run_view(self.console, self.ctx)
|
|
96
|
+
else:
|
|
97
|
+
theme = DEFAULT_TRANSCRIPT_THEME
|
|
98
|
+
snapshot, state = presenter_prepare_viewer_snapshot(self.ctx, glyphs=None, theme=theme)
|
|
99
|
+
presenter_render_transcript_view(self.console, snapshot, theme=theme)
|
|
100
|
+
presenter_render_transcript_events(self.console, state.events)
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
# Interaction loops
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
def _fallback_loop(self) -> None:
|
|
106
|
+
"""Fallback interaction loop for non-interactive terminals."""
|
|
107
|
+
while True:
|
|
108
|
+
try:
|
|
109
|
+
ch = click.getchar()
|
|
110
|
+
except (EOFError, KeyboardInterrupt):
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
if ch in {"\r", "\n"}:
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
if ch == "\x14" or ch.lower() == "t": # Ctrl+T or t
|
|
117
|
+
self.toggle_view()
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if ch.lower() == "e":
|
|
121
|
+
self.export_transcript()
|
|
122
|
+
self._print_command_hint()
|
|
123
|
+
else:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
def _handle_command(self, raw: str) -> bool:
|
|
127
|
+
"""Handle a command input.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
raw: Raw command string.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True to continue, False to exit.
|
|
134
|
+
"""
|
|
135
|
+
lowered = raw.lower()
|
|
136
|
+
if lowered in {"exit", "quit", "q"}:
|
|
137
|
+
return True
|
|
138
|
+
if lowered in {"export", "e"}:
|
|
139
|
+
self.export_transcript()
|
|
140
|
+
self._print_command_hint()
|
|
141
|
+
return False
|
|
142
|
+
self.console.print("[dim]Commands: export, exit.[/dim]")
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
# Actions
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
def toggle_view(self) -> None:
|
|
149
|
+
"""Switch between default result view and verbose transcript."""
|
|
150
|
+
self._view_mode = "transcript" if self._view_mode == "default" else "default"
|
|
151
|
+
self._render()
|
|
152
|
+
self._print_command_hint()
|
|
153
|
+
|
|
154
|
+
def export_transcript(self) -> None:
|
|
155
|
+
"""Prompt user for a destination and export the cached transcript."""
|
|
156
|
+
entry = self.ctx.manifest_entry
|
|
157
|
+
default_name = suggest_filename(entry)
|
|
158
|
+
default_path = Path.cwd() / default_name
|
|
159
|
+
|
|
160
|
+
def _display_path(path: Path) -> str:
|
|
161
|
+
raw = str(path)
|
|
162
|
+
return raw if len(raw) <= 80 else f"…{raw[-77:]}"
|
|
163
|
+
|
|
164
|
+
selection = self._prompt_export_choice(default_path, _display_path(default_path))
|
|
165
|
+
if selection is None:
|
|
166
|
+
self._legacy_export_prompt(default_path, _display_path)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
action, _ = selection
|
|
170
|
+
if action == "cancel":
|
|
171
|
+
self.console.print(EXPORT_CANCELLED_MESSAGE)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
if action == "default":
|
|
175
|
+
destination = default_path
|
|
176
|
+
else:
|
|
177
|
+
destination = self._prompt_custom_destination()
|
|
178
|
+
if destination is None:
|
|
179
|
+
self.console.print(EXPORT_CANCELLED_MESSAGE)
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
target = self._export_callback(destination)
|
|
184
|
+
self.console.print(f"[green]Transcript exported to {target}[/green]")
|
|
185
|
+
except FileNotFoundError as exc:
|
|
186
|
+
self.console.print(f"[red]{exc}[/red]")
|
|
187
|
+
except Exception as exc: # pragma: no cover - unexpected IO failures
|
|
188
|
+
self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
|
|
189
|
+
|
|
190
|
+
def _prompt_export_choice(self, default_path: Path, default_display: str) -> tuple[str, Any] | None:
|
|
191
|
+
"""Render interactive export menu with numeric shortcuts."""
|
|
192
|
+
if not self.console.is_terminal:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
return prompt_export_choice_questionary(default_path, default_display)
|
|
196
|
+
|
|
197
|
+
def _prompt_custom_destination(self) -> Path | None:
|
|
198
|
+
"""Prompt for custom export path with filesystem completion."""
|
|
199
|
+
if not self.console.is_terminal:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
question = questionary.path(
|
|
204
|
+
"Destination path (Tab to autocomplete):",
|
|
205
|
+
default="",
|
|
206
|
+
only_directories=False,
|
|
207
|
+
)
|
|
208
|
+
response = questionary_safe_ask(question)
|
|
209
|
+
except Exception:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
if not response:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
candidate = Path(response.strip()).expanduser()
|
|
216
|
+
if not candidate.is_absolute():
|
|
217
|
+
candidate = Path.cwd() / candidate
|
|
218
|
+
return candidate
|
|
219
|
+
|
|
220
|
+
def _legacy_export_prompt(self, default_path: Path, formatter: Callable[[Path], str]) -> None:
|
|
221
|
+
"""Fallback export workflow when interactive UI is unavailable."""
|
|
222
|
+
self.console.print("[dim]Export options (fallback mode)[/dim]")
|
|
223
|
+
self.console.print(f" 1. Save to default ({formatter(default_path)})")
|
|
224
|
+
self.console.print(" 2. Choose a different path")
|
|
225
|
+
self.console.print(" 3. Cancel")
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
choice = click.prompt(
|
|
229
|
+
"Select option",
|
|
230
|
+
type=click.Choice(["1", "2", "3"], case_sensitive=False),
|
|
231
|
+
default="1",
|
|
232
|
+
show_choices=False,
|
|
233
|
+
)
|
|
234
|
+
except (EOFError, KeyboardInterrupt):
|
|
235
|
+
self.console.print(EXPORT_CANCELLED_MESSAGE)
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
if choice == "3":
|
|
239
|
+
self.console.print(EXPORT_CANCELLED_MESSAGE)
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
if choice == "1":
|
|
243
|
+
destination = default_path
|
|
244
|
+
else:
|
|
245
|
+
try:
|
|
246
|
+
destination_str = click.prompt("Enter destination path", default="")
|
|
247
|
+
except (EOFError, KeyboardInterrupt):
|
|
248
|
+
self.console.print(EXPORT_CANCELLED_MESSAGE)
|
|
249
|
+
return
|
|
250
|
+
if not destination_str.strip():
|
|
251
|
+
self.console.print(EXPORT_CANCELLED_MESSAGE)
|
|
252
|
+
return
|
|
253
|
+
destination = Path(destination_str.strip()).expanduser()
|
|
254
|
+
if not destination.is_absolute():
|
|
255
|
+
destination = Path.cwd() / destination
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
target = self._export_callback(destination)
|
|
259
|
+
self.console.print(f"[green]Transcript exported to {target}[/green]")
|
|
260
|
+
except FileNotFoundError as exc:
|
|
261
|
+
self.console.print(f"[red]{exc}[/red]")
|
|
262
|
+
except Exception as exc: # pragma: no cover - unexpected IO failures
|
|
263
|
+
self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
|
|
264
|
+
|
|
265
|
+
def _print_command_hint(self) -> None:
|
|
266
|
+
"""Print command hint for user interaction."""
|
|
267
|
+
self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
|
|
268
|
+
self.console.print()
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _extract_direct_tool(
|
|
272
|
+
tool_info: dict[str, Any],
|
|
273
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
274
|
+
"""Extract direct tool from tool_info.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
tool_info: Tool info dictionary.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Tuple of (tool_name, tool_info) or None.
|
|
281
|
+
"""
|
|
282
|
+
if isinstance(tool_info, dict):
|
|
283
|
+
name = tool_info.get("name")
|
|
284
|
+
if name:
|
|
285
|
+
return name, tool_info
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def _extract_completed_name(event: dict[str, Any]) -> str | None:
|
|
290
|
+
"""Extract completed tool name from event content.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
event: Event dictionary.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Tool name or None.
|
|
297
|
+
"""
|
|
298
|
+
content = event.get("content") or ""
|
|
299
|
+
if isinstance(content, str) and content.startswith("Completed "):
|
|
300
|
+
name = content.replace("Completed ", "").strip()
|
|
301
|
+
if name:
|
|
302
|
+
return name
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
def _ensure_step_entry(
|
|
306
|
+
self,
|
|
307
|
+
steps: dict[str, dict[str, Any]],
|
|
308
|
+
order: list[str],
|
|
309
|
+
name: str,
|
|
310
|
+
) -> dict[str, Any]:
|
|
311
|
+
"""Ensure step entry exists, creating if needed.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
steps: Steps dictionary.
|
|
315
|
+
order: Order list.
|
|
316
|
+
name: Step name.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Step dictionary.
|
|
320
|
+
"""
|
|
321
|
+
if name not in steps:
|
|
322
|
+
steps[name] = {
|
|
323
|
+
"name": name,
|
|
324
|
+
"title": name,
|
|
325
|
+
"is_delegate": is_delegation_tool(name),
|
|
326
|
+
"duration": None,
|
|
327
|
+
"started_at": None,
|
|
328
|
+
"finished": False,
|
|
329
|
+
}
|
|
330
|
+
order.append(name)
|
|
331
|
+
return steps[name]
|
|
332
|
+
|
|
333
|
+
def _apply_step_update(
|
|
334
|
+
self,
|
|
335
|
+
step: dict[str, Any],
|
|
336
|
+
metadata: dict[str, Any],
|
|
337
|
+
info: dict[str, Any],
|
|
338
|
+
event: dict[str, Any],
|
|
339
|
+
) -> None:
|
|
340
|
+
"""Apply update to step from event metadata.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
step: Step dictionary to update.
|
|
344
|
+
metadata: Event metadata.
|
|
345
|
+
info: Step info dictionary.
|
|
346
|
+
event: Event dictionary.
|
|
347
|
+
"""
|
|
348
|
+
status = metadata.get("status")
|
|
349
|
+
event_time = metadata.get("time")
|
|
350
|
+
|
|
351
|
+
if status == "running" and step.get("started_at") is None and isinstance(event_time, (int, float)):
|
|
352
|
+
try:
|
|
353
|
+
step["started_at"] = float(event_time)
|
|
354
|
+
except Exception:
|
|
355
|
+
step["started_at"] = None
|
|
356
|
+
|
|
357
|
+
if self._is_step_finished(metadata, event):
|
|
358
|
+
step["finished"] = True
|
|
359
|
+
|
|
360
|
+
duration = self._compute_step_duration(step, info, metadata)
|
|
361
|
+
if duration is not None:
|
|
362
|
+
step["duration"] = duration
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def run_viewer_session(
|
|
366
|
+
console: Console,
|
|
367
|
+
ctx: ViewerContext,
|
|
368
|
+
export_callback: Callable[[Path], Path],
|
|
369
|
+
*,
|
|
370
|
+
initial_view: str = "default",
|
|
371
|
+
) -> None:
|
|
372
|
+
"""Entry point for creating and running the post-run viewer."""
|
|
373
|
+
viewer = PostRunViewer(console, ctx, export_callback, initial_view=initial_view)
|
|
374
|
+
viewer.run()
|