Skip to content

Commit d78cb78

Browse files
authored
Merge pull request #43 from PyAutoLabs/feature/dashboard-per-paper-actions
feat: dashboard per-paper browsing + one-tap read/cite/intake actions
2 parents 7c5f0a6 + 62f8f26 commit d78cb78

5 files changed

Lines changed: 474 additions & 27 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Queue actions
2+
3+
# The dashboard's per-paper ✅ button opens a prefilled `queue-read` issue;
4+
# this workflow marks that paper's reading-queue.md line
5+
# `DONE <date> — <title>` (never deleted — the reading history), pushes the
6+
# commit, re-renders the dashboard, and closes the issue. 📥 `queue-intake`
7+
# and 📑 `queue-cite` issues are NOT handled here — they stay open as filing
8+
# work items (full wiki filing / bib-entry-plus-minimal-section).
9+
#
10+
# Only owner/member/collaborator-authored issues act: this is a public repo,
11+
# and a stranger's issue must never mutate the queue (they cannot apply the
12+
# label either, but the gate does not rely on that).
13+
14+
on:
15+
issues:
16+
types: [opened]
17+
18+
# contents: write → the queue commit; issues: write → comment + close;
19+
# actions: write → re-dispatch the Dashboard workflow (token pushes do not
20+
# retrigger `on: push` workflows).
21+
permissions:
22+
contents: write
23+
issues: write
24+
actions: write
25+
26+
concurrency:
27+
group: queue-actions
28+
cancel-in-progress: false
29+
30+
jobs:
31+
mark-read:
32+
if: >-
33+
contains(github.event.issue.labels.*.name, 'queue-read') &&
34+
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'),
35+
github.event.issue.author_association)
36+
runs-on: ubuntu-latest
37+
env:
38+
GH_TOKEN: ${{ github.token }}
39+
ISSUE: ${{ github.event.issue.number }}
40+
steps:
41+
- uses: actions/checkout@v4
42+
43+
- name: Mark the queue line DONE
44+
id: mark
45+
env:
46+
# env, never inline interpolation — the body is untrusted text.
47+
ISSUE_BODY: ${{ github.event.issue.body }}
48+
run: |
49+
printf '%s' "$ISSUE_BODY" > /tmp/issue_body.txt
50+
status=$(python scripts/queue_mark_done.py \
51+
--queue reading-queue.md --body-file /tmp/issue_body.txt) || true
52+
echo "status=${status}" >> "$GITHUB_OUTPUT"
53+
echo "queue_mark_done: ${status}"
54+
55+
- name: Commit + push (explicit path only)
56+
if: steps.mark.outputs.status == 'marked'
57+
run: |
58+
git config user.name "github-actions[bot]"
59+
git config user.email "github-actions[bot]@users.noreply.github.com"
60+
git add reading-queue.md
61+
git commit -m "queue: mark paper read (#${ISSUE}) [skip ci]"
62+
for attempt in 1 2 3; do
63+
if git push; then exit 0; fi
64+
git pull --rebase origin main || exit 1
65+
done
66+
echo "::error::could not push the queue commit after 3 attempts"
67+
exit 1
68+
69+
- name: Refresh the dashboard
70+
if: steps.mark.outputs.status == 'marked'
71+
run: gh workflow run knowledge_board.yml --ref main
72+
73+
- name: Close or report on the issue
74+
env:
75+
STATUS: ${{ steps.mark.outputs.status }}
76+
run: |
77+
case "$STATUS" in
78+
marked)
79+
gh issue close "$ISSUE" --comment \
80+
"Marked read in reading-queue.md — the line stays, DONE-prefixed (the reading history). Dashboard refresh dispatched." ;;
81+
already-done)
82+
gh issue close "$ISSUE" --comment \
83+
"Already marked read in reading-queue.md — nothing to change." ;;
84+
*)
85+
gh issue comment "$ISSUE" --body \
86+
"Could not process this queue action (status: ${STATUS:-error}) — the paper's line was not found in its section of reading-queue.md. Handle manually; leaving the issue open." ;;
87+
esac

