feat: complete mobile Agenda discovery (Closes #703)
All checks were successful
CI / lint (pull_request) Successful in 1m27s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-13 03:25:25 +00:00
parent 467c8c665d
commit 6b56188d30
3 changed files with 97 additions and 3 deletions

View File

@ -147,6 +147,7 @@
let lastContextSnapshot = null;
let notificationPagination = { page: 1, total: 0, has_more: false };
let workPagination = {};
let agendaChecking = false;
let hasContextSnapshot = false;
let selectedReview = null;
let reviewTrigger = null;
@ -2202,6 +2203,9 @@
'No assigned work, review requests, or unread updates.';
updateWorkPaginationControls();
renderMyWork();
if (selectedWorkFilter === 'agenda' && workPagination.issue?.has_more && !agendaChecking) {
completeAgendaIssues();
}
if (reconcileSession && workSession.active()) workSession.reconcile();
updateWorkSessionActions();
}
@ -2217,6 +2221,20 @@
return [];
}
async function completeAgendaIssues() {
if (selectedWorkFilter !== 'agenda' || !workPagination.issue?.has_more || !lastContextSnapshot) return;
agendaChecking = true;
qs('#my-work-action-status').textContent = 'Checking all assigned deadlines…';
renderMyWork();
const complete = await workPager.loadAll('issue', () => lastContextSnapshot?.issues || []);
agendaChecking = false;
if (selectedWorkFilter !== 'agenda') return;
qs('#my-work-action-status').textContent = complete ?
'All assigned deadlines checked.' :
'Agenda check paused. Retry to check older assigned deadlines.';
renderMyWork();
}
function renderDrafts() {
const list = qs('#my-work-list');
const displayedDrafts = findQueueItems(lastDrafts, queueFindQuery);
@ -2489,9 +2507,12 @@
return '<article class="' + cardClasses + '">' + selector + '<a class="my-work-card-main pull-trigger" href="' + escAttr(routeHref) + '" data-pull-index="' + index + '">' + contents + '</a>' + readUpdate + markRead + planningActions + '</article>';
}
return '<article class="' + cardClasses + '">' + selector + '<a class="my-work-card-main update-trigger" href="' + escAttr(routeHref) + '" data-update-index="' + index + '">' + contents + '</a>' + markRead + planningActions + '</article>';
}).join('') : (showEmptyStart ? '' : '<div class="muted">' + (incomplete ?
}).join('') : (showEmptyStart ? '' : '<div class="muted">' +
(selectedWorkFilter === 'agenda' && workPagination.issue?.has_more ?
(agendaChecking ? 'Checking all assigned deadlines…' :
'Older assigned deadlines remain unchecked. Retry the Agenda check.') : (incomplete ?
'More work is available. Load the next page.' :
'No ' + (selectedWorkFilter === 'attention' ? 'items need attention' : (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'later' ? 'deferred work' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))))) + '.') + '</div>');
'No ' + (selectedWorkFilter === 'attention' ? 'items need attention' : (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'later' ? 'deferred work' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))))) + '.')) + '</div>');
cardPlanning.wire();
document.querySelectorAll('[data-select-notification-id]').forEach(input => {
input.addEventListener('change', () => {
@ -5778,6 +5799,10 @@
notificationPager.loadMore(lastNotifications)
);
qs('#load-more-work').addEventListener('click', async () => {
if (selectedWorkFilter === 'agenda') {
await completeAgendaIssues();
return;
}
const stream = activeWorkStreams().find(item => workPagination[item]?.has_more);
if (!stream || !lastContextSnapshot) return;
const button = qs('#load-more-work');
@ -5931,6 +5956,7 @@
qs('#active-work-queue').textContent = button.firstChild.textContent.trim() + ' (' + selectedCount + ')';
renderMyWork();
updateWorkPaginationControls();
if (selectedWorkFilter === 'agenda') completeAgendaIssues();
if (!preserveRoute) workRoute.queue(filter);
return true;
}

View File

@ -277,12 +277,13 @@ function createNotificationPager({ load, onNotifications, onPagination, onStatus
function createWorkPager({ load, onItems, onPagination, onStatus }) {
let pagination = {};
const pending = new Set();
const completing = new Map();
const labels = {
issue: 'issues',
pull: 'pull requests',
review: 'review requests',
};
return {
const pager = {
reset(next) {
Object.entries(next || {}).forEach(([stream, value]) => {
const current = pagination[stream];
@ -331,7 +332,20 @@ function createWorkPager({ load, onItems, onPagination, onStatus }) {
pending.delete(stream);
}
},
loadAll(stream, getExisting) {
if (completing.has(stream)) return completing.get(stream);
const completion = (async () => {
while (pagination[stream]?.has_more) {
const loaded = await pager.loadMore(stream, getExisting());
if (!loaded) return false;
}
return true;
})().finally(() => completing.delete(stream));
completing.set(stream, completion);
return completion;
},
};
return pager;
}
function createNotificationReader({

View File

@ -173,6 +173,60 @@ process.stdout.write(JSON.stringify(work.agendaMyWork(items, new Date('2026-08-1
assert json.loads(result.stdout) == ["a/r#2", "a/r#10", "z/r#1"]
def test_agenda_pager_loads_every_issue_page_single_flight_and_retries_failed_page():
script = f"""
const work = require({json.dumps(str(MY_WORK))});
const calls = [];
let failPage = 3;
let items = [{{id:1, title:'first'}}];
let pagination = {{issue:{{page:1,total:4,has_more:true}}}};
const pager = work.createWorkPager({{
load: async (stream, page) => {{
calls.push(page);
await new Promise(resolve => setTimeout(resolve, 5));
if (page === failPage) throw new Error('offline');
return {{page,total:4,has_more:page < 4,items:[{{id:page,title:'page '+page}}]}};
}},
onItems: (_stream, next) => {{ items = next; }},
onPagination: next => {{ pagination = next; }},
onStatus: () => {{}},
}});
pager.reset(pagination);
async function run() {{
const first = pager.loadAll('issue', () => items);
const duplicate = pager.loadAll('issue', () => items);
const failed = await first;
const samePromise = first === duplicate;
failPage = 0;
const retried = await pager.loadAll('issue', () => items);
process.stdout.write(JSON.stringify({{
failed, samePromise, retried, calls, ids:items.map(item => item.id), pagination,
}}));
}}
run();
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"failed": False,
"samePromise": True,
"retried": True,
"calls": [2, 3, 3, 4],
"ids": [1, 2, 3, 4],
"pagination": {"issue": {"page": 4, "total": 4, "has_more": False}},
}
@pytest.mark.anyio
async def test_agenda_activation_checks_all_issue_pages_before_showing_empty_state():
html = await dashboard()
assert "workPager.loadAll('issue'" in html
assert "Checking all assigned deadlines…" in html
assert "Agenda check paused. Retry to check older assigned deadlines." in html
assert "selectedWorkFilter === 'agenda' && workPagination.issue?.has_more" in html
@pytest.mark.anyio
async def test_mobile_queue_finder_is_labeled_thumb_safe_and_offers_older_search():
html = await dashboard()