feat(artifact-presenter): open text files in Orca

This commit is contained in:
jb
2026-09-02 21:07:07 -04:00
parent 9e1d59fdee
commit a11ca1743c
4 changed files with 155 additions and 14 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ Below you will find my preferences so that we can be more aligned as we work tog
- Plan before we implement. Investigate the codebase, do research, and grill me with questions. I'd rather spend some time answering questions first and get a result I am happy with rather than something we'd have to do again.
- Prototype or create PoCs for large features first. If we can identify if something will or will not work early, that will save time in the long run.
- Do not touch production or live databases unless explicitly instructed to do so.
- When presenting any single local file for my review, or a structured long-form report, plan, ADR, review, or summary, load the [`artifact-presenter`](../skills/document/artifact-presenter/SKILL.md) skill and open the complete file with mcat in its reusable Herdr pane instead of returning the full artifact inline.
- When presenting any single local file for my review, or a structured long-form report, plan, ADR, review, or summary, load the [`artifact-presenter`](../skills/document/artifact-presenter/SKILL.md) skill and open the complete file through its environment-aware presenter instead of returning the full artifact inline.
- After reporting status, if there is no input required from me, continue work on the task.
## Subagents and Parallel work
+8 -6
View File
@@ -1,6 +1,6 @@
---
name: artifact-presenter
description: Present local artifacts for review: use Browser Harness for interactive web artifacts, especially HTML, and mcat for non-web files, Markdown, and static source.
description: Present local artifacts for review: use Browser Harness for interactive web artifacts, Orca for editor-friendly text in Orca-managed sessions, and mcat for other non-web files.
---
# Artifact presenter
@@ -26,7 +26,7 @@ Keep ordinary explanations, code diffs, logs, multi-file comparisons, and user-r
Choose the presentation path:
- **Interactive web artifacts, especially `.html`:** use the Browser Harness/Browser Use-backed workflow.
- **Non-web files, Markdown, and static source:** use mcat in the reusable Herdr pane.
- **Non-web artifacts:** run this skill's presenter helper. It opens editor-friendly UTF-8 text directly in Orca when the harness is running in an Orca-managed terminal; otherwise it uses mcat in the reusable Herdr pane. Binary and media files stay on the mcat path.
Completion criterion: select exactly one existing or generated artifact path before presentation.
@@ -46,7 +46,7 @@ Load and follow the [`browser-harness`](../browser-harness/SKILL.md) skill. Open
Completion criterion: the active browser target reports the local artifact URL and page title.
## Present non-web artifacts with mcat
## Present non-web artifacts
Run the helper using its absolute path resolved from this skill directory:
@@ -54,9 +54,11 @@ Run the helper using its absolute path resolved from this skill directory:
python3 <skill-directory>/scripts/present.py <artifact-path>
```
The helper reuses the pane labeled `artifact presenter` in the current Herdr workspace. It serializes pane discovery, creation, and replacement per workspace, with an additional per-pane lock during viewer handoff. When that pane is already displaying an artifact, it closes the active `mcat` viewer and waits for the pane shell before launching the replacement through a shell-neutral helper command with encoded path arguments. It confirms the helper started the viewer rather than trusting terminal input delivery alone. It leaves unrelated foreground programs untouched and creates a new viewer pane instead. When no reusable pane exists, it creates a right split without taking focus. It launches `mcat <artifact-path>` so mcat can infer and display documents, source, archives, PDFs, images, and videos. It falls back to artifact-only delivery when Herdr or mcat is unavailable.
The helper detects an Orca-managed harness from Orca's terminal environment markers (`TERM_PROGRAM=Orca` or scoped `ORCA_*` workspace, worktree, pane, or terminal IDs). For editor-friendly UTF-8 text, it resolves the session's version-appropriate CLI and runs `orca file open <artifact-path> --json`. Orca only opens paths inside its selected worktree, so a rejected path falls through to the normal non-web fallback.
Completion criterion: capture whether the helper opened, reused, or skipped the viewer pane.
The fallback reuses the pane labeled `artifact presenter` in the current Herdr workspace. It serializes pane discovery, creation, and replacement per workspace, with an additional per-pane lock during viewer handoff. When that pane is already displaying an artifact, it closes the active `mcat` viewer and waits for the pane shell before launching the replacement through a shell-neutral helper command with encoded path arguments. It confirms the helper started the viewer rather than trusting terminal input delivery alone. It leaves unrelated foreground programs untouched and creates a new viewer pane instead. When no reusable pane exists, it creates a right split without taking focus. It launches `mcat <artifact-path>` so mcat can infer and display documents, source, archives, PDFs, images, and videos. It falls back to artifact-only delivery when neither Orca nor the Herdr/mcat path can present the file.
Completion criterion: capture whether Orca opened the file, the helper opened or reused an mcat pane, or presentation fell back to the artifact path only.
## Convert only when requested
@@ -77,6 +79,6 @@ Return:
- a one- to three-sentence result summary;
- the artifact path;
- for interactive web artifacts, that Browser Harness presented the artifact and verified its URL/title;
- for other artifacts, whether the reusable mcat pane was opened or reused, or why presentation fell back to the file only.
- for other artifacts, whether Orca opened the file, the reusable mcat pane was opened or reused, or why presentation fell back to the file only.
Do not repeat the complete artifact in chat. Honor a later request to paste any section inline.
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Open one review artifact in a reusable right-side Herdr mcat pane."""
"""Present one review artifact through Orca or a reusable Herdr mcat pane."""
from __future__ import annotations
@@ -22,6 +22,13 @@ from typing import Callable, Mapping, Sequence
PANE_LABEL = "artifact presenter"
VIEWER_PROCESS_NAME = "mcat"
TEXT_SAMPLE_BYTES = 8192
ORCA_SESSION_ENVIRONMENT_KEYS = (
"ORCA_WORKSPACE_ID",
"ORCA_WORKTREE_ID",
"ORCA_PANE_KEY",
"ORCA_TERMINAL_HANDLE",
)
PANE_READY_ATTEMPTS = 40
PANE_READY_DELAY_SECONDS = 0.05
PANE_SHELL_STABLE_CHECKS = 2
@@ -57,9 +64,66 @@ class PresentationResult:
def message(self) -> str:
if self.status == "artifact-only":
return f"artifact-only: {self.reason}; {self.artifact}"
if self.status == "orca-opened":
return f"orca-opened: {self.artifact}"
return f"{self.status}: pane {self.pane_id}; {self.artifact}"
def is_orca_managed_session(env: Mapping[str, str]) -> bool:
"""Return whether this process belongs to an Orca-managed terminal session."""
if env.get("TERM_PROGRAM", "").casefold() == "orca":
return True
return any(env.get(key) for key in ORCA_SESSION_ENVIRONMENT_KEYS)
def is_editor_friendly_text(artifact: Path) -> bool:
"""Recognize UTF-8 text without maintaining a source-extension allowlist."""
with artifact.open("rb") as handle:
sample = handle.read(TEXT_SAMPLE_BYTES)
if b"\0" in sample:
return False
try:
sample.decode("utf-8")
except UnicodeDecodeError:
return False
return True
def orca_executable(
env: Mapping[str, str],
which: FindExecutable,
) -> str | None:
configured = env.get("ORCA_CLI_COMMAND")
if configured:
return which(configured)
if env.get("ORCA_DEV_REPO_ROOT"):
return which("orca-dev")
return which("orca")
def open_in_orca(
orca: str,
artifact: Path,
run: RunCommand,
) -> PresentationResult:
completed = run(
[orca, "file", "open", str(artifact), "--json"],
check=True,
capture_output=True,
text=True,
)
response = json.loads(completed.stdout)
result = response.get("result") if isinstance(response, dict) else None
if (
not isinstance(response, dict)
or response.get("ok") is not True
or not isinstance(result, dict)
or result.get("opened") is not True
):
raise ValueError("Orca did not confirm that it opened the file")
return PresentationResult("orca-opened", artifact)
def run_json(
executable: str,
args: Sequence[str],
@@ -345,6 +409,14 @@ def present_document(
if not artifact.is_file():
raise ValueError(f"artifact is not a file: {artifact}")
if is_orca_managed_session(env) and is_editor_friendly_text(artifact):
orca = orca_executable(env, which)
if orca:
try:
return open_in_orca(orca, artifact, run)
except (json.JSONDecodeError, OSError, subprocess.SubprocessError, ValueError):
pass
herdr = which("herdr")
mcat = which("mcat")
workspace_id = env.get("HERDR_WORKSPACE_ID")
@@ -120,6 +120,22 @@ class FakeRunner:
return subprocess.CompletedProcess(argv, 0, json.dumps(output), "")
class OrcaAwareRunner(FakeRunner):
def __init__(self, *args, fail_orca_open: bool = False, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.fail_orca_open = fail_orca_open
def __call__(self, command, **kwargs):
argv = [str(value) for value in command]
if argv[1:3] == ["file", "open"]:
self.calls.append(argv)
if self.fail_orca_open:
raise subprocess.CalledProcessError(1, argv)
output = {"ok": True, "result": {"opened": True}}
return subprocess.CompletedProcess(argv, 0, json.dumps(output), "")
return super().__call__(command, **kwargs)
class ArtifactPresenterTests(unittest.TestCase):
def setUp(self) -> None:
self.tempdir = tempfile.TemporaryDirectory()
@@ -132,6 +148,7 @@ class ArtifactPresenterTests(unittest.TestCase):
self.executables = {
"herdr": "/usr/bin/herdr",
"mcat": "/usr/bin/mcat",
"orca": "/usr/bin/orca",
}
def tearDown(self) -> None:
@@ -381,21 +398,71 @@ class ArtifactPresenterTests(unittest.TestCase):
)
self.assertFalse(any(call[1:3] == ["pane", "split"] for call in runner.calls))
def test_presents_non_markdown_file_directly_with_mcat(self) -> None:
video = self.artifact.with_name("review.mp4")
video.write_bytes(b"video fixture")
runner = FakeRunner(
def test_opens_editor_friendly_file_in_orca_managed_session(self) -> None:
source = self.artifact.with_name("module.ts")
source.write_text("export const answer = 42;\n", encoding="utf-8")
runner = OrcaAwareRunner()
result = MODULE.present_document(
source,
env={**self.env, "ORCA_WORKSPACE_ID": "workspace"},
which=self.which,
run=runner,
)
self.assertEqual(result.status, "orca-opened")
self.assertEqual(
runner.calls,
[["/usr/bin/orca", "file", "open", str(source.resolve()), "--json"]],
)
def test_uses_configured_orca_cli_command(self) -> None:
self.executables["orca-wsl"] = "/opt/orca/orca-wsl"
runner = OrcaAwareRunner()
result = MODULE.present_document(
self.artifact,
env={"TERM_PROGRAM": "Orca", "ORCA_CLI_COMMAND": "orca-wsl"},
which=self.which,
run=runner,
)
self.assertEqual(result.status, "orca-opened")
self.assertEqual(runner.calls[0][0], "/opt/orca/orca-wsl")
def test_falls_back_to_mcat_when_orca_cannot_open_text_file(self) -> None:
runner = OrcaAwareRunner(
[{"pane_id": "workspace:p-viewer", "label": MODULE.PANE_LABEL}],
fail_orca_open=True,
)
result = MODULE.present_document(
video,
env=self.env,
self.artifact,
env={**self.env, "ORCA_TERMINAL_HANDLE": "term-1"},
which=self.which,
run=runner,
)
self.assertEqual(result.status, "reused")
self.assertEqual(runner.calls[0][1:3], ["file", "open"])
self.mcat_run_call(runner, "workspace:p-viewer")
def test_presents_binary_file_directly_with_mcat_in_orca_session(self) -> None:
video = self.artifact.with_name("review.mp4")
video.write_bytes(b"video\0fixture")
runner = OrcaAwareRunner(
[{"pane_id": "workspace:p-viewer", "label": MODULE.PANE_LABEL}],
)
result = MODULE.present_document(
video,
env={**self.env, "TERM_PROGRAM": "Orca"},
which=self.which,
run=runner,
)
self.assertEqual(result.status, "reused")
self.assertFalse(any(call[1:3] == ["file", "open"] for call in runner.calls))
self.mcat_run_call(runner, "workspace:p-viewer", video)
def test_encodes_artifact_paths_before_terminal_delivery(self) -> None: