Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

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"]