@yemi33/minions 0.1.2131 → 0.1.2133
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 +202 -29
- package/dashboard/js/render-watches.js +2 -2
- package/dashboard/pages/home.html +2 -2
- package/dashboard/pages/inbox.html +2 -2
- package/dashboard/pages/meetings.html +1 -1
- package/dashboard/pages/pipelines.html +1 -1
- package/dashboard/pages/plans.html +1 -1
- package/dashboard/pages/prs.html +1 -1
- package/dashboard/pages/schedule.html +1 -1
- package/dashboard/pages/watches.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/styles.css +28 -0
- package/dashboard.js +63 -2
- package/package.json +1 -1
|
@@ -7,6 +7,27 @@ let _steerInFlight = false;
|
|
|
7
7
|
let _lastRenderedText = '';
|
|
8
8
|
let _runtimeTimer = null;
|
|
9
9
|
|
|
10
|
+
// W-mq1jbd8z0008dfa0 — scrollback state. The tail-region wrapper keeps
|
|
11
|
+
// prepended scrollback chunks safe when the 3s poll re-renders the tail.
|
|
12
|
+
//
|
|
13
|
+
// _oldestLoadedByte — byte offset of the OLDEST bytes currently rendered in
|
|
14
|
+
// #live-messages (anything older lives on disk).
|
|
15
|
+
// _logFileSize — most recent X-Log-Size value from the server. Used to
|
|
16
|
+
// seed _oldestLoadedByte on the first tail poll.
|
|
17
|
+
// _scrollbackFetchInFlight — guard so the scroll handler can't fire concurrent
|
|
18
|
+
// pagination requests.
|
|
19
|
+
// _tailRegionAgentId — agentId the current tail region belongs to. Switching
|
|
20
|
+
// agents resets all scrollback state so chunks from a
|
|
21
|
+
// previous agent never bleed into the new tail.
|
|
22
|
+
let _oldestLoadedByte = 0;
|
|
23
|
+
let _logFileSize = 0;
|
|
24
|
+
let _scrollbackFetchInFlight = false;
|
|
25
|
+
let _tailRegionAgentId = null;
|
|
26
|
+
|
|
27
|
+
const SCROLLBACK_CHUNK = 65536; // 64KB per scroll-up
|
|
28
|
+
const SCROLLBACK_TRIGGER_PX = 200; // load older content when within 200px of top
|
|
29
|
+
const TAIL_REGION_ID = 'live-tail-region';
|
|
30
|
+
|
|
10
31
|
function _currentAgentRuntime() {
|
|
11
32
|
var agent = (agentData || []).find(function(a) { return a.id === currentAgentId; });
|
|
12
33
|
return agent && agent.runtime ? agent.runtime : '';
|
|
@@ -25,16 +46,34 @@ function _updateRuntimeCounter() {
|
|
|
25
46
|
el.textContent = (hr > 0 ? hr + 'h ' : '') + min + 'm ' + sec + 's';
|
|
26
47
|
}
|
|
27
48
|
|
|
49
|
+
// Resolve (or lazily create) the inner tail-region wrapper. All tail-poll
|
|
50
|
+
// renders write into this region; scrollback chunks live OUTSIDE it (above),
|
|
51
|
+
// so a tail re-render never clobbers the prepended scrollback.
|
|
52
|
+
function _ensureTailRegion() {
|
|
53
|
+
const container = document.getElementById('live-messages');
|
|
54
|
+
if (!container) return null;
|
|
55
|
+
let region = document.getElementById(TAIL_REGION_ID);
|
|
56
|
+
if (!region) {
|
|
57
|
+
region = document.createElement('div');
|
|
58
|
+
region.id = TAIL_REGION_ID;
|
|
59
|
+
region.style.display = 'contents'; // transparent flex container
|
|
60
|
+
container.appendChild(region);
|
|
61
|
+
}
|
|
62
|
+
return region;
|
|
63
|
+
}
|
|
64
|
+
|
|
28
65
|
function renderLiveChatMessage(raw) {
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
66
|
+
const container = document.getElementById('live-messages');
|
|
67
|
+
if (!container) return;
|
|
68
|
+
const region = _ensureTailRegion();
|
|
69
|
+
if (!region) return;
|
|
31
70
|
const html = renderAgentOutput(raw);
|
|
32
71
|
// eslint-disable-next-line no-unsanitized/method -- reason: renderAgentOutput() escapes all user-controlled fields before assembling HTML (see dashboard/js/render-utils.js)
|
|
33
|
-
if (html)
|
|
72
|
+
if (html) region.insertAdjacentHTML('beforeend', html);
|
|
34
73
|
|
|
35
74
|
// Auto-scroll
|
|
36
|
-
if (
|
|
37
|
-
|
|
75
|
+
if (container.scrollHeight - container.scrollTop - container.clientHeight < 150) {
|
|
76
|
+
container.scrollTop = container.scrollHeight;
|
|
38
77
|
}
|
|
39
78
|
}
|
|
40
79
|
|
|
@@ -45,6 +84,12 @@ function startLiveStream(agentId) {
|
|
|
45
84
|
const msgEl = document.getElementById('live-messages');
|
|
46
85
|
if (msgEl) msgEl.innerHTML = '';
|
|
47
86
|
_lastRenderedText = '';
|
|
87
|
+
_oldestLoadedByte = 0;
|
|
88
|
+
_logFileSize = 0;
|
|
89
|
+
_scrollbackFetchInFlight = false;
|
|
90
|
+
_tailRegionAgentId = agentId;
|
|
91
|
+
_attachScrollHandler();
|
|
92
|
+
_updateJumpToLatestButton();
|
|
48
93
|
|
|
49
94
|
// W-mpob4nyk0006580e — clear any stale banner from a previous agent.
|
|
50
95
|
const bannerEl = document.getElementById('live-terminal-banner');
|
|
@@ -123,30 +168,156 @@ async function refreshLiveOutput() {
|
|
|
123
168
|
if (!currentAgentId || currentTab !== 'live') { stopLivePolling(); return; }
|
|
124
169
|
if (_steerInFlight) return; // Don't clobber immediate steering feedback
|
|
125
170
|
try {
|
|
126
|
-
const
|
|
171
|
+
const r = await safeFetch('/api/agent/' + currentAgentId + '/live?tail=16384');
|
|
172
|
+
const text = await r.text();
|
|
173
|
+
// X-Log-Size lets us seed _oldestLoadedByte on the first tick + know
|
|
174
|
+
// whether more older content exists above what's currently rendered.
|
|
175
|
+
const logSizeHeader = r.headers.get('X-Log-Size');
|
|
176
|
+
if (logSizeHeader != null) _logFileSize = parseInt(logSizeHeader, 10) || 0;
|
|
127
177
|
const el = document.getElementById('live-messages');
|
|
128
|
-
if (el)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
} else {
|
|
137
|
-
el.innerHTML = '';
|
|
138
|
-
renderLiveChatMessage(text);
|
|
139
|
-
}
|
|
140
|
-
_lastRenderedText = text;
|
|
141
|
-
if (wasAtBottom) el.scrollTop = el.scrollHeight;
|
|
142
|
-
else {
|
|
143
|
-
el.scrollTop = savedScrollTop;
|
|
144
|
-
el.scrollLeft = savedScrollLeft;
|
|
145
|
-
}
|
|
178
|
+
if (!el) return;
|
|
179
|
+
const region = _ensureTailRegion();
|
|
180
|
+
if (!region) return;
|
|
181
|
+
// Seed _oldestLoadedByte the first time we have a server file-size.
|
|
182
|
+
// The tail fetch covers the last min(16384, _logFileSize) bytes, so the
|
|
183
|
+
// oldest byte currently rendered is that lower bound.
|
|
184
|
+
if (_logFileSize > 0 && _oldestLoadedByte === 0 && _lastRenderedText === '') {
|
|
185
|
+
_oldestLoadedByte = Math.max(0, _logFileSize - 16384);
|
|
146
186
|
}
|
|
187
|
+
const wasAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
|
|
188
|
+
const savedScrollTop = el.scrollTop;
|
|
189
|
+
const savedScrollLeft = el.scrollLeft;
|
|
190
|
+
const incrementalSafe = _currentAgentRuntime() !== 'copilot';
|
|
191
|
+
// Incremental render: only parse new content if text is an extension of previous
|
|
192
|
+
if (incrementalSafe && _lastRenderedText && text.length > _lastRenderedText.length && text.startsWith(_lastRenderedText.slice(0, 200))) {
|
|
193
|
+
renderLiveChatMessage(text.slice(_lastRenderedText.length));
|
|
194
|
+
} else {
|
|
195
|
+
// Clear the tail region only (NOT the whole #live-messages) so
|
|
196
|
+
// scrollback chunks prepended above the region survive the rerender.
|
|
197
|
+
region.innerHTML = '';
|
|
198
|
+
renderLiveChatMessage(text);
|
|
199
|
+
}
|
|
200
|
+
_lastRenderedText = text;
|
|
201
|
+
if (wasAtBottom) el.scrollTop = el.scrollHeight;
|
|
202
|
+
else {
|
|
203
|
+
el.scrollTop = savedScrollTop;
|
|
204
|
+
el.scrollLeft = savedScrollLeft;
|
|
205
|
+
}
|
|
206
|
+
_updateJumpToLatestButton();
|
|
147
207
|
} catch (e) { console.error('live-stream reload:', e.message); }
|
|
148
208
|
}
|
|
149
209
|
|
|
210
|
+
// W-mq1jbd8z0008dfa0 — scrollback: load older transcript content when the
|
|
211
|
+
// user scrolls near the top.
|
|
212
|
+
async function _loadOlderScrollback() {
|
|
213
|
+
if (_scrollbackFetchInFlight) return;
|
|
214
|
+
if (_oldestLoadedByte <= 0) return;
|
|
215
|
+
if (!currentAgentId || _tailRegionAgentId !== currentAgentId) return;
|
|
216
|
+
_scrollbackFetchInFlight = true;
|
|
217
|
+
const container = document.getElementById('live-messages');
|
|
218
|
+
if (!container) { _scrollbackFetchInFlight = false; return; }
|
|
219
|
+
// Loading indicator at the top — structural HTML string literal, no user
|
|
220
|
+
// data interpolated, so lint passes without a disable directive.
|
|
221
|
+
const loadingId = '_scrollback-loading';
|
|
222
|
+
container.insertAdjacentHTML('afterbegin', '<div id="' + loadingId + '" style="padding:4px 8px;color:var(--muted);font-size:var(--text-sm);font-style:italic;text-align:center">Loading older\u2026</div>');
|
|
223
|
+
const from = Math.max(0, _oldestLoadedByte - SCROLLBACK_CHUNK);
|
|
224
|
+
const to = _oldestLoadedByte;
|
|
225
|
+
try {
|
|
226
|
+
const r = await safeFetch('/api/agent/' + currentAgentId + '/live-output?from=' + from + '&to=' + to);
|
|
227
|
+
const text = await r.text();
|
|
228
|
+
const logSizeHeader = r.headers.get('X-Log-Size');
|
|
229
|
+
if (logSizeHeader != null) _logFileSize = parseInt(logSizeHeader, 10) || _logFileSize;
|
|
230
|
+
const html = renderAgentOutput(text);
|
|
231
|
+
// Capture scrollHeight BEFORE prepend so we can restore the user's visible
|
|
232
|
+
// position afterward — otherwise the scrollbar jumps as content stacks up.
|
|
233
|
+
const savedScrollHeight = container.scrollHeight;
|
|
234
|
+
const savedScrollTop = container.scrollTop;
|
|
235
|
+
// Remove the loading indicator
|
|
236
|
+
const loadingEl = document.getElementById(loadingId);
|
|
237
|
+
if (loadingEl) loadingEl.remove();
|
|
238
|
+
if (html) {
|
|
239
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: renderAgentOutput() escapes all user-controlled fields before assembling HTML
|
|
240
|
+
container.insertAdjacentHTML('afterbegin', html);
|
|
241
|
+
}
|
|
242
|
+
// Anchor the visible position so prepending doesn't visually jump the user.
|
|
243
|
+
container.scrollTop = savedScrollTop + (container.scrollHeight - savedScrollHeight);
|
|
244
|
+
_oldestLoadedByte = from;
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.error('scrollback fetch:', e.message);
|
|
247
|
+
const loadingEl = document.getElementById(loadingId);
|
|
248
|
+
if (loadingEl) loadingEl.remove();
|
|
249
|
+
} finally {
|
|
250
|
+
_scrollbackFetchInFlight = false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Wire scroll handler + "Jump to latest" button. Idempotent: re-attaches
|
|
255
|
+
// when the live tab re-renders the container.
|
|
256
|
+
function _attachScrollHandler() {
|
|
257
|
+
const container = document.getElementById('live-messages');
|
|
258
|
+
if (!container || container.dataset.scrollbackHooked === '1') return;
|
|
259
|
+
container.dataset.scrollbackHooked = '1';
|
|
260
|
+
container.addEventListener('scroll', function() {
|
|
261
|
+
// Load older content when within 200px of the top.
|
|
262
|
+
if (container.scrollTop <= SCROLLBACK_TRIGGER_PX && _oldestLoadedByte > 0 && !_scrollbackFetchInFlight) {
|
|
263
|
+
_loadOlderScrollback();
|
|
264
|
+
}
|
|
265
|
+
_updateJumpToLatestButton();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function _ensureJumpToLatestButton() {
|
|
270
|
+
// The button is positioned absolute inside #live-chat (already
|
|
271
|
+
// position:relative-friendly because of its flex column layout). Inject
|
|
272
|
+
// once; #live-chat is re-rendered on tab switch, which removes it, so
|
|
273
|
+
// this function lazy-creates as needed.
|
|
274
|
+
const liveChat = document.getElementById('live-chat');
|
|
275
|
+
if (!liveChat) return null;
|
|
276
|
+
// Ensure positioning context for absolute child.
|
|
277
|
+
if (liveChat.style.position !== 'relative' && getComputedStyle(liveChat).position === 'static') {
|
|
278
|
+
liveChat.style.position = 'relative';
|
|
279
|
+
}
|
|
280
|
+
let btn = document.getElementById('live-jump-latest');
|
|
281
|
+
if (!btn) {
|
|
282
|
+
btn = document.createElement('button');
|
|
283
|
+
btn.id = 'live-jump-latest';
|
|
284
|
+
btn.type = 'button';
|
|
285
|
+
btn.textContent = '\u2193 Jump to latest';
|
|
286
|
+
btn.style.cssText = [
|
|
287
|
+
'position:absolute',
|
|
288
|
+
'right:16px',
|
|
289
|
+
'bottom:96px',
|
|
290
|
+
'padding:6px 12px',
|
|
291
|
+
'background:var(--blue)',
|
|
292
|
+
'color:#fff',
|
|
293
|
+
'border:none',
|
|
294
|
+
'border-radius:var(--radius-sm)',
|
|
295
|
+
'font-size:var(--text-sm)',
|
|
296
|
+
'cursor:pointer',
|
|
297
|
+
'box-shadow:0 2px 6px rgba(0,0,0,0.25)',
|
|
298
|
+
'z-index:5',
|
|
299
|
+
'display:none',
|
|
300
|
+
].join(';');
|
|
301
|
+
btn.addEventListener('click', function() {
|
|
302
|
+
const container = document.getElementById('live-messages');
|
|
303
|
+
if (!container) return;
|
|
304
|
+
container.scrollTop = container.scrollHeight;
|
|
305
|
+
_updateJumpToLatestButton();
|
|
306
|
+
});
|
|
307
|
+
liveChat.appendChild(btn);
|
|
308
|
+
}
|
|
309
|
+
return btn;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function _updateJumpToLatestButton() {
|
|
313
|
+
const container = document.getElementById('live-messages');
|
|
314
|
+
if (!container) return;
|
|
315
|
+
const btn = _ensureJumpToLatestButton();
|
|
316
|
+
if (!btn) return;
|
|
317
|
+
const atBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 150;
|
|
318
|
+
btn.style.display = atBottom ? 'none' : 'block';
|
|
319
|
+
}
|
|
320
|
+
|
|
150
321
|
async function sendSteering() {
|
|
151
322
|
const input = document.getElementById('live-steer-input');
|
|
152
323
|
if (!input || !input.value.trim() || !currentAgentId) return;
|
|
@@ -156,13 +327,15 @@ async function sendSteering() {
|
|
|
156
327
|
// Pause polling so the immediate feedback isn't clobbered
|
|
157
328
|
_steerInFlight = true;
|
|
158
329
|
|
|
159
|
-
// Immediate feedback — show the message right away
|
|
160
|
-
|
|
161
|
-
|
|
330
|
+
// Immediate feedback — show the message right away. Render INTO the tail
|
|
331
|
+
// region so a subsequent scrollback prepend stays above the steering bubble.
|
|
332
|
+
const region = _ensureTailRegion();
|
|
333
|
+
const container = document.getElementById('live-messages');
|
|
334
|
+
if (region) {
|
|
162
335
|
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: steering message)
|
|
163
|
-
|
|
336
|
+
region.insertAdjacentHTML('beforeend', '<div style="align-self:flex-end;background:var(--blue);color:#fff;padding:6px 12px;border-radius:12px 12px 2px 12px;max-width:80%;margin:4px 0;font-size:var(--text-md)">' + escHtml(message) +
|
|
164
337
|
'<div id="steer-pending" style="font-size:var(--text-xs);opacity:0.7;margin-top:2px">\u2197 Sending...</div></div>');
|
|
165
|
-
|
|
338
|
+
if (container) container.scrollTop = container.scrollHeight;
|
|
166
339
|
}
|
|
167
340
|
showToast('cmd-toast', 'Steering message sent to ' + currentAgentId, true);
|
|
168
341
|
|
|
@@ -503,7 +503,7 @@ function _watchFormHtml() {
|
|
|
503
503
|
'<div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px;display:flex;flex-direction:column;gap:8px">' +
|
|
504
504
|
'<div style="color:var(--text);font-size:var(--text-md);display:flex;align-items:center;justify-content:space-between">' +
|
|
505
505
|
'<span>Follow-up Action(s) <span style="font-size:var(--text-sm);color:var(--muted)">(runs after the inbox notification when the watch fires; 1 step = single, 2+ = chain)</span></span>' +
|
|
506
|
-
'<button type="button" class="
|
|
506
|
+
'<button type="button" class="btn-add btn-add-lg" onclick="_addWatchStepRow()">+ Add step</button>' +
|
|
507
507
|
'</div>' +
|
|
508
508
|
'<div id="watch-edit-steps"></div>' +
|
|
509
509
|
'</div>' +
|
|
@@ -511,7 +511,7 @@ function _watchFormHtml() {
|
|
|
511
511
|
'<div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px;display:flex;flex-direction:column;gap:8px">' +
|
|
512
512
|
'<div style="color:var(--text);font-size:var(--text-md);display:flex;align-items:center;justify-content:space-between">' +
|
|
513
513
|
'<span>Requires <span style="font-size:var(--text-sm);color:var(--muted)">(cross-target AND-join — every requirement must also be true to fire)</span></span>' +
|
|
514
|
-
'<button type="button" class="
|
|
514
|
+
'<button type="button" class="btn-add btn-add-lg" onclick="_addRequireRow()">+ Add requirement</button>' +
|
|
515
515
|
'</div>' +
|
|
516
516
|
'<div id="watch-edit-requires"></div>' +
|
|
517
517
|
'</div>' +
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<span style="color:var(--blue);font-weight:600">Command Center</span>
|
|
13
13
|
<span id="cmd-powered-by">Ask anything, dispatch work, manage plans</span>
|
|
14
14
|
<button class="cmd-history-btn" onclick="cmdShowHistory()">Past Commands</button>
|
|
15
|
-
<button class="
|
|
15
|
+
<button class="btn-add btn-add-lg" onclick="openQuickNoteModal()">+ Note</button>
|
|
16
16
|
</div>
|
|
17
17
|
</section>
|
|
18
18
|
<section>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<div class="agents" id="agents-grid">Loading...</div>
|
|
21
21
|
</section>
|
|
22
22
|
<section>
|
|
23
|
-
<h2>Pinned Context <button class="
|
|
23
|
+
<h2>Pinned Context <button class="btn-add" style="margin-left:8px" onclick="openPinNoteModal()">+ Pin</button> <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">injected into all agent prompts</span></h2>
|
|
24
24
|
<div id="pinned-content"><p class="empty">No pinned notes.</p></div>
|
|
25
25
|
</section>
|
|
26
26
|
<section>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<section>
|
|
2
|
-
<h2>Notes Inbox <span class="count" id="inbox-count">0</span> <button class="
|
|
2
|
+
<h2>Notes Inbox <span class="count" id="inbox-count">0</span> <button class="btn-add btn-add-lg" style="margin-left:8px" onclick="openQuickNoteModal()">+ Note</button> <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">auto-consolidates at <span id="inbox-threshold">3</span> notes</span></h2>
|
|
3
3
|
<div class="inbox-list" id="inbox-list">Loading...</div>
|
|
4
4
|
</section>
|
|
5
5
|
<section>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<div id="notes-list">Loading...</div>
|
|
8
8
|
</section>
|
|
9
9
|
<section>
|
|
10
|
-
<h2>Knowledge Base <span class="count" id="kb-count">0</span> <button class="
|
|
10
|
+
<h2>Knowledge Base <span class="count" id="kb-count">0</span> <button class="btn-add" style="margin-left:8px" onclick="openCreateKbModal()">+ New</button><button id="kb-sweep-btn" onclick="kbSweep()" style="font-size:var(--text-xs);padding:2px 8px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);border-radius:4px;cursor:pointer;margin-left:8px;vertical-align:middle">sweep</button><span id="kb-swept-time" style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0;margin-left:8px"></span></h2>
|
|
11
11
|
<div id="kb-sweep-toast" class="cmd-toast cmd-toast-inline" style="margin:6px 0"></div>
|
|
12
12
|
<div class="kb-tabs" id="kb-tabs"></div>
|
|
13
13
|
<div class="kb-list" id="kb-list"><p class="empty">No knowledge entries yet. Notes are classified here after consolidation.</p></div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<section>
|
|
2
2
|
<h2>Team Meetings <span class="count" id="meetings-count">0</span>
|
|
3
|
-
<button class="
|
|
3
|
+
<button class="btn-add" style="margin-left:8px" onclick="openCreateMeetingModal()">+ New Meeting</button>
|
|
4
4
|
<span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">multi-round agent discussions — investigate, debate, conclude</span>
|
|
5
5
|
</h2>
|
|
6
6
|
<div id="meetings-content"><p class="empty">No meetings yet. Start one to have agents investigate, debate, and conclude on a topic.</p></div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<section>
|
|
2
2
|
<h2>Pipelines <span class="count" id="pipelines-count">0</span>
|
|
3
|
-
<button class="
|
|
3
|
+
<button class="btn-add" style="margin-left:8px" onclick="openCreatePipelineModal()">+ New Pipeline</button>
|
|
4
4
|
<span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">multi-stage workflows with dependencies — chain meetings, plans, tasks, merges in any order, on a schedule or manual</span>
|
|
5
5
|
</h2>
|
|
6
6
|
<div id="pipelines-content"><p class="empty">No pipelines yet. Create one to chain stages like audit → meeting → plan → merge.</p></div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<section>
|
|
2
2
|
<h2>Plans <span class="count" id="plans-count">0</span>
|
|
3
|
-
<button class="
|
|
3
|
+
<button class="btn-add" style="margin-left:8px" onclick="openCreatePlanModal()">+ New Plan</button>
|
|
4
4
|
<span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">create via CC or + New Plan, then execute to generate a PRD</span>
|
|
5
5
|
</h2>
|
|
6
6
|
<div id="plans-list"><p class="empty">No plans yet. Create one via the command center or click + New Plan.</p></div>
|
package/dashboard/pages/prs.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<section class="pr-panel" id="pr-section">
|
|
2
2
|
<h2>Pull Requests <span class="count" id="pr-count">0</span>
|
|
3
|
-
<button class="
|
|
3
|
+
<button class="btn-add" style="margin-left:8px" onclick="openAddPrModal()">+ Link PR</button>
|
|
4
4
|
<span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">created by agents, tracked with review + build status</span>
|
|
5
5
|
</h2>
|
|
6
6
|
<div id="pr-toast" class="cmd-toast cmd-toast-inline" style="margin:6px 0"></div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<section id="scheduled-section">
|
|
2
2
|
<h2>Scheduled Tasks <span class="count" id="scheduled-count">0</span>
|
|
3
|
-
<button class="
|
|
3
|
+
<button class="btn-add" style="margin-left:8px" onclick="openCreateScheduleModal()">+ New</button>
|
|
4
4
|
<span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">single recurring tasks on a cron — for multi-step workflows, use Pipelines</span>
|
|
5
5
|
</h2>
|
|
6
6
|
<div id="scheduled-content"><p class="empty">No scheduled tasks. Add one to automate recurring work.</p></div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<section id="watches-section">
|
|
2
2
|
<h2>Watches <span class="count" id="watches-count">0</span>
|
|
3
|
-
<button class="
|
|
3
|
+
<button class="btn-add" style="margin-left:8px" onclick="openCreateWatchModal()">+ New</button>
|
|
4
4
|
<span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">persistent watches that monitor PRs, work items, and branches for changes</span>
|
|
5
5
|
</h2>
|
|
6
6
|
<div id="watches-content"><p class="empty">No active watches. Create one to monitor PRs, work items, or branches.</p></div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<section id="work-items-section" style="overflow:visible">
|
|
2
2
|
<h2>Work Items <span class="count" id="wi-count">0</span>
|
|
3
|
-
<button class="
|
|
3
|
+
<button class="btn-add" style="margin-left:8px" onclick="openCreateWorkItemModal()">+ New</button>
|
|
4
4
|
<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;margin-left:4px" onclick="toggleWorkItemArchive()">See Archive</button>
|
|
5
5
|
<span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">tasks dispatched to agents — auto-created from PRDs or added manually</span>
|
|
6
6
|
</h2>
|
package/dashboard/styles.css
CHANGED
|
@@ -426,6 +426,34 @@
|
|
|
426
426
|
.btn-danger { color: var(--red); border-color: var(--red); }
|
|
427
427
|
.btn-warning { color: var(--yellow); border-color: var(--yellow); }
|
|
428
428
|
|
|
429
|
+
/* W-mq10dtaw000l7ad5 — "+ Add" / "+ New" button family. Single source of
|
|
430
|
+
truth for the green-bordered, green-text, "+" buttons used across the
|
|
431
|
+
dashboard: section-header creators (work, plans, prs, knowledge, inbox,
|
|
432
|
+
schedules, pipelines, watches, meetings, home pin/note) AND inline
|
|
433
|
+
form-row adders ("+ Add step", "+ Add requirement"). Change the green
|
|
434
|
+
hex / border / padding / hover once here and every "+ X" button updates.
|
|
435
|
+
Variant:
|
|
436
|
+
.btn-add — compact text-xs / 1px 6px (section-header default)
|
|
437
|
+
.btn-add-lg — text-sm / 2px 8px (form rows + larger header buttons)
|
|
438
|
+
Positional spacing (e.g. margin-left:8px after an h2 count) lives at
|
|
439
|
+
the callsite, not on the button itself. Do NOT re-create the green
|
|
440
|
+
inline style for a new "+ X" button — extend this primitive instead. */
|
|
441
|
+
.btn-add {
|
|
442
|
+
background: var(--surface2);
|
|
443
|
+
border: 1px solid var(--green);
|
|
444
|
+
color: var(--green);
|
|
445
|
+
font-size: var(--text-xs);
|
|
446
|
+
padding: 1px 6px;
|
|
447
|
+
border-radius: var(--radius-sm);
|
|
448
|
+
cursor: pointer;
|
|
449
|
+
transition: background var(--transition-base);
|
|
450
|
+
}
|
|
451
|
+
.btn-add:hover { background: rgba(63,185,80,0.1); }
|
|
452
|
+
.btn-add:focus-visible { outline: 2px solid var(--green); outline-offset: 1px; }
|
|
453
|
+
.btn-add:disabled,
|
|
454
|
+
.btn-add.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
|
|
455
|
+
.btn-add.btn-add-lg { font-size: var(--text-sm); padding: 2px 8px; }
|
|
456
|
+
|
|
429
457
|
/* Badge — unified badge/pill base */
|
|
430
458
|
.badge {
|
|
431
459
|
font-size: var(--text-sm); font-weight: 600;
|
package/dashboard.js
CHANGED
|
@@ -6132,12 +6132,69 @@ const server = http.createServer(async (req, res) => {
|
|
|
6132
6132
|
const prevPath = path.join(agentDir, 'live-output-prev.log');
|
|
6133
6133
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
6134
6134
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
|
6135
|
+
|
|
6136
|
+
// W-mq1jbd8z0008dfa0 — scrollback support. When `from`/`to` are supplied,
|
|
6137
|
+
// serve an explicit byte range from live-output.log (or live-output-prev.log
|
|
6138
|
+
// when `prev=1` is set) instead of the tail window. The client uses this to
|
|
6139
|
+
// load progressively older transcript content as the user scrolls up.
|
|
6140
|
+
// `X-Log-Size` is always emitted (for both tail and range responses) so the
|
|
6141
|
+
// client knows whether more older content exists above its current top.
|
|
6142
|
+
const MAX_RANGE_BYTES = 524288; // 512KB hard cap per request
|
|
6143
|
+
const prevOnly = params.get('prev') === '1';
|
|
6144
|
+
const hasRange = params.has('from') || params.has('to');
|
|
6145
|
+
|
|
6146
|
+
if (hasRange) {
|
|
6147
|
+
if (!params.has('from') || !params.has('to')) {
|
|
6148
|
+
return jsonReply(res, 400, { error: 'from and to must both be provided for a range read' });
|
|
6149
|
+
}
|
|
6150
|
+
const rawFrom = params.get('from');
|
|
6151
|
+
const rawTo = params.get('to');
|
|
6152
|
+
if (!/^\d+$/.test(rawFrom) || !/^\d+$/.test(rawTo)) {
|
|
6153
|
+
return jsonReply(res, 400, { error: 'from and to must be non-negative integers' });
|
|
6154
|
+
}
|
|
6155
|
+
const from = parseInt(rawFrom, 10);
|
|
6156
|
+
const to = parseInt(rawTo, 10);
|
|
6157
|
+
if (!(from < to)) {
|
|
6158
|
+
return jsonReply(res, 400, { error: 'from must be less than to' });
|
|
6159
|
+
}
|
|
6160
|
+
if ((to - from) > MAX_RANGE_BYTES) {
|
|
6161
|
+
return jsonReply(res, 400, { error: `range exceeds MAX_RANGE_BYTES (${MAX_RANGE_BYTES})` });
|
|
6162
|
+
}
|
|
6163
|
+
const targetPath = prevOnly ? prevPath : livePath;
|
|
6164
|
+
try {
|
|
6165
|
+
const stat = fs.statSync(targetPath);
|
|
6166
|
+
res.setHeader('X-Log-Size', String(stat.size));
|
|
6167
|
+
res.setHeader('X-Log-Truncated', 'false');
|
|
6168
|
+
// Past EOF — return empty body (NOT 416, the client polls and 416s
|
|
6169
|
+
// would spam the console).
|
|
6170
|
+
if (from >= stat.size) { res.end(''); return; }
|
|
6171
|
+
const clampedTo = Math.min(to, stat.size);
|
|
6172
|
+
const length = clampedTo - from;
|
|
6173
|
+
const buf = Buffer.alloc(length);
|
|
6174
|
+
const fd = fs.openSync(targetPath, 'r');
|
|
6175
|
+
try {
|
|
6176
|
+
fs.readSync(fd, buf, 0, length, from);
|
|
6177
|
+
} finally {
|
|
6178
|
+
fs.closeSync(fd);
|
|
6179
|
+
}
|
|
6180
|
+
res.end(buf.toString('utf8'));
|
|
6181
|
+
} catch {
|
|
6182
|
+
// File missing — fail soft to match the tail path's UX.
|
|
6183
|
+
res.setHeader('X-Log-Size', '0');
|
|
6184
|
+
res.setHeader('X-Log-Truncated', 'false');
|
|
6185
|
+
res.end('');
|
|
6186
|
+
}
|
|
6187
|
+
return;
|
|
6188
|
+
}
|
|
6189
|
+
|
|
6135
6190
|
const rawTail = parseInt(params.get('tail'));
|
|
6136
6191
|
if (params.has('tail') && isNaN(rawTail)) return jsonReply(res, 400, { error: 'tail must be a number' });
|
|
6137
6192
|
const tailBytes = isNaN(rawTail) ? 8192 : Math.max(1, Math.min(65536, rawTail));
|
|
6138
6193
|
// Read only the tail bytes from disk instead of entire file
|
|
6139
6194
|
try {
|
|
6140
6195
|
const stat = fs.statSync(livePath);
|
|
6196
|
+
res.setHeader('X-Log-Size', String(stat.size));
|
|
6197
|
+
res.setHeader('X-Log-Truncated', 'false');
|
|
6141
6198
|
if (stat.size === 0) { res.end('No live output. Agent may not be running.'); return; }
|
|
6142
6199
|
|
|
6143
6200
|
// Fall back to previous session log when current is sparse (fixes #543)
|
|
@@ -6173,6 +6230,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
6173
6230
|
if (fs.existsSync(prevPath)) {
|
|
6174
6231
|
const prevStat = fs.statSync(prevPath);
|
|
6175
6232
|
if (prevStat.size > 0) {
|
|
6233
|
+
res.setHeader('X-Log-Size', String(prevStat.size));
|
|
6234
|
+
res.setHeader('X-Log-Truncated', 'false');
|
|
6176
6235
|
const start = Math.max(0, prevStat.size - tailBytes);
|
|
6177
6236
|
const buf = Buffer.alloc(Math.min(tailBytes, prevStat.size));
|
|
6178
6237
|
const fd = fs.openSync(prevPath, 'r');
|
|
@@ -6183,6 +6242,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
6183
6242
|
}
|
|
6184
6243
|
}
|
|
6185
6244
|
} catch { /* fall through */ }
|
|
6245
|
+
if (!res.getHeader('X-Log-Size')) res.setHeader('X-Log-Size', '0');
|
|
6246
|
+
if (!res.getHeader('X-Log-Truncated')) res.setHeader('X-Log-Truncated', 'false');
|
|
6186
6247
|
res.end('No live output. Agent may not be running.');
|
|
6187
6248
|
}
|
|
6188
6249
|
return;
|
|
@@ -11566,8 +11627,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11566
11627
|
{ method: 'POST', path: '/api/agents/cancel', desc: 'Cancel an active agent by ID or task substring', params: 'agent? or agentId?, task?', handler: handleAgentsCancel },
|
|
11567
11628
|
{ method: 'POST', path: /^\/api\/agent\/([\w-]+)\/kill$/, template: '/api/agent/:id/kill', desc: 'Kill a running agent: stop process, clear dispatch, reset work items to pending', handler: handleAgentKill },
|
|
11568
11629
|
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, template: '/api/agent/:id/live-stream', desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
|
|
11569
|
-
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live(?:\?.*)?$/, template: '/api/agent/:id/live', desc: 'Tail live output for a working agent', params: 'tail? (bytes, default 8192)', handler: handleAgentLive },
|
|
11570
|
-
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-output(?:\?.*)?$/, template: '/api/agent/:id/live-output', desc: 'Tail live output for a working agent (alias for /live)', params: 'tail? (bytes, default 8192)', handler: handleAgentLive },
|
|
11630
|
+
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live(?:\?.*)?$/, template: '/api/agent/:id/live', desc: 'Tail or range-read live output for a working agent', params: 'tail? (bytes, default 8192) | from?+to? (byte range, max 524288) | prev?=1 (read live-output-prev.log)', handler: handleAgentLive },
|
|
11631
|
+
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-output(?:\?.*)?$/, template: '/api/agent/:id/live-output', desc: 'Tail or range-read live output for a working agent (alias for /live)', params: 'tail? (bytes, default 8192) | from?+to? (byte range, max 524288) | prev?=1 (read live-output-prev.log)', handler: handleAgentLive },
|
|
11571
11632
|
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/output(?:\?.*)?$/, template: '/api/agent/:id/output', desc: 'Fetch final output.log for an agent', handler: handleAgentOutput },
|
|
11572
11633
|
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)$/, template: '/api/agent/:id', desc: 'Get detailed agent info', handler: handleAgentDetail },
|
|
11573
11634
|
{ method: 'GET', path: /^\/api\/dispatch\/([\w.-]+)\/completion-report$/, template: '/api/dispatch/:id/completion-report', desc: 'Read structured completion report for a dispatch', handler: (req, res, match) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2133",
|
|
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"
|