From 4915ca0bb5b3e3393d3a6bd9f090668b0ebffac2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 15:40:29 +0000 Subject: [PATCH] intake: hold 50 in the Recent feed, show 10, reveal the rest on tap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The table is a glance, not a log. Twenty rows already pushed the Epics below a scroll on a phone, and capping the feed at what fits on screen is the wrong trade: a quiet fortnight and a busy week want different depths. So the feed now holds 50 (RECENT_MAX) and shows 10 (RECENT_PAGE), with the rest one tap away — and each surface reveals it with what it actually renders: - Pages twin: every row ships in the DOM, the overflow marked `hidden`, and a `…` button flips ten at a time before retiring itself. Hidden rather than absent, so a reader with JS off gets the whole feed instead of ten rows and a dead button. - Markdown page: GitHub strips that script, so the reveal is `
` — NESTED, so each tap shows the next ten and leaves another `…` behind it. Sibling blocks would let a reader open page 4 without page 3, which is not what "show me more" means on a list ordered by date. Each page carries its own header row, since a markdown table cannot span an HTML block boundary. Also fixes the blurb printing literal backticks on the Pages page — it is shared with the markdown renderer and was never run through `_summary_label`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L4HPWjv5rdzBAkKpfW1SbR --- agents/conductors/intake/AGENTS.md | 8 ++- agents/conductors/intake/_intake.py | 91 +++++++++++++++++++++++++---- tests/test_intake_dashboard.py | 68 +++++++++++++++++++++ 3 files changed, 155 insertions(+), 12 deletions(-) diff --git a/agents/conductors/intake/AGENTS.md b/agents/conductors/intake/AGENTS.md index 9d9205a..b9b7ceb 100644 --- a/agents/conductors/intake/AGENTS.md +++ b/agents/conductors/intake/AGENTS.md @@ -88,9 +88,13 @@ schema — light structure over free-form prose. | **reconcile** | `intake reconcile [prefix]` | rank backlog prompts that look already-shipped (vs the `complete/` records / `active/`); always read-only — retiring stays human | | **reconcile --repo** | `intake reconcile --repo [prefix]` | **also** read the target repo's source for identifiers the prompts name — the one signal that sees a prompt with no Mind-side trace. Opt-in; the default path is offline | -**Recent** is the one section laid out by *date* rather than by state: the 20 +**Recent** is the one section laid out by *date* rather than by state: the 50 newest events on the **work in hand** — issued, parked, filed — merged across -the live buckets and sitting between the Backlog and the Epics. Every other +the live buckets and sitting between the Backlog and the Epics. It *holds* 50 +and *shows* 10 (`RECENT_MAX` / `RECENT_PAGE`): the table is a glance, not a +log, so the rest is one tap away — a `…` button on the Pages twin, and nested +`
` on the markdown page, which GitHub renders where it strips the +script. Every other section answers "what should I do now?"; recency is orthogonal to state, so none of them can answer "what has been happening?". Dates come from the registry key that names the event (`issued:` / `parked:` / `filed:`, PyAutoMind diff --git a/agents/conductors/intake/_intake.py b/agents/conductors/intake/_intake.py index c7bb1b7..9be5fb0 100755 --- a/agents/conductors/intake/_intake.py +++ b/agents/conductors/intake/_intake.py @@ -501,7 +501,12 @@ def _clip(text: str, limit: int = 130) -> str: # records deep and ships ~200 a month, so including it made the table a list of # receipts — twenty things nobody can act on, on the page whose whole job is # work in hand. `complete/index.md` is where shipped work is read. -RECENT_MAX = 20 +# How deep the feed goes, and how much of it is on screen at once. The table +# is a glance, not a log: ten rows answer "what has been happening?" without +# pushing the Epics below a scroll, and the rest is one tap away — so a quiet +# week still shows a fortnight of context and a busy one does not bury it. +RECENT_MAX = 50 +RECENT_PAGE = 10 # The verb each event reads as in the feed. Past tense throughout — every row # is something that already happened. @@ -820,6 +825,9 @@ def _epic_members(c: dict) -> dict: "`complete/index.md`, and a thousand records deep it would crowd out " "everything anyone can still act on.") +RECENT_PAGING_NOTE = ( + " Showing the newest {page}; \u2026 opens the next {page}.") + def _dated(row: dict) -> str: """`— issued 2026-08-19`, the facet every live task row now carries. @@ -834,6 +842,47 @@ def _dated(row: dict) -> str: return f" — {event} {row['date']}" +def _recent_blurb(rows: list) -> str: + """The section's prose — the paging sentence only when there IS paging.""" + text = RECENT_BLURB.format(n=len(rows)) + if len(rows) > RECENT_PAGE: + text += RECENT_PAGING_NOTE.format(page=RECENT_PAGE) + return text + + +RECENT_TABLE_HEAD = ["| Date | Event | Task |", "|------|-------|------|"] + + +def _recent_rows(rows: list) -> list: + return [f"| {r['date']} | {r['event']} | {_cell(_recent_link(r))} |" + for r in rows] + + +def _recent_pages(rows: list, page: int = RECENT_PAGE) -> list: + """The feed as nested `
`: a page on screen, the rest one tap in. + + GitHub strips the JavaScript the Pages twin uses for this, so the markdown + page reveals with the one interactive element it does render — `
`, + NESTED, so each tap shows the next page and leaves another `…` behind it. + Sibling blocks would let a reader open page 4 without page 3, which is not + what "show me more" means when the list is ordered by date. + + Each page carries its own header row: a markdown table cannot span an HTML + block boundary, so the alternative is a headerless slab of pipes. The blank + lines are load-bearing — without them GitHub treats the table as raw text + inside the `
` (same rule as `_task_row`). + """ + head, rest = rows[:page], rows[page:] + block = RECENT_TABLE_HEAD + _recent_rows(head) + if not rest: + return block + shown = min(page, len(rest)) + return block + ["", + f"
… {shown} more " + f"({len(rest)} left)", + ""] + _recent_pages(rest, page) + ["", "
"] + + def _recent_link(e: dict) -> str: """The task cell of a Recent row — its title, linked to where it lives.""" return f"{_summary_label(_clip(e['title'], 70))}" @@ -969,12 +1018,8 @@ def render_dashboard(c: dict) -> str: # it is picked up. recent = c.get("recent") or [] if recent: - L += ["## Recent", "", - RECENT_BLURB.format(n=len(recent)), "", - "| Date | Event | Task |", - "|------|-------|------|"] - L += [f"| {r['date']} | {r['event']} | {_cell(_recent_link(r))} |" - for r in recent] + L += ["## Recent", "", _recent_blurb(recent), ""] + L += _recent_pages(recent) L += ["", "_Dates come from each task's registry entry — " "`lifecycle.py dates` reports anything undated._", ""] @@ -1062,6 +1107,10 @@ def render_dashboard(c: dict) -> str: padding-top:.58rem} table.recent td.pick{width:2.6rem;padding-right:0} table.recent button.copy{width:2.2rem;height:2.2rem;font-size:.95rem} +button.more{display:block;width:100%;margin:.6rem 0;padding:.5rem; + border:1px solid var(--line);border-radius:8px;background:var(--btn); + color:var(--muted);cursor:pointer;font:inherit;font-size:.9em} +button.more:hover{color:var(--fg)} """ # One tap on 📋 → the command is on the clipboard; the button flashes ✓. The @@ -1077,6 +1126,18 @@ def render_dashboard(c: dict) -> str: setTimeout(()=>{b.textContent="\\ud83d\\udccb";b.classList.remove("ok");},1200);} document.addEventListener("click",e=>{ const b=e.target.closest("button.copy");if(b)copyCmd(b);}); +// Recent shows one page and reveals the next on each tap of the \u2026 button, +// which retires itself once the feed is exhausted. Every row is already in the +// DOM, so this never re-renders or re-sorts anything. +document.addEventListener("click",e=>{ + const b=e.target.closest("button.more");if(!b)return; + const t=document.querySelector("table.recent");if(!t)return; + const hidden=[...t.querySelectorAll("tr[hidden]")]; + const page=Number(b.dataset.page)||10; + hidden.slice(0,page).forEach(r=>r.removeAttribute("hidden")); + const left=hidden.length-Math.min(page,hidden.length); + if(left<=0){b.remove();return;} + b.textContent="\u2026 "+Math.min(page,left)+" more ("+left+" left)";}); """ @@ -1228,10 +1289,16 @@ def h2(title, src): recent = c.get("recent") or [] if recent: H += ['' + h2("Recent", "dashboard.md#recent"), - f'

{RECENT_BLURB.format(n=len(recent))}

', + # `_summary_label` turns the blurb's `code` spans into ; + # markdown backticks render literally on this page. + f'

{_summary_label(_recent_blurb(recent))}

', ''] - for r in recent: - H += ["", + for i, r in enumerate(recent): + # Every row ships in the DOM; the ones past the first page start + # hidden, so revealing them is a flag flip rather than a re-render + # — and a reader with JS off sees the whole feed rather than ten + # rows and a dead button. + H += ["" if i >= RECENT_PAGE else "", f'', f'', f'', @@ -1240,6 +1307,10 @@ def h2(title, src): f'Claude command">📋', ""] H += ["
{r["date"]}{_summary_label(r["event"])}{link(r["path"], _summary_label(_clip(r["title"], 70)))}
"] + rest = len(recent) - RECENT_PAGE + if rest > 0: + H += [f''] known = {e["slug"] for e in c.get("epics") or []} stray = [s for s in members if s not in known] diff --git a/tests/test_intake_dashboard.py b/tests/test_intake_dashboard.py index fcd32ea..de7999d 100644 --- a/tests/test_intake_dashboard.py +++ b/tests/test_intake_dashboard.py @@ -607,3 +607,71 @@ def test_an_undated_row_gets_no_placeholder(tmp_path): "- prompt: active/sprocket_calibration.md\n"}) row = _page(mind).split("## In flight")[1].split("
")[1] assert "—" not in row.split("")[0] + + +# --------------------------------------------------------------------------- # +# recent: fifty deep, ten on screen +# --------------------------------------------------------------------------- # +def _many(root, n): + """A Mind whose planned.md holds `n` dated tasks, newest first by slug.""" + return _mind(root, registries={"planned.md": "".join( + f"## task-{i:03d}\n- filed: 2026-01-01\n\n" for i in range(n))}) + + +def test_the_feed_runs_deeper_than_the_page(tmp_path): + """Fifty is what the feed HOLDS; ten is what it SHOWS.""" + rows = _intake.census(_many(tmp_path, 80))["recent"] + assert len(rows) == _intake.RECENT_MAX == 50 + assert _intake.RECENT_PAGE == 10 + + +def test_markdown_shows_one_page_then_nests_the_rest(tmp_path): + """GitHub strips the JS the Pages twin uses, so the markdown page reveals + with `
` — nested, so each tap shows the next page and leaves + another one behind it.""" + page = _page(_many(tmp_path, 80)) + section = page.split("## Recent")[1] + before = section.split("
")[0] + assert before.count("| 2026-01-01 |") == 10 + assert section.count("
") == 4 + assert "… 10 more (40 left)" in section + assert "… 10 more (10 left)" in section + + +def test_each_revealed_page_carries_its_own_table_header(tmp_path): + """A markdown table cannot span an HTML block boundary — without a header + per page the reveal is a headerless slab of pipes.""" + section = _page(_many(tmp_path, 80)).split("## Recent")[1] + assert section.count("| Date | Event | Task |") == 5 + + +def test_a_feed_that_fits_on_one_page_has_no_reveal(tmp_path): + page = _page(_many(tmp_path, 6)) + section = page.split("## Recent")[1] + assert "
" not in section + assert "…" not in section + assert "opens the next" not in section + + +def test_html_hides_the_overflow_rows_and_offers_a_button(tmp_path): + html = _intake.render_dashboard_html(_intake.census(_many(tmp_path, 80))) + section = html.split("

Recent")[1] + assert section.count("") == 10 + assert section.count("") == 40 + assert '' in section + + +def test_html_ships_every_row_so_a_reader_without_js_sees_the_feed(tmp_path): + """Hidden, not absent: with JS off the whole feed is there rather than ten + rows and a dead button.""" + html = _intake.render_dashboard_html(_intake.census(_many(tmp_path, 80))) + section = html.split("

Recent")[1] + assert section.count("complete/index.md" in html + assert "`complete/index.md`" not in html