Skip to content

Commit 4915ca0

Browse files
committed
intake: hold 50 in the Recent feed, show 10, reveal the rest on tap
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 `<details>` — 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4HPWjv5rdzBAkKpfW1SbR
1 parent 3cd1336 commit 4915ca0

3 files changed

Lines changed: 155 additions & 12 deletions

File tree

agents/conductors/intake/AGENTS.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,13 @@ schema — light structure over free-form prose.
8888
| **reconcile** | `intake reconcile [prefix]` | rank backlog prompts that look already-shipped (vs the `complete/` records / `active/`); always read-only — retiring stays human |
8989
| **reconcile --repo** | `intake reconcile --repo <target> [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 |
9090

91-
**Recent** is the one section laid out by *date* rather than by state: the 20
91+
**Recent** is the one section laid out by *date* rather than by state: the 50
9292
newest events on the **work in hand** — issued, parked, filed — merged across
93-
the live buckets and sitting between the Backlog and the Epics. Every other
93+
the live buckets and sitting between the Backlog and the Epics. It *holds* 50
94+
and *shows* 10 (`RECENT_MAX` / `RECENT_PAGE`): the table is a glance, not a
95+
log, so the rest is one tap away — a `` button on the Pages twin, and nested
96+
`<details>` on the markdown page, which GitHub renders where it strips the
97+
script. Every other
9498
section answers "what should I do now?"; recency is orthogonal to state, so
9599
none of them can answer "what has been happening?". Dates come from the
96100
registry key that names the event (`issued:` / `parked:` / `filed:`, PyAutoMind

agents/conductors/intake/_intake.py

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,12 @@ def _clip(text: str, limit: int = 130) -> str:
501501
# records deep and ships ~200 a month, so including it made the table a list of
502502
# receipts — twenty things nobody can act on, on the page whose whole job is
503503
# work in hand. `complete/index.md` is where shipped work is read.
504-
RECENT_MAX = 20
504+
# How deep the feed goes, and how much of it is on screen at once. The table
505+
# is a glance, not a log: ten rows answer "what has been happening?" without
506+
# pushing the Epics below a scroll, and the rest is one tap away — so a quiet
507+
# week still shows a fortnight of context and a busy one does not bury it.
508+
RECENT_MAX = 50
509+
RECENT_PAGE = 10
505510

506511
# The verb each event reads as in the feed. Past tense throughout — every row
507512
# is something that already happened.
@@ -820,6 +825,9 @@ def _epic_members(c: dict) -> dict:
820825
"`complete/index.md`, and a thousand records deep it would crowd out "
821826
"everything anyone can still act on.")
822827

828+
RECENT_PAGING_NOTE = (
829+
" Showing the newest {page}; \u2026 opens the next {page}.")
830+
823831

824832
def _dated(row: dict) -> str:
825833
"""`— issued 2026-08-19`, the facet every live task row now carries.
@@ -834,6 +842,47 @@ def _dated(row: dict) -> str:
834842
return f" — {event} {row['date']}"
835843

836844

845+
def _recent_blurb(rows: list) -> str:
846+
"""The section's prose — the paging sentence only when there IS paging."""
847+
text = RECENT_BLURB.format(n=len(rows))
848+
if len(rows) > RECENT_PAGE:
849+
text += RECENT_PAGING_NOTE.format(page=RECENT_PAGE)
850+
return text
851+
852+
853+
RECENT_TABLE_HEAD = ["| Date | Event | Task |", "|------|-------|------|"]
854+
855+
856+
def _recent_rows(rows: list) -> list:
857+
return [f"| {r['date']} | {r['event']} | {_cell(_recent_link(r))} |"
858+
for r in rows]
859+
860+
861+
def _recent_pages(rows: list, page: int = RECENT_PAGE) -> list:
862+
"""The feed as nested `<details>`: a page on screen, the rest one tap in.
863+
864+
GitHub strips the JavaScript the Pages twin uses for this, so the markdown
865+
page reveals with the one interactive element it does render — `<details>`,
866+
NESTED, so each tap shows the next page and leaves another `…` behind it.
867+
Sibling blocks would let a reader open page 4 without page 3, which is not
868+
what "show me more" means when the list is ordered by date.
869+
870+
Each page carries its own header row: a markdown table cannot span an HTML
871+
block boundary, so the alternative is a headerless slab of pipes. The blank
872+
lines are load-bearing — without them GitHub treats the table as raw text
873+
inside the `<details>` (same rule as `_task_row`).
874+
"""
875+
head, rest = rows[:page], rows[page:]
876+
block = RECENT_TABLE_HEAD + _recent_rows(head)
877+
if not rest:
878+
return block
879+
shown = min(page, len(rest))
880+
return block + ["",
881+
f"<details><summary>… {shown} more "
882+
f"({len(rest)} left)</summary>",
883+
""] + _recent_pages(rest, page) + ["", "</details>"]
884+
885+
837886
def _recent_link(e: dict) -> str:
838887
"""The task cell of a Recent row — its title, linked to where it lives."""
839888
return f"<a href=\"{e['path']}\">{_summary_label(_clip(e['title'], 70))}</a>"
@@ -969,12 +1018,8 @@ def render_dashboard(c: dict) -> str:
9691018
# it is picked up.
9701019
recent = c.get("recent") or []
9711020
if recent:
972-
L += ["## Recent", "",
973-
RECENT_BLURB.format(n=len(recent)), "",
974-
"| Date | Event | Task |",
975-
"|------|-------|------|"]
976-
L += [f"| {r['date']} | {r['event']} | {_cell(_recent_link(r))} |"
977-
for r in recent]
1021+
L += ["## Recent", "", _recent_blurb(recent), ""]
1022+
L += _recent_pages(recent)
9781023
L += ["", "_Dates come from each task's registry entry — "
9791024
"`lifecycle.py dates` reports anything undated._", ""]
9801025

@@ -1062,6 +1107,10 @@ def render_dashboard(c: dict) -> str:
10621107
padding-top:.58rem}
10631108
table.recent td.pick{width:2.6rem;padding-right:0}
10641109
table.recent button.copy{width:2.2rem;height:2.2rem;font-size:.95rem}
1110+
button.more{display:block;width:100%;margin:.6rem 0;padding:.5rem;
1111+
border:1px solid var(--line);border-radius:8px;background:var(--btn);
1112+
color:var(--muted);cursor:pointer;font:inherit;font-size:.9em}
1113+
button.more:hover{color:var(--fg)}
10651114
"""
10661115

10671116
# One tap on 📋 → the command is on the clipboard; the button flashes ✓. The
@@ -1077,6 +1126,18 @@ def render_dashboard(c: dict) -> str:
10771126
setTimeout(()=>{b.textContent="\\ud83d\\udccb";b.classList.remove("ok");},1200);}
10781127
document.addEventListener("click",e=>{
10791128
const b=e.target.closest("button.copy");if(b)copyCmd(b);});
1129+
// Recent shows one page and reveals the next on each tap of the \u2026 button,
1130+
// which retires itself once the feed is exhausted. Every row is already in the
1131+
// DOM, so this never re-renders or re-sorts anything.
1132+
document.addEventListener("click",e=>{
1133+
const b=e.target.closest("button.more");if(!b)return;
1134+
const t=document.querySelector("table.recent");if(!t)return;
1135+
const hidden=[...t.querySelectorAll("tr[hidden]")];
1136+
const page=Number(b.dataset.page)||10;
1137+
hidden.slice(0,page).forEach(r=>r.removeAttribute("hidden"));
1138+
const left=hidden.length-Math.min(page,hidden.length);
1139+
if(left<=0){b.remove();return;}
1140+
b.textContent="\u2026 "+Math.min(page,left)+" more ("+left+" left)";});
10801141
"""
10811142

10821143

@@ -1228,10 +1289,16 @@ def h2(title, src):
12281289
recent = c.get("recent") or []
12291290
if recent:
12301291
H += ['<a id="recent"></a>' + h2("Recent", "dashboard.md#recent"),
1231-
f'<p class="muted">{RECENT_BLURB.format(n=len(recent))}</p>',
1292+
# `_summary_label` turns the blurb's `code` spans into <code>;
1293+
# markdown backticks render literally on this page.
1294+
f'<p class="muted">{_summary_label(_recent_blurb(recent))}</p>',
12321295
'<table class="recent">']
1233-
for r in recent:
1234-
H += ["<tr>",
1296+
for i, r in enumerate(recent):
1297+
# Every row ships in the DOM; the ones past the first page start
1298+
# hidden, so revealing them is a flag flip rather than a re-render
1299+
# — and a reader with JS off sees the whole feed rather than ten
1300+
# rows and a dead button.
1301+
H += ["<tr hidden>" if i >= RECENT_PAGE else "<tr>",
12351302
f'<td class="when">{r["date"]}</td>',
12361303
f'<td class="what">{_summary_label(r["event"])}</td>',
12371304
f'<td>{link(r["path"], _summary_label(_clip(r["title"], 70)))}</td>',
@@ -1240,6 +1307,10 @@ def h2(title, src):
12401307
f'Claude command">📋</button></td>',
12411308
"</tr>"]
12421309
H += ["</table>"]
1310+
rest = len(recent) - RECENT_PAGE
1311+
if rest > 0:
1312+
H += [f'<button class="more" data-page="{RECENT_PAGE}">'
1313+
f'… {min(RECENT_PAGE, rest)} more ({rest} left)</button>']
12431314

12441315
known = {e["slug"] for e in c.get("epics") or []}
12451316
stray = [s for s in members if s not in known]

tests/test_intake_dashboard.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,3 +607,71 @@ def test_an_undated_row_gets_no_placeholder(tmp_path):
607607
"- prompt: active/sprocket_calibration.md\n"})
608608
row = _page(mind).split("## In flight")[1].split("<details>")[1]
609609
assert "—" not in row.split("</summary>")[0]
610+
611+
612+
# --------------------------------------------------------------------------- #
613+
# recent: fifty deep, ten on screen
614+
# --------------------------------------------------------------------------- #
615+
def _many(root, n):
616+
"""A Mind whose planned.md holds `n` dated tasks, newest first by slug."""
617+
return _mind(root, registries={"planned.md": "".join(
618+
f"## task-{i:03d}\n- filed: 2026-01-01\n\n" for i in range(n))})
619+
620+
621+
def test_the_feed_runs_deeper_than_the_page(tmp_path):
622+
"""Fifty is what the feed HOLDS; ten is what it SHOWS."""
623+
rows = _intake.census(_many(tmp_path, 80))["recent"]
624+
assert len(rows) == _intake.RECENT_MAX == 50
625+
assert _intake.RECENT_PAGE == 10
626+
627+
628+
def test_markdown_shows_one_page_then_nests_the_rest(tmp_path):
629+
"""GitHub strips the JS the Pages twin uses, so the markdown page reveals
630+
with `<details>` — nested, so each tap shows the next page and leaves
631+
another one behind it."""
632+
page = _page(_many(tmp_path, 80))
633+
section = page.split("## Recent")[1]
634+
before = section.split("<details>")[0]
635+
assert before.count("| 2026-01-01 |") == 10
636+
assert section.count("<details>") == 4
637+
assert "… 10 more (40 left)" in section
638+
assert "… 10 more (10 left)" in section
639+
640+
641+
def test_each_revealed_page_carries_its_own_table_header(tmp_path):
642+
"""A markdown table cannot span an HTML block boundary — without a header
643+
per page the reveal is a headerless slab of pipes."""
644+
section = _page(_many(tmp_path, 80)).split("## Recent")[1]
645+
assert section.count("| Date | Event | Task |") == 5
646+
647+
648+
def test_a_feed_that_fits_on_one_page_has_no_reveal(tmp_path):
649+
page = _page(_many(tmp_path, 6))
650+
section = page.split("## Recent")[1]
651+
assert "<details>" not in section
652+
assert "…" not in section
653+
assert "opens the next" not in section
654+
655+
656+
def test_html_hides_the_overflow_rows_and_offers_a_button(tmp_path):
657+
html = _intake.render_dashboard_html(_intake.census(_many(tmp_path, 80)))
658+
section = html.split("<h2>Recent")[1]
659+
assert section.count("<tr>") == 10
660+
assert section.count("<tr hidden>") == 40
661+
assert '<button class="more" data-page="10">… 10 more (40 left)</button>' in section
662+
663+
664+
def test_html_ships_every_row_so_a_reader_without_js_sees_the_feed(tmp_path):
665+
"""Hidden, not absent: with JS off the whole feed is there rather than ten
666+
rows and a dead button."""
667+
html = _intake.render_dashboard_html(_intake.census(_many(tmp_path, 80)))
668+
section = html.split("<h2>Recent")[1]
669+
assert section.count("<tr") == 50
670+
671+
672+
def test_the_html_blurb_renders_its_code_spans(tmp_path):
673+
"""The blurb is shared with the markdown page; its backticks would
674+
otherwise print literally here."""
675+
html = _intake.render_dashboard_html(_intake.census(_many(tmp_path, 80)))
676+
assert "<code>complete/index.md</code>" in html
677+
assert "`complete/index.md`" not in html

0 commit comments

Comments
 (0)