scripts/board.py

Lines changed: 181 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
block holding a paste-ready Claude Code prompt that executes this repo's own
77
documented workflow (``bibliography/README.md`` "Adding a paper",
88
``wiki/CLAUDE.md``'s schema) — plus contents cards with ``/memory <domain>``
9-
recall chips.
9+
recall chips. Reading-queue sections expand to the individual papers: each
10+
title links out (arXiv abstract page, or a title search when the line has no
11+
ref) and carries three prefilled-GitHub-issue actions — 📥 intake-into-memory
12+
(really interesting: full filing), 📑 make-citeable (worth citing, not
13+
pivotal: bib entry + minimal sources section), both open work items carrying
14+
the human's free-text notes, and ✅ read-don't-file (processed automatically
15+
by ``queue_actions.yml`` via ``queue_mark_done.py``). Nothing changes state
16+
until the human submits the issue.
1017
1118
**Contents-level only.** The board shows titles and counts, never claim text
1219
or summaries — the knowledge itself stays in the wiki pages.
@@ -35,6 +42,7 @@
3542
import subprocess
3643
import sys
3744
from pathlib import Path
45+
from urllib.parse import quote as _quote
3846

3947
MEMORY_HOME = Path(__file__).resolve().parents[1]
4048

@@ -48,6 +56,11 @@
4856
# queue is also the reading history.
4957
QUEUE_SECTION_RE = re.compile(r"^##\s+(.+?)\s*$")
5058
QUEUE_DONE_RE = re.compile(r"^DONE\b")
59+
QUEUE_DONE_LINE_RE = re.compile(r"^DONE\s+(\d{4}-\d{2}-\d{2})\s*—\s*(.*)$")
60+
# A paper line may end ` — <arXiv id or URL>`; anything else after an em dash
61+
# is part of the title.
62+
QUEUE_REF_RE = re.compile(
63+
r"^(.*\S)\s+—\s+((?:arXiv:)?\d{4}\.\d{4,5}(?:v\d+)?|https?://\S+)$")
5164

5265

5366
def _owner_repo() -> tuple[str, str]:
@@ -113,21 +126,22 @@ def collect(root: Path | None = None) -> dict:
113126

114127
queue = root / "reading-queue.md"
115128
if queue.exists():
116-
section, count, done = None, 0, 0
129+
section, papers = None, []
117130
def _flush():
118131
if section is not None:
119-
snapshot["queue"].append({"section": section, "count": count,
120-
"done": done})
132+
snapshot["queue"].append({
133+
"section": section,
134+
"count": sum(1 for p in papers if not p["done"]),
135+
"done": sum(1 for p in papers if p["done"]),
136+
"papers": papers,
137+
})
121138
for line in queue.read_text(errors="replace").splitlines():
122139
m = QUEUE_SECTION_RE.match(line)
123140
if m and not m.group(1).startswith("#"):
124141
_flush()
125-
section, count, done = m.group(1), 0, 0
142+
section, papers = m.group(1), []
126143
elif section is not None and line.strip():
127-
if QUEUE_DONE_RE.match(line.strip()):
128-
done += 1
129-
else:
130-
count += 1
144+
papers.append(_parse_paper(line.strip()))
131145
_flush()
132146

133147
slugs = [s.split("|")[0].strip() for s in all_slugs]
@@ -141,6 +155,91 @@ def _flush():
141155

142156

143157
# --- pure helpers --------------------------------------------------------------
158+
def _parse_paper(text: str) -> dict:
159+
"""One queue line → {title, ref, done, done_date, line} (line = as written)."""
160+
raw, done_date = text, None
161+
m = QUEUE_DONE_LINE_RE.match(text)
162+
if m:
163+
done_date, text = m.group(1), m.group(2)
164+
elif QUEUE_DONE_RE.match(text):
165+
done_date, text = "", text[4:].lstrip(" —-")
166+
ref = None
167+
m = QUEUE_REF_RE.match(text)
168+
if m:
169+
text, ref = m.group(1), m.group(2)
170+
return {"title": text, "ref": ref, "done": done_date is not None,
171+
"done_date": done_date or None, "line": raw}
172+
173+
174+
def paper_url(paper: dict) -> str:
175+
"""Where to read the paper: its abstract page, or an arXiv title search."""
176+
ref = paper.get("ref")
177+
if ref:
178+
if ref.startswith("http"):
179+
return ref
180+
return "https://arxiv.org/abs/" + ref.removeprefix("arXiv:")
181+
return ("https://arxiv.org/search/?searchtype=title&query="
182+
+ _quote(paper.get("title", ""), safe=""))
183+
184+
185+
def _queue_issue_url(snapshot: dict, section: str, paper: dict,
186+
action: str) -> str:
187+
"""A prefilled new-issue URL for a per-paper action.
188+
189+
Three tiers — 'intake' (really interesting: full wiki filing), 'cite'
190+
(worth citing, not pivotal: bib entry + a minimal sources section), and
191+
'read' (done with it: DONE-mark only, processed automatically by
192+
queue_actions.yml). Nothing changes state until the human submits the
193+
issue on GitHub; intake/cite issues stay open as the filing work item,
194+
and their `notes:` field is free text the human edits before submitting.
195+
Empty when the checkout has no remote (spawned templates).
196+
"""
197+
repo_url = _repo_url(snapshot)
198+
if not repo_url:
199+
return ""
200+
where = f"section: {section}\nline: {paper['line']}"
201+
notes = ("notes: (optional — replace this with why you added it or "
202+
"what was noteworthy, in your own words; whoever files the "
203+
"paper folds it in)")
204+
if action == "read":
205+
issue_title = f"queue read: {paper['title']}"[:200]
206+
label = "queue-read"
207+
body = ("Mark this reading-queue paper as read without filing it "
208+
"(the line stays, DONE-prefixed — the reading history).\n\n"
209+
f"{where}\n\n"
210+
"Processed automatically by queue_actions.yml.")
211+
elif action == "cite":
212+
issue_title = f"queue cite: {paper['title']}"[:200]
213+
label = "queue-cite"
214+
body = ("Make this reading-queue paper citeable — read, worth "
215+
"citing, but not pivotal enough for full wiki treatment.\n\n"
216+
f"{where}\n\n"
217+
f"{notes}\n\n"
218+
"Workflow (bibliography/README.md \"Adding a paper\", "
219+
"wiki/CLAUDE.md): verify the paper against an authoritative "
220+
"record, add its canonical entry to the bibliography/ BibTeX "
221+
"file, add a minimal section to the matching "
222+
"wiki/<domain>/sources/ page — canonical key plus the notes "
223+
"above, nothing deeper — mark its queue line "
224+
"'DONE <date> — <title>' in reading-queue.md, and run "
225+
"make validate.")
226+
else:
227+
issue_title = f"queue intake: {paper['title']}"[:200]
228+
label = "queue-intake"
229+
body = ("File this reading-queue paper into memory.\n\n"
230+
f"{where}\n\n"
231+
f"{notes}\n\n"
232+
"Workflow (bibliography/README.md \"Adding a paper\", "
233+
"wiki/CLAUDE.md): verify the paper against an authoritative "
234+
"record, add its canonical entry to the bibliography/ BibTeX "
235+
"file, stub it in the matching wiki/<domain>/sources/ page — "
236+
"incorporating the notes above — mark its queue line "
237+
"'DONE <date> — <title>' in reading-queue.md, and run "
238+
"make validate.")
239+
return (f"{repo_url}/issues/new?title={_quote(issue_title, safe='')}"
240+
f"&body={_quote(body, safe='')}&labels={_quote(label, safe='')}")
241+
242+
144243
def _totals(snapshot: dict) -> dict:
145244
wikis = snapshot.get("wikis") or []
146245
statuses: dict[str, int] = {}
@@ -216,7 +315,20 @@ def _render_md(snapshot: dict) -> str:
216315
lines += ["", "## Reading queue", ""]
217316
for q in snapshot.get("queue") or []:
218317
done = f", {q['done']} read" if q.get("done") else ""
219-
lines.append(f"- {q['section']}: {q['count']} waiting{done}")
318+
lines += [f"<details><summary>{q['section']}: {q['count']} "
319+
f"waiting{done}</summary>", ""]
320+
for p in q.get("papers") or []:
321+
if p["done"]:
322+
continue
323+
label = p["title"].replace("[", "\\[").replace("]", "\\]")
324+
bits = [f"- [{label}]({paper_url(p)})"]
325+
for action, chip in (("intake", "intake 📥"), ("cite", "cite 📑"),
326+
("read", "read ✅")):
327+
u = _queue_issue_url(snapshot, q["section"], p, action)
328+
if u:
329+
bits.append(f"[{chip}]({u})")
330+
lines.append(" · ".join(bits))
331+
lines += ["", "</details>"]
220332
if not snapshot.get("queue"):
221333
lines.append("- _(queue empty or unavailable)_")
222334
url = pages_url(snapshot)
@@ -241,7 +353,7 @@ def _copy_btn(payload: str, label: str = "copy") -> str:
241353
return (f"<button class='copy' type='button' "
242354
f"title='{_html.escape(label, quote=True)}' "
243355
f"data-copy=\"{_html.escape(payload, quote=True)}\" "
244-
f"onclick='cp(this)'>📋</button>")
356+
f"onclick='cp(this,event)'>📋</button>")
245357

