Adds read-only /worktrees and /api/worktrees routes that scan branches/ directories, classify worktree state, flag missing preserved worktrees from the issue lock, and surface the canonical cleanup prompt without deletion. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
130 lines
4.3 KiB
Python
130 lines
4.3 KiB
Python
"""HTML views for branches/ worktree hygiene dashboard (#432)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
|
|
from webui.layout import render_page
|
|
from webui.worktree_scanner import HygieneSnapshot, WorktreeEntry
|
|
|
|
|
|
def _escape(text: str) -> str:
|
|
return html.escape(text, quote=True)
|
|
|
|
|
|
def _badge(classification: str) -> str:
|
|
return (
|
|
f'<span class="badge badge-{_escape(classification)}">'
|
|
f"{_escape(classification)}</span>"
|
|
)
|
|
|
|
|
|
def _entry_row(entry: WorktreeEntry) -> str:
|
|
branch = entry.branch or "—"
|
|
head = entry.head_sha[:12] if entry.head_sha else "—"
|
|
dirty = (
|
|
f"{entry.dirty_tracked} tracked"
|
|
+ (" + untracked" if entry.dirty_untracked else "")
|
|
if entry.dirty_tracked or entry.dirty_untracked
|
|
else "clean"
|
|
)
|
|
return (
|
|
"<tr>"
|
|
f"<td><code>{_escape(entry.rel_path)}</code></td>"
|
|
f"<td>{_badge(entry.classification)}</td>"
|
|
f"<td><code>{_escape(branch)}</code></td>"
|
|
f"<td><code>{_escape(head)}</code></td>"
|
|
f"<td>{_escape(dirty)}</td>"
|
|
f"<td>{'yes' if entry.detached else 'no'}</td>"
|
|
f"<td class='muted'>{_escape(entry.notes)}</td>"
|
|
"</tr>"
|
|
)
|
|
|
|
|
|
WORKTREE_PAGE_SCRIPT = """
|
|
<script>
|
|
document.querySelectorAll('.copy-cleanup-btn').forEach((btn) => {
|
|
btn.addEventListener('click', async () => {
|
|
const node = document.getElementById('cleanup-prompt');
|
|
if (!node) return;
|
|
try {
|
|
await navigator.clipboard.writeText(node.textContent || '');
|
|
const prior = btn.textContent;
|
|
btn.textContent = 'Copied';
|
|
setTimeout(() => { btn.textContent = prior; }, 1200);
|
|
} catch (_err) {
|
|
btn.textContent = 'Copy failed';
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
"""
|
|
|
|
WORKTREE_PAGE_STYLES = """
|
|
<style>
|
|
table.worktrees td:nth-child(7) { font-size: 0.85rem; }
|
|
.anomaly {
|
|
margin: 1rem 0;
|
|
padding: 0.75rem 0.9rem;
|
|
border: 1px solid #7a3b3b;
|
|
border-radius: 8px;
|
|
background: rgba(122, 59, 59, 0.15);
|
|
color: #f0c4c4;
|
|
}
|
|
.badge-active-pr { color: #9ec8f0; border-color: #3d5f7a; }
|
|
.badge-active-issue { color: #8fd19e; border-color: #3d6b4a; }
|
|
.badge-dirty { color: #e0c27a; border-color: #6b5730; }
|
|
.badge-stale-clean { color: #c9b8e8; border-color: #5a4a78; }
|
|
.badge-detached-review { color: #9ec8f0; border-color: #3d5f7a; }
|
|
.badge-unsafe-unknown { color: #f0a8a8; border-color: #7a3b3b; }
|
|
.badge-orphan { color: #f0a8a8; border-color: #7a3b3b; }
|
|
</style>
|
|
"""
|
|
|
|
|
|
def render_worktrees_page(snapshot: HygieneSnapshot) -> str:
|
|
error_block = ""
|
|
if snapshot.scan_error:
|
|
error_block = (
|
|
'<div class="stub"><p><strong>Partial scan:</strong> '
|
|
f"{_escape(snapshot.scan_error)}</p></div>"
|
|
)
|
|
|
|
anomaly_block = ""
|
|
if snapshot.anomalies:
|
|
items = "".join(f"<li>{_escape(text)}</li>" for text in snapshot.anomalies)
|
|
anomaly_block = (
|
|
'<section class="anomaly"><h3>Missing preserved worktree anomalies</h3>'
|
|
f"<ul>{items}</ul></section>"
|
|
)
|
|
|
|
if snapshot.entries:
|
|
rows = "".join(_entry_row(entry) for entry in snapshot.entries)
|
|
table = (
|
|
"<table class='registry worktrees'>"
|
|
"<thead><tr>"
|
|
"<th>Path</th><th>Class</th><th>Branch</th><th>HEAD</th>"
|
|
"<th>Dirty</th><th>Detached</th><th>Notes</th>"
|
|
"</tr></thead>"
|
|
f"<tbody>{rows}</tbody></table>"
|
|
)
|
|
else:
|
|
table = "<p class='muted'>No directories under <code>branches/</code>.</p>"
|
|
|
|
body = (
|
|
"<h2>Worktree hygiene</h2>"
|
|
"<p>Read-only scan of local <code>branches/</code> worktrees. "
|
|
"No deletion actions — copy the canonical cleanup prompt when merge is confirmed.</p>"
|
|
f"{error_block}"
|
|
f"{anomaly_block}"
|
|
f"<p class='meta'>Repo root: <code>{_escape(snapshot.repo_root)}</code> · "
|
|
f"entries: {len(snapshot.entries)}</p>"
|
|
f"{table}"
|
|
"<h3>Canonical cleanup prompt</h3>"
|
|
f'<pre class="prompt-text" id="cleanup-prompt">{_escape(snapshot.cleanup_prompt)}</pre>'
|
|
'<button type="button" class="copy-btn copy-cleanup-btn">Copy cleanup prompt</button>'
|
|
"<p><a href=\"/api/worktrees\">JSON API</a></p>"
|
|
f"{WORKTREE_PAGE_STYLES}"
|
|
f"{WORKTREE_PAGE_SCRIPT}"
|
|
)
|
|
return render_page(title="Worktrees", body_html=body) |