feat: add unread updates to My Work (#133)
CI / lint (pull_request) Successful in 12s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-06 19:59:39 +00:00
1 parent d75a55f3f7
commit f34fc8b5f5
7 files changed
+336 -13

No files matched your search

+20 -6
View File
@@ -131,6 +131,7 @@ textarea { resize: vertical; min-height: 120px; }
<button class="work-filter" data-work-filter="issue" aria-pressed="false">Issues <span data-work-count="issue">0</span></button>
<button class="work-filter" data-work-filter="pull" aria-pressed="false">PRs <span data-work-count="pull">0</span></button>
<button class="work-filter" data-work-filter="review" aria-pressed="false">Reviews <span data-work-count="review">0</span></button>
<button class="work-filter" data-work-filter="update" aria-pressed="false">Updates <span data-work-count="update">0</span></button>
</div>
</div>
<div class="my-work-list" id="my-work-list"></div>
@@ -300,11 +301,12 @@ textarea { resize: vertical; min-height: 120px; }
let selectedWorkFilter = 'all';
try {
const savedFilter = sessionStorage.getItem(WORK_FILTER_KEY);
if (['all', 'issue', 'pull', 'review'].includes(savedFilter)) selectedWorkFilter = savedFilter;
if (['all', 'issue', 'pull', 'review', 'update'].includes(savedFilter)) selectedWorkFilter = savedFilter;
} catch (e) {
console.warn('Could not restore My Work filter', e);
}
let lastMyWork = [];
let lastNotifications = [];
let hasContextSnapshot = false;
let selectedReview = null;
let reviewTrigger = null;
@@ -381,7 +383,7 @@ textarea { resize: vertical; min-height: 120px; }
});
qs('#my-work').removeAttribute('data-stale');
qs('#my-work-status').textContent = lastMyWork.length ?
summarizeMyWork(lastMyWork) : 'No assigned work or review requests.';
summarizeMyWork(lastMyWork) : 'No assigned work, review requests, or unread updates.';
renderMyWork();
}
@@ -389,7 +391,7 @@ textarea { resize: vertical; min-height: 120px; }
const visible = filterMyWork(lastMyWork, selectedWorkFilter);
qs('#my-work-list').innerHTML = visible.length ? visible.map(item => {
const contents =
'<span class="small">' + escapeHtml(item.key) + ' · ' + escapeHtml(item.kind === 'pull' ? 'PR' : 'Issue') + '</span>' +
'<span class="small">' + escapeHtml(item.key) + ' · ' + escapeHtml(item.kind === 'pull' ? 'PR' : (item.kind === 'update' ? 'Update' : 'Issue')) + '</span>' +
'<span class="my-work-card-title">' + escapeHtml(item.title) + '</span>' +
'<span class="pill">' + escapeHtml(item.reason) + '</span>' +
(item.updated_at ? '<span class="small"> · Updated ' + escapeHtml(fmt(item.updated_at)) + '</span>' : '');
@@ -398,7 +400,7 @@ textarea { resize: vertical; min-height: 120px; }
return '<button class="my-work-card review-trigger" data-review-index="' + index + '">' + contents + '</button>';
}
return '<a class="my-work-card" href="' + escAttr(item.url) + '" target="_blank" rel="noopener noreferrer">' + contents + '</a>';
}).join('') : '<div class="muted">No ' + (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items')) + '.</div>';
}).join('') : '<div class="muted">No ' + (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))) + '.</div>';
document.querySelectorAll('[data-review-index]').forEach(button => {
button.addEventListener('click', () => openReviewSheet(lastMyWork[Number(button.dataset.reviewIndex)], button));
});
@@ -533,6 +535,13 @@ textarea { resize: vertical; min-height: 120px; }
'Update failed · showing last known work' : 'Work inbox unavailable.';
}
function markNotificationsStale() {
qs('#my-work').setAttribute('data-stale', 'true');
qs('#my-work-status').textContent = lastNotifications.length ?
'Unread updates unavailable · showing last known updates' :
'Unread updates unavailable · assigned work is fresh';
}
function paintDeltas(deltas) {
const el = qs('#ai');
el.innerHTML = deltas.length ? deltas.map(d => '<div class="suggestion ' + d.priority + '"><span class="pill">' + escapeHtml(d.priority) + '</span> <strong>' + escapeHtml(d.action) + '</strong> ' + escapeHtml(d.target || '') + '<div class="muted">' + escapeHtml(d.panel) + '</div></div>').join('') : '<div class="muted">No suggestions yet.</div>';
@@ -553,8 +562,13 @@ textarea { resize: vertical; min-height: 120px; }
}
function renderLiveSnapshot(snapshot) {
if (snapshot.context) renderContextSnapshot(snapshot.context);
else handleContextError(new Error('Context section unavailable'));
const notificationsFresh = Array.isArray(snapshot.notifications);
if (notificationsFresh) lastNotifications = snapshot.notifications;
if (snapshot.context) {
snapshot.context.notifications = lastNotifications;
renderContextSnapshot(snapshot.context);
if (!notificationsFresh) markNotificationsStale();
} else handleContextError(new Error('Context section unavailable'));
if (snapshot.events) {
paintEventStream(snapshot.events);
setEventStreamStatus('Updated ' + fmt(new Date()));
+38 -4
View File
@@ -4,7 +4,7 @@ function buildMyWork(data) {
const pulls = (data.pull_requests || []).map((item) => ({ ...item, kind: 'pull' }));
const priorityLabels = ['p0', 'priority-high', 'critical'];
return issues.concat(pulls).map((item) => {
const work = issues.concat(pulls).map((item) => {
const labels = item.labels || [];
const priorityLabel = labels.find((label) =>
priorityLabels.includes(String(label).toLowerCase())
@@ -16,11 +16,41 @@ function buildMyWork(data) {
key: (item.repository || 'unknown') + '#' + item.number,
is_review: isReview,
is_assigned: assigned,
has_update: false,
reason: priorityLabel ? priorityLabel + ' priority' :
(isReview ? 'Needs your review' : (assigned ? 'Assigned to you' : 'Open work')),
_priority: priorityLabel ? 0 : (isReview ? 1 : (assigned ? 2 : 3)),
_priority: priorityLabel ? 0 : (isReview ? 2 : (assigned ? 3 : 4)),
};
}).sort((left, right) =>
});
const byKey = new Map(work.map((item) => [item.kind + ':' + item.key, item]));
(data.notifications || []).filter((item) => item && item.unread).forEach((update) => {
const key = (update.repository || 'unknown') + '#' + update.number;
const subjectKind = String(update.subject_type || '').toLowerCase().includes('pull') ? 'pull' : 'issue';
const existing = byKey.get(subjectKind + ':' + key);
if (existing) {
existing.has_update = true;
existing.url = update.url || existing.url;
existing.updated_at = update.updated_at || existing.updated_at;
existing._priority = Math.min(existing._priority, 1);
return;
}
if (!update.url) return;
const item = {
...update,
key,
kind: 'update',
is_review: false,
is_assigned: false,
has_update: true,
reason: 'Unread update',
_priority: 1,
};
work.push(item);
byKey.set(subjectKind + ':' + key, item);
});
return work.sort((left, right) =>
left._priority - right._priority ||
String(right.updated_at || '').localeCompare(String(left.updated_at || '')) ||
left.key.localeCompare(right.key)
@@ -30,15 +60,18 @@ function buildMyWork(data) {
function filterMyWork(items, selectedFilter) {
if (selectedFilter === 'all') return items;
if (selectedFilter === 'review') return items.filter((item) => item.is_review);
if (selectedFilter === 'update') return items.filter((item) => item.has_update);
return items.filter((item) => item.kind === selectedFilter);
}
function summarizeMyWork(items) {
const updates = items.filter((item) => item.has_update).length;
const reviews = items.filter((item) => item.is_review).length;
const assigned = items.filter((item) => item.is_assigned).length;
const updateLabel = updates + ' unread update' + (updates === 1 ? '' : 's');
const reviewLabel = reviews + ' review' + (reviews === 1 ? '' : 's');
const assignedLabel = assigned + ' assigned';
return reviewLabel + ' · ' + assignedLabel;
return (updates ? updateLabel + ' · ' : '') + reviewLabel + ' · ' + assignedLabel;
}
function countMyWork(items) {
@@ -47,6 +80,7 @@ function countMyWork(items) {
issue: items.filter((item) => item.kind === 'issue').length,
pull: items.filter((item) => item.kind === 'pull' && !item.is_review).length,
review: items.filter((item) => item.is_review).length,
update: items.filter((item) => item.has_update).length,
};
}