@yemi33/minions 0.1.2143 → 0.1.2145

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/bin/minions.js CHANGED
@@ -1338,6 +1338,91 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
1338
1338
  ensureInstalled();
1339
1339
  const { doctor } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
1340
1340
  doctor(MINIONS_HOME).then(ok => process.exit(ok ? 0 : 1));
1341
+ } else if (cmd === 'watchdog') {
1342
+ // External recovery scheduled by the OS. Survives cluster-kill scenarios
1343
+ // the in-process supervisor can't (Windows job-object teardown when the
1344
+ // parent terminal closes, Linux OOM-killer, cgroup limits, pkill, reboot).
1345
+ ensureInstalled();
1346
+ const watchdog = require(path.join(MINIONS_HOME, 'engine', 'watchdog'));
1347
+ const sub = rest[0];
1348
+ const minionsBin = __filename;
1349
+ if (sub === 'tick') {
1350
+ const shared = require(path.join(MINIONS_HOME, 'engine', 'shared'));
1351
+ watchdog.tick({
1352
+ minionsHome: MINIONS_HOME,
1353
+ minionsBin,
1354
+ dashPort: resolveDashboardPort([]).port,
1355
+ readEnginePid,
1356
+ isPortListening,
1357
+ isStopIntentSet: shared.isStopIntentSet || (() => false),
1358
+ }).then(result => {
1359
+ // ALWAYS exit 0 — the scheduler must never see a failure for the
1360
+ // recovery we're trying to make boring. The `result` is logged by tick
1361
+ // to engine/watchdog-stdio.log; print it for interactive invocations.
1362
+ if (process.stdout.isTTY) {
1363
+ console.log(JSON.stringify(result, null, 2));
1364
+ }
1365
+ process.exit(0);
1366
+ }).catch(err => {
1367
+ // Defense in depth: tick promises never to throw, but if it does,
1368
+ // swallow the error and exit 0 so the scheduler keeps ticking.
1369
+ try { console.error(`watchdog tick: ${err && err.message || err}`); } catch {}
1370
+ process.exit(0);
1371
+ });
1372
+ } else if (sub === 'install') {
1373
+ const intervalArg = rest.find(a => a.startsWith('--interval='));
1374
+ const intervalMin = intervalArg ? parseInt(intervalArg.split('=')[1], 10) : undefined;
1375
+ try {
1376
+ const r = watchdog.install({
1377
+ minionsBin,
1378
+ minionsHome: MINIONS_HOME,
1379
+ intervalMin,
1380
+ });
1381
+ console.log(`\n Watchdog installed via ${r.scheduler} (every ${r.intervalMin} min).`);
1382
+ if (r.taskName) console.log(` Task: ${r.taskName}`);
1383
+ if (r.launcherPath) console.log(` Launcher: ${r.launcherPath}`);
1384
+ if (r.plistPath) console.log(` Plist: ${r.plistPath}`);
1385
+ if (r.servicePath) console.log(` Service: ${r.servicePath}`);
1386
+ if (r.timerPath) console.log(` Timer: ${r.timerPath}`);
1387
+ if (r.lingerHint) console.log(` Note: ${r.lingerHint}`);
1388
+ console.log(`\n MINIONS_HOME=${MINIONS_HOME} (baked into scheduler entry — re-run install if it moves)`);
1389
+ console.log(` Logs: ${path.join(MINIONS_HOME, 'engine', 'watchdog-stdio.log')}\n`);
1390
+ process.exit(0);
1391
+ } catch (err) {
1392
+ console.error(`\n ERROR: ${err && err.message || err}\n`);
1393
+ process.exit(1);
1394
+ }
1395
+ } else if (sub === 'uninstall') {
1396
+ try {
1397
+ const r = watchdog.uninstall({ minionsHome: MINIONS_HOME });
1398
+ console.log(`\n Watchdog uninstalled from ${r.scheduler}.${r.removed ? '' : ' (already absent — no-op)'}\n`);
1399
+ process.exit(0);
1400
+ } catch (err) {
1401
+ console.error(`\n ERROR: ${err && err.message || err}\n`);
1402
+ process.exit(1);
1403
+ }
1404
+ } else if (sub === 'status' || !sub) {
1405
+ try {
1406
+ const r = watchdog.status();
1407
+ console.log(`\n Watchdog (${r.scheduler}): ${r.installed ? 'installed' : 'NOT installed'}`);
1408
+ if (r.taskName) console.log(` Task: ${r.taskName}`);
1409
+ if (r.plistPath) console.log(` Plist: ${r.plistPath}${r.loaded ? ' (loaded)' : ''}`);
1410
+ if (r.timerPath) console.log(` Timer: ${r.timerPath}${r.active ? ' (active)' : ''}`);
1411
+ if (r.details) {
1412
+ console.log('\n Scheduler details:');
1413
+ console.log(r.details.split('\n').map(l => ` ${l}`).join('\n'));
1414
+ }
1415
+ console.log('');
1416
+ process.exit(r.installed ? 0 : 1);
1417
+ } catch (err) {
1418
+ console.error(`\n ERROR: ${err && err.message || err}\n`);
1419
+ process.exit(1);
1420
+ }
1421
+ } else {
1422
+ console.error(`\n Unknown watchdog subcommand: ${sub}`);
1423
+ console.error(` Usage: minions watchdog (install|uninstall|status|tick)\n`);
1424
+ process.exit(1);
1425
+ }
1341
1426
  } else if (cmd === 'dash' || cmd === 'dashboard') {
1342
1427
  ensureInstalled();
1343
1428
  // If dashboard is already running, just open the browser. The runtime
@@ -0,0 +1,16 @@
1
+ diff a/bin/minions.js b/bin/minions.js (rejected hunks)
2
+ @@ -852,6 +852,14 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
3
+
4
+ Dashboard:
5
+ minions dash Start web dashboard (default :7331)
6
+ +
7
+ + Watchdog (out-of-process recovery):
8
+ + minions watchdog install [--interval=5]
9
+ + Register OS scheduler task to probe + heal every N minutes
10
+ + (Windows Task Scheduler / macOS launchd / Linux systemd --user)
11
+ + minions watchdog uninstall Remove the scheduled task (idempotent)
12
+ + minions watchdog status Show registration + last-run details from the OS scheduler
13
+ + minions watchdog tick One-shot probe + recovery (used by the scheduler; safe to run by hand)
14
+ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
15
+ Dev mode (this checkout, contributors only):
16
+ minions --dev <cmd> Run against this checkout instead of ~/.minions/
@@ -1293,6 +1293,20 @@ async function refresh(opts) {
1293
1293
 
1294
1294
  refresh();
1295
1295
  _syncPinsFromServer(); // Load server-side pins on startup
1296
+ // Gate the "Try new Slim UX" promo button behind the slim-ux-promo feature
1297
+ // flag (off by default). The button ships hidden in layout.html; reveal it
1298
+ // only when the flag is on. The classic HTML (and its window.MINIONS_FEATURES
1299
+ // bootstrap) is built once at dashboard startup, so a runtime flag toggle
1300
+ // wouldn't be reflected there — read the LIVE flag state from /api/features
1301
+ // instead so flipping the flag takes effect on reload with no restart.
1302
+ (function _gateSlimUxPromoButton() {
1303
+ fetch('/api/features').then(function(r) { return r.ok ? r.json() : null; }).then(function(data) {
1304
+ if (!data || !Array.isArray(data.features)) return;
1305
+ var promo = data.features.find(function(f) { return f.id === 'slim-ux-promo'; });
1306
+ var btn = document.getElementById('try-slim-ux-btn');
1307
+ if (btn && promo && promo.enabled) btn.style.display = '';
1308
+ }).catch(function() { /* offline / error — leave the button hidden */ });
1309
+ })();
1296
1310
  // W-mpmwxkrw000872ec — reconcile the font-size preference from the server
1297
1311
  // once on cold load. The inline bootstrap in layout.html has already applied
1298
1312
  // localStorage's value (if any) to avoid flash; this fetch promotes the
@@ -102,8 +102,15 @@ function openPinnedView(idx) {
102
102
  const entry = (window._pinnedEntries || [])[idx];
103
103
  if (!entry) return;
104
104
  document.getElementById('modal-title').textContent = entry.title;
105
- // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
106
- document.getElementById('modal-body').innerHTML = renderMd(entry.content);
105
+ // Content and Edit button share one top-aligned flex row: rendered text fills
106
+ // the left (flex:1, starting at the top), the Edit button sits top-right — so
107
+ // both read as anchored to the top of the dialog body.
108
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields; the wrapper + Edit button are string literals with a numeric idx
109
+ document.getElementById('modal-body').innerHTML =
110
+ '<div style="display:flex;align-items:flex-start;gap:12px">' +
111
+ '<div style="flex:1;min-width:0">' + renderMd(entry.content) + '</div>' +
112
+ '<button class="pr-pager-btn" style="flex:none" onclick="openPinnedEdit(' + idx + ')">Edit</button>' +
113
+ '</div>';
107
114
  document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
108
115
  document.getElementById('modal-body').style.whiteSpace = 'normal';
109
116
  document.getElementById('modal').classList.add('open');
@@ -112,4 +119,113 @@ function openPinnedView(idx) {
112
119
  showModalQa();
113
120
  }
114
121
 
115
- window.MinionsPinned = { renderPinned, openPinNoteModal, submitPinnedNote, removePinnedNote, openPinnedView };
122
+ // Edit an existing pinned note. Opened from the "Edit" button in the view
123
+ // modal. The form is DOM-built (not innerHTML) so pre-filled user content
124
+ // can't inject markup, and the document-Q&A panel is hidden while editing.
125
+ function openPinnedEdit(idx) {
126
+ const entry = (window._pinnedEntries || [])[idx];
127
+ if (!entry) return;
128
+ const originalTitle = entry.title;
129
+ document.getElementById('modal-title').textContent = 'Edit Pinned Note';
130
+ const body = document.getElementById('modal-body');
131
+ // The view modal sets a serif font + normal whitespace for rendered markdown;
132
+ // reset so the form inputs use the default UI font.
133
+ body.style.fontFamily = '';
134
+ body.style.whiteSpace = '';
135
+ // Hide the doc-Q&A panel the view modal opened — irrelevant while editing.
136
+ const qa = document.getElementById('modal-qa'); if (qa) qa.style.display = 'none';
137
+
138
+ const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text)';
139
+ function field(labelText) {
140
+ const label = document.createElement('label');
141
+ label.style.cssText = 'color:var(--text);font-size:var(--text-md)';
142
+ label.appendChild(document.createTextNode(labelText));
143
+ return label;
144
+ }
145
+
146
+ const wrap = document.createElement('div');
147
+ wrap.style.cssText = 'display:flex;flex-direction:column;gap:12px';
148
+
149
+ const titleLabel = field('Title');
150
+ const titleInput = document.createElement('input');
151
+ titleInput.id = 'pin-edit-title';
152
+ titleInput.style.cssText = inputStyle;
153
+ titleInput.value = entry.title || '';
154
+ titleLabel.appendChild(titleInput);
155
+
156
+ const contentLabel = field('Content');
157
+ const contentInput = document.createElement('textarea');
158
+ contentInput.id = 'pin-edit-content';
159
+ contentInput.rows = 6;
160
+ contentInput.style.cssText = inputStyle + ';resize:vertical;font-family:inherit';
161
+ contentInput.value = entry.content || '';
162
+ contentLabel.appendChild(contentInput);
163
+
164
+ const levelLabel = field('Level');
165
+ const levelSelect = document.createElement('select');
166
+ levelSelect.id = 'pin-edit-level';
167
+ levelSelect.style.cssText = inputStyle;
168
+ [['info', 'Info'], ['warning', 'Warning'], ['critical', 'Critical']].forEach(function(o) {
169
+ const opt = document.createElement('option');
170
+ opt.value = o[0];
171
+ opt.textContent = o[1];
172
+ if ((entry.level || 'info') === o[0]) opt.selected = true;
173
+ levelSelect.appendChild(opt);
174
+ });
175
+ levelLabel.appendChild(levelSelect);
176
+
177
+ const actions = document.createElement('div');
178
+ actions.style.cssText = 'display:flex;justify-content:flex-end;gap:8px';
179
+ const cancelBtn = document.createElement('button');
180
+ cancelBtn.className = 'pr-pager-btn';
181
+ cancelBtn.textContent = 'Cancel';
182
+ cancelBtn.onclick = function() { openPinnedView(idx); };
183
+ const saveBtn = document.createElement('button');
184
+ saveBtn.style.cssText = 'padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer';
185
+ saveBtn.textContent = 'Save';
186
+ saveBtn.onclick = function(ev) { submitPinnedEdit(originalTitle, ev); };
187
+ actions.appendChild(cancelBtn);
188
+ actions.appendChild(saveBtn);
189
+
190
+ wrap.appendChild(titleLabel);
191
+ wrap.appendChild(contentLabel);
192
+ wrap.appendChild(levelLabel);
193
+ wrap.appendChild(actions);
194
+ body.replaceChildren(wrap);
195
+ document.getElementById('modal').classList.add('open');
196
+ }
197
+
198
+ async function submitPinnedEdit(originalTitle, e) {
199
+ const btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
200
+ const title = (document.getElementById('pin-edit-title').value || '').trim();
201
+ const content = document.getElementById('pin-edit-content').value;
202
+ const level = document.getElementById('pin-edit-level').value;
203
+ if (!title || !content) { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } alert('Title and content required'); return; }
204
+ try { closeModal(); } catch { /* may not be open */ }
205
+
206
+ // Optimistic update: swap the edited entry in window._pinnedEntries (keyed on
207
+ // the original title) and re-render immediately. Snapshot to revert on failure.
208
+ const prevEntries = Array.isArray(window._pinnedEntries) ? window._pinnedEntries.slice() : [];
209
+ const nextEntries = prevEntries.map(en => en.title === originalTitle ? { title, content, level: level || 'info' } : en);
210
+ window._pinnedEntries = nextEntries;
211
+ try { renderPinned(nextEntries); } catch { /* DOM may be missing — non-fatal */ }
212
+ showToast('cmd-toast', 'Note updated', true);
213
+
214
+ try {
215
+ const res = await fetch('/api/pinned/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ originalTitle, title, content, level }) });
216
+ if (res.ok) {
217
+ refresh();
218
+ } else {
219
+ window._pinnedEntries = prevEntries;
220
+ try { renderPinned(prevEntries); } catch { /* ignore */ }
221
+ const d = await res.json().catch(() => ({}));
222
+ showToast('cmd-toast', 'Update failed: ' + (d.error || 'unknown'), false);
223
+ }
224
+ } catch (err) {
225
+ window._pinnedEntries = prevEntries;
226
+ try { renderPinned(prevEntries); } catch { /* ignore */ }
227
+ showToast('cmd-toast', 'Error: ' + err.message, false);
228
+ }
229
+ }
230
+
231
+ window.MinionsPinned = { renderPinned, openPinNoteModal, submitPinnedNote, removePinnedNote, openPinnedView, openPinnedEdit, submitPinnedEdit };
@@ -3,6 +3,26 @@
3
3
  // Signal the engine to tick immediately (pick up new work without waiting 60s)
4
4
  function wakeEngine() { fetch('/api/engine/wakeup', { method: 'POST' }).catch(() => {}); }
5
5
 
6
+ // "Try new Slim UX" — flips the slim-ux feature flag ON then reloads so the
7
+ // root route serves the slim cockpit (the takeover is checked at request time,
8
+ // so no engine restart is needed). The first-visit welcome popup is gated on
9
+ // the browser side in dashboard/slim/js/settings.js, not here.
10
+ async function trySlimUx(btn) {
11
+ if (btn) { btn.disabled = true; btn.textContent = 'Switching…'; }
12
+ try {
13
+ const res = await fetch('/api/features/toggle', {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({ id: 'slim-ux', enabled: true }),
17
+ });
18
+ if (!res.ok) throw new Error('HTTP ' + res.status);
19
+ location.reload();
20
+ } catch (e) {
21
+ if (btn) { btn.disabled = false; btn.textContent = '✨ Try new Slim UX'; }
22
+ alert('Could not switch to Slim UX: ' + (e && e.message ? e.message : 'unknown error'));
23
+ }
24
+ }
25
+
6
26
  // Optimistic delete suppression
7
27
  const _deletedIds = new Map();
8
28
  function markDeleted(key) { _deletedIds.set(key, Date.now() + 10000); }
@@ -25,6 +25,7 @@
25
25
  <header>
26
26
  <h1>Minions Mission Control</h1>
27
27
  <div style="display:flex;align-items:center;gap:8px;">
28
+ <button id="try-slim-ux-btn" class="try-slim-ux-btn" onclick="trySlimUx(this)" title="Switch to the experimental Slim UX — a focused, single-screen cockpit" style="display:none">&#10024; Try new Slim UX</button>
28
29
  <span class="engine-badge stopped" id="engine-badge">STOPPED</span>
29
30
  <span id="version-banner" style="font-size:var(--text-xs);color:var(--muted)"></span>
30
31
  <div class="timestamp" id="ts">—</div>
@@ -43,10 +43,15 @@
43
43
  </div>
44
44
  <div class="actions-buttons">
45
45
  <button id="slim-linkpr-btn" class="link-pr-btn" type="button">
46
- <span class="link-pr-icon">&#x1F517;</span>
46
+ <span class="link-pr-icon act-ic-link"></span>
47
47
  <span class="link-pr-label">Link PR</span>
48
48
  <span class="link-pr-sub">track a pull request</span>
49
49
  </button>
50
+ <button id="slim-pin-btn" class="link-pr-btn" type="button">
51
+ <span class="link-pr-icon act-ic-pin"></span>
52
+ <span class="link-pr-label">Pin Content</span>
53
+ <span class="link-pr-sub">context for all agents</span>
54
+ </button>
50
55
  </div>
51
56
  </div>
52
57
 
@@ -96,6 +101,12 @@
96
101
  <div class="cockpit-value dim">0</div>
97
102
  <div class="cockpit-detail">no watches set</div>
98
103
  </div>
104
+ <div class="cockpit-tile" data-tile="pinned">
105
+ <button id="slim-tile-pin-chip" class="linkpr-chip on-tile" type="button" title="Pin content">+ Pin</button>
106
+ <div class="cockpit-label"><span class="cockpit-dot"></span> Pinned context</div>
107
+ <div class="cockpit-value dim">0</div>
108
+ <div class="cockpit-detail">nothing pinned</div>
109
+ </div>
99
110
  </div>
100
111
  </div>
101
112
  </div>
@@ -117,6 +128,107 @@
117
128
  </div>
118
129
  </div>
119
130
 
131
+ <!-- First-visit welcome popup. Shown exactly once per browser — gated on the
132
+ localStorage key 'minions:slim-ux-welcomed', set on dismiss. Wired in
133
+ dashboard/slim/js/settings.js (maybeShowSlimWelcome / dismissSlimWelcome).
134
+ Switching the slim-ux flag off and on again will NOT re-show it, matching
135
+ "only the first time users see the Slim UX". -->
136
+ <div class="modal-bg" id="slim-welcome-modal">
137
+ <div class="modal slim-welcome">
138
+ <div class="modal-header">
139
+ <h3>&#10024; Welcome to the new Slim UX</h3>
140
+ </div>
141
+ <div class="modal-body">
142
+ <p>A focused, single-screen cockpit for your Minions team. Here&rsquo;s what&rsquo;s new:</p>
143
+ <ul class="welcome-list">
144
+ <li><strong>Command Center front and center</strong> &mdash; chat with your team from the main panel, with multi-tab conversations that carry over from the classic dashboard.</li>
145
+ <li><strong>Live status tiles</strong> &mdash; engine, dispatches, queue, PRs and watches at a glance. Click any tile for details.</li>
146
+ <li><strong>Your team</strong> &mdash; every agent as a card with live status.</li>
147
+ <li><strong>Recent activity</strong> &mdash; a running feed of completions and pull requests.</li>
148
+ </ul>
149
+ <p class="welcome-note">Your data and engine are unchanged &mdash; this is just a new view. Switch back any time with <strong>&#8592; Classic dashboard</strong> in the top bar.</p>
150
+ <p class="welcome-note">Please add your feedback using the <strong>Report Bug</strong> button.</p>
151
+ </div>
152
+ <div class="modal-footer">
153
+ <button id="slim-welcome-back" class="btn-secondary" type="button">Back to classic</button>
154
+ <button id="slim-welcome-dismiss" class="btn-primary" type="button">Got it &mdash; let&rsquo;s go</button>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Report Bug dialog — files a GitHub issue on the Minions repo via
160
+ POST /api/issues/create (the same endpoint the classic dashboard uses).
161
+ Wired in dashboard/slim/js/settings.js. -->
162
+ <div class="modal-bg" id="slim-bug-modal">
163
+ <div class="modal">
164
+ <div class="modal-header">
165
+ <h3>&#x1F41B; Report a bug</h3>
166
+ <button id="slim-bug-close" class="icon-btn" title="Close">&times;</button>
167
+ </div>
168
+ <div class="modal-body">
169
+ <p>File a bug on the Minions repo. Include steps to reproduce and expected vs actual behavior.</p>
170
+ <label class="linkpr-label">Title
171
+ <input id="slim-bug-title" class="linkpr-input" type="text" placeholder="Short description of the bug">
172
+ </label>
173
+ <label class="linkpr-label">Description
174
+ <textarea id="slim-bug-desc" class="linkpr-input" rows="6" placeholder="Steps to reproduce, expected vs actual behavior&hellip;"></textarea>
175
+ </label>
176
+ <div id="slim-bug-msg" style="margin-top:8px;font-size:var(--text-base);min-height:16px"></div>
177
+ </div>
178
+ <div class="modal-footer">
179
+ <button id="slim-bug-cancel" class="btn-secondary" type="button">Cancel</button>
180
+ <button id="slim-bug-submit" class="btn-primary" type="button">File Bug</button>
181
+ </div>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- Pinned context list — opened from the "Pinned context" status tile. Lists
186
+ all pinned notes with view/edit/unpin; "+ Pin content" opens the editor.
187
+ Backed by /api/pinned (GET via /api/status), /api/pinned/update, /remove.
188
+ Wired in dashboard/slim/js/pinned.js. -->
189
+ <div class="modal-bg" id="slim-pinned-modal">
190
+ <div class="modal">
191
+ <div class="modal-header">
192
+ <h3>Pinned context</h3>
193
+ <button id="slim-pinned-add" class="linkpr-chip" type="button" style="margin-left:auto;margin-right:8px">+ Pin content</button>
194
+ <button id="slim-pinned-close" class="icon-btn" title="Close">&times;</button>
195
+ </div>
196
+ <div class="modal-body" id="slim-pinned-body"></div>
197
+ </div>
198
+ </div>
199
+
200
+ <!-- Pin editor — create (Pin Content action / "+ Pin") or edit an existing
201
+ note. Submits to POST /api/pinned (create) or /api/pinned/update (edit). -->
202
+ <div class="modal-bg" id="slim-pin-edit-modal">
203
+ <div class="modal">
204
+ <div class="modal-header">
205
+ <h3 id="slim-pin-edit-heading">Pin content</h3>
206
+ <button id="slim-pin-edit-close" class="icon-btn" title="Close">&times;</button>
207
+ </div>
208
+ <div class="modal-body">
209
+ <p>Context all agents see, prepended to every prompt as &ldquo;read first&rdquo;.</p>
210
+ <label class="linkpr-label">Title
211
+ <input id="slim-pin-title" class="linkpr-input" type="text" placeholder="e.g. API freeze until Friday">
212
+ </label>
213
+ <label class="linkpr-label">Content
214
+ <textarea id="slim-pin-content" class="linkpr-input" rows="6" placeholder="What should every agent know?&hellip;"></textarea>
215
+ </label>
216
+ <label class="linkpr-label">Level
217
+ <select id="slim-pin-level" class="linkpr-input">
218
+ <option value="info">Info</option>
219
+ <option value="warning">Warning</option>
220
+ <option value="critical">Critical</option>
221
+ </select>
222
+ </label>
223
+ <div id="slim-pin-msg" style="margin-top:8px;font-size:var(--text-base);min-height:16px"></div>
224
+ </div>
225
+ <div class="modal-footer">
226
+ <button id="slim-pin-cancel" class="btn-secondary" type="button">Cancel</button>
227
+ <button id="slim-pin-submit" class="btn-primary" type="button">Pin</button>
228
+ </div>
229
+ </div>
230
+ </div>
231
+
120
232
  <!-- Settings dialog (preserved from r2): lists experimental flags so the
121
233
  user can flip 'slim-ux' off and revert to the original dashboard. -->
122
234
  <div class="modal-bg" id="slim-settings-modal">
@@ -0,0 +1,11 @@
1
+ diff a/dashboard/slim/body.html b/dashboard/slim/body.html (rejected hunks)
2
+ @@ -8,7 +8,8 @@
3
+ silently break (handler attaches before crash, button still renders,
4
+ click fires but no global handler). addEventListener attaches inside
5
+ the same scope and is observable in DevTools when wiring fails. -->
6
+ - <button id="slim-new-chat-btn" class="icon-btn" title="New chat (opens a new tab)">&#x270E;</button>
7
+ + <button id="slim-back-classic-btn" class="topbar-back-btn" title="Return to the classic dashboard">&#8592; Classic dashboard</button>
8
+ + <button id="slim-report-bug-btn" class="topbar-back-btn" title="Report a bug in Minions">Report Bug</button>
9
+ <button id="slim-settings-btn" class="icon-btn" title="Settings">&#9881;</button>
10
+ </div>
11
+ </div>
@@ -0,0 +1,12 @@
1
+ diff a/dashboard/slim/js/command-send.js b/dashboard/slim/js/command-send.js (rejected hunks)
2
+ @@ -195,8 +195,8 @@
3
+ }
4
+
5
+ // ── Wiring ──────────────────────────────────────────────────────
6
+ - var newChatBtn = document.getElementById('slim-new-chat-btn');
7
+ - if (newChatBtn) newChatBtn.addEventListener('click', function() { newTab(); });
8
+ + // (The header "new chat" button was replaced by "Report Bug"; new tabs are
9
+ + // created via the "+" affordance in the chat tab bar — see renderTabBar.)
10
+ sendBtn.addEventListener('click', sendMessage);
11
+ stopBtn.addEventListener('click', abortActive);
12
+ inputEl.addEventListener('keydown', function(ev) {
@@ -1,5 +1,14 @@
1
1
 
2
2
  // ── Helpers ────────────────────────────────────────────────────
3
+ // Read a feature flag from window.MINIONS_FEATURES (injected into the slim
4
+ // HTML head before this IIFE). Mirrors MinionsFeatures.isOn from the classic
5
+ // dashboard without pulling features-client.js into the slim bundle.
6
+ function slimFeatureOn(id) {
7
+ var f = window.MINIONS_FEATURES || { flags: {}, defaults: {} };
8
+ if (Object.prototype.hasOwnProperty.call(f.flags || {}, id)) return f.flags[id] === true;
9
+ return !!(f.defaults && f.defaults[id] === true);
10
+ }
11
+
3
12
  function escHtml(s) {
4
13
  return String(s == null ? '' : s)
5
14
  .replace(/&/g, '&amp;')