feat(skills): pql-board panel from template — support/make-panel generator
The interactive panel is no longer hand-composed per render: support/ panel-template.html carries the widget markup (CDS tokens, pick-up/detail sendPrompt contract) and support/make-panel fills it with live pql data — actionable WIP + unblocked leaf work from the active phase AND the maintenance initiative T-1037 (the unblocked head T-1174 lives there, not under the phase epic — a phase-only query missed it), sorted status→priority→recency with overflow noted. SKILL.md §Interactive panel now instructs: run the script, paste stdout as widget_code verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,25 +63,23 @@ Real one-click interactivity lives in the in-chat widget — render it when the
|
||||
user asks for "the panel", "pick-up panel", or wants to act on tickets rather
|
||||
than read them.
|
||||
|
||||
1. Pull live data (small, native projections — never embed the whole store):
|
||||
1. Generate the widget code with the bundled script (template + live pql data
|
||||
— do NOT hand-compose the HTML):
|
||||
|
||||
```bash
|
||||
pql ticket list --status in_progress --fields id,type,priority,title
|
||||
pql ticket list --under T-750 --unblocked --leaf --fields id,type,priority,title
|
||||
.claude/skills/pql-board/support/make-panel # stdout = widget_code
|
||||
```
|
||||
|
||||
Panel contents: WIP + the unblocked ready work of the active phase (cap at
|
||||
~10 rows; if more, take highest priority first and say so under the panel).
|
||||
Flags: `--phase T-NNN` (default T-750, the active phase epic), `--cap N`
|
||||
(default 10). Contents: actionable WIP + unblocked leaf work from the
|
||||
active phase AND the maintenance initiative (T-1037), sorted
|
||||
status→priority→recency, overflow noted in the panel. Template:
|
||||
`support/panel-template.html` (edit that file to change the panel's look;
|
||||
the pick-up/detail sendPrompt contract lives there).
|
||||
|
||||
2. Render with `mcp__visualize__show_widget` (load its `read_me` first,
|
||||
`interactive` module). Panel shape — compact bordered rows, CDS tokens:
|
||||
- Each row: mono ticket id (link to the artifact deep-link `#T-NNN`),
|
||||
title, status/priority pill, and two buttons:
|
||||
- **Pick up ↗** → `sendPrompt('pick up the following ticket: T-NNN — <title>')`
|
||||
- **Detail ↗** → `sendPrompt('show me T-NNN in detail')`
|
||||
- Footer buttons: **Refresh board ↗** → `sendPrompt('/pql-board')`,
|
||||
**Next batch ↗** → `sendPrompt('/whats-next')`.
|
||||
- Widget rules: no emoji, sentence case, `↗` suffix on sendPrompt buttons.
|
||||
2. Render with `mcp__visualize__show_widget`: load its `read_me` first
|
||||
(`interactive` module) if not yet loaded this conversation, then pass the
|
||||
script's stdout verbatim as `widget_code`.
|
||||
|
||||
3. **On receiving a "pick up the following ticket:" prompt** (from the panel
|
||||
or typed): treat it as single-ticket batch activation — the /whats-next
|
||||
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fill panel-template.html with live pql data → widget_code for show_widget.
|
||||
|
||||
Emits the interactive in-chat pql panel (see ../SKILL.md §Interactive panel):
|
||||
WIP + unblocked leaf work of the active phase, capped, with one-click
|
||||
Pick up ↗ / Detail ↗ sendPrompt actions. Claude runs this and pastes stdout
|
||||
verbatim as the show_widget widget_code.
|
||||
|
||||
Usage: .claude/skills/pql-board/support/make-panel [--phase T-NNN] [--cap N]
|
||||
(defaults: active phase T-750, cap 10; output on stdout)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ARTIFACT_URL = "https://claude.ai/code/artifact/0064e59d-84a9-4a31-a830-f7d677df9d92"
|
||||
FIELDS = "id,type,priority,status,title,updated_at"
|
||||
PRIORITY_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
||||
STATUS_RANK = {"in_progress": 0, "review": 1, "ready": 2, "backlog": 3}
|
||||
ACTIONABLE_TYPES = ("story", "task", "bug")
|
||||
MAINTENANCE = "T-1037"
|
||||
|
||||
|
||||
def pql(*args):
|
||||
out = subprocess.run(
|
||||
["pql", *args], capture_output=True, text=True, check=True
|
||||
).stdout
|
||||
return json.loads(out)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--phase", default="T-750")
|
||||
ap.add_argument("--cap", type=int, default=10)
|
||||
args = ap.parse_args()
|
||||
|
||||
wip = pql("ticket", "list", "--status", "in_progress", "--fields", FIELDS)
|
||||
phase = pql(
|
||||
"ticket", "list", "--under", args.phase, "--unblocked", "--leaf",
|
||||
"--fields", FIELDS,
|
||||
)
|
||||
maint = pql(
|
||||
"ticket", "list", "--under", MAINTENANCE, "--unblocked", "--leaf",
|
||||
"--fields", FIELDS,
|
||||
)
|
||||
|
||||
seen, rows = set(), []
|
||||
for t in wip + phase + maint:
|
||||
if t["id"] in seen or t["type"] not in ACTIONABLE_TYPES:
|
||||
continue
|
||||
if t["status"] in ("done", "cancelled"):
|
||||
continue
|
||||
seen.add(t["id"])
|
||||
rows.append(t)
|
||||
|
||||
rows.sort(key=lambda t: t.get("updated_at") or "", reverse=True)
|
||||
rows.sort(
|
||||
key=lambda t: (
|
||||
STATUS_RANK.get(t["status"], 3),
|
||||
PRIORITY_RANK.get(t.get("priority"), 2),
|
||||
)
|
||||
)
|
||||
overflow = max(0, len(rows) - args.cap)
|
||||
rows = rows[: args.cap]
|
||||
|
||||
tpl = (Path(__file__).parent / "panel-template.html").read_text()
|
||||
data = json.dumps(
|
||||
{"rows": rows, "url": ARTIFACT_URL, "overflow": overflow},
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
).replace("</", "<\\/")
|
||||
print(tpl.replace("__URL__", ARTIFACT_URL).replace("__DATA__", data))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
<h2 class="sr-only" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">Interactive pql panel: work-in-progress and unblocked ready tickets with one-click pick-up and detail actions.</h2>
|
||||
<div id="pp-rows" style="border:0.5px solid var(--border);border-radius:var(--radius);overflow:hidden;"></div>
|
||||
<div id="pp-overflow" style="font-size:12px;color:var(--text-muted);margin-top:6px;"></div>
|
||||
<div style="display:flex;gap:8px;margin-top:12px;">
|
||||
<button onclick="sendPrompt('/pql-board')" style="font-size:13px;"><i class="ti ti-refresh" style="font-size:15px;vertical-align:-2px" aria-hidden="true"></i> Refresh board ↗</button>
|
||||
<button onclick="sendPrompt('/whats-next')" style="font-size:13px;">Next batch ↗</button>
|
||||
<span style="flex:1"></span>
|
||||
<a href="__URL__" style="font-size:13px;align-self:center;"><i class="ti ti-external-link" style="font-size:15px;vertical-align:-2px" aria-hidden="true"></i> Open board</a>
|
||||
</div>
|
||||
<script type="application/json" id="pp-data">__DATA__</script>
|
||||
<script>
|
||||
const D=JSON.parse(document.getElementById("pp-data").textContent);
|
||||
const PR={critical:["var(--bg-danger)","var(--text-danger)"],high:["var(--bg-warning)","var(--text-warning)"],
|
||||
medium:["var(--surface-1)","var(--text-secondary)"],low:["var(--surface-1)","var(--text-muted)"]};
|
||||
const ST={in_progress:["var(--bg-accent)","var(--text-accent)"],ready:["var(--bg-success)","var(--text-success)"],
|
||||
backlog:["var(--surface-1)","var(--text-muted)"],review:["var(--bg-warning)","var(--text-warning)"]};
|
||||
document.getElementById("pp-rows").innerHTML=D.rows.map((t,i)=>{
|
||||
const p=PR[t.priority]||PR.medium, s=ST[t.status]||ST.backlog;
|
||||
return `<div style="display:flex;align-items:center;gap:10px;padding:9px 12px;${i?"border-top:0.5px solid var(--border);":""}">
|
||||
<a href="${D.url}#${t.id}" style="font:500 13px var(--font-mono);">${t.id}</a>
|
||||
<span style="font-size:14px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${t.title.replace(/"/g,""")}">${t.title}</span>
|
||||
<span style="font-size:11px;padding:2px 7px;border-radius:999px;background:${s[0]};color:${s[1]};">${t.status.replace("_"," ")}</span>
|
||||
<span style="font-size:11px;padding:2px 7px;border-radius:999px;background:${p[0]};color:${p[1]};">${t.priority}</span>
|
||||
<button data-act="pick" data-i="${i}" style="font-size:12px;padding:4px 8px;">Pick up ↗</button>
|
||||
<button data-act="detail" data-i="${i}" style="font-size:12px;padding:4px 8px;">Detail ↗</button>
|
||||
</div>`;}).join("");
|
||||
if(D.overflow>0) document.getElementById("pp-overflow").textContent=
|
||||
`…and ${D.overflow} more unblocked — open the board for the full list.`;
|
||||
document.querySelectorAll("#pp-rows button").forEach(b=>b.onclick=()=>{
|
||||
const t=D.rows[+b.dataset.i];
|
||||
sendPrompt(b.dataset.act==="pick"
|
||||
? `pick up the following ticket: ${t.id} — ${t.title}`
|
||||
: `show me ${t.id} in detail`);
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user