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
This commit is contained in:
2026-07-07 14:53:16 -04:00
parent 6670c72e65
commit f845864889
4 changed files with 127 additions and 87 deletions
+21 -2
View File
@@ -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=(
"<h2>Prompt not found</h2>"
f"<p>No library entry for <code>{prompt_id}</code>.</p>"
'<p><a href="/prompts">← All prompts</a></p>'
),
),
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"]),
+20 -15
View File
@@ -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 #<pr> / issue #<n> 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,
+25 -18
View File
@@ -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 = (
"<h2>Prompt library</h2>"
"<p>Short copy/paste task prompts derived from canonical workflows. "
"Full policy remains in the cited workflow files — not duplicated here.</p>"
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"<code>{_escape(entry.workflow_hash[:12])}</code>"
@@ -77,4 +61,27 @@ document.querySelectorAll('.copy-btn').forEach((btn) => {
});
});
</script>
"""
"""
def render_prompts_page() -> str:
entries = load_prompt_library()
cards = "".join(_render_prompt_card(entry) for entry in entries)
body = (
"<h2>Prompt library</h2>"
"<p>Short copy/paste task prompts derived from canonical workflows. "
"Full policy remains in the cited workflow files — not duplicated here.</p>"
f"{cards}"
"<p><a href=\"/api/prompts\">JSON API</a></p>"
f"{PROMPT_PAGE_SCRIPT}"
)
return render_page(title="Prompts", body_html=body)
def render_prompt_detail(entry: PromptEntry) -> str:
body = (
f"<p><a href=\"/prompts\">← All prompts</a></p>"
f"{_render_prompt_card(entry)}"
f"{PROMPT_PAGE_SCRIPT}"
)
return render_page(title=entry.label, body_html=body)