246358

247359
def _bar(statuses: dict) -> str:
@@ -259,17 +371,45 @@ def _render_html(snapshot: dict) -> str:
259371
pct = round(100 * t["resolved"] / t["sections"]) if t["sections"] else 0
260372
repo_url = _repo_url(snapshot)
261373

262-
queue_rows = []
374+
queue_blocks = []
263375
for q in snapshot.get("queue") or []:
264-
done = (f" <span class='meta'>· {q['done']} read</span>"
265-
if q.get("done") else "")
266-
queue_rows.append(
267-
f"<tr><td class='name'>{_html.escape(q['section'])}</td>"
268-
f"<td>{q['count']} waiting{done} "
376+
done = (f" · {q['done']} read" if q.get("done") else "")
377+
items = []
378+
hist_items = []
379+
for p in q.get("papers") or []:
380+
if p["done"]:
381+
hist_items.append(
382+
f"<li class='done'>DONE {_html.escape(p.get('done_date') or '?')}"
383+
f" — {_html.escape(p['title'])}</li>")
384+
continue
385+
acts = []
386+
for action, icon, hint in (
387+
("intake", "📥", "intake into memory: really interesting — "
388+
"opens a prefilled issue (add your notes) "
389+
"that becomes the full filing work item"),
390+
("cite", "📑", "make citeable: worth citing, not pivotal — "
391+
"opens a prefilled issue (add your notes) "
392+
"for a bib entry + minimal sources section"),
393+
("read", "✅", "read — don't file: opens a prefilled issue; "
394+
"submitting marks this line DONE")):
395+
u = _queue_issue_url(snapshot, q["section"], p, action)
396+
if u:
397+
acts.append(f"<a class='act' href=\"{_html.escape(u, quote=True)}\" "
398+
f"title='{_html.escape(hint, quote=True)}'>{icon}</a>")
399+
items.append(
400+
f"<li><a href=\"{_html.escape(paper_url(p), quote=True)}\">"
401+
f"{_html.escape(p['title'])}</a>{''.join(acts)}</li>")
402+
hist = (f"<details class='hist'><summary class='meta'>reading history "
403+
f"({len(hist_items)})</summary><ul class='papers'>"
404+
f"{''.join(hist_items)}</ul></details>" if hist_items else "")
405+
queue_blocks.append(
406+
f"<details class='qsec'><summary><span class='name'>"
407+
f"{_html.escape(q['section'])}</span> <span class='meta'>"
408+
f"{q['count']} waiting{done}</span> "
269409
f"{_copy_btn(_read_prompt(snapshot, q['section']), 'copy: file the next paper')}"
270-
f"</td></tr>")
271-
if not queue_rows:
272-
queue_rows.append("<tr><td colspan='2'>queue empty or unavailable</td></tr>")
410+
f"</summary><ul class='papers'>{''.join(items)}</ul>{hist}</details>")
411+
if not queue_blocks:
412+
queue_blocks.append("<p class='meta'>queue empty or unavailable</p>")
273413

