4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
164 lines
5.5 KiB
Python
164 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import shutil
|
|
import threading
|
|
import time
|
|
from typing import Any
|
|
|
|
from rich.console import Console
|
|
from rich.text import Text
|
|
|
|
from platform.terminal.theme import BRAND, DIM, SECONDARY
|
|
from surfaces.interactive_shell.ui.components.time_format import _elapsed_hms
|
|
from surfaces.interactive_shell.ui.output.events import ProgressEvent
|
|
from surfaces.interactive_shell.ui.output.labels import (
|
|
_node_label,
|
|
_node_phase_label,
|
|
build_progress_step_text,
|
|
)
|
|
|
|
# Timestamp + indent; keep append-only hint lines on one physical row.
|
|
_HINT_LINE_OVERHEAD = 20
|
|
|
|
|
|
def _terminal_columns() -> int:
|
|
try:
|
|
cols = shutil.get_terminal_size(fallback=(80, 24)).columns
|
|
except OSError:
|
|
cols = 80
|
|
return max(40, cols - 1)
|
|
|
|
|
|
def _fit_hint_prefix(prefix: str, *, cols: int | None = None) -> str:
|
|
"""Truncate hint text so an append-only lap line stays on a single row."""
|
|
budget = max(16, (cols if cols is not None else _terminal_columns()) - _HINT_LINE_OVERHEAD)
|
|
if len(prefix) <= budget:
|
|
return prefix
|
|
if budget <= 3:
|
|
return prefix[:budget]
|
|
return f"{prefix[: budget - 3]}..."
|
|
|
|
|
|
class _ReplEventLogDisplay:
|
|
"""Append-only investigation progress for the interactive REPL.
|
|
|
|
Live animation is delegated to the prompt spinner (``SpinnerState``): raw
|
|
cursor-up frames cannot rewrite a row under ``patch_stdout(raw=True)``, so
|
|
the display prints step/lap history append-only and drives the spinner with
|
|
per-stage phase labels via the ``console_state`` registration.
|
|
"""
|
|
|
|
def __init__(self, model: str = "", mode: str = "local", t0: float | None = None) -> None:
|
|
self._model = model
|
|
self._mode = mode
|
|
self._t0 = t0 if t0 is not None else time.monotonic()
|
|
self._active_steps: dict[str, dict[str, Any]] = {}
|
|
self._current_phase = "LOAD"
|
|
self._lock = threading.Lock()
|
|
self._console = Console(highlight=False)
|
|
self._last_emitted_hint: str | None = None
|
|
|
|
def stop(self) -> None:
|
|
from surfaces.interactive_shell.ui.output.console_state import (
|
|
_capture_footer_snapshot,
|
|
get_investigation_spinner,
|
|
)
|
|
|
|
spinner = get_investigation_spinner()
|
|
if spinner is not None:
|
|
spinner.stop()
|
|
_capture_footer_snapshot(self)
|
|
|
|
def _emit(self, line: Text | Any) -> None:
|
|
from surfaces.interactive_shell.ui.components.choice_menu import prepare_repl_output_line
|
|
|
|
prepare_repl_output_line()
|
|
self._console.print(line)
|
|
|
|
def animate_hint(self, text: str) -> None:
|
|
"""Print one compact append-only lap-status line.
|
|
|
|
The live "still working" cue is the prompt spinner (driven from
|
|
``step_start``); this only records lap hints as scrollback history and
|
|
dedupes consecutive identical lines.
|
|
"""
|
|
prefix = _fit_hint_prefix(text.rstrip("· \t"))
|
|
if prefix == self._last_emitted_hint:
|
|
return
|
|
self._last_emitted_hint = prefix
|
|
elapsed_total = time.monotonic() - self._t0
|
|
t = Text()
|
|
t.append(f"{_elapsed_hms(elapsed_total)} ", style=SECONDARY)
|
|
t.append(" ↳ ", style=DIM)
|
|
t.append(prefix, style=SECONDARY)
|
|
self._emit(t)
|
|
|
|
def step_start(self, node_name: str) -> None:
|
|
from surfaces.interactive_shell.ui.output.console_state import get_investigation_spinner
|
|
|
|
spinner = get_investigation_spinner()
|
|
if spinner is not None:
|
|
spinner.set_phase(_node_label(node_name))
|
|
with self._lock:
|
|
self._active_steps[node_name] = {
|
|
"t0": time.monotonic(),
|
|
"subtext": None,
|
|
"subtext_until": 0.0,
|
|
}
|
|
self._current_phase = _node_phase_label(node_name)
|
|
self._last_emitted_hint = None
|
|
self._emit(
|
|
build_progress_step_text(
|
|
node_name=node_name,
|
|
elapsed_total=time.monotonic() - self._t0,
|
|
status="active",
|
|
)
|
|
)
|
|
|
|
def set_tool_details(
|
|
self,
|
|
*,
|
|
visible: bool,
|
|
records: list[dict[str, Any]],
|
|
summary: str,
|
|
clear: bool = False,
|
|
) -> None:
|
|
pass
|
|
|
|
def step_complete(self, node_name: str, event: ProgressEvent) -> None:
|
|
self._last_emitted_hint = None
|
|
with self._lock:
|
|
info = self._active_steps.pop(node_name, {})
|
|
subtext = info.get("subtext")
|
|
line = build_progress_step_text(
|
|
node_name=node_name,
|
|
elapsed_total=time.monotonic() - self._t0,
|
|
elapsed_step_ms=event.elapsed_ms,
|
|
status=event.status,
|
|
message=event.message,
|
|
)
|
|
if subtext:
|
|
line.append(f" ↳ {subtext}", style=BRAND)
|
|
self._emit(line)
|
|
|
|
def step_subtext(self, node_name: str, text: str, duration: float = 4.0) -> None:
|
|
if not text.strip():
|
|
return
|
|
with self._lock:
|
|
if node_name in self._active_steps:
|
|
self._active_steps[node_name]["subtext"] = text
|
|
self._active_steps[node_name]["subtext_until"] = time.monotonic() + duration
|
|
|
|
def print_above(self, text: str) -> None:
|
|
if not text.strip():
|
|
return
|
|
from rich.markdown import Markdown
|
|
|
|
from platform.terminal.theme import MARKDOWN_THEME
|
|
|
|
with self._console.use_theme(MARKDOWN_THEME):
|
|
self._emit(Markdown(text, code_theme="ansi_dark"))
|
|
|
|
def print_above_renderable(self, renderable: Any) -> None:
|
|
self._emit(renderable)
|