feat(artifact-presenter): open text files in Orca
This commit is contained in:
@@ -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.
|
- 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.
|
- 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.
|
- 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.
|
- After reporting status, if there is no input required from me, continue work on the task.
|
||||||
|
|
||||||
## Subagents and Parallel work
|
## Subagents and Parallel work
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: artifact-presenter
|
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
|
# Artifact presenter
|
||||||
@@ -26,7 +26,7 @@ Keep ordinary explanations, code diffs, logs, multi-file comparisons, and user-r
|
|||||||
Choose the presentation path:
|
Choose the presentation path:
|
||||||
|
|
||||||
- **Interactive web artifacts, especially `.html`:** use the Browser Harness/Browser Use-backed workflow.
|
- **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.
|
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.
|
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:
|
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>
|
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
|
## Convert only when requested
|
||||||
|
|
||||||
@@ -77,6 +79,6 @@ Return:
|
|||||||
- a one- to three-sentence result summary;
|
- a one- to three-sentence result summary;
|
||||||
- the artifact path;
|
- the artifact path;
|
||||||
- for interactive web artifacts, that Browser Harness presented the artifact and verified its URL/title;
|
- 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.
|
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
|
#!/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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -22,6 +22,13 @@ from typing import Callable, Mapping, Sequence
|
|||||||
|
|
||||||
PANE_LABEL = "artifact presenter"
|
PANE_LABEL = "artifact presenter"
|
||||||
VIEWER_PROCESS_NAME = "mcat"
|
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_ATTEMPTS = 40
|
||||||
PANE_READY_DELAY_SECONDS = 0.05
|
PANE_READY_DELAY_SECONDS = 0.05
|
||||||
PANE_SHELL_STABLE_CHECKS = 2
|
PANE_SHELL_STABLE_CHECKS = 2
|
||||||
@@ -57,9 +64,66 @@ class PresentationResult:
|
|||||||
def message(self) -> str:
|
def message(self) -> str:
|
||||||
if self.status == "artifact-only":
|
if self.status == "artifact-only":
|
||||||
return f"artifact-only: {self.reason}; {self.artifact}"
|
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}"
|
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(
|
def run_json(
|
||||||
executable: str,
|
executable: str,
|
||||||
args: Sequence[str],
|
args: Sequence[str],
|
||||||
@@ -345,6 +409,14 @@ def present_document(
|
|||||||
if not artifact.is_file():
|
if not artifact.is_file():
|
||||||
raise ValueError(f"artifact is not a file: {artifact}")
|
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")
|
herdr = which("herdr")
|
||||||
mcat = which("mcat")
|
mcat = which("mcat")
|
||||||
workspace_id = env.get("HERDR_WORKSPACE_ID")
|
workspace_id = env.get("HERDR_WORKSPACE_ID")
|
||||||
|
|||||||
@@ -120,6 +120,22 @@ class FakeRunner:
|
|||||||
return subprocess.CompletedProcess(argv, 0, json.dumps(output), "")
|
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):
|
class ArtifactPresenterTests(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.tempdir = tempfile.TemporaryDirectory()
|
self.tempdir = tempfile.TemporaryDirectory()
|
||||||
@@ -132,6 +148,7 @@ class ArtifactPresenterTests(unittest.TestCase):
|
|||||||
self.executables = {
|
self.executables = {
|
||||||
"herdr": "/usr/bin/herdr",
|
"herdr": "/usr/bin/herdr",
|
||||||
"mcat": "/usr/bin/mcat",
|
"mcat": "/usr/bin/mcat",
|
||||||
|
"orca": "/usr/bin/orca",
|
||||||
}
|
}
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
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))
|
self.assertFalse(any(call[1:3] == ["pane", "split"] for call in runner.calls))
|
||||||
|
|
||||||
def test_presents_non_markdown_file_directly_with_mcat(self) -> None:
|
def test_opens_editor_friendly_file_in_orca_managed_session(self) -> None:
|
||||||
video = self.artifact.with_name("review.mp4")
|
source = self.artifact.with_name("module.ts")
|
||||||
video.write_bytes(b"video fixture")
|
source.write_text("export const answer = 42;\n", encoding="utf-8")
|
||||||
runner = FakeRunner(
|
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}],
|
[{"pane_id": "workspace:p-viewer", "label": MODULE.PANE_LABEL}],
|
||||||
|
fail_orca_open=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = MODULE.present_document(
|
result = MODULE.present_document(
|
||||||
video,
|
self.artifact,
|
||||||
env=self.env,
|
env={**self.env, "ORCA_TERMINAL_HANDLE": "term-1"},
|
||||||
which=self.which,
|
which=self.which,
|
||||||
run=runner,
|
run=runner,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(result.status, "reused")
|
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)
|
self.mcat_run_call(runner, "workspace:p-viewer", video)
|
||||||
|
|
||||||
def test_encodes_artifact_paths_before_terminal_delivery(self) -> None:
|
def test_encodes_artifact_paths_before_terminal_delivery(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user