feat(tui): replace Textual with a Go/Bubble Tea interface (#941)

This commit is contained in:
oyasumi
2026-08-03 19:23:07 -07:00
committed by GitHub
parent a51ca18666
commit 5bb9fe896b
128 changed files with 16229 additions and 6562 deletions
+3 -3
View File
@@ -148,16 +148,16 @@ def _post_json(path: str, payload: dict[str, Any], *, timeout: int) -> tuple[int
"""
url = f"{_app_url()}{path}"
try:
response = requests.post(
with requests.post(
url,
json=payload,
headers={"Accept": "application/json"},
timeout=timeout,
)
) as response:
return response.status_code, _parse_body(response.content)
except requests.RequestException as exc:
logger.warning("relay request to %s failed: %s", path, exc)
raise RelayError("unavailable") from exc
return response.status_code, _parse_body(response.content)
def _parse_body(raw: bytes) -> dict[str, Any]:
@@ -3,16 +3,32 @@
import type { ToolRendererProps } from "@/types/events";
import { shortPath } from "./utils";
/** Mirrors the OSS TUI `ViewImageRenderer`: surfaces load errors, otherwise a
* compact "view image <path>" line. */
const IMAGE_DATA_URI_RE = /data:image\/(png|jpe?g|gif|webp);base64,([A-Za-z0-9+/]+={0,2})/;
function extractImageDataUri(res: unknown): string | null {
let s: string | null = null;
if (typeof res === "string") {
s = res;
} else if (res && typeof res === "object") {
const o = res as Record<string, unknown>;
if (typeof o.image_url === "string") s = o.image_url;
else if (typeof o.url === "string") s = o.url;
}
if (!s) return null;
const m = IMAGE_DATA_URI_RE.exec(s);
if (!m || m[2].length < 100 || m[2].length % 4 !== 0) return null;
return `data:image/${m[1]};base64,${m[2]}`;
}
/** Renders the view_image result as an inline image, like the strix-app
* renderer; falls back to the load-error text when there is no payload. */
export default function ViewImageRenderer({ args, result }: ToolRendererProps) {
const path = ((args.path as string) ?? "").trim();
const res = result as Record<string, unknown> | string | null;
const imgSrc = extractImageDataUri(result);
let error: string | null = null;
if (typeof res === "string") {
const trimmed = res.trim();
// A string result that isn't an image payload or structured data is an error message
if (!imgSrc && typeof result === "string") {
const trimmed = result.trim();
if (trimmed && !trimmed.toLowerCase().startsWith("data:image/") && !trimmed.startsWith("{")) {
error = trimmed;
}
@@ -24,6 +40,13 @@ export default function ViewImageRenderer({ args, result }: ToolRendererProps) {
<span className="text-sky-400/80 font-semibold text-sm shrink-0">view image</span>
{path && <span className="text-[#888] font-mono text-[13px] break-all">{shortPath(path)}</span>}
</div>
{imgSrc && (
<img
src={imgSrc}
alt={path || "Tool image output"}
className="mt-1.5 max-w-full max-h-96 rounded-lg border border-white/[0.06] object-contain"
/>
)}
{error && <div className="text-red-400/70 text-[13px] mt-1">{error}</div>}
</div>
);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>Strix Results</title>
<script type="module" crossorigin src="./assets/index-CGvQq6oe.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-C3kQ5kk8.css">
<script type="module" crossorigin src="./assets/index-DBJ-RJqo.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DKbLYAbP.css">
</head>
<body>
<div id="root"></div>
+2 -4
View File
@@ -7,6 +7,7 @@ import logging
from typing import TYPE_CHECKING, Any
from strix.core.paths import run_record_path
from strix.interface.tui.live_view import TuiLiveView
if TYPE_CHECKING:
@@ -41,12 +42,9 @@ def severity_counts(vulns: list[Any]) -> dict[str, int]:
def build_run_state(run_dir: Path) -> dict[str, Any]:
"""Agent graph + full per-agent event/message stream.
Reuses the Textual-free ``TuiLiveView`` projection so the viewer and the TUI
Reuses the shared ``TuiLiveView`` projection so the viewer and the TUI
share one parser for ``agents.json`` + ``agents.db`` and never drift.
"""
# Imported lazily so importing strix.interface.viewer does not eagerly pull the TUI.
from strix.interface.tui.live_view import TuiLiveView
view = TuiLiveView()
view.hydrate_from_run_dir(run_dir)
return {"agents": list(view.agents.values()), "events": view.events}