@yemi33/minions 0.1.2088 → 0.1.2090
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/js/live-stream.js +6 -0
- package/dashboard/js/refresh.js +17 -8
- package/dashboard/js/render-agents.js +2 -0
- package/dashboard/js/render-dispatch.js +17 -6
- package/dashboard/js/render-inbox.js +10 -5
- package/dashboard/js/render-kb.js +9 -4
- package/dashboard/js/render-meetings.js +16 -11
- package/dashboard/js/render-pipelines.js +15 -5
- package/dashboard/js/render-prd.js +2 -0
- package/dashboard/js/render-prs.js +22 -9
- package/dashboard/js/render-schedules.js +8 -4
- package/dashboard/js/render-utils.js +33 -1
- package/dashboard/js/render-watches.js +7 -3
- package/dashboard/js/render-work-items.js +9 -9
- package/dashboard/js/utils.js +4 -4
- package/dashboard-build.js +1 -1
- package/docs/deprecated.json +0 -13
- package/package.json +1 -1
|
@@ -127,6 +127,8 @@ async function refreshLiveOutput() {
|
|
|
127
127
|
const el = document.getElementById('live-messages');
|
|
128
128
|
if (el) {
|
|
129
129
|
const wasAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
|
|
130
|
+
const savedScrollTop = el.scrollTop;
|
|
131
|
+
const savedScrollLeft = el.scrollLeft;
|
|
130
132
|
const incrementalSafe = _currentAgentRuntime() !== 'copilot';
|
|
131
133
|
// Incremental render: only parse new content if text is an extension of previous
|
|
132
134
|
if (incrementalSafe && _lastRenderedText && text.length > _lastRenderedText.length && text.startsWith(_lastRenderedText.slice(0, 200))) {
|
|
@@ -137,6 +139,10 @@ async function refreshLiveOutput() {
|
|
|
137
139
|
}
|
|
138
140
|
_lastRenderedText = text;
|
|
139
141
|
if (wasAtBottom) el.scrollTop = el.scrollHeight;
|
|
142
|
+
else {
|
|
143
|
+
el.scrollTop = savedScrollTop;
|
|
144
|
+
el.scrollLeft = savedScrollLeft;
|
|
145
|
+
}
|
|
140
146
|
}
|
|
141
147
|
} catch (e) { console.error('live-stream reload:', e.message); }
|
|
142
148
|
}
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -182,7 +182,6 @@ let _lastChangedFlags = null;
|
|
|
182
182
|
function _safeRender(name, fn) {
|
|
183
183
|
try { fn(); }
|
|
184
184
|
catch (e) {
|
|
185
|
-
// eslint-disable-next-line no-console
|
|
186
185
|
console.error('[render] ' + name + ' threw:', e);
|
|
187
186
|
}
|
|
188
187
|
}
|
|
@@ -396,7 +395,8 @@ function _refreshSidebarCounterSummaries() {
|
|
|
396
395
|
// entry read it — drop the dead endpoint + fetch. (Review finding #9.)
|
|
397
396
|
}
|
|
398
397
|
|
|
399
|
-
function _processStatusUpdate(data) {
|
|
398
|
+
function _processStatusUpdate(data, opts) {
|
|
399
|
+
opts = opts || {};
|
|
400
400
|
// Kick off the dedicated-endpoint summary refreshes (issue #2949). They
|
|
401
401
|
// run in the background and update window._last* globals that the sidebar
|
|
402
402
|
// _pageCounters read. No await — the current tick continues immediately.
|
|
@@ -687,7 +687,11 @@ function _processStatusUpdate(data) {
|
|
|
687
687
|
// endpoint that re-runs getDispatchQueue() server-side on every
|
|
688
688
|
// request (issue #2949). Completion-report sidecars are now loaded
|
|
689
689
|
// server-side too, so the client no longer needs the cached overlay.
|
|
690
|
+
// Scroll-preservation opts (preserveCompletedScroll) are threaded
|
|
691
|
+
// through to renderDispatch so the completed-dispatch pane keeps its
|
|
692
|
+
// scroll position across refreshes (W-mpp75wkg000b3018).
|
|
690
693
|
_safeRender('dispatch', function() {
|
|
694
|
+
const dispatchOpts = { preserveCompletedScroll: opts.preserveCompletedScroll };
|
|
691
695
|
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
692
696
|
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
693
697
|
window._lastRequestedSeq.dispatch = seq;
|
|
@@ -697,7 +701,7 @@ function _processStatusUpdate(data) {
|
|
|
697
701
|
if (seq < (window._lastRequestedSeq.dispatch || 0)) return;
|
|
698
702
|
const d = fresh || window._lastDispatch;
|
|
699
703
|
if (d) window._lastDispatch = d;
|
|
700
|
-
_safeRender('dispatch', function() { renderDispatch(d); });
|
|
704
|
+
_safeRender('dispatch', function() { renderDispatch(d, dispatchOpts); });
|
|
701
705
|
// Dispatch is the third cross-slice edge: derivePlanStatus reads
|
|
702
706
|
// window._lastDispatch.active/.pending for executing/converting
|
|
703
707
|
// plan-status badges. Without this trigger, plan-status pills lag
|
|
@@ -706,7 +710,7 @@ function _processStatusUpdate(data) {
|
|
|
706
710
|
_fireCrossSliceRender('dispatch', d);
|
|
707
711
|
})
|
|
708
712
|
.catch(function () {
|
|
709
|
-
if (window._lastDispatch) _safeRender('dispatch', function() { renderDispatch(window._lastDispatch); });
|
|
713
|
+
if (window._lastDispatch) _safeRender('dispatch', function() { renderDispatch(window._lastDispatch, dispatchOpts); });
|
|
710
714
|
});
|
|
711
715
|
});
|
|
712
716
|
// prunePrdRequeueState moved into the /api/work-items .then handler so it
|
|
@@ -714,15 +718,19 @@ function _processStatusUpdate(data) {
|
|
|
714
718
|
// window._lastWorkItems. (Review finding #5.)
|
|
715
719
|
// Engine log (last 50 entries) comes from /state/engine/log.json with a
|
|
716
720
|
// client-side .slice(-50) to mirror queries.js getEngineLog (issue #2949).
|
|
721
|
+
// Scroll-preservation opts (preserveEngineLogScroll) are threaded through
|
|
722
|
+
// to renderEngineLog so the log pane keeps its scroll position across
|
|
723
|
+
// refreshes (W-mpp75wkg000b3018).
|
|
717
724
|
_safeRender('engineLog', function() {
|
|
725
|
+
const logOpts = { preserveEngineLogScroll: opts.preserveEngineLogScroll };
|
|
718
726
|
Promise.resolve(fetchEngineLogFromDisk())
|
|
719
727
|
.then(function (fresh) {
|
|
720
728
|
const list = fresh && fresh.length ? fresh : (window._lastEngineLog || []);
|
|
721
729
|
window._lastEngineLog = list;
|
|
722
|
-
_safeRender('engineLog', function() { renderEngineLog(list); });
|
|
730
|
+
_safeRender('engineLog', function() { renderEngineLog(list, logOpts); });
|
|
723
731
|
})
|
|
724
732
|
.catch(function () {
|
|
725
|
-
if (Array.isArray(window._lastEngineLog)) _safeRender('engineLog', function() { renderEngineLog(window._lastEngineLog); });
|
|
733
|
+
if (Array.isArray(window._lastEngineLog)) _safeRender('engineLog', function() { renderEngineLog(window._lastEngineLog, logOpts); });
|
|
726
734
|
});
|
|
727
735
|
});
|
|
728
736
|
// Metrics now come from /api/metrics (issue #2949) — the enrichment
|
|
@@ -1144,7 +1152,8 @@ document.addEventListener('visibilitychange', function() {
|
|
|
1144
1152
|
_lastVisibilityChangeAt = now;
|
|
1145
1153
|
});
|
|
1146
1154
|
|
|
1147
|
-
async function refresh() {
|
|
1155
|
+
async function refresh(opts) {
|
|
1156
|
+
opts = opts || {};
|
|
1148
1157
|
if (_refreshInFlight) return;
|
|
1149
1158
|
// Backoff gate — only active while the unreachable banner is up. Skips
|
|
1150
1159
|
// setInterval ticks until _nextPollAllowedAt is reached so a downed
|
|
@@ -1241,7 +1250,7 @@ async function refresh() {
|
|
|
1241
1250
|
_lastChangedFlags = _diagChanges;
|
|
1242
1251
|
}
|
|
1243
1252
|
try {
|
|
1244
|
-
_processStatusUpdate(data);
|
|
1253
|
+
_processStatusUpdate(data, opts);
|
|
1245
1254
|
} finally {
|
|
1246
1255
|
if (_diagOn) {
|
|
1247
1256
|
_lastChangedFlags = null;
|
|
@@ -47,6 +47,7 @@ function _modelChipHtml(model) {
|
|
|
47
47
|
function renderAgents(agents) {
|
|
48
48
|
agentData = agents;
|
|
49
49
|
const grid = document.getElementById('agents-grid');
|
|
50
|
+
const scrollState = captureDashboardScrollState(grid, ['.agent-result']);
|
|
50
51
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml()/renderMd() (fields: agent id, emoji, name, status, role, lastAction, warning, resultSummary, blocking tool)
|
|
51
52
|
grid.innerHTML = agents.map(a => `
|
|
52
53
|
<div class="agent-card ${statusColor(a.status)}" data-agent-id="${escapeHtml(a.id)}" onclick="if(shouldIgnoreSelectionClick(event))return;openAgentDetail(this.dataset.agentId)">
|
|
@@ -67,6 +68,7 @@ function renderAgents(agents) {
|
|
|
67
68
|
${a.resultSummary ? `<div class="agent-result" title="${escapeHtml(a.resultSummary)}">${renderMd(a.resultSummary.slice(0, 200))}${a.resultSummary.length > 200 ? '...' : ''}</div>` : ''}
|
|
68
69
|
</div>
|
|
69
70
|
`).join('');
|
|
71
|
+
restoreDashboardScrollState(grid, scrollState);
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
async function openAgentDetail(id) {
|
|
@@ -64,10 +64,10 @@ async function fetchDispatchFromDisk(cachedDispatch) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function _completedPrev() { if (_completedPage > 0) { _completedPage--; refresh(); } }
|
|
68
|
-
function _completedNext() { _completedPage++; refresh(); } // clamped in renderDispatch
|
|
69
|
-
function _logPrev() { if (_logPage > 0) { _logPage--; refresh(); } }
|
|
70
|
-
function _logNext() { _logPage++; refresh(); } // clamped in renderEngineLog
|
|
67
|
+
function _completedPrev() { if (_completedPage > 0) { _completedPage--; refresh({ preserveCompletedScroll: false }); } }
|
|
68
|
+
function _completedNext() { _completedPage++; refresh({ preserveCompletedScroll: false }); } // clamped in renderDispatch
|
|
69
|
+
function _logPrev() { if (_logPage > 0) { _logPage--; refresh({ preserveEngineLogScroll: false }); } }
|
|
70
|
+
function _logNext() { _logPage++; refresh({ preserveEngineLogScroll: false }); } // clamped in renderEngineLog
|
|
71
71
|
|
|
72
72
|
// Engine restart grace state (W-mpfw3hgm001gc594). After the operator clicks
|
|
73
73
|
// "Restart engine" we suppress the STALE indicators for ENGINE_RESTART_GRACE_MS
|
|
@@ -245,7 +245,8 @@ function renderGhThrottleAlert(ghThrottle) {
|
|
|
245
245
|
el.style.display = 'flex';
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
function renderDispatch(dispatch) {
|
|
248
|
+
function renderDispatch(dispatch, opts) {
|
|
249
|
+
opts = opts || {};
|
|
249
250
|
if (!dispatch) return;
|
|
250
251
|
|
|
251
252
|
// Stats
|
|
@@ -294,6 +295,9 @@ function renderDispatch(dispatch) {
|
|
|
294
295
|
// Completed
|
|
295
296
|
const completedEl = document.getElementById('completed-content');
|
|
296
297
|
const completedCount = document.getElementById('completed-count');
|
|
298
|
+
const completedScroll = opts.preserveCompletedScroll === false
|
|
299
|
+
? null
|
|
300
|
+
: captureDashboardScrollState(completedEl, [':self', '.pr-table-wrap']);
|
|
297
301
|
const completed = (dispatch.completed || []).slice().reverse();
|
|
298
302
|
completedCount.textContent = completed.length;
|
|
299
303
|
|
|
@@ -331,6 +335,7 @@ function renderDispatch(dispatch) {
|
|
|
331
335
|
} else {
|
|
332
336
|
completedEl.innerHTML = '<p class="empty">No completed dispatches yet.</p>';
|
|
333
337
|
}
|
|
338
|
+
restoreDashboardScrollState(completedEl, completedScroll);
|
|
334
339
|
}
|
|
335
340
|
|
|
336
341
|
// Pull the engine log straight off disk through /state/engine/log.json.
|
|
@@ -351,11 +356,16 @@ async function fetchEngineLogFromDisk() {
|
|
|
351
356
|
}
|
|
352
357
|
}
|
|
353
358
|
|
|
354
|
-
function renderEngineLog(log) {
|
|
359
|
+
function renderEngineLog(log, opts) {
|
|
360
|
+
opts = opts || {};
|
|
355
361
|
const el = document.getElementById('engine-log');
|
|
356
362
|
if (!el) return;
|
|
363
|
+
const scrollState = opts.preserveEngineLogScroll === false
|
|
364
|
+
? null
|
|
365
|
+
: captureDashboardScrollState(el, [':self']);
|
|
357
366
|
if (!log || log.length === 0) {
|
|
358
367
|
el.innerHTML = '<div class="empty">No log entries yet.</div>';
|
|
368
|
+
restoreDashboardScrollState(el, scrollState);
|
|
359
369
|
return;
|
|
360
370
|
}
|
|
361
371
|
const reversed = log.slice().reverse();
|
|
@@ -380,6 +390,7 @@ function renderEngineLog(log) {
|
|
|
380
390
|
});
|
|
381
391
|
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal numeric page bounds and fixed renderPager() callback names (no user data flows in)
|
|
382
392
|
if (logPager) el.insertAdjacentHTML('beforeend', logPager);
|
|
393
|
+
restoreDashboardScrollState(el, scrollState);
|
|
383
394
|
}
|
|
384
395
|
|
|
385
396
|
function shortTime(t) {
|
|
@@ -75,10 +75,11 @@ async function fetchNotesFromDisk() {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function _inboxPrev() { if (_inboxPage > 0) { _inboxPage--; renderInbox(inboxData); } }
|
|
79
|
-
function _inboxNext() { _inboxPage++; renderInbox(inboxData); }
|
|
78
|
+
function _inboxPrev() { if (_inboxPage > 0) { _inboxPage--; renderInbox(inboxData, { preserveScroll: false }); } }
|
|
79
|
+
function _inboxNext() { _inboxPage++; renderInbox(inboxData, { preserveScroll: false }); }
|
|
80
80
|
|
|
81
|
-
function renderInbox(inbox) {
|
|
81
|
+
function renderInbox(inbox, opts) {
|
|
82
|
+
opts = opts || {};
|
|
82
83
|
invalidatePinsCache();
|
|
83
84
|
inbox = inbox.filter(function(item) { return !isDeleted('inbox:' + item.name); });
|
|
84
85
|
// Stable sort — pinned items float to top
|
|
@@ -89,7 +90,8 @@ function renderInbox(inbox) {
|
|
|
89
90
|
const list = document.getElementById('inbox-list');
|
|
90
91
|
const count = document.getElementById('inbox-count');
|
|
91
92
|
count.textContent = inbox.length;
|
|
92
|
-
|
|
93
|
+
const scrollState = opts.preserveScroll === false ? null : captureDashboardScrollState(list, [':self']);
|
|
94
|
+
if (!inbox.length) { list.innerHTML = '<p class="empty">No messages yet.</p>'; restoreDashboardScrollState(list, scrollState); return; }
|
|
93
95
|
|
|
94
96
|
const totalInboxPages = Math.ceil(inbox.length / INBOX_PER_PAGE);
|
|
95
97
|
if (_inboxPage >= totalInboxPages) _inboxPage = totalInboxPages - 1;
|
|
@@ -122,6 +124,7 @@ function renderInbox(inbox) {
|
|
|
122
124
|
});
|
|
123
125
|
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal numeric page bounds and fixed renderPager() callback names (no user data flows in)
|
|
124
126
|
if (inboxPager) list.insertAdjacentHTML('beforeend', inboxPager);
|
|
127
|
+
restoreDashboardScrollState(list, scrollState);
|
|
125
128
|
restoreNotifBadges();
|
|
126
129
|
}
|
|
127
130
|
|
|
@@ -150,6 +153,7 @@ function renderNotes(notes) {
|
|
|
150
153
|
const el = document.getElementById('notes-list');
|
|
151
154
|
const content = typeof notes === 'object' ? notes.content : notes;
|
|
152
155
|
const updatedAt = typeof notes === 'object' ? notes.updatedAt : null;
|
|
156
|
+
const scrollState = captureDashboardScrollState(el, ['.notes-preview']);
|
|
153
157
|
|
|
154
158
|
// Show last updated timestamp
|
|
155
159
|
const updatedEl = document.getElementById('notes-updated');
|
|
@@ -157,9 +161,10 @@ function renderNotes(notes) {
|
|
|
157
161
|
updatedEl.textContent = 'updated ' + formatLocalDateTime(updatedAt);
|
|
158
162
|
}
|
|
159
163
|
|
|
160
|
-
if (!content || !content.trim()) { el.innerHTML = '<p class="empty">No team notes yet.</p>'; return; }
|
|
164
|
+
if (!content || !content.trim()) { el.innerHTML = '<p class="empty">No team notes yet.</p>'; restoreDashboardScrollState(el, scrollState); return; }
|
|
161
165
|
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
|
|
162
166
|
el.innerHTML = '<div class="notes-preview" data-file="notes.md" onclick="openNotesModal()" title="Click to expand">' + renderMd(content) + '</div>';
|
|
167
|
+
restoreDashboardScrollState(el, scrollState);
|
|
163
168
|
el.querySelector('.notes-preview')._rawContent = content;
|
|
164
169
|
restoreNotifBadges();
|
|
165
170
|
}
|
|
@@ -17,8 +17,8 @@ let _kbActiveTab = 'all';
|
|
|
17
17
|
const KB_PER_PAGE = 30;
|
|
18
18
|
let _kbPage = 0;
|
|
19
19
|
|
|
20
|
-
function _kbPrev() { if (_kbPage > 0) { _kbPage--; renderKnowledgeBase(); } }
|
|
21
|
-
function _kbNext() { _kbPage++; renderKnowledgeBase(); }
|
|
20
|
+
function _kbPrev() { if (_kbPage > 0) { _kbPage--; renderKnowledgeBase({ preserveScroll: false }); } }
|
|
21
|
+
function _kbNext() { _kbPage++; renderKnowledgeBase({ preserveScroll: false }); }
|
|
22
22
|
|
|
23
23
|
function _formatSweepElapsed(startedAt) {
|
|
24
24
|
if (!startedAt) return '';
|
|
@@ -58,11 +58,13 @@ function kbPinnedNewestFirst(a, b) {
|
|
|
58
58
|
return kbNewestFirst(a, b);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function renderKnowledgeBase() {
|
|
61
|
+
function renderKnowledgeBase(opts) {
|
|
62
|
+
opts = opts || {};
|
|
62
63
|
_syncPinsFromServer();
|
|
63
64
|
const tabsEl = document.getElementById('kb-tabs');
|
|
64
65
|
const listEl = document.getElementById('kb-list');
|
|
65
66
|
const countEl = document.getElementById('kb-count');
|
|
67
|
+
const scrollState = opts.preserveScroll === false ? null : captureDashboardScrollState(listEl, [':self']);
|
|
66
68
|
|
|
67
69
|
// Single pass: flatten all KB items and count pinned
|
|
68
70
|
const allItems = [];
|
|
@@ -92,6 +94,7 @@ function renderKnowledgeBase() {
|
|
|
92
94
|
if (allItems.length === 0) {
|
|
93
95
|
tabsEl.innerHTML = '';
|
|
94
96
|
listEl.innerHTML = '<p class="empty">No knowledge entries yet. Notes are classified here after consolidation.</p>';
|
|
97
|
+
restoreDashboardScrollState(listEl, scrollState);
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -126,6 +129,7 @@ function renderKnowledgeBase() {
|
|
|
126
129
|
|
|
127
130
|
if (items.length === 0) {
|
|
128
131
|
listEl.innerHTML = '<p class="empty">No entries in this category.</p>';
|
|
132
|
+
restoreDashboardScrollState(listEl, scrollState);
|
|
129
133
|
return;
|
|
130
134
|
}
|
|
131
135
|
|
|
@@ -163,13 +167,14 @@ function renderKnowledgeBase() {
|
|
|
163
167
|
});
|
|
164
168
|
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal numeric page bounds and fixed renderPager() callback names (no user data flows in)
|
|
165
169
|
if (kbPager) listEl.insertAdjacentHTML('beforeend', kbPager);
|
|
170
|
+
restoreDashboardScrollState(listEl, scrollState);
|
|
166
171
|
restoreNotifBadges();
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
function kbSetTab(tab) {
|
|
170
175
|
_kbActiveTab = tab;
|
|
171
176
|
_kbPage = 0;
|
|
172
|
-
renderKnowledgeBase();
|
|
177
|
+
renderKnowledgeBase({ preserveScroll: false });
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
async function kbSweep() {
|
|
@@ -62,16 +62,19 @@ async function fetchMeetingsFromDisk() {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function renderMeetings(meetings) {
|
|
65
|
+
function renderMeetings(meetings, opts) {
|
|
66
|
+
opts = opts || {};
|
|
66
67
|
meetings = (meetings || []).filter(function(m) { return !isDeleted('mtg:' + m.id); });
|
|
67
68
|
meetings.sort((a, b) => (b.createdAt || b.completedAt || '').localeCompare(a.createdAt || a.completedAt || ''));
|
|
68
69
|
_lastMeetingsForPaging = meetings;
|
|
69
70
|
const el = document.getElementById('meetings-content');
|
|
70
71
|
const countEl = document.getElementById('meetings-count');
|
|
72
|
+
const scrollState = opts.preserveScroll === false ? null : captureDashboardScrollState(el, [':self']);
|
|
71
73
|
if (!meetings || meetings.length === 0) {
|
|
72
74
|
countEl.textContent = '0';
|
|
73
75
|
el.innerHTML = '<p class="empty">No meetings yet. Start one to have agents investigate, debate, and conclude on a topic.</p>';
|
|
74
76
|
_mtgTotalPages = 1;
|
|
77
|
+
restoreDashboardScrollState(el, scrollState);
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -88,6 +91,7 @@ function renderMeetings(meetings) {
|
|
|
88
91
|
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal archived count and fixed toggle markup (no user data flows in)
|
|
89
92
|
if (archived.length) el.insertAdjacentHTML('beforeend', '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:10px" onclick="_toggleArchivedMeetings()">Show ' + archived.length + ' archived</button></div>');
|
|
90
93
|
_mtgTotalPages = 1;
|
|
94
|
+
restoreDashboardScrollState(el, scrollState);
|
|
91
95
|
return;
|
|
92
96
|
}
|
|
93
97
|
|
|
@@ -143,29 +147,30 @@ function renderMeetings(meetings) {
|
|
|
143
147
|
el.insertAdjacentHTML('beforeend', '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:10px" onclick="_toggleArchivedMeetings()">' +
|
|
144
148
|
(_showArchived ? 'Hide' : 'Show') + ' ' + archived.length + ' archived</button></div>');
|
|
145
149
|
}
|
|
150
|
+
restoreDashboardScrollState(el, scrollState);
|
|
146
151
|
restoreNotifBadges();
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
function _rerenderMeetingPageFromCache() {
|
|
150
|
-
renderMeetings(_lastMeetingsForPaging || []);
|
|
154
|
+
function _rerenderMeetingPageFromCache(opts) {
|
|
155
|
+
renderMeetings(_lastMeetingsForPaging || [], opts);
|
|
151
156
|
}
|
|
152
157
|
function _mtgPrev() {
|
|
153
158
|
if (_mtgPage > 0) {
|
|
154
159
|
_mtgPage--;
|
|
155
|
-
_rerenderMeetingPageFromCache();
|
|
160
|
+
_rerenderMeetingPageFromCache({ preserveScroll: false });
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
function _mtgNext() {
|
|
159
164
|
if (_mtgPage < _mtgTotalPages - 1) {
|
|
160
165
|
_mtgPage++;
|
|
161
|
-
_rerenderMeetingPageFromCache();
|
|
166
|
+
_rerenderMeetingPageFromCache({ preserveScroll: false });
|
|
162
167
|
}
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
function _toggleArchivedMeetings() {
|
|
166
171
|
_showArchived = !_showArchived;
|
|
167
172
|
_mtgPage = 0;
|
|
168
|
-
_rerenderMeetingPageFromCache();
|
|
173
|
+
_rerenderMeetingPageFromCache({ preserveScroll: false });
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
let _meetingPollInterval = null;
|
|
@@ -204,14 +209,14 @@ function _renderMeetingDetail(m) {
|
|
|
204
209
|
if (m.findings?.[agent]) {
|
|
205
210
|
html += '<div style="padding:8px 12px;font-size:11px;border-bottom:1px solid var(--border)">' +
|
|
206
211
|
'<div style="color:var(--muted);font-size:10px;margin-bottom:4px">Round 1 — Findings</div>' +
|
|
207
|
-
'<div style="word-break:break-word;max-height:300px;overflow-y:auto">' + renderMd(m.findings[agent].content || '') + '</div></div>';
|
|
212
|
+
'<div class="meeting-scroll-panel" style="word-break:break-word;max-height:300px;overflow-y:auto">' + renderMd(m.findings[agent].content || '') + '</div></div>';
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
// Debate
|
|
211
216
|
if (m.debate?.[agent]) {
|
|
212
217
|
html += '<div style="padding:8px 12px;font-size:11px;border-bottom:1px solid var(--border)">' +
|
|
213
218
|
'<div style="color:var(--muted);font-size:10px;margin-bottom:4px">Round 2 — Debate</div>' +
|
|
214
|
-
'<div style="word-break:break-word;max-height:300px;overflow-y:auto">' + renderMd(m.debate[agent].content || '') + '</div></div>';
|
|
219
|
+
'<div class="meeting-scroll-panel" style="word-break:break-word;max-height:300px;overflow-y:auto">' + renderMd(m.debate[agent].content || '') + '</div></div>';
|
|
215
220
|
}
|
|
216
221
|
|
|
217
222
|
// Status
|
|
@@ -226,7 +231,7 @@ function _renderMeetingDetail(m) {
|
|
|
226
231
|
if (m.conclusion) {
|
|
227
232
|
html += '<div style="background:rgba(63,185,80,0.08);border:1px solid var(--green);border-radius:6px;padding:10px 14px">' +
|
|
228
233
|
'<div style="color:var(--green);font-weight:600;font-size:12px;margin-bottom:6px">Conclusion (by ' + escHtml(m.conclusion.agent || '?') + ')</div>' +
|
|
229
|
-
'<div style="font-size:12px;word-break:break-word;max-height:400px;overflow-y:auto">' + renderMd(m.conclusion.content || '') + '</div></div>';
|
|
234
|
+
'<div class="meeting-scroll-panel" style="font-size:12px;word-break:break-word;max-height:400px;overflow-y:auto">' + renderMd(m.conclusion.content || '') + '</div></div>';
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
// Human notes
|
|
@@ -271,12 +276,12 @@ function _renderMeetingDetail(m) {
|
|
|
271
276
|
|
|
272
277
|
document.getElementById('modal-title').textContent = 'Meeting: ' + m.title;
|
|
273
278
|
var body = document.getElementById('modal-body');
|
|
274
|
-
var
|
|
279
|
+
var scrollState = captureDashboardScrollState(body, [':self', '.meeting-scroll-panel']);
|
|
275
280
|
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled markdown fields before assembling HTML (see dashboard/js/utils.js); ids and labels use escHtml()
|
|
276
281
|
body.innerHTML = html;
|
|
277
282
|
body.style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
278
283
|
body.style.whiteSpace = 'normal';
|
|
279
|
-
body
|
|
284
|
+
restoreDashboardScrollState(body, scrollState);
|
|
280
285
|
|
|
281
286
|
// Wire up doc-chat Q&A panel for the meeting transcript
|
|
282
287
|
const transcript = (m.transcript || []).map(t =>
|
|
@@ -290,16 +290,19 @@ function _renderPipelineTriggerLabel(cron) {
|
|
|
290
290
|
return escHtml(human) + ' <span style="opacity:0.6">(' + escHtml(tz) + ')</span>';
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
function renderPipelines(pipelines) {
|
|
293
|
+
function renderPipelines(pipelines, opts) {
|
|
294
|
+
opts = opts || {};
|
|
294
295
|
pipelines = (pipelines || []).filter(function(p) { return !isDeleted('pipeline:' + p.id); });
|
|
295
296
|
_pipelinesData = pipelines;
|
|
296
297
|
const el = document.getElementById('pipelines-content');
|
|
297
298
|
const countEl = document.getElementById('pipelines-count');
|
|
298
299
|
if (!el) return;
|
|
300
|
+
const scrollState = opts.preserveScroll === false ? null : captureDashboardScrollState(el, ['.pl-node-chain']);
|
|
299
301
|
if (!pipelines || pipelines.length === 0) {
|
|
300
302
|
countEl.textContent = '0';
|
|
301
303
|
el.innerHTML = '<p class="empty">No pipelines yet. Create one to chain stages like audit \u2192 meeting \u2192 plan \u2192 merge.</p>';
|
|
302
304
|
_pipelineTotalPages = 1;
|
|
305
|
+
restoreDashboardScrollState(el, scrollState);
|
|
303
306
|
return;
|
|
304
307
|
}
|
|
305
308
|
countEl.textContent = pipelines.length;
|
|
@@ -360,23 +363,29 @@ function renderPipelines(pipelines) {
|
|
|
360
363
|
'<button class="pr-pager-btn ' + (_pipelinePage >= totalPages - 1 ? 'disabled' : '') + '" onclick="_pipelineNext()">Next</button>' +
|
|
361
364
|
'</div></div>');
|
|
362
365
|
}
|
|
366
|
+
restoreDashboardScrollState(el, scrollState);
|
|
363
367
|
}
|
|
364
368
|
|
|
365
369
|
function _pipelinePrev() {
|
|
366
370
|
if (_pipelinePage > 0) {
|
|
367
371
|
_pipelinePage--;
|
|
368
|
-
renderPipelines(_pipelinesData);
|
|
372
|
+
renderPipelines(_pipelinesData, { preserveScroll: false });
|
|
369
373
|
}
|
|
370
374
|
}
|
|
371
375
|
|
|
372
376
|
function _pipelineNext() {
|
|
373
377
|
if (_pipelinePage < _pipelineTotalPages - 1) {
|
|
374
378
|
_pipelinePage++;
|
|
375
|
-
renderPipelines(_pipelinesData);
|
|
379
|
+
renderPipelines(_pipelinesData, { preserveScroll: false });
|
|
376
380
|
}
|
|
377
381
|
}
|
|
378
382
|
|
|
379
383
|
function openPipelineDetail(id) {
|
|
384
|
+
var modalBody = document.getElementById('modal-body');
|
|
385
|
+
var preserveModalScroll = document.getElementById('modal')?.classList?.contains('open') && _pipelinePollId === id;
|
|
386
|
+
var modalScrollState = preserveModalScroll
|
|
387
|
+
? captureDashboardScrollState(modalBody, [':self', '.pl-node-chain', '.pipeline-stage-output'])
|
|
388
|
+
: null;
|
|
380
389
|
_stopPipelinePoll();
|
|
381
390
|
var p = _pipelinesData.find(function(x) { return x.id === id; });
|
|
382
391
|
if (!p) { alert('Pipeline not found'); return; }
|
|
@@ -444,7 +453,7 @@ function openPipelineDetail(id) {
|
|
|
444
453
|
'</div>' +
|
|
445
454
|
_renderMonitoredResources(s.monitoredResources || []) +
|
|
446
455
|
_renderArtifactLinks(stageRun.artifacts, id) +
|
|
447
|
-
(stageRun.output ? '<div style="margin-top:6px;font-size:11px;max-height:150px;overflow-y:auto">' + renderMd(stageRun.output.slice(0, 500)) + '</div>' : '') +
|
|
456
|
+
(stageRun.output ? '<div class="pipeline-stage-output" style="margin-top:6px;font-size:11px;max-height:150px;overflow-y:auto">' + renderMd(stageRun.output.slice(0, 500)) + '</div>' : '') +
|
|
448
457
|
(stageStatus === 'waiting-human' ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);border-color:var(--green);margin-top:6px" onclick="_continuePipeline(\'' + escHtml(id) + '\',\'' + escHtml(s.id) + '\',this)">Continue</button>' : '') +
|
|
449
458
|
'</div>';
|
|
450
459
|
});
|
|
@@ -477,7 +486,8 @@ function openPipelineDetail(id) {
|
|
|
477
486
|
|
|
478
487
|
document.getElementById('modal-title').textContent = 'Pipeline: ' + displayTitle;
|
|
479
488
|
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes user-controlled stage output before assembling HTML; all other user data wrapped in escHtml() (see dashboard/js/utils.js)
|
|
480
|
-
|
|
489
|
+
modalBody.innerHTML = html;
|
|
490
|
+
restoreDashboardScrollState(modalBody, modalScrollState);
|
|
481
491
|
document.getElementById('modal').classList.add('open');
|
|
482
492
|
|
|
483
493
|
// Live-poll while modal is open — always poll (not just active runs)
|
|
@@ -160,6 +160,7 @@ function _renderPrdWorkItemIdBadge(workItemId, prdItemId, size, padding) {
|
|
|
160
160
|
function renderPrdProgress(prog) {
|
|
161
161
|
const el = document.getElementById('prd-progress-content');
|
|
162
162
|
const countEl = document.getElementById('prd-progress-count');
|
|
163
|
+
const scrollState = captureDashboardScrollState(el, ['.prd-items-list', '.pl-node-chain']);
|
|
163
164
|
if (!prog) { el.innerHTML = ''; countEl.textContent = '—'; return; }
|
|
164
165
|
const visibleItems = (prog.items || []).filter(i => !isDeleted('plan:' + (i.source || '')) && !isDeleted(_prdItemDeleteKey(i.source, i.id)));
|
|
165
166
|
|
|
@@ -654,6 +655,7 @@ function renderPrdProgress(prog) {
|
|
|
654
655
|
|
|
655
656
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() or renderMd() by PRD renderers (fields: item ids/names/descriptions, source, projects, PR links, branches, agent labels)
|
|
656
657
|
el.innerHTML = html;
|
|
658
|
+
restoreDashboardScrollState(el, scrollState);
|
|
657
659
|
restoreNotifBadges();
|
|
658
660
|
}
|
|
659
661
|
|
|
@@ -207,7 +207,21 @@ function prTableHtml(rows) {
|
|
|
207
207
|
'</tr></thead><tbody>' + rows + '</tbody></table></div>';
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
function
|
|
210
|
+
function _capturePrScrollState(el) {
|
|
211
|
+
const tableWrap = el ? el.querySelector('.pr-table-wrap') : null;
|
|
212
|
+
return tableWrap ? { left: tableWrap.scrollLeft || 0, top: tableWrap.scrollTop || 0 } : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function _restorePrScrollState(el, state) {
|
|
216
|
+
if (!state) return;
|
|
217
|
+
const tableWrap = el ? el.querySelector('.pr-table-wrap') : null;
|
|
218
|
+
if (!tableWrap) return;
|
|
219
|
+
tableWrap.scrollLeft = state.left || 0;
|
|
220
|
+
tableWrap.scrollTop = state.top || 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderPrs(prs, opts) {
|
|
224
|
+
opts = opts || {};
|
|
211
225
|
prs = prs.filter(p => !isDeleted('pr:' + p.id));
|
|
212
226
|
allPrs = prs;
|
|
213
227
|
const el = document.getElementById('pr-content');
|
|
@@ -218,6 +232,7 @@ function renderPrs(prs) {
|
|
|
218
232
|
return;
|
|
219
233
|
}
|
|
220
234
|
const totalPages = Math.ceil(prs.length / PR_PER_PAGE);
|
|
235
|
+
const previousPage = prPage;
|
|
221
236
|
if (prPage >= totalPages) prPage = totalPages - 1;
|
|
222
237
|
const start = prPage * PR_PER_PAGE;
|
|
223
238
|
const pagePrs = prs.slice(start, start + PR_PER_PAGE);
|
|
@@ -235,18 +250,16 @@ function renderPrs(prs) {
|
|
|
235
250
|
'</div>';
|
|
236
251
|
}
|
|
237
252
|
|
|
238
|
-
const
|
|
239
|
-
|
|
253
|
+
const savedScroll = opts.preserveScroll === false || previousPage !== prPage
|
|
254
|
+
? null
|
|
255
|
+
: _capturePrScrollState(el);
|
|
240
256
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all PR user data wrapped in escapeHtml() by prRow() (fields: PR id, title, description, agent, branch, review/build/status labels)
|
|
241
257
|
el.innerHTML = prTableHtml(rows) + pager;
|
|
242
|
-
|
|
243
|
-
const newWrap = el.querySelector('.pr-table-wrap');
|
|
244
|
-
if (newWrap) newWrap.scrollLeft = savedScroll;
|
|
245
|
-
}
|
|
258
|
+
_restorePrScrollState(el, savedScroll);
|
|
246
259
|
}
|
|
247
260
|
|
|
248
|
-
function prPrev() { if (prPage > 0) { prPage--; renderPrs(allPrs); } }
|
|
249
|
-
function prNext() { const totalPages = Math.ceil(allPrs.length / PR_PER_PAGE); if (prPage < totalPages-1) { prPage++; renderPrs(allPrs); } }
|
|
261
|
+
function prPrev() { if (prPage > 0) { prPage--; renderPrs(allPrs, { preserveScroll: false }); } }
|
|
262
|
+
function prNext() { const totalPages = Math.ceil(allPrs.length / PR_PER_PAGE); if (prPage < totalPages-1) { prPage++; renderPrs(allPrs, { preserveScroll: false }); } }
|
|
250
263
|
|
|
251
264
|
function openAllPrs() {
|
|
252
265
|
const modalEl = document.querySelector('#modal .modal');
|
|
@@ -247,7 +247,7 @@ function _renderViewToggle() {
|
|
|
247
247
|
|
|
248
248
|
function _schedSetView(mode) {
|
|
249
249
|
_schedViewMode = mode;
|
|
250
|
-
renderSchedules(window._lastSchedules || []);
|
|
250
|
+
renderSchedules(window._lastSchedules || [], { preserveScroll: false });
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
function _renderScheduleCalendar(schedules) {
|
|
@@ -310,14 +310,17 @@ function _renderScheduleCalendar(schedules) {
|
|
|
310
310
|
return html;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
function renderSchedules(schedules) {
|
|
313
|
+
function renderSchedules(schedules, opts) {
|
|
314
|
+
opts = opts || {};
|
|
314
315
|
schedules = schedules.filter(function(s) { return !isDeleted('sched:' + s.id); });
|
|
315
316
|
const el = document.getElementById('scheduled-content');
|
|
316
317
|
const countEl = document.getElementById('scheduled-count');
|
|
317
318
|
countEl.textContent = schedules.length;
|
|
318
319
|
window._lastSchedules = schedules;
|
|
320
|
+
const scrollState = opts.preserveScroll === false ? null : captureDashboardScrollState(el, ['.pr-table-wrap']);
|
|
319
321
|
if (!schedules.length) {
|
|
320
322
|
el.innerHTML = '<p class="empty">No scheduled tasks. Add one to automate recurring work.</p>';
|
|
323
|
+
restoreDashboardScrollState(el, scrollState);
|
|
321
324
|
return;
|
|
322
325
|
}
|
|
323
326
|
|
|
@@ -371,10 +374,11 @@ function renderSchedules(schedules) {
|
|
|
371
374
|
|
|
372
375
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() by schedule renderers (fields: schedule id/title/cron/type/project/agent/description)
|
|
373
376
|
el.innerHTML = html;
|
|
377
|
+
restoreDashboardScrollState(el, scrollState);
|
|
374
378
|
}
|
|
375
379
|
|
|
376
|
-
function _schedPrev() { if (_schedPage > 0) { _schedPage--; renderSchedules(window._lastSchedules || []); } }
|
|
377
|
-
function _schedNext() { var tp = Math.ceil((window._lastSchedules || []).length / SCHED_PER_PAGE); if (_schedPage < tp-1) { _schedPage++; renderSchedules(window._lastSchedules || []); } }
|
|
380
|
+
function _schedPrev() { if (_schedPage > 0) { _schedPage--; renderSchedules(window._lastSchedules || [], { preserveScroll: false }); } }
|
|
381
|
+
function _schedNext() { var tp = Math.ceil((window._lastSchedules || []).length / SCHED_PER_PAGE); if (_schedPage < tp-1) { _schedPage++; renderSchedules(window._lastSchedules || [], { preserveScroll: false }); } }
|
|
378
382
|
|
|
379
383
|
function openScheduleDetail(id) {
|
|
380
384
|
const s = (window._lastSchedules || []).find(x => x.id === id);
|
|
@@ -405,4 +405,36 @@ function pinButton(pinKey, pinned, source, opts) {
|
|
|
405
405
|
(pinned ? 'Unpin' : 'Pin') + '</button>';
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
|
|
408
|
+
function _scrollNodesForSelector(root, selector) {
|
|
409
|
+
if (!root || !selector) return [];
|
|
410
|
+
if (selector === ':self') return [root];
|
|
411
|
+
var out = [];
|
|
412
|
+
if (root.matches && root.matches(selector)) out.push(root);
|
|
413
|
+
if (root.querySelectorAll) {
|
|
414
|
+
root.querySelectorAll(selector).forEach(function(el) { out.push(el); });
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function captureDashboardScrollState(root, selectors) {
|
|
420
|
+
var state = [];
|
|
421
|
+
(selectors || []).forEach(function(selector) {
|
|
422
|
+
var nodes = _scrollNodesForSelector(root, selector);
|
|
423
|
+
nodes.forEach(function(el, index) {
|
|
424
|
+
state.push({ selector: selector, index: index, top: el.scrollTop || 0, left: el.scrollLeft || 0 });
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
return state;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function restoreDashboardScrollState(root, state) {
|
|
431
|
+
(state || []).forEach(function(item) {
|
|
432
|
+
var nodes = _scrollNodesForSelector(root, item.selector);
|
|
433
|
+
var el = nodes[item.index];
|
|
434
|
+
if (!el) return;
|
|
435
|
+
el.scrollTop = item.top || 0;
|
|
436
|
+
el.scrollLeft = item.left || 0;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
window.MinionsRenderUtils = { formatToolSummary, renderAgentOutput, renderTerminalBanner, renderPager, pinButton, captureDashboardScrollState, restoreDashboardScrollState };
|
|
@@ -133,16 +133,19 @@ function _parseIntervalStr(s) {
|
|
|
133
133
|
let _watchPage = 0;
|
|
134
134
|
const WATCH_PER_PAGE = 15;
|
|
135
135
|
|
|
136
|
-
function renderWatches(watchesData) {
|
|
136
|
+
function renderWatches(watchesData, opts) {
|
|
137
|
+
opts = opts || {};
|
|
137
138
|
var watches = (watchesData || []).filter(function(w) { return !isDeleted('watch:' + w.id); });
|
|
138
139
|
var el = document.getElementById('watches-content');
|
|
139
140
|
var countEl = document.getElementById('watches-count');
|
|
140
141
|
if (!el) return;
|
|
141
142
|
if (countEl) countEl.textContent = watches.length;
|
|
142
143
|
window._lastWatches = watches;
|
|
144
|
+
var scrollState = opts.preserveScroll === false ? null : captureDashboardScrollState(el, ['.pr-table-wrap']);
|
|
143
145
|
|
|
144
146
|
if (!watches.length) {
|
|
145
147
|
el.innerHTML = '<p class="empty">No active watches. Create one to monitor PRs or work items.</p>';
|
|
148
|
+
restoreDashboardScrollState(el, scrollState);
|
|
146
149
|
return;
|
|
147
150
|
}
|
|
148
151
|
|
|
@@ -205,10 +208,11 @@ function renderWatches(watchesData) {
|
|
|
205
208
|
|
|
206
209
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: watch id/target/description/action type/owner/timestamps)
|
|
207
210
|
el.innerHTML = html;
|
|
211
|
+
restoreDashboardScrollState(el, scrollState);
|
|
208
212
|
}
|
|
209
213
|
|
|
210
|
-
function _watchPrev() { if (_watchPage > 0) { _watchPage--; renderWatches(window._lastWatches || []); } }
|
|
211
|
-
function _watchNext() { var tp = Math.ceil((window._lastWatches || []).length / WATCH_PER_PAGE); if (_watchPage < tp - 1) { _watchPage++; renderWatches(window._lastWatches || []); } }
|
|
214
|
+
function _watchPrev() { if (_watchPage > 0) { _watchPage--; renderWatches(window._lastWatches || [], { preserveScroll: false }); } }
|
|
215
|
+
function _watchNext() { var tp = Math.ceil((window._lastWatches || []).length / WATCH_PER_PAGE); if (_watchPage < tp - 1) { _watchPage++; renderWatches(window._lastWatches || [], { preserveScroll: false }); } }
|
|
212
216
|
|
|
213
217
|
// ─── Detail Modal ───────────────────────────────────────────────────────────
|
|
214
218
|
|
|
@@ -177,7 +177,8 @@ function wiRow(item) {
|
|
|
177
177
|
'</tr>';
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
function renderWorkItems(items) {
|
|
180
|
+
function renderWorkItems(items, opts) {
|
|
181
|
+
opts = opts || {};
|
|
181
182
|
items = items.filter(function(w) { return !isDeleted('wi:' + w.id); });
|
|
182
183
|
// Sort: active/dispatched first, then by most recent activity
|
|
183
184
|
const statusOrder = { dispatched: 0, pending: 1, queued: 1, failed: 2, done: 3 };
|
|
@@ -198,6 +199,7 @@ function renderWorkItems(items) {
|
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
const totalPages = Math.ceil(items.length / WI_PER_PAGE);
|
|
202
|
+
const previousPage = wiPage;
|
|
201
203
|
if (wiPage >= totalPages) wiPage = totalPages - 1;
|
|
202
204
|
const start = wiPage * WI_PER_PAGE;
|
|
203
205
|
const pageItems = items.slice(start, start + WI_PER_PAGE);
|
|
@@ -217,14 +219,12 @@ function renderWorkItems(items) {
|
|
|
217
219
|
'</div>';
|
|
218
220
|
}
|
|
219
221
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
+
const scrollState = opts.preserveScroll === false || previousPage !== wiPage
|
|
223
|
+
? null
|
|
224
|
+
: captureDashboardScrollState(el, ['.pr-table-wrap']);
|
|
222
225
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() by wiRow() (fields: work item id/title/source/status/agent/PR links/dates/follow-up metadata)
|
|
223
226
|
el.innerHTML = html;
|
|
224
|
-
|
|
225
|
-
const newWrap = el.querySelector('.pr-table-wrap');
|
|
226
|
-
if (newWrap) newWrap.scrollLeft = savedScroll;
|
|
227
|
-
}
|
|
227
|
+
restoreDashboardScrollState(el, scrollState);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
async function editWorkItem(id, source) {
|
|
@@ -437,8 +437,8 @@ async function retryWorkItem(id, source) {
|
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems); } }
|
|
441
|
-
function wiNext() { const tp = Math.ceil(allWorkItems.length / WI_PER_PAGE); if (wiPage < tp-1) { wiPage++; renderWorkItems(allWorkItems); } }
|
|
440
|
+
function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems, { preserveScroll: false }); } }
|
|
441
|
+
function wiNext() { const tp = Math.ceil(allWorkItems.length / WI_PER_PAGE); if (wiPage < tp-1) { wiPage++; renderWorkItems(allWorkItems, { preserveScroll: false }); } }
|
|
442
442
|
|
|
443
443
|
let _feedbackRating = null;
|
|
444
444
|
function feedbackWorkItem(id, source) {
|
package/dashboard/js/utils.js
CHANGED
|
@@ -53,8 +53,8 @@ function kbPinKey(cat, file) { return 'knowledge/' + cat + '/' + file; }
|
|
|
53
53
|
function _togglePinAndRefresh(key, source) {
|
|
54
54
|
var pinned = togglePin(key);
|
|
55
55
|
showToast('cmd-toast', pinned ? 'Pinned to top' : 'Unpinned', true);
|
|
56
|
-
if (source === 'inbox') renderInbox(inboxData);
|
|
57
|
-
else if (source === 'kb') renderKnowledgeBase();
|
|
56
|
+
if (source === 'inbox') renderInbox(inboxData, { preserveScroll: false });
|
|
57
|
+
else if (source === 'kb') renderKnowledgeBase({ preserveScroll: false });
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// Modal navigation stack — enables back button when opening a modal from another modal
|
|
@@ -93,8 +93,8 @@ function toggleModalPin() {
|
|
|
93
93
|
var pinned = togglePin(_modalFilePath);
|
|
94
94
|
showToast('cmd-toast', pinned ? 'Pinned to top' : 'Unpinned', true);
|
|
95
95
|
updateModalPinBtn();
|
|
96
|
-
if (_modalFilePath.startsWith('notes/inbox/')) renderInbox(inboxData);
|
|
97
|
-
else if (_modalFilePath.startsWith('knowledge/')) renderKnowledgeBase();
|
|
96
|
+
if (_modalFilePath.startsWith('notes/inbox/')) renderInbox(inboxData, { preserveScroll: false });
|
|
97
|
+
else if (_modalFilePath.startsWith('knowledge/')) renderKnowledgeBase({ preserveScroll: false });
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Canonical HTML-escape helper (SEC-03). Use this in all new code and for any user-controlled
|
package/dashboard-build.js
CHANGED
|
@@ -29,7 +29,7 @@ function buildDashboardHtml() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const jsFiles = [
|
|
32
|
-
'utils', 'state', 'features-client', 'detail-panel', 'live-stream',
|
|
32
|
+
'utils', 'state', 'features-client', 'render-utils', 'detail-panel', 'live-stream',
|
|
33
33
|
'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
|
|
34
34
|
'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
|
|
35
35
|
'render-other', 'render-managed', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
|
package/docs/deprecated.json
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
1
|
[
|
|
2
|
-
{
|
|
3
|
-
"id": "status-retention-stale-default-migration",
|
|
4
|
-
"description": "applyStatusWorkItemsRetentionMigration + applyStatusMeetingsRetentionMigration: one-time migrations that drop engine.statusWorkItemsRetentionDays=7 and engine.statusMeetingsRetentionDays=7 from loaded config so the new defaults (0, no trim) reach installs whose config.json was persisted while 7 was the baked-in default. The 7-day cutoffs were added alongside the slim projections (W-mphejzmj000718bf for workItems, W-mphlrxx6000a8760 for meetings) but surfaced as data loss to users (completed rows disappearing from /api/status after a week). The slim projections (which deliver the bulk of the payload savings) still run unconditionally; only the date filters were demoted to opt-in. shared.js mutates the in-memory config; engine/cli.js follows up with guarded shared.mutateJsonFileLocked calls on config.json so the fields are also removed on disk — affected installs are permanently corrected the first time the engine boots after this ships.",
|
|
5
|
-
"code": [
|
|
6
|
-
{ "file": "engine/shared.js", "note": "applyStatusWorkItemsRetentionMigration / applyStatusMeetingsRetentionMigration definitions + _resetStaleRetentionMigrationFlag / _resetStaleMeetingsRetentionMigrationFlag test helpers — pure, in-memory" },
|
|
7
|
-
{ "file": "engine/cli.js", "note": "Two boot blocks inside start() — each applies the in-memory pass THEN mutateJsonFileLocked on config.json to delete the field with skipWriteIfUnchanged so non-7 installs don't churn the file" },
|
|
8
|
-
{ "file": "dashboard.js", "note": "Initial CONFIG load + reloadConfig() both apply the in-memory migrations so the dashboard picks up the new defaults even if it boots before the engine writes config.json" }
|
|
9
|
-
],
|
|
10
|
-
"deprecated": "2026-05-29",
|
|
11
|
-
"targetRemovalDate": "2026-06-01",
|
|
12
|
-
"cleanup": "On or after 2026-06-01 (3 days), delete both migration functions + their reset helpers from engine/shared.js (including the shared.js export lines), the boot calls + mutateJsonFileLocked blocks in engine/cli.js, the call sites in dashboard.js, the tests in test/unit/status-workitems-retention.test.js and test/unit/status-meetings-retention.test.js gated on the function names, and this entry. Three days is enough because the on-disk rewrite happens on first engine boot — any install whose engine has rebooted at least once since this shipped has already had its config.json corrected and is no longer dependent on the in-memory shim.",
|
|
13
|
-
"notes": "Persistent custom values (any non-7 integer) are preserved untouched. The only at-risk users are those who explicitly want a 7-day window — they can re-set via the Settings page after removal. If a user never restarts the engine in the 3-day window, the shim still strips the persisted 7 on dashboard-only boot via the in-memory pass; the on-disk write is a hardening pass for the common case, not a strict requirement."
|
|
14
|
-
},
|
|
15
2
|
{
|
|
16
3
|
"id": "config-poll-key-migration",
|
|
17
4
|
"location": "engine/queries.js:126-163",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2090",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|