274414
todo_rows = []
275415
for w in snapshot.get("wikis") or []:
@@ -328,10 +468,25 @@ def _render_html(snapshot: dict) -> str:
328468
color: #c9d1d9; cursor: pointer; padding: .05rem .45rem;
329469
margin-left: .35rem; font-size: .85rem; line-height: 1.4; }}
330470
button.copy:hover {{ background: #30363d; }}
471+
details.qsec {{ border-top: 1px solid #21262d; padding: .5rem .25rem; }}
472+
details.qsec > summary {{ cursor: pointer; list-style: none; }}
473+
details.qsec > summary::-webkit-details-marker {{ display: none; }}
474+
details.qsec > summary .name::before {{ content: "▸ "; color: #8b949e; }}
475+
details.qsec[open] > summary .name::before {{ content: "▾ "; }}
476+
.name {{ font-weight: 600; }}
477+
ul.papers {{ margin: .5rem 0 .25rem; padding-left: 1.3rem; }}
478+
ul.papers li {{ margin: .4rem 0; }}
479+
ul.papers li.done {{ color: #8b949e; }}
480+
a.act {{ margin-left: .35rem; padding: .05rem .35rem; font-size: .85rem;
481+
border: 1px solid #30363d; border-radius: 6px; background: #21262d; }}
482+
a.act:hover {{ background: #30363d; text-decoration: none; }}
483+
details.hist {{ margin: .25rem 0 .25rem 1.3rem; }}
484+
details.hist > summary {{ cursor: pointer; }}
331485
footer {{ margin-top: 2rem; color: #8b949e; font-size: .8rem; }}
332486
</style>
333487
<script>
334-
function cp(b){{var t=b.getAttribute('data-copy');
488+
function cp(b,e){{if(e){{e.preventDefault();e.stopPropagation();}}
489+
var t=b.getAttribute('data-copy');
335490
if(navigator.clipboard&&navigator.clipboard.writeText){{
336491
navigator.clipboard.writeText(t).then(function(){{ok(b)}},function(){{fb(t)}});
337492
}}else{{fb(t)}}}}
@@ -343,9 +498,12 @@ def _render_html(snapshot: dict) -> str:
343498
<p><span class="pill">{t['pages']} pages · {pct}% cited</span> <span class="meta"><a href="dashboard.md">markdown version</a></span></p>
344499
<p class="meta">Contents and work queues for the organism's long-term memory
345500
— titles and counts only; the knowledge itself lives in the wiki pages.
346-
📋 copies a paste-ready prompt for a Claude Code chat.</p>
501+
📋 copies a paste-ready prompt for a Claude Code chat; on a paper, the
502+
buttons open a prefilled GitHub issue — edit in your notes, submit to act:
503+
📥 intake into memory (really interesting), 📑 make citeable (worth citing,
504+
not pivotal), ✅ read — don't file (marked DONE automatically).</p>
347505
<h2>Reading queue <span class='meta'>({t['queued']} papers waiting)</span></h2>
348-
<table>{''.join(queue_rows)}</table>
506+
{''.join(queue_blocks)}
349507
<h2>Citation work queue <span class='meta'>({t['todo']} sections need a canonical key)</span></h2>
350508
<table>{''.join(todo_rows)}</table>
351509
<h2>Sub-wikis <span class='meta'>({snapshot.get('bib_entries', 0)} bibliography entries ·

0 commit comments

Comments
 (0)