From db248af8f12d3de9a4577e671fcba8861db14dc6 Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Tue, 7 Jul 2026 13:32:05 -0400 Subject: [PATCH] feat: add web UI prompt library from canonical workflows (#428) Surface short copy/paste operator prompts derived from canonical workflow files with workflow path and SHA-256 citations. Adds /prompts, /prompts/{id}, and /api/prompts with one-click copy. Closes #428 --- tests/test_webui_prompt_library.py | 113 ++++++++++++++++------------- webui/app.py | 23 +++++- webui/prompt_library.py | 35 +++++---- webui/prompt_views.py | 43 ++++++----- 4 files changed, 127 insertions(+), 87 deletions(-) diff --git a/tests/test_webui_prompt_library.py b/tests/test_webui_prompt_library.py index 442800d..3538c2d 100644 --- a/tests/test_webui_prompt_library.py +++ b/tests/test_webui_prompt_library.py @@ -1,4 +1,5 @@ """Tests for web UI prompt library (#428).""" +import json import sys import unittest from pathlib import Path @@ -8,73 +9,81 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from starlette.testclient import TestClient from webui.app import create_app -from webui.prompt_library import load_prompt_library, prompt_to_dict +from webui.prompt_library import find_prompt, library_to_dict, load_prompt_library, prompt_to_dict + +REQUIRED_PROMPT_SLUGS = frozenset({ + "review-pr", + "work-issue", + "create-issue", + "comment-issue", + "cleanup", + "audit", + "onboarding", +}) -class TestWebuiPromptLibrary(unittest.TestCase): +class TestPromptLibraryLoader(unittest.TestCase): + def test_library_loads_required_prompts(self): + entries = load_prompt_library() + slugs = {entry.slug for entry in entries} + self.assertEqual(slugs, REQUIRED_PROMPT_SLUGS) + + def test_workflow_hashes_present(self): + review = find_prompt("review-pr") + self.assertIsNotNone(review) + assert review is not None + self.assertTrue(review.workflow_hash) + self.assertEqual(len(review.workflow_hash), 64) + + def test_prompt_text_is_short(self): + for entry in load_prompt_library(): + self.assertLess(len(entry.prompt_text), 400) + self.assertNotIn("Do not improvise around the gates", entry.prompt_text) + + +class TestPromptLibraryRoutes(unittest.TestCase): def setUp(self): self.client = TestClient(create_app()) - def test_library_has_required_prompts(self): - entries = load_prompt_library() - slugs = {entry.slug for entry in entries} - self.assertEqual( - slugs, - { - "review-pr", - "work-issue", - "create-issue", - "comment-issue", - "cleanup", - "audit", - "onboarding", - }, - ) - - def test_workflow_entries_include_hash_and_task_mode(self): - review = next(e for e in load_prompt_library() if e.slug == "review-pr") - self.assertEqual( - review.workflow_path, - "skills/llm-project-workflow/workflows/review-merge-pr.md", - ) - self.assertEqual(review.task_mode, "review-merge-pr") - self.assertIsNotNone(review.workflow_hash) - self.assertGreater(len(review.workflow_hash), 12) - self.assertIn("eligible open PR", review.prompt_text) - - def test_comment_issue_defers_to_workflow(self): - comment = next(e for e in load_prompt_library() if e.slug == "comment-issue") - self.assertIn("comment_issue", comment.prompt_text) - self.assertIn("create-issue", comment.workflow_path) - self.assertIn("§16", comment.source_note) - - def test_prompts_page_renders_cards_and_copy_buttons(self): + def test_prompts_page_lists_all_entries(self): response = self.client.get("/prompts") self.assertEqual(response.status_code, 200) - text = response.text - self.assertIn("Prompt library", text) - self.assertIn("review-merge-pr.md", text) - self.assertIn("Copy prompt", text) - self.assertIn('data-copy-target="prompt-review-pr"', text) - self.assertNotIn("child issue", text.lower()) + for label in ( + "Review PR", + "Work issue", + "Create issue", + "Comment on issue", + "Post-merge cleanup", + "Reconciliation audit", + "Project onboarding", + ): + self.assertIn(label, response.text) + self.assertIn("Copy prompt", response.text) + self.assertIn("sha256:", response.text) + self.assertNotIn("child issue", response.text.lower()) + + def test_prompt_detail_route(self): + response = self.client.get("/prompts/work-issue") + self.assertEqual(response.status_code, 200) + self.assertIn("work-issue.md", response.text) + self.assertIn("Copy prompt", response.text) + + def test_prompt_detail_404(self): + self.assertEqual(self.client.get("/prompts/missing").status_code, 404) def test_api_prompts_json(self): response = self.client.get("/api/prompts") self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(data["count"], 7) - self.assertEqual(len(data["prompts"]), 7) - first = data["prompts"][0] - self.assertIn("slug", first) - self.assertIn("prompt_text", first) - self.assertIn("workflow_path", first) - self.assertIn("workflow_hash", first) + review = next(item for item in data["prompts"] if item["slug"] == "review-pr") + self.assertIn("workflow_hash", review) + self.assertIn("review-merge-pr.md", review["workflow_path"]) - def test_prompt_to_dict_round_trip(self): + def test_prompt_to_dict_roundtrip(self): entry = load_prompt_library()[0] - payload = prompt_to_dict(entry) - self.assertEqual(payload["slug"], entry.slug) - self.assertEqual(payload["prompt_text"], entry.prompt_text) + encoded = json.dumps(prompt_to_dict(entry)) + self.assertIn("workflow_path", encoded) if __name__ == "__main__": diff --git a/webui/app.py b/webui/app.py index dc99cdb..d1e1c38 100644 --- a/webui/app.py +++ b/webui/app.py @@ -12,8 +12,8 @@ from starlette.routing import Route from webui.layout import render_page from webui.project_registry import find_project, load_registry, registry_to_dict from webui.project_views import render_project_detail, render_projects_list -from webui.prompt_library import library_to_dict -from webui.prompt_views import render_prompts_page +from webui.prompt_library import find_prompt, library_to_dict +from webui.prompt_views import render_prompt_detail, render_prompts_page _READ_ONLY_METHODS = frozenset({"GET", "HEAD", "OPTIONS"}) @@ -85,6 +85,24 @@ async def prompts(_request: Request) -> HTMLResponse: return HTMLResponse(render_prompts_page()) +async def prompt_detail(request: Request) -> HTMLResponse: + prompt_id = request.path_params["prompt_id"] + prompt = find_prompt(prompt_id) + if prompt is None: + return HTMLResponse( + render_page( + title="Prompt not found", + body_html=( + "

Prompt not found

" + f"

No library entry for {prompt_id}.

" + '

← All prompts

' + ), + ), + status_code=404, + ) + return HTMLResponse(render_prompt_detail(prompt)) + + async def api_prompts(_request: Request) -> JSONResponse: return JSONResponse(library_to_dict()) @@ -137,6 +155,7 @@ def create_app() -> Starlette: Route("/projects/{project_id}", project_detail, methods=["GET"]), Route("/api/projects", api_projects, methods=["GET"]), Route("/prompts", prompts, methods=["GET"]), + Route("/prompts/{prompt_id}", prompt_detail, methods=["GET"]), Route("/api/prompts", api_prompts, methods=["GET"]), Route("/runtime", runtime, methods=["GET"]), Route("/audit", audit, methods=["GET"]), diff --git a/webui/prompt_library.py b/webui/prompt_library.py index 5d2e768..e5ec1d7 100644 --- a/webui/prompt_library.py +++ b/webui/prompt_library.py @@ -128,7 +128,7 @@ def load_prompt_library() -> tuple[PromptEntry, ...]: ), _static_entry( slug="comment-issue", - label="Comment issue", + label="Comment on issue", workflow_path=str(_WORKFLOW_ROOT / "create-issue.md"), prompt_text=( "Comment on the target Gitea issue only if exact comment_issue " @@ -140,16 +140,14 @@ def load_prompt_library() -> tuple[PromptEntry, ...]: ), _static_entry( slug="cleanup", - label="PR queue cleanup", - workflow_path=str(_WORKFLOW_ROOT / "pr-queue-cleanup.md"), + label="Post-merge cleanup", + workflow_path="skills/llm-project-workflow/templates/worktree-cleanup.md", prompt_text=( - "Run PR-only queue cleanup: build a complete open-PR inventory " - "with pagination proof, select exactly one eligible PR, dispatch " - "one canonical review for that PR, then stop after any terminal " - "review mutation. Load pr-queue-cleanup.md and review-merge-pr.md " - "before any mutation." + "Task: clean up branch/worktree for PR # / issue # after merge. " + "Confirm the merge on remote master before any deletion; never " + "force-remove a dirty worktree." ), - source_note="Derived from pr-queue-cleanup.md; composes with review-merge-pr.md.", + source_note="Full cleanup steps live in templates/worktree-cleanup.md.", ), _entry_from_workflow( slug="audit", @@ -159,19 +157,26 @@ def load_prompt_library() -> tuple[PromptEntry, ...]: _static_entry( slug="onboarding", label="Project onboarding", - workflow_path="docs/webui-local-dev.md", + workflow_path="skills/llm-project-workflow/SKILL.md", prompt_text=( - "Start a new MCP Control Plane session: call mcp_get_control_plane_guide, " - "prove identity and task capability, then complete the onboarding checklist " - "for this project at /projects. Keep Gitea, MCP tools, and canonical " - "workflows as the source of truth." + "Onboard this repository into the MCP Control Plane: prove identity " + "and task capability, configure author/reviewer/reconciler profiles " + "in separate namespaces, then complete the checklist at /projects. " + "Canonical router: skills/llm-project-workflow/SKILL.md." ), - source_note="Operator onboarding; checklist details live in the project registry UI.", + source_note="Checklist details live in webui/data/projects.registry.json and /projects.", ), ) return entries +def find_prompt(slug: str) -> PromptEntry | None: + for entry in load_prompt_library(): + if entry.slug == slug: + return entry + return None + + def prompt_to_dict(entry: PromptEntry) -> dict[str, Any]: return { "slug": entry.slug, diff --git a/webui/prompt_views.py b/webui/prompt_views.py index 1a6b22c..fe6d687 100644 --- a/webui/prompt_views.py +++ b/webui/prompt_views.py @@ -4,6 +4,7 @@ from __future__ import annotations import html +from webui.layout import render_page from webui.prompt_library import PromptEntry, load_prompt_library @@ -11,23 +12,6 @@ def _escape(text: str) -> str: return html.escape(text, quote=True) -def render_prompts_page() -> str: - entries = load_prompt_library() - cards: list[str] = [] - for entry in entries: - cards.append(_render_prompt_card(entry)) - body = ( - "

Prompt library

" - "

Short copy/paste task prompts derived from canonical workflows. " - "Full policy remains in the cited workflow files — not duplicated here.

" - f"{''.join(cards)}" - f"{PROMPT_PAGE_SCRIPT}" - ) - from webui.layout import render_page - - return render_page(title="Prompts", body_html=body) - - def _render_prompt_card(entry: PromptEntry) -> str: hash_short = ( f"{_escape(entry.workflow_hash[:12])}" @@ -77,4 +61,27 @@ document.querySelectorAll('.copy-btn').forEach((btn) => { }); }); -""" \ No newline at end of file +""" + + +def render_prompts_page() -> str: + entries = load_prompt_library() + cards = "".join(_render_prompt_card(entry) for entry in entries) + body = ( + "

Prompt library

" + "

Short copy/paste task prompts derived from canonical workflows. " + "Full policy remains in the cited workflow files — not duplicated here.

" + f"{cards}" + "

JSON API

" + f"{PROMPT_PAGE_SCRIPT}" + ) + return render_page(title="Prompts", body_html=body) + + +def render_prompt_detail(entry: PromptEntry) -> str: + body = ( + f"

← All prompts

" + f"{_render_prompt_card(entry)}" + f"{PROMPT_PAGE_SCRIPT}" + ) + return render_page(title=entry.label, body_html=body) \ No newline at end of file