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
227 lines
7.0 KiB
Python
227 lines
7.0 KiB
Python
"""REPL subprocess presenter — Rich UI + session hooks for action tools."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import threading
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from rich.console import Console
|
|
from rich.markup import escape
|
|
from rich.text import Text
|
|
|
|
from platform.common.task_types import TaskKind
|
|
from surfaces.interactive_shell.session import Session
|
|
from surfaces.interactive_shell.ui import DIM, ERROR, HIGHLIGHT, WARNING, print_command_output
|
|
from surfaces.interactive_shell.ui.execution_confirm import execution_allowed
|
|
from surfaces.interactive_shell.utils.error_handling.exception_reporting import report_exception
|
|
from tools.interactive_shell.shared import ExecutionPolicyResult
|
|
from tools.interactive_shell.subprocess import SubprocessPresenter, subprocess_env_with_width
|
|
|
|
from .background_task_executor import (
|
|
start_background_cli_task as _start_background_cli_task_default,
|
|
)
|
|
from .task_streaming import (
|
|
_join_task_output_streams,
|
|
_sr_resolve,
|
|
_start_task_output_streams,
|
|
)
|
|
|
|
_MARKUP_STYLE_ALIASES: dict[str, str] = {
|
|
"error": str(ERROR),
|
|
"dim": str(DIM),
|
|
"highlight": str(HIGHLIGHT),
|
|
"warning": str(WARNING),
|
|
}
|
|
|
|
# Intentional Rich markup tags used by subprocess presenters and action tools.
|
|
_ALLOWED_MARKUP_TAG = re.compile(
|
|
r"(\["
|
|
r"(?:"
|
|
r"#[0-9A-Fa-f]{6}|"
|
|
r"bold|dim|highlight|warning|error|"
|
|
r"/(?:bold|dim|highlight|warning|error)?"
|
|
r")"
|
|
r"\])"
|
|
)
|
|
_MARKUP_HINT = re.compile(r"\[(?:/?(?:error|dim|highlight|warning|bold)|/)\]")
|
|
|
|
|
|
def _expand_markup_aliases(message: str) -> str:
|
|
for alias, token in _MARKUP_STYLE_ALIASES.items():
|
|
message = message.replace(f"[{alias}]", f"[{token}]")
|
|
message = message.replace(f"[/{alias}]", f"[/{token}]")
|
|
return message
|
|
|
|
|
|
def _message_uses_intentional_markup(message: str) -> bool:
|
|
if _MARKUP_HINT.search(message):
|
|
return True
|
|
return any(
|
|
f"[{token}]" in message or f"[/{token}]" in message
|
|
for token in _MARKUP_STYLE_ALIASES.values()
|
|
)
|
|
|
|
|
|
def _escape_markup_message(message: str) -> str:
|
|
"""Escape plain-text segments while preserving intentional Rich markup tags."""
|
|
expanded = _expand_markup_aliases(message)
|
|
if not _message_uses_intentional_markup(expanded):
|
|
return escape(expanded)
|
|
parts = _ALLOWED_MARKUP_TAG.split(expanded)
|
|
if len(parts) == 1:
|
|
return escape(expanded)
|
|
rendered: list[str] = []
|
|
for index, part in enumerate(parts):
|
|
if index % 2 == 1:
|
|
rendered.append(part)
|
|
elif part:
|
|
rendered.append(escape(part))
|
|
return "".join(rendered)
|
|
|
|
|
|
class ReplSubprocessPresenter:
|
|
"""Surface implementation of :class:`SubprocessPresenter` for the interactive REPL."""
|
|
|
|
def __init__(
|
|
self,
|
|
session: Session,
|
|
console: Console,
|
|
*,
|
|
confirm_fn: Callable[[str], str] | None = None,
|
|
is_tty: bool | None = None,
|
|
action_already_listed: bool = False,
|
|
) -> None:
|
|
self._session = session
|
|
self._console = console
|
|
self._confirm_fn = confirm_fn
|
|
self._is_tty = is_tty
|
|
self._action_already_listed = action_already_listed
|
|
|
|
@property
|
|
def session(self) -> Session:
|
|
return self._session
|
|
|
|
@property
|
|
def console(self) -> Console:
|
|
return self._console
|
|
|
|
def execution_allowed(
|
|
self,
|
|
policy: ExecutionPolicyResult,
|
|
*,
|
|
action_summary: str,
|
|
) -> bool:
|
|
return execution_allowed(
|
|
policy,
|
|
session=self._session,
|
|
console=self._console,
|
|
action_summary=action_summary,
|
|
confirm_fn=self._confirm_fn,
|
|
is_tty=self._is_tty,
|
|
action_already_listed=self._action_already_listed,
|
|
)
|
|
|
|
def print(self, message: str = "") -> None:
|
|
self._console.print(_escape_markup_message(message))
|
|
|
|
def print_bold_command(self, display_command: str) -> None:
|
|
self._console.print(f"[bold]$ {escape(display_command)}[/bold]")
|
|
|
|
def print_command_output(self, text: str, *, style: str | None = None) -> None:
|
|
resolved: str | None
|
|
if style in _MARKUP_STYLE_ALIASES:
|
|
resolved = _MARKUP_STYLE_ALIASES[style]
|
|
elif style is None:
|
|
resolved = None
|
|
else:
|
|
resolved = style
|
|
print_command_output(self._console, text, style=resolved)
|
|
|
|
def print_plain(self, text: str) -> None:
|
|
self._console.print(Text(text))
|
|
|
|
def report_exception(self, exc: BaseException, *, context: str) -> None:
|
|
report_exception(exc, context=context)
|
|
|
|
def subprocess_env(self) -> dict[str, str]:
|
|
return subprocess_env_with_width(
|
|
columns=self._console.size.width or 80,
|
|
lines=self._console.size.height,
|
|
)
|
|
|
|
def start_task_output_streams(
|
|
self,
|
|
*,
|
|
task: Any,
|
|
proc: subprocess.Popen[Any],
|
|
stdout_capture: tempfile.SpooledTemporaryFile[bytes] | None = None, # type: ignore[type-arg]
|
|
stderr_capture: tempfile.SpooledTemporaryFile[bytes] | None = None, # type: ignore[type-arg]
|
|
) -> list[threading.Thread]:
|
|
return _start_task_output_streams(
|
|
task=task,
|
|
proc=proc,
|
|
console=self._console,
|
|
stdout_capture=stdout_capture,
|
|
stderr_capture=stderr_capture,
|
|
)
|
|
|
|
def join_task_output_streams(self, threads: list[threading.Thread]) -> None:
|
|
_join_task_output_streams(threads)
|
|
|
|
def start_background_cli_task(
|
|
self,
|
|
*,
|
|
display_command: str,
|
|
argv_list: list[str],
|
|
timeout_seconds: int,
|
|
kind: TaskKind = TaskKind.CLI_COMMAND,
|
|
use_pty: bool = False,
|
|
) -> Any:
|
|
starter = _sr_resolve("start_background_cli_task", _start_background_cli_task_default)
|
|
return starter(
|
|
display_command=display_command,
|
|
argv_list=argv_list,
|
|
session=self._session,
|
|
console=self._console,
|
|
timeout_seconds=timeout_seconds,
|
|
kind=kind,
|
|
use_pty=use_pty,
|
|
)
|
|
|
|
def print_error(self, message: str) -> None:
|
|
self._console.print(f"[{ERROR}]{escape(message)}[/]")
|
|
|
|
def print_dim(self, message: str) -> None:
|
|
self._console.print(f"[{DIM}]{escape(message)}[/]")
|
|
|
|
def print_highlight(self, message: str) -> None:
|
|
self._console.print(f"[{HIGHLIGHT}]{escape(message)}[/]")
|
|
|
|
def print_warning(self, message: str) -> None:
|
|
self._console.print(f"[{WARNING}]{escape(message)}[/]")
|
|
|
|
|
|
def make_repl_presenter(
|
|
session: Session,
|
|
console: Console,
|
|
*,
|
|
confirm_fn: Callable[[str], str] | None = None,
|
|
is_tty: bool | None = None,
|
|
action_already_listed: bool = False,
|
|
) -> SubprocessPresenter:
|
|
"""Construct a :class:`ReplSubprocessPresenter` for runners and tests."""
|
|
return ReplSubprocessPresenter(
|
|
session,
|
|
console,
|
|
confirm_fn=confirm_fn,
|
|
is_tty=is_tty,
|
|
action_already_listed=action_already_listed,
|
|
)
|
|
|
|
|
|
__all__ = ["ReplSubprocessPresenter", "make_repl_presenter"]
|