@yemi33/minions 0.1.2194 → 0.1.2196
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/command-center.js +2 -0
- package/dashboard/js/fre.js +125 -1
- package/dashboard/js/modal.js +35 -1
- package/dashboard/js/refresh.js +7 -1
- package/dashboard/js/render-agents.js +18 -0
- package/dashboard/js/render-kb.js +27 -2
- package/dashboard/js/render-meetings.js +36 -3
- package/dashboard/js/render-pipelines.js +15 -10
- package/dashboard/js/render-plans.js +73 -3
- package/dashboard/js/render-prd.js +29 -5
- package/dashboard/js/render-prs.js +174 -3
- package/dashboard/js/render-schedules.js +12 -0
- package/dashboard/js/render-utils.js +151 -1
- package/dashboard/js/render-watches.js +61 -7
- package/dashboard/js/render-work-items.js +161 -20
- package/dashboard/js/state.js +110 -1
- package/dashboard/js/utils.js +246 -14
- package/dashboard/layout.html +2 -0
- package/dashboard/slim/body.html +23 -13
- package/dashboard/slim/js/knowledge.js +576 -0
- package/dashboard/slim/js/members.js +43 -0
- package/dashboard/slim/js/modals-tiles.js +6 -4
- package/dashboard/slim/js/pinned.js +7 -19
- package/dashboard/slim/js/status.js +17 -21
- package/dashboard/slim/styles.css +94 -36
- package/dashboard/styles.css +34 -0
- package/dashboard-build.js +1 -1
- package/dashboard.js +50 -2
- package/engine/lifecycle.js +6 -0
- package/engine/pipeline.js +10 -0
- package/engine/queries.js +81 -0
- package/engine/scheduler.js +19 -1
- package/package.json +1 -1
|
@@ -69,6 +69,29 @@
|
|
|
69
69
|
grid.appendChild(frag);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Live "Working for" ticker for the agent detail modal. Mirrors the legacy
|
|
73
|
+
// renderer's .agent-runtime-tick pattern (dashboard/js/render-agents.js): a
|
|
74
|
+
// data-started element refreshed by a 1s interval, cleared on modal close so
|
|
75
|
+
// no interval leaks once the popout is dismissed.
|
|
76
|
+
var _agentDetailTimer = null;
|
|
77
|
+
|
|
78
|
+
// Format an elapsed-ms span as 'Xh Ym Zs', dropping the hours segment when 0.
|
|
79
|
+
function _fmtAgentElapsed(ms) {
|
|
80
|
+
if (!(ms > 0)) ms = 0;
|
|
81
|
+
var sec = Math.floor(ms / 1000) % 60, min = Math.floor(ms / 60000) % 60, hr = Math.floor(ms / 3600000);
|
|
82
|
+
return (hr > 0 ? hr + 'h ' : '') + min + 'm ' + sec + 's';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _tickAgentDetailRuntime() {
|
|
86
|
+
var el = document.getElementById('slim-agent-working-tick');
|
|
87
|
+
if (!el) { _stopAgentDetailRuntime(); return; }
|
|
88
|
+
el.textContent = _fmtAgentElapsed(Date.now() - new Date(el.dataset.started).getTime());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _stopAgentDetailRuntime() {
|
|
92
|
+
if (_agentDetailTimer) { clearInterval(_agentDetailTimer); _agentDetailTimer = null; }
|
|
93
|
+
}
|
|
94
|
+
|
|
72
95
|
// Append a labelled key/value row to the agent detail modal body.
|
|
73
96
|
function appendAgentRow(body, key, value, muted) {
|
|
74
97
|
var row = document.createElement('div');
|
|
@@ -92,6 +115,8 @@
|
|
|
92
115
|
var titleEl = document.getElementById('slim-agent-title');
|
|
93
116
|
if (!a || !modal || !body) return;
|
|
94
117
|
if (titleEl) titleEl.textContent = a.name || a.id;
|
|
118
|
+
// Clear any ticker left over from a previously-opened agent before rebuild.
|
|
119
|
+
_stopAgentDetailRuntime();
|
|
95
120
|
body.textContent = '';
|
|
96
121
|
|
|
97
122
|
// Header: emoji + name + role.
|
|
@@ -131,6 +156,24 @@
|
|
|
131
156
|
|
|
132
157
|
appendAgentRow(body, 'Last result', a.resultSummary || 'No recent output', !a.resultSummary);
|
|
133
158
|
|
|
159
|
+
// Only running agents get a live elapsed-time row; idle/done/error don't.
|
|
160
|
+
if (a.status === 'working' && a.started_at) {
|
|
161
|
+
var workRow = document.createElement('div');
|
|
162
|
+
workRow.className = 'agent-detail-row';
|
|
163
|
+
var workKey = document.createElement('div');
|
|
164
|
+
workKey.className = 'agent-detail-key';
|
|
165
|
+
workKey.textContent = 'Working for';
|
|
166
|
+
var workVal = document.createElement('div');
|
|
167
|
+
workVal.className = 'agent-detail-val';
|
|
168
|
+
workVal.id = 'slim-agent-working-tick';
|
|
169
|
+
workVal.dataset.started = a.started_at;
|
|
170
|
+
workRow.appendChild(workKey);
|
|
171
|
+
workRow.appendChild(workVal);
|
|
172
|
+
body.appendChild(workRow);
|
|
173
|
+
_tickAgentDetailRuntime();
|
|
174
|
+
_agentDetailTimer = setInterval(_tickAgentDetailRuntime, 1000);
|
|
175
|
+
}
|
|
176
|
+
|
|
134
177
|
modal.classList.add('open');
|
|
135
178
|
}
|
|
136
179
|
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
if (ev.key === 'Escape' && modal.classList.contains('open')) close();
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
// Stop the agent-detail "Working for" ticker on every dismiss path so the
|
|
16
|
+
// 1s interval started in openAgentDetail can't leak after the modal closes.
|
|
17
|
+
bindModalClose('slim-agent-modal', 'slim-agent-close', _stopAgentDetailRuntime);
|
|
16
18
|
bindModalClose('slim-tools-modal', 'slim-tools-close');
|
|
17
19
|
bindModalClose('slim-tile-modal', 'slim-tile-close');
|
|
18
20
|
|
|
@@ -249,9 +251,9 @@
|
|
|
249
251
|
// Populate + open the cockpit-tile detail modal from the latest status
|
|
250
252
|
// snapshot. Mirrors the corresponding old-dashboard tab for each tile.
|
|
251
253
|
function openTileModal(key) {
|
|
252
|
-
// The
|
|
253
|
-
// rather than the read-only tile detail view.
|
|
254
|
-
if (key === '
|
|
254
|
+
// The Knowledge tile opens the unified Knowledge control panel (Pinned
|
|
255
|
+
// Context + Notes + KB tabs) rather than the read-only tile detail view.
|
|
256
|
+
if (key === 'knowledge') { openKnowledgeModal(); return; }
|
|
255
257
|
var view = TILE_VIEWS[key];
|
|
256
258
|
if (!view) return;
|
|
257
259
|
var modal = document.getElementById('slim-tile-modal');
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
var d = await res.json().catch(function() { return {}; });
|
|
67
67
|
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
68
68
|
closeSlimPinEditor();
|
|
69
|
-
scheduleStatusRefresh(400); // refresh the
|
|
70
|
-
// If the
|
|
69
|
+
scheduleStatusRefresh(400); // refresh the Knowledge tile count
|
|
70
|
+
// If the Knowledge modal's Pinned tab is showing, re-render it once the
|
|
71
|
+
// snapshot catches up.
|
|
71
72
|
setTimeout(function() {
|
|
72
|
-
|
|
73
|
-
if (listModal && listModal.classList.contains('open')) renderSlimPinnedList();
|
|
73
|
+
if (document.getElementById('slim-pinned-list')) renderSlimPinnedList();
|
|
74
74
|
}, 700);
|
|
75
75
|
} catch (e) {
|
|
76
76
|
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
|
|
@@ -79,19 +79,10 @@
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// ── List / view / unpin ──────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (!modal) return;
|
|
85
|
-
renderSlimPinnedList();
|
|
86
|
-
modal.classList.add('open');
|
|
87
|
-
}
|
|
88
|
-
function closeSlimPinnedList() {
|
|
89
|
-
var modal = document.getElementById('slim-pinned-modal');
|
|
90
|
-
if (modal) modal.classList.remove('open');
|
|
91
|
-
}
|
|
92
|
-
|
|
82
|
+
// Rendered into #slim-pinned-list — the container the Knowledge modal's
|
|
83
|
+
// Pinned-Context tab builds. No-op when that container isn't mounted.
|
|
93
84
|
function renderSlimPinnedList() {
|
|
94
|
-
var body = document.getElementById('slim-pinned-
|
|
85
|
+
var body = document.getElementById('slim-pinned-list');
|
|
95
86
|
if (!body) return;
|
|
96
87
|
var entries = slimPinnedEntries();
|
|
97
88
|
body.textContent = '';
|
|
@@ -166,13 +157,10 @@
|
|
|
166
157
|
}
|
|
167
158
|
}
|
|
168
159
|
|
|
169
|
-
bindModalClose('slim-pinned-modal', 'slim-pinned-close');
|
|
170
160
|
bindModalClose('slim-pin-edit-modal', 'slim-pin-edit-close');
|
|
171
161
|
(function bindPinnedUi() {
|
|
172
162
|
var pinBtn = document.getElementById('slim-pin-btn');
|
|
173
163
|
if (pinBtn) pinBtn.addEventListener('click', function() { openSlimPinEditor(null); });
|
|
174
|
-
var addBtn = document.getElementById('slim-pinned-add');
|
|
175
|
-
if (addBtn) addBtn.addEventListener('click', function() { openSlimPinEditor(null); });
|
|
176
164
|
var tileChip = document.getElementById('slim-tile-pin-chip');
|
|
177
165
|
if (tileChip) tileChip.addEventListener('click', function(ev) { ev.stopPropagation(); openSlimPinEditor(null); });
|
|
178
166
|
var cancel = document.getElementById('slim-pin-cancel');
|
|
@@ -16,11 +16,10 @@
|
|
|
16
16
|
}, delay || 0);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
// whenever it changes (W-
|
|
21
|
-
// — only deltas from a previously-seen number trigger the
|
|
22
|
-
// the dashboard doesn't
|
|
23
|
-
var FLASH_ON_CHANGE_KEYS = { dispatches: true, prs: true };
|
|
19
|
+
// Track each tile's last numeric value so the number element can fade
|
|
20
|
+
// out-and-back-in whenever it changes (W-mqbaeuhm). Suppressed on first paint
|
|
21
|
+
// — only deltas from a previously-seen number trigger the fade, so opening
|
|
22
|
+
// the dashboard doesn't animate every tile.
|
|
24
23
|
var lastTileValues = Object.create(null);
|
|
25
24
|
|
|
26
25
|
// Apply lit-state and value/detail text to a tile.
|
|
@@ -36,17 +35,17 @@
|
|
|
36
35
|
if (detEl) detEl.textContent = detail || '';
|
|
37
36
|
tile.classList.remove('lit-blue', 'lit-green', 'lit-amber', 'lit-orange', 'lit-red');
|
|
38
37
|
if (lit) tile.classList.add('lit-' + lit);
|
|
39
|
-
//
|
|
40
|
-
// restarts the animation even when the class was already present
|
|
41
|
-
// previous tick (otherwise repeated changes would only fire on the
|
|
42
|
-
// delta after class application). Only numeric
|
|
43
|
-
// ignore null/undefined
|
|
44
|
-
if (
|
|
38
|
+
// Fade the number out-and-back-in when it changes. Remove → force reflow →
|
|
39
|
+
// re-add restarts the animation even when the class was already present
|
|
40
|
+
// from the previous tick (otherwise repeated changes would only fire on the
|
|
41
|
+
// first delta after class application). Only numeric→different-numeric
|
|
42
|
+
// counts as a change; ignore null/undefined and unchanged values.
|
|
43
|
+
if (valEl && typeof value === 'number') {
|
|
45
44
|
var prev = lastTileValues[key];
|
|
46
45
|
if (typeof prev === 'number' && prev !== value) {
|
|
47
|
-
|
|
48
|
-
void
|
|
49
|
-
|
|
46
|
+
valEl.classList.remove('value-changed');
|
|
47
|
+
void valEl.offsetWidth;
|
|
48
|
+
valEl.classList.add('value-changed');
|
|
50
49
|
}
|
|
51
50
|
lastTileValues[key] = value;
|
|
52
51
|
}
|
|
@@ -150,14 +149,11 @@
|
|
|
150
149
|
activeWatches.length ? 'blue' : null
|
|
151
150
|
);
|
|
152
151
|
|
|
153
|
-
// ── Pinned
|
|
152
|
+
// ── Knowledge tile (Pinned Context + Notes + KB) ──────────────
|
|
153
|
+
// Pinned count is live from the poll; notes/KB counts are fetched lazily
|
|
154
|
+
// (see knowledge.js loadKnowledgeCounts) to keep them off the 5s poll.
|
|
154
155
|
var pinned = Array.isArray(data.pinned) ? data.pinned : [];
|
|
155
|
-
|
|
156
|
-
'pinned',
|
|
157
|
-
pinned.length,
|
|
158
|
-
pinned.length ? (pinned.length === 1 ? '1 note for agents' : pinned.length + ' notes for agents') : 'nothing pinned',
|
|
159
|
-
pinned.length ? 'blue' : null
|
|
160
|
-
);
|
|
156
|
+
if (typeof renderKnowledgeTile === 'function') renderKnowledgeTile(pinned.length);
|
|
161
157
|
|
|
162
158
|
// ── Team member cards ──────────────────────────────────────
|
|
163
159
|
renderMembers(Array.isArray(data.agents) ? data.agents : []);
|
|
@@ -701,7 +701,7 @@
|
|
|
701
701
|
.tile-section-label { font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase; color: var(--text); }
|
|
702
702
|
.tile-section-count { color: var(--muted); }
|
|
703
703
|
|
|
704
|
-
/* Pinned-context list rows (
|
|
704
|
+
/* Pinned-context list rows (Knowledge modal → Pinned Context tab). */
|
|
705
705
|
.pinned-row {
|
|
706
706
|
border: 1px solid var(--border);
|
|
707
707
|
border-radius: var(--radius);
|
|
@@ -724,6 +724,85 @@
|
|
|
724
724
|
.pinned-row-actions .btn-secondary { padding: 4px 12px; font-size: var(--text-base); }
|
|
725
725
|
.pinned-row-unpin { color: var(--red); border-color: var(--red); }
|
|
726
726
|
|
|
727
|
+
/* Knowledge control panel (slim-knowledge-modal): Pinned Context / Notes /
|
|
728
|
+
KB tabs in one box. Reuses .tile-* and .pinned-row primitives. */
|
|
729
|
+
.slim-knowledge-modal-inner { width: 720px; max-width: calc(100vw - 32px); }
|
|
730
|
+
.kn-tabs { display: flex; gap: 6px; margin-left: 16px; }
|
|
731
|
+
.kn-tab {
|
|
732
|
+
border: 1px solid var(--border);
|
|
733
|
+
background: var(--surface2);
|
|
734
|
+
color: var(--muted);
|
|
735
|
+
font-size: var(--text-sm);
|
|
736
|
+
padding: 4px 12px;
|
|
737
|
+
border-radius: 999px;
|
|
738
|
+
cursor: pointer;
|
|
739
|
+
white-space: nowrap;
|
|
740
|
+
}
|
|
741
|
+
.kn-tab:hover { color: var(--text); border-color: var(--blue); }
|
|
742
|
+
.kn-tab.active {
|
|
743
|
+
color: var(--text);
|
|
744
|
+
border-color: var(--blue);
|
|
745
|
+
background: color-mix(in srgb, var(--blue) 14%, var(--surface2));
|
|
746
|
+
}
|
|
747
|
+
.kn-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
748
|
+
.kn-toolbar-end { justify-content: flex-end; }
|
|
749
|
+
.kn-section-head {
|
|
750
|
+
margin: 16px 0 8px;
|
|
751
|
+
font-size: var(--text-md);
|
|
752
|
+
font-weight: 600;
|
|
753
|
+
color: var(--text);
|
|
754
|
+
border-top: 1px solid var(--border);
|
|
755
|
+
padding-top: 12px;
|
|
756
|
+
}
|
|
757
|
+
.kn-msg { font-size: var(--text-base); min-height: 16px; color: var(--muted); }
|
|
758
|
+
.kn-msg-inline { margin-right: auto; }
|
|
759
|
+
.kn-notes-textarea { font-family: var(--mono, monospace); white-space: pre; }
|
|
760
|
+
.kn-create-form {
|
|
761
|
+
border: 1px solid var(--border);
|
|
762
|
+
border-radius: var(--radius);
|
|
763
|
+
background: var(--surface2);
|
|
764
|
+
padding: 12px;
|
|
765
|
+
margin-bottom: 12px;
|
|
766
|
+
}
|
|
767
|
+
/* KB list rows */
|
|
768
|
+
.kb-row {
|
|
769
|
+
border: 1px solid var(--border);
|
|
770
|
+
border-radius: var(--radius);
|
|
771
|
+
background: var(--surface2);
|
|
772
|
+
padding: 8px 10px;
|
|
773
|
+
margin-bottom: 8px;
|
|
774
|
+
cursor: pointer;
|
|
775
|
+
}
|
|
776
|
+
.kb-row:hover { border-color: var(--blue); }
|
|
777
|
+
.kb-row-pinned { border-color: var(--amber); }
|
|
778
|
+
.kb-row-top { display: flex; align-items: center; gap: 8px; }
|
|
779
|
+
.kb-row-title {
|
|
780
|
+
flex: 1; min-width: 0;
|
|
781
|
+
font-weight: 600; font-size: var(--text-md); color: var(--text);
|
|
782
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
783
|
+
}
|
|
784
|
+
.kb-pin-btn {
|
|
785
|
+
flex: 0 0 auto;
|
|
786
|
+
border: none; background: transparent; cursor: pointer;
|
|
787
|
+
color: var(--muted); font-size: var(--text-lg); line-height: 1; padding: 0 2px;
|
|
788
|
+
}
|
|
789
|
+
.kb-pin-btn.on { color: var(--amber); }
|
|
790
|
+
.kb-pin-btn:hover { color: var(--amber); }
|
|
791
|
+
.kb-row-meta { font-size: var(--text-base); color: var(--muted); margin-top: 3px; }
|
|
792
|
+
.kb-row-preview {
|
|
793
|
+
margin-top: 5px;
|
|
794
|
+
font-size: var(--text-base); color: var(--muted); line-height: 1.5;
|
|
795
|
+
word-break: break-word;
|
|
796
|
+
}
|
|
797
|
+
.kb-entry-content {
|
|
798
|
+
white-space: pre-wrap; word-break: break-word;
|
|
799
|
+
font-family: var(--mono, monospace); font-size: var(--text-base);
|
|
800
|
+
color: var(--text); line-height: 1.55;
|
|
801
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
802
|
+
border-radius: var(--radius); padding: 12px; margin: 0;
|
|
803
|
+
max-height: 60vh; overflow-y: auto;
|
|
804
|
+
}
|
|
805
|
+
|
|
727
806
|
.chat-thinking {
|
|
728
807
|
color: var(--muted);
|
|
729
808
|
font-size: var(--text-md);
|
|
@@ -893,9 +972,9 @@
|
|
|
893
972
|
.cockpit-grid > [data-tile="queued"],
|
|
894
973
|
.cockpit-grid > [data-tile="dispatches"],
|
|
895
974
|
.cockpit-grid > [data-tile="prs"] { grid-column: span 2; }
|
|
896
|
-
/* Row 3: Watches |
|
|
975
|
+
/* Row 3: Watches | Knowledge — two equal columns (3 of 6 each). */
|
|
897
976
|
.cockpit-grid > [data-tile="watches"],
|
|
898
|
-
.cockpit-grid > [data-tile="
|
|
977
|
+
.cockpit-grid > [data-tile="knowledge"] { grid-column: span 3; }
|
|
899
978
|
.cockpit-tile {
|
|
900
979
|
background: var(--surface2);
|
|
901
980
|
border: 1px solid var(--border);
|
|
@@ -905,37 +984,26 @@
|
|
|
905
984
|
flex-direction: column;
|
|
906
985
|
gap: 4px;
|
|
907
986
|
transition: border-color 0.2s, background 0.2s;
|
|
908
|
-
/* Per-state flash color (rgb triplet, no rgba wrapper) consumed by
|
|
909
|
-
`cockpit-flash` so the change-flash ring matches the tile's lit state
|
|
910
|
-
(amber for working dispatches, orange for queued, red for failing
|
|
911
|
-
PRs, etc.). Each lit-* class below overrides this. Default is blue
|
|
912
|
-
for unlit / generic tiles. */
|
|
913
|
-
--flash-color: 88, 166, 255;
|
|
914
987
|
}
|
|
915
988
|
.cockpit-tile.lit-blue {
|
|
916
989
|
border-color: rgba(88, 166, 255, 0.6);
|
|
917
990
|
background: rgba(88, 166, 255, 0.06);
|
|
918
|
-
--flash-color: 88, 166, 255;
|
|
919
991
|
}
|
|
920
992
|
.cockpit-tile.lit-green {
|
|
921
993
|
border-color: rgba(63, 185, 80, 0.6);
|
|
922
994
|
background: rgba(63, 185, 80, 0.06);
|
|
923
|
-
--flash-color: 63, 185, 80;
|
|
924
995
|
}
|
|
925
996
|
.cockpit-tile.lit-amber {
|
|
926
997
|
border-color: rgba(210, 153, 34, 0.6);
|
|
927
998
|
background: rgba(210, 153, 34, 0.06);
|
|
928
|
-
--flash-color: 210, 153, 34;
|
|
929
999
|
}
|
|
930
1000
|
.cockpit-tile.lit-orange {
|
|
931
1001
|
border-color: rgba(234, 88, 12, 0.6);
|
|
932
1002
|
background: rgba(234, 88, 12, 0.08);
|
|
933
|
-
--flash-color: 234, 88, 12;
|
|
934
1003
|
}
|
|
935
1004
|
.cockpit-tile.lit-red {
|
|
936
1005
|
border-color: rgba(248, 81, 73, 0.6);
|
|
937
1006
|
background: rgba(248, 81, 73, 0.08);
|
|
938
|
-
--flash-color: 248, 81, 73;
|
|
939
1007
|
}
|
|
940
1008
|
.cockpit-label {
|
|
941
1009
|
font-size: var(--text-base);
|
|
@@ -998,32 +1066,22 @@
|
|
|
998
1066
|
.cockpit-tile[data-tile="dispatches"].lit-amber {
|
|
999
1067
|
animation: slim-pulse 1.6s ease-in-out infinite;
|
|
1000
1068
|
}
|
|
1001
|
-
/*
|
|
1002
|
-
changed
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
and clobber one of them). The ring expands then fades — fully visible
|
|
1006
|
-
even when the underlying tile is already lit. Respects
|
|
1069
|
+
/* Value-fade: when a cockpit tile's number changes, status.js toggles
|
|
1070
|
+
`.value-changed` on the `.cockpit-value` element to fade the number out
|
|
1071
|
+
then back in around the freshly-swapped text. Applies to every numeric
|
|
1072
|
+
tile (engine/dispatches/prs/watches/pinned). Respects
|
|
1007
1073
|
prefers-reduced-motion. */
|
|
1008
|
-
.cockpit-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}
|
|
1016
|
-
.cockpit-tile.flash-change::after {
|
|
1017
|
-
animation: cockpit-flash 0.75s ease-out;
|
|
1018
|
-
}
|
|
1019
|
-
@keyframes cockpit-flash {
|
|
1020
|
-
0% { box-shadow: 0 0 0 0 rgba(var(--flash-color), 0); }
|
|
1021
|
-
30% { box-shadow: 0 0 0 6px rgba(var(--flash-color), 0.7); }
|
|
1022
|
-
100% { box-shadow: 0 0 0 0 rgba(var(--flash-color), 0); }
|
|
1074
|
+
.cockpit-value.value-changed {
|
|
1075
|
+
animation: cockpit-value-fade 0.45s ease;
|
|
1076
|
+
}
|
|
1077
|
+
@keyframes cockpit-value-fade {
|
|
1078
|
+
0% { opacity: 1; transform: translateY(0); }
|
|
1079
|
+
45% { opacity: 0; transform: translateY(-2px); }
|
|
1080
|
+
100% { opacity: 1; transform: translateY(0); }
|
|
1023
1081
|
}
|
|
1024
1082
|
@media (prefers-reduced-motion: reduce) {
|
|
1025
1083
|
.cockpit-tile[data-tile="dispatches"].lit-amber { animation: none; }
|
|
1026
|
-
.cockpit-
|
|
1084
|
+
.cockpit-value.value-changed { animation: none; }
|
|
1027
1085
|
}
|
|
1028
1086
|
|
|
1029
1087
|
/* ── Status panel sub-divisions: Team cards over System tiles,
|
package/dashboard/styles.css
CHANGED
|
@@ -1042,6 +1042,40 @@
|
|
|
1042
1042
|
.modal-close { background: none; border: none; color: var(--muted); font-size: var(--text-2xl); cursor: pointer; padding: var(--space-2) var(--space-4); }
|
|
1043
1043
|
.modal-close:hover { color: var(--text); }
|
|
1044
1044
|
|
|
1045
|
+
/* P-ce1e5e47 — modal stack chrome (back button + depth chip). The back
|
|
1046
|
+
button reuses .modal-copy chrome; modal-back-btn just tightens the
|
|
1047
|
+
leading icon. The depth chip is a small badge that shows n/5 when the
|
|
1048
|
+
modal stack has depth > 1. */
|
|
1049
|
+
.modal-back-btn { color: var(--blue); }
|
|
1050
|
+
.modal-back-btn:hover { color: var(--text); border-color: var(--blue); }
|
|
1051
|
+
.modal-stack-chip {
|
|
1052
|
+
display: inline-flex; align-items: center; padding: var(--space-1) var(--space-4);
|
|
1053
|
+
border-radius: 10px; font-size: var(--text-xs); color: var(--muted);
|
|
1054
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
1055
|
+
font-variant-numeric: tabular-nums;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/* P-ce1e5e47 — artifact-link chips. Re-usable inline pill used by chip
|
|
1059
|
+
callsites (renderArtifactLink in P-e265cd31 routes through this class).
|
|
1060
|
+
Hover lifts via border accent. .deleted shows a struck-out non-clickable
|
|
1061
|
+
hint so dead refs still render in context. */
|
|
1062
|
+
.artifact-chip {
|
|
1063
|
+
display: inline-flex; align-items: center; gap: var(--space-1);
|
|
1064
|
+
padding: var(--space-1) var(--space-4); border-radius: 10px;
|
|
1065
|
+
font-size: var(--text-sm); cursor: pointer;
|
|
1066
|
+
background: var(--surface2); border: 1px solid var(--border); color: var(--text);
|
|
1067
|
+
transition: border-color var(--transition-base), color var(--transition-base);
|
|
1068
|
+
user-select: none;
|
|
1069
|
+
}
|
|
1070
|
+
.artifact-chip:hover { border-color: var(--blue); color: var(--blue); }
|
|
1071
|
+
.artifact-chip-icon { font-size: var(--text-sm); line-height: 1; }
|
|
1072
|
+
.artifact-chip-label { font-size: var(--text-sm); }
|
|
1073
|
+
.artifact-chip.deleted {
|
|
1074
|
+
cursor: not-allowed; text-decoration: line-through;
|
|
1075
|
+
color: var(--muted); opacity: 0.6;
|
|
1076
|
+
}
|
|
1077
|
+
.artifact-chip.deleted:hover { border-color: var(--border); color: var(--muted); }
|
|
1078
|
+
|
|
1045
1079
|
/* Custom confirm dialog (W-mq5dk1lq) — promise-based replacement for
|
|
1046
1080
|
native window.confirm(). Scaffold in layout.html (#confirm-dialog),
|
|
1047
1081
|
behavior in dashboard/js/confirm-dialog.js. Reuses .modal-bg / .modal
|
package/dashboard-build.js
CHANGED
|
@@ -71,7 +71,7 @@ function buildDashboardHtml() {
|
|
|
71
71
|
// original single-file source) inside the wrapper that layout.html provides.
|
|
72
72
|
const SLIM_JS_ORDER = [
|
|
73
73
|
'helpers', 'settings', 'link-pr', 'chat', 'projects',
|
|
74
|
-
'command-send', 'status', 'members', 'modals-tiles', 'history', 'pinned',
|
|
74
|
+
'command-send', 'status', 'members', 'modals-tiles', 'history', 'pinned', 'knowledge',
|
|
75
75
|
];
|
|
76
76
|
|
|
77
77
|
// Cache for the assembled slim source fragments (layout/css/body/js). Keyed on
|
package/dashboard.js
CHANGED
|
@@ -6050,6 +6050,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
6050
6050
|
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
6051
6051
|
}
|
|
6052
6052
|
|
|
6053
|
+
// GET /api/prs/<id> — return a single fully-enriched PR record by canonical
|
|
6054
|
+
// id (`<host>:<slug>#<number>`) or by bare number (P-79b47b0c). The in-stack
|
|
6055
|
+
// PR modal (renderPrs.openPrDetail) calls this on demand. Always returns the
|
|
6056
|
+
// record exactly as queries.getPullRequests() produces it (no slimming).
|
|
6057
|
+
async function handlePrsById(req, res, match) {
|
|
6058
|
+
try {
|
|
6059
|
+
const id = decodeURIComponent(match[1] || '').trim();
|
|
6060
|
+
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
6061
|
+
const prs = queries.getPullRequests();
|
|
6062
|
+
const found = prs.find(p => p && (p.id === id || String(p.number) === id));
|
|
6063
|
+
if (!found) return jsonReply(res, 404, { error: 'pr not found' });
|
|
6064
|
+
return jsonReply(res, 200, { pr: found });
|
|
6065
|
+
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
6066
|
+
}
|
|
6067
|
+
|
|
6053
6068
|
async function handleWorkItemsReopen(req, res) {
|
|
6054
6069
|
try {
|
|
6055
6070
|
const body = await readBody(req);
|
|
@@ -8413,10 +8428,31 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8413
8428
|
// Add frontmatter if not present
|
|
8414
8429
|
const today = new Date().toISOString().slice(0, 10);
|
|
8415
8430
|
let kbContent = content;
|
|
8431
|
+
// P-34fa5d79 — Note ↔ KB linkage. Parse the inbox note's own frontmatter
|
|
8432
|
+
// (if any) to recover the WI it cited so we can stamp the new KB file
|
|
8433
|
+
// with source_note + source_wi. This is the inverse-link the KB modal
|
|
8434
|
+
// renders as "From note:" / "From WI:" chips.
|
|
8435
|
+
const _inboxFm = queries._parseNoteFrontmatter(content) || {};
|
|
8436
|
+
const _sourceWi = queries._wiIdFromNoteFrontmatter(_inboxFm);
|
|
8416
8437
|
if (!content.startsWith('---')) {
|
|
8417
8438
|
const titleMatch = content.match(/^#+ (.+)$/m);
|
|
8418
8439
|
const title = titleMatch ? titleMatch[1].trim() : name.replace('.md', '');
|
|
8419
|
-
|
|
8440
|
+
const wiLine = _sourceWi ? `source_wi: ${_sourceWi}\n` : '';
|
|
8441
|
+
kbContent = `---\ntitle: ${title}\ncategory: ${category}\ndate: ${today}\nsource: inbox/${name}\nsource_note: ${name}\n${wiLine}---\n\n${content}`;
|
|
8442
|
+
} else {
|
|
8443
|
+
// Inject source_note / source_wi into the existing frontmatter block.
|
|
8444
|
+
// Replace the inbox frontmatter terminator with the new lines + the
|
|
8445
|
+
// terminator; preserves the rest of the body verbatim.
|
|
8446
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
8447
|
+
if (fmMatch) {
|
|
8448
|
+
const existingFm = fmMatch[1];
|
|
8449
|
+
const additions = [];
|
|
8450
|
+
if (!/^source_note:/m.test(existingFm)) additions.push(`source_note: ${name}`);
|
|
8451
|
+
if (_sourceWi && !/^source_wi:/m.test(existingFm)) additions.push(`source_wi: ${_sourceWi}`);
|
|
8452
|
+
if (additions.length > 0) {
|
|
8453
|
+
kbContent = `---\n${existingFm}\n${additions.join('\n')}\n---\n` + content.slice(fmMatch[0].length);
|
|
8454
|
+
}
|
|
8455
|
+
}
|
|
8420
8456
|
}
|
|
8421
8457
|
|
|
8422
8458
|
// Write to knowledge base
|
|
@@ -12124,6 +12160,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
12124
12160
|
builder: () => queries.getPullRequests(),
|
|
12125
12161
|
});
|
|
12126
12162
|
}},
|
|
12163
|
+
// GET /api/prs/<id> — single fully-enriched PR record by canonical id
|
|
12164
|
+
// (`<host>:<slug>#<number>`, URL-encode the `#`) or by bare number.
|
|
12165
|
+
// Backs renderPrs.openPrDetail() in the in-stack PR modal (P-79b47b0c).
|
|
12166
|
+
// Regex uses (.+) (not [^/?]+) because canonical ids embed `/` in the slug.
|
|
12167
|
+
{ method: 'GET', path: /^\/api\/prs\/(.+)$/, template: '/api/prs/<id>', desc: 'Fetch a single fully-enriched PR record by canonical id (`<host>:<slug>#<number>`, URL-encode the `#`) or by bare number. Backs the in-stack PR detail modal.', handler: handlePrsById },
|
|
12127
12168
|
{ method: 'GET', path: '/api/dispatch', desc: 'Live dispatch queue with completion-report summaries (pending/active/completed slices)', handler: (req, res) => {
|
|
12128
12169
|
return serveFreshJson(req, res, {
|
|
12129
12170
|
tag: 'dispatch',
|
|
@@ -12220,7 +12261,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
12220
12261
|
const runEntry = runs[s.id];
|
|
12221
12262
|
const _lastRun = typeof runEntry === 'string' ? runEntry : (runEntry?.lastRun || runEntry?.lastCompletedAt || null);
|
|
12222
12263
|
const extra = typeof runEntry === 'object' && runEntry ? { _lastWorkItemId: runEntry.lastWorkItemId, _lastResult: runEntry.lastResult, _lastCompletedAt: runEntry.lastCompletedAt } : {};
|
|
12223
|
-
|
|
12264
|
+
// P-c549d07e — surface the ring buffer of recent WI ids (newest
|
|
12265
|
+
// first, hard cap 5) so the schedule detail modal can render a
|
|
12266
|
+
// "Recent dispatches" chip row routed through openArtifact('wi').
|
|
12267
|
+
// Empty / missing array on legacy entries → empty list.
|
|
12268
|
+
const _recentWorkItemIds = (typeof runEntry === 'object' && runEntry && Array.isArray(runEntry.recentWorkItemIds))
|
|
12269
|
+
? runEntry.recentWorkItemIds.slice(0, 5)
|
|
12270
|
+
: [];
|
|
12271
|
+
return { ...s, _lastRun, ...extra, _recentWorkItemIds };
|
|
12224
12272
|
});
|
|
12225
12273
|
},
|
|
12226
12274
|
});
|
package/engine/lifecycle.js
CHANGED
|
@@ -5144,7 +5144,13 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5144
5144
|
const itemId = meta.item.id;
|
|
5145
5145
|
const schedRunsPath = path.join(ENGINE_DIR, 'schedule-runs.json');
|
|
5146
5146
|
mutateJsonFileLocked(schedRunsPath, (runs) => {
|
|
5147
|
+
// P-c549d07e — preserve existing fields (esp. recentWorkItemIds ring
|
|
5148
|
+
// populated at dispatch time by engine/scheduler.writeScheduleRunEntry).
|
|
5149
|
+
// The literal-object replacement that lived here previously silently
|
|
5150
|
+
// clobbered the ring on every successful completion.
|
|
5151
|
+
const existing = typeof runs[scheduleId] === 'object' && runs[scheduleId] ? runs[scheduleId] : {};
|
|
5147
5152
|
runs[scheduleId] = {
|
|
5153
|
+
...existing,
|
|
5148
5154
|
lastRun: typeof runs[scheduleId] === 'string' ? runs[scheduleId] : (runs[scheduleId]?.lastRun || ts()),
|
|
5149
5155
|
lastWorkItemId: itemId,
|
|
5150
5156
|
lastResult: effectiveSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR,
|
package/engine/pipeline.js
CHANGED
|
@@ -427,6 +427,12 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
427
427
|
...(wiBranch ? { branch: wiBranch } : {}),
|
|
428
428
|
_pipelineRun: run.runId,
|
|
429
429
|
_pipelineStage: stage.id,
|
|
430
|
+
// P-c549d07e — back-link to the dispatcher (pipeline:<id>) so the
|
|
431
|
+
// WI detail modal can render a clickable chip via openArtifact()
|
|
432
|
+
// → openPipelineDetail. Kept alongside the existing createdBy /
|
|
433
|
+
// _pipelineRun / _pipelineStage fields so we don't disturb any
|
|
434
|
+
// existing readers — meta.spawnedBy is purely additive.
|
|
435
|
+
meta: { spawnedBy: 'pipeline:' + run.pipelineId },
|
|
430
436
|
});
|
|
431
437
|
return workItems;
|
|
432
438
|
});
|
|
@@ -561,6 +567,8 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
561
567
|
planFile: existingPlanFile, created: ts(), createdBy: 'pipeline:' + run.pipelineId,
|
|
562
568
|
// W-mp8ho6w500034a58: PLAN_TO_PRD is read-only — no branch needed.
|
|
563
569
|
_pipelineRun: run.runId, _pipelineStage: stage.id,
|
|
570
|
+
// P-c549d07e — back-link chip (see executeTaskStage above).
|
|
571
|
+
meta: { spawnedBy: 'pipeline:' + run.pipelineId },
|
|
564
572
|
...(project ? { project: project.name } : {}),
|
|
565
573
|
});
|
|
566
574
|
}
|
|
@@ -657,6 +665,8 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
657
665
|
// W-mp8ho6w500034a58: PLAN_TO_PRD is read-only — no branch needed.
|
|
658
666
|
_pipelineRun: run.runId,
|
|
659
667
|
_pipelineStage: stage.id,
|
|
668
|
+
// P-c549d07e — back-link chip (see executeTaskStage above).
|
|
669
|
+
meta: { spawnedBy: 'pipeline:' + run.pipelineId },
|
|
660
670
|
...(project ? { project: project.name } : {}),
|
|
661
671
|
});
|
|
662
672
|
}
|