@yemi33/minions 0.1.2176 → 0.1.2177

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.
@@ -3,10 +3,13 @@
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.
6
+ // "Try new Slim UX" — flips the slim-ux feature flag ON then navigates to the
7
+ // root route so it serves the slim cockpit (the takeover is checked at request
8
+ // time, so no engine restart is needed). We navigate to '/' rather than
9
+ // location.reload() because the slim takeover is gated on pathname === '/';
10
+ // reloading from a non-home classic page (/work, /prs, …) preserves the path
11
+ // and silently re-serves the classic dashboard. The first-visit welcome popup
12
+ // is gated on the browser side in dashboard/slim/js/settings.js, not here.
10
13
  async function trySlimUx(btn) {
11
14
  if (btn) { btn.disabled = true; btn.textContent = 'Switching…'; }
12
15
  try {
@@ -16,7 +19,7 @@ async function trySlimUx(btn) {
16
19
  body: JSON.stringify({ id: 'slim-ux', enabled: true }),
17
20
  });
18
21
  if (!res.ok) throw new Error('HTTP ' + res.status);
19
- location.reload();
22
+ location.href = '/';
20
23
  } catch (e) {
21
24
  if (btn) { btn.disabled = false; btn.textContent = '✨ Try new Slim UX'; }
22
25
  alert('Could not switch to Slim UX: ' + (e && e.message ? e.message : 'unknown error'));
@@ -21,7 +21,10 @@
21
21
  <div class="panel panel-actions">
22
22
  <div class="panel-header">
23
23
  Actions
24
- <span class="panel-sub">Command Center &amp; quick triggers</span>
24
+ <span class="panel-head-meta">
25
+ <span class="cc-model-badge" id="cc-model-badge" hidden></span>
26
+ <span class="panel-sub">Command Center &amp; quick triggers</span>
27
+ </span>
25
28
  </div>
26
29
  <div class="actions-chat">
27
30
  <div class="chat-tabs" id="chat-tabs"></div>
@@ -39,7 +42,7 @@
39
42
  rows="2"
40
43
  placeholder="Ask anything or give a command... (Enter to send, Shift+Enter for newline)"
41
44
  ></textarea>
42
- <button id="chat-stop" class="chat-stop" type="button" title="Stop the in-flight response">Stop</button>
45
+ <button id="chat-stop" class="chat-stop" type="button" title="Stop the in-flight response" aria-label="Stop agent"><svg class="chat-stop-icon" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2"></circle><rect x="8" y="8" width="8" height="8" rx="1" fill="currentColor"></rect></svg></button>
43
46
  <button id="chat-send" class="chat-send" type="button">Send</button>
44
47
  </div>
45
48
  </div>
@@ -77,23 +80,23 @@
77
80
  count now lives in the Team cards above, so that tile is removed. -->
78
81
  <div class="cockpit-section">
79
82
  <div class="cockpit-grid" id="cockpit-grid">
80
- <div class="cockpit-tile" data-tile="engine">
83
+ <div class="cockpit-tile cockpit-tile--full" data-tile="engine">
81
84
  <div class="cockpit-label"><span class="cockpit-dot"></span> Engine</div>
82
85
  <div class="cockpit-value dim">—</div>
83
86
  <div class="cockpit-detail">checking&hellip;</div>
84
87
  </div>
85
- <div class="cockpit-tile" data-tile="dispatches">
86
- <div class="cockpit-label"><span class="cockpit-dot"></span> Active dispatches</div>
87
- <div class="cockpit-value dim">0</div>
88
- <div class="cockpit-detail">none in flight</div>
89
- </div>
90
88
  <div class="cockpit-tile" data-tile="queued">
91
89
  <div class="cockpit-label"><span class="cockpit-dot"></span> Queued work</div>
92
90
  <div class="cockpit-value dim">0</div>
93
91
  <div class="cockpit-detail">queue empty</div>
94
92
  </div>
93
+ <div class="cockpit-tile" data-tile="dispatches">
94
+ <div class="cockpit-label"><span class="cockpit-dot"></span> Active dispatches</div>
95
+ <div class="cockpit-value dim">0</div>
96
+ <div class="cockpit-detail">no agents working</div>
97
+ </div>
95
98
  <div class="cockpit-tile" data-tile="prs">
96
- <button id="slim-tile-linkpr-chip" class="linkpr-chip on-tile" type="button" title="Link a pull request">+ Link PR</button>
99
+ <button id="slim-tile-linkpr-chip" class="linkpr-chip on-tile" type="button" title="Link a pull request" aria-label="Link a pull request">+</button>
97
100
  <div class="cockpit-label"><span class="cockpit-dot"></span> Active PRs</div>
98
101
  <div class="cockpit-value dim">0</div>
99
102
  <div class="cockpit-detail">no open PRs</div>
@@ -102,7 +102,11 @@
102
102
  var res = await fetch('/api/command-center/stream', {
103
103
  method: 'POST',
104
104
  headers: { 'Content-Type': 'application/json' },
105
- body: JSON.stringify({ message: text, tabId: tabId, sessionId: sessionId, currentProject: currentProject || undefined }),
105
+ // currentProject is the slim project picker's explicit selection (null
106
+ // when the indicator reads "Select project"). noProjectSelected lets the
107
+ // server inject a "no project — ask the user" CC preamble instead of
108
+ // silently defaulting; only the slim composer sends it (W-mqayzsj3).
109
+ body: JSON.stringify({ message: text, tabId: tabId, sessionId: sessionId, currentProject: currentProject || undefined, noProjectSelected: currentProject ? undefined : true }),
106
110
  signal: signal,
107
111
  });
108
112
  if (!res.ok) {
@@ -23,7 +23,15 @@
23
23
  body.appendChild(d);
24
24
  }
25
25
 
26
- // One list row for the tile detail modal. opts: { title, meta, chip:{text,cls}, href }.
26
+ // One list row for the tile detail modal.
27
+ // opts: { title, meta, metaNodes, subtle, subtleBeforeMeta, chip:{text,cls}, href }.
28
+ // meta — plain-text meta line.
29
+ // metaNodes — array of Nodes for the meta line, interleaved with
30
+ // ' · ' separators; takes precedence over `meta`.
31
+ // subtle — optional muted second line (e.g. org/project); skipped
32
+ // when empty.
33
+ // subtleBeforeMeta — when true, subtle renders directly under the title
34
+ // (above the meta row); default is below the meta row.
27
35
  // Built as an <a> when href is present so PR rows open the PR in a new tab.
28
36
  function tileCard(opts) {
29
37
  var card = document.createElement(opts.href ? 'a' : 'div');
@@ -43,15 +51,45 @@
43
51
  top.appendChild(chip);
44
52
  }
45
53
  card.appendChild(top);
46
- if (opts.meta) {
47
- var meta = document.createElement('div');
48
- meta.className = 'tile-item-meta';
49
- meta.textContent = opts.meta;
50
- card.appendChild(meta);
54
+ function appendSubtle() {
55
+ if (!opts.subtle) return;
56
+ var subtle = document.createElement('div');
57
+ subtle.className = 'tile-item-subtle';
58
+ subtle.textContent = opts.subtle;
59
+ card.appendChild(subtle);
51
60
  }
61
+ function appendMeta() {
62
+ if (opts.metaNodes && opts.metaNodes.length) {
63
+ var metaRow = document.createElement('div');
64
+ metaRow.className = 'tile-item-meta';
65
+ opts.metaNodes.forEach(function(node, i) {
66
+ if (i > 0) metaRow.appendChild(document.createTextNode(' · '));
67
+ metaRow.appendChild(node);
68
+ });
69
+ card.appendChild(metaRow);
70
+ } else if (opts.meta) {
71
+ var meta = document.createElement('div');
72
+ meta.className = 'tile-item-meta';
73
+ meta.textContent = opts.meta;
74
+ card.appendChild(meta);
75
+ }
76
+ }
77
+ if (opts.subtleBeforeMeta) { appendSubtle(); appendMeta(); }
78
+ else { appendMeta(); appendSubtle(); }
52
79
  return card;
53
80
  }
54
81
 
82
+ // Parse the org/project slug from a canonical PR id, dropping the host prefix
83
+ // and the trailing "#<num>". E.g. "ado:office/iss/constellation#5315603" →
84
+ // "office/iss/constellation", "github:yemi33/minions#2911" → "yemi33/minions".
85
+ function parsePrSlug(id) {
86
+ if (!id || typeof id !== 'string') return '';
87
+ var hash = id.indexOf('#');
88
+ var head = hash >= 0 ? id.slice(0, hash) : id;
89
+ var colon = head.indexOf(':');
90
+ return colon >= 0 ? head.slice(colon + 1) : head;
91
+ }
92
+
55
93
  // Per-tile body renderers for the detail modal. Each reads the latest status
56
94
  // snapshot and fills the modal body; openTileModal looks them up by key.
57
95
  function renderEngineTileBody(body, data) {
@@ -83,17 +121,61 @@
83
121
  }
84
122
 
85
123
  function renderPrTileBody(body, data) {
124
+ // Emoji glyphs make build/review status scannable at a glance; the
125
+ // accompanying label keeps the row readable when the glyph alone is
126
+ // ambiguous (✅ vs ✅, ❌ vs ❌, ⏳ vs 💬). The raw status string also
127
+ // stays in the title attribute for hover discoverability.
128
+ // Keyed by normalized (lowercased) status; missing/unknown → ➖.
129
+ var BUILD_STATUS = {
130
+ success: { glyph: '✅', label: 'passing' },
131
+ succeeded: { glyph: '✅', label: 'passing' },
132
+ passing: { glyph: '✅', label: 'passing' },
133
+ failing: { glyph: '❌', label: 'failing' },
134
+ failed: { glyph: '❌', label: 'failing' },
135
+ error: { glyph: '❌', label: 'failing' },
136
+ pending: { glyph: '⏳', label: 'pending' },
137
+ running: { glyph: '⏳', label: 'running' },
138
+ queued: { glyph: '⏳', label: 'queued' },
139
+ in_progress: { glyph: '⏳', label: 'in progress' },
140
+ };
141
+ var REVIEW_STATUS = {
142
+ approved: { glyph: '✅', label: 'approved' },
143
+ 'changes-requested': { glyph: '❌', label: 'changes requested' },
144
+ changes_requested: { glyph: '❌', label: 'changes requested' },
145
+ 'requested-changes': { glyph: '❌', label: 'changes requested' },
146
+ rejected: { glyph: '❌', label: 'rejected' },
147
+ pending: { glyph: '💬', label: 'pending' },
148
+ commented: { glyph: '💬', label: 'commented' },
149
+ };
150
+ function statusSpan(subsystem, raw, table) {
151
+ var info = table[String(raw || '').toLowerCase()];
152
+ var glyph = info ? info.glyph : '➖';
153
+ var label = info ? info.label : (raw ? String(raw) : 'none');
154
+ var span = document.createElement('span');
155
+ span.className = 'pr-status';
156
+ span.textContent = glyph + ' ' + subsystem + ' ' + label;
157
+ span.title = subsystem + ': ' + (raw || 'unknown');
158
+ return span;
159
+ }
86
160
  var prs = Array.isArray(data.pullRequests) ? data.pullRequests : [];
87
161
  if (!prs.length) { tileEmpty(body, 'No tracked pull requests.'); return; }
88
162
  prs.forEach(function(p) {
89
163
  var num = p.prNumber || p.number || p.id;
90
164
  var build = p.buildStatus || '';
165
+ var review = p.reviewStatus || '';
91
166
  var cls = p.status === 'merged' ? 'green'
92
167
  : (build === 'failing' ? 'red'
93
168
  : ((p.status === 'active' || p.status === 'linked') ? 'blue' : ''));
169
+ var statusText = document.createElement('span');
170
+ statusText.textContent = p.status || 'unknown';
94
171
  body.appendChild(tileCard({
95
172
  title: (num ? '#' + num + ' ' : '') + (p.title || '(untitled PR)'),
96
- meta: [p.status || 'unknown', build ? 'build ' + build : 'no build', p.reviewStatus ? 'review ' + p.reviewStatus : null].filter(Boolean).join(' · '),
173
+ // Project slug first (under title), then build/review row below the
174
+ // project is the strongest disambiguator across PRs and benefits from
175
+ // being adjacent to the title.
176
+ subtle: parsePrSlug(p.id),
177
+ subtleBeforeMeta: true,
178
+ metaNodes: [statusText, statusSpan('build', build, BUILD_STATUS), statusSpan('review', review, REVIEW_STATUS)],
97
179
  chip: { text: p.status || '—', cls: cls },
98
180
  href: p.url || null,
99
181
  }));
@@ -1,8 +1,12 @@
1
1
  // Project context picker: populated from /api/status (which already returns
2
- // the configured project list). Single-project: rendered as a static label
3
- // so it's still visible but not clickable. Multi-project: <select> dropdown.
4
- // Zero-project: a hint to add one. Selection persists per-browser in
5
- // localStorage and is included in every /api/command-center/stream call.
2
+ // the configured project list). One-or-more projects: a <select> dropdown
3
+ // whose first option is a "Select project" sentinel (value ""), italicized
4
+ // via CSS, so the indicator reads "Select project" until the user explicitly
5
+ // picks one and can always be cleared back to it. Zero-project: a hint to
6
+ // add one. No auto-default: we never silently fall back to projects[0] /
7
+ // last-used / constellation, so project-scoped CC actions can't land in the
8
+ // wrong project (W-mqayzsj3). Selection persists per-browser in localStorage
9
+ // and is included in every /api/command-center/stream call.
6
10
  // Build the "+ Add" button as a real DOM node (was an HTML string + innerHTML
7
11
  // injection — SEC-03 ratchet prefers we avoid that pattern, see
8
12
  // test/unit.test.js DYNAMIC_INNERHTML_BASELINE).
@@ -22,19 +26,20 @@
22
26
  empty.textContent = 'No projects linked yet.';
23
27
  return empty;
24
28
  }
25
- // Strip variant: single-project static label (no picker, just the name).
26
- function makeContextStaticNode(name) {
27
- var staticSpan = document.createElement('span');
28
- staticSpan.className = 'context-static';
29
- staticSpan.textContent = name;
30
- return staticSpan;
31
- }
32
- // Strip variant: multi-project <select>. Returns the select so the caller
33
- // can attach a change listener.
29
+ // Strip variant: project <select>. The first option is a "Select project"
30
+ // sentinel (value "") that is selected whenever no project is explicitly
31
+ // chosen, so the indicator never auto-defaults and the user can always clear
32
+ // their choice back to it. Returns the select so the caller can attach a
33
+ // change listener.
34
34
  function makeContextSelect(projects, selected) {
35
35
  var sel = document.createElement('select');
36
36
  sel.id = 'chat-context-select';
37
37
  sel.setAttribute('aria-label', 'Working in project');
38
+ var placeholder = document.createElement('option');
39
+ placeholder.value = '';
40
+ placeholder.textContent = 'Select project';
41
+ if (!selected) placeholder.selected = true;
42
+ sel.appendChild(placeholder);
38
43
  for (var i = 0; i < projects.length; i++) {
39
44
  var p = projects[i];
40
45
  var opt = document.createElement('option');
@@ -69,22 +74,25 @@
69
74
  currentProject = null;
70
75
  try { localStorage.removeItem(SLIM_PROJECT_KEY); } catch (_e) {}
71
76
  }
72
- if (!currentProject) {
73
- currentProject = projects[0];
74
- try { localStorage.setItem(SLIM_PROJECT_KEY, currentProject); } catch (_e) {}
75
- }
76
- if (projects.length === 1) {
77
- stripEl.style.display = '';
78
- controlsEl.replaceChildren(makeContextStaticNode(currentProject), makeContextAddBtn());
79
- } else {
80
- var sel = makeContextSelect(projects, currentProject);
81
- stripEl.style.display = '';
82
- controlsEl.replaceChildren(sel, makeContextAddBtn());
83
- sel.addEventListener('change', function(ev) {
84
- currentProject = ev.target.value || null;
77
+ // No auto-default: when nothing is explicitly selected, currentProject
78
+ // stays null and the picker shows the "Select project" sentinel
79
+ // (italicized via CSS). We deliberately do NOT fall back to projects[0]
80
+ // here (W-mqayzsj3) so CC turns and project-scoped actions don't
81
+ // silently target the wrong project.
82
+ var sel = makeContextSelect(projects, currentProject);
83
+ stripEl.style.display = '';
84
+ controlsEl.replaceChildren(sel, makeContextAddBtn());
85
+ sel.addEventListener('change', function(ev) {
86
+ var val = ev.target.value || '';
87
+ if (val) {
88
+ currentProject = val;
85
89
  try { localStorage.setItem(SLIM_PROJECT_KEY, currentProject); } catch (_e) {}
86
- });
87
- }
90
+ } else {
91
+ // User cleared back to "Select project" — drop the persisted choice.
92
+ currentProject = null;
93
+ try { localStorage.removeItem(SLIM_PROJECT_KEY); } catch (_e) {}
94
+ }
95
+ });
88
96
  }
89
97
  var addBtn = document.getElementById('chat-context-add');
90
98
  if (addBtn) addBtn.addEventListener('click', openAddProjectModal);
@@ -16,6 +16,13 @@
16
16
  }, delay || 0);
17
17
  }
18
18
 
19
+ // Tiles whose numeric value should fire a one-shot cockpit-flash animation
20
+ // whenever it changes (W-mqb02bga: status panel). Suppressed on first paint
21
+ // — only deltas from a previously-seen number trigger the flash, so opening
22
+ // the dashboard doesn't pulse every tile.
23
+ var FLASH_ON_CHANGE_KEYS = { dispatches: true, prs: true };
24
+ var lastTileValues = Object.create(null);
25
+
19
26
  // Apply lit-state and value/detail text to a tile.
20
27
  function updateTile(key, value, detail, lit) {
21
28
  var tile = document.querySelector('.cockpit-tile[data-tile="' + key + '"]');
@@ -27,16 +34,57 @@
27
34
  valEl.classList.toggle('dim', !lit);
28
35
  }
29
36
  if (detEl) detEl.textContent = detail || '';
30
- tile.classList.remove('lit-blue', 'lit-green', 'lit-amber', 'lit-red');
37
+ tile.classList.remove('lit-blue', 'lit-green', 'lit-amber', 'lit-orange', 'lit-red');
31
38
  if (lit) tile.classList.add('lit-' + lit);
39
+ // Flash the tile when its number changes. Remove → force reflow → re-add
40
+ // restarts the animation even when the class was already present from the
41
+ // previous tick (otherwise repeated changes would only fire on the first
42
+ // delta after class application). Only numeric values count as a change;
43
+ // ignore null/undefined transitions.
44
+ if (FLASH_ON_CHANGE_KEYS[key] && typeof value === 'number') {
45
+ var prev = lastTileValues[key];
46
+ if (typeof prev === 'number' && prev !== value) {
47
+ tile.classList.remove('flash-change');
48
+ void tile.offsetWidth;
49
+ tile.classList.add('flash-change');
50
+ }
51
+ lastTileValues[key] = value;
52
+ }
32
53
  }
33
54
 
34
55
  // Latest /api/status snapshot — read by the cockpit-tile detail modals.
35
56
  var lastStatusData = null;
36
57
 
58
+ // Reflect the Command Center's resolved runtime + model in the Actions panel
59
+ // header (slim-ux only — classic chrome is untouched). Source is the same
60
+ // /api/status payload the cockpit polls: autoMode.ccModel (config.engine
61
+ // ccModel → defaultModel → runtime default) and autoMode.ccCli. When no model
62
+ // is resolvable (runtime picks its own default), the badge stays hidden rather
63
+ // than guessing. textContent keeps it XSS-safe — the values come from config.
64
+ function updateCcModelBadge(model, cli) {
65
+ var badge = document.getElementById('cc-model-badge');
66
+ if (!badge) return;
67
+ var m = (model === null || model === undefined) ? '' : String(model).trim();
68
+ if (!m) {
69
+ badge.hidden = true;
70
+ badge.textContent = '';
71
+ badge.removeAttribute('title');
72
+ return;
73
+ }
74
+ var c = (cli === null || cli === undefined) ? '' : String(cli).trim();
75
+ var label = c ? (c + ' · ' + m) : m;
76
+ badge.textContent = label;
77
+ badge.title = 'Command Center model: ' + label;
78
+ badge.hidden = false;
79
+ }
80
+
37
81
  function applyStatus(data) {
38
82
  if (!data) return;
39
83
  lastStatusData = data;
84
+ // ── Command Center model badge (Actions panel header) ──────
85
+ var autoMode = data.autoMode || {};
86
+ updateCcModelBadge(autoMode.ccModel, autoMode.ccCli);
87
+
40
88
  // ── Engine tile ────────────────────────────────────────────
41
89
  var engine = data.engine || {};
42
90
  // /api/status exposes `state` ('running'|'paused'|'stopping'|'stopped')
@@ -65,14 +113,14 @@
65
113
  updateTile(
66
114
  'dispatches',
67
115
  active.length,
68
- active.length ? 'in flight' : 'none in flight',
69
- active.length ? 'blue' : null
116
+ active.length ? 'agents working' : 'no agents working',
117
+ active.length ? 'amber' : null
70
118
  );
71
119
  updateTile(
72
120
  'queued',
73
121
  pending.length,
74
122
  pending.length ? 'waiting to spawn' : 'queue empty',
75
- pending.length ? 'amber' : null
123
+ pending.length ? 'orange' : null
76
124
  );
77
125
 
78
126
  // ── PR tile (status snapshot from cached pull-requests) ─────
@@ -20,9 +20,15 @@
20
20
  --blue: #58a6ff;
21
21
  --green: #3fb950;
22
22
  --amber: #d29922;
23
+ --orange: #ea580c;
23
24
  --red: #f85149;
24
25
  --radius: 6px;
25
26
 
27
+ /* Spacing scale (W-mqazlkym) — 4/8/12/16/20 rhythm for the chat
28
+ surface. Slim ships its CSS independently, so the scale is declared
29
+ here rather than reusing dashboard/styles.css's --space-* tokens. */
30
+ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px;
31
+
26
32
  /* Typography — mirrors dashboard/styles.css :root tokens
27
33
  (W-mq1c8og40003a7cf). Slim ships its CSS independently
28
34
  (template-baked into slim/layout.html), so we duplicate the
@@ -221,6 +227,34 @@
221
227
  color: var(--muted);
222
228
  font-size: var(--text-base);
223
229
  }
230
+ /* Right-aligned cluster in the Actions panel header: keeps the CC model
231
+ badge tucked next to the "Command Center" sub-label without disturbing
232
+ the header's space-between layout. */
233
+ .panel-header .panel-head-meta {
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 8px;
237
+ min-width: 0;
238
+ }
239
+ /* Compact pill surfacing the Command Center's resolved runtime/model
240
+ (e.g. "copilot · gpt-5.4"). Muted, monospace, low-key — visible but
241
+ unobtrusive. Populated by status.js#updateCcModelBadge. */
242
+ .cc-model-badge {
243
+ text-transform: none;
244
+ letter-spacing: 0;
245
+ font-weight: 500;
246
+ font-size: var(--text-sm);
247
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
248
+ color: var(--muted);
249
+ background: var(--surface2);
250
+ border: 1px solid var(--border);
251
+ border-radius: 999px;
252
+ padding: 1px 8px;
253
+ white-space: nowrap;
254
+ overflow: hidden;
255
+ text-overflow: ellipsis;
256
+ max-width: 220px;
257
+ }
224
258
  .panel-body {
225
259
  flex: 1;
226
260
  overflow-y: auto;
@@ -252,9 +286,9 @@
252
286
  .chat-tabs {
253
287
  display: flex;
254
288
  align-items: center;
255
- gap: 6px;
289
+ gap: var(--space-2);
256
290
  flex-wrap: wrap;
257
- padding: 8px 12px 0;
291
+ padding: var(--space-2) var(--space-3) 0;
258
292
  width: 100%;
259
293
  max-width: 800px;
260
294
  margin-left: auto;
@@ -310,10 +344,13 @@
310
344
  .chat-messages {
311
345
  flex: 1;
312
346
  overflow-y: auto;
313
- padding: 12px;
347
+ /* Extra top padding so the first message clears the tab/context chip
348
+ row above it (#10); 12px sides keep the left grid line shared with the
349
+ input row and action toolbar (#3). */
350
+ padding: var(--space-4) var(--space-3) var(--space-3);
314
351
  display: flex;
315
352
  flex-direction: column;
316
- gap: 10px;
353
+ gap: var(--space-3);
317
354
  min-height: 0;
318
355
  width: 100%;
319
356
  max-width: 800px;
@@ -329,7 +366,9 @@
329
366
  }
330
367
  .chat-msg {
331
368
  max-width: 90%;
332
- padding: 10px 14px;
369
+ /* Uniform bubble padding across variants (#2); 20px horizontal gives
370
+ wrapped technical text >=16px right-edge clearance (#8). */
371
+ padding: var(--space-4) var(--space-5);
333
372
  border-radius: var(--radius);
334
373
  white-space: pre-wrap;
335
374
  word-wrap: break-word;
@@ -415,8 +454,10 @@
415
454
  .chat-queue-resume:hover { filter: brightness(1.1); }
416
455
  .chat-input-wrap {
417
456
  display: flex;
418
- gap: 8px;
419
- padding: 10px 12px;
457
+ gap: var(--space-3);
458
+ /* Breathing room around the textarea; the larger gap separates it from
459
+ the Send/Stop buttons (#5). */
460
+ padding: var(--space-3);
420
461
  border-top: 1px solid var(--border);
421
462
  flex-shrink: 0;
422
463
  background: var(--surface);
@@ -433,8 +474,8 @@
433
474
  .chat-context-strip {
434
475
  display: flex;
435
476
  align-items: center;
436
- gap: 10px;
437
- padding: 6px 12px;
477
+ gap: var(--space-3);
478
+ padding: var(--space-2) var(--space-3);
438
479
  border-top: 1px solid var(--border);
439
480
  flex-shrink: 0;
440
481
  background: var(--surface);
@@ -462,6 +503,16 @@
462
503
  cursor: pointer;
463
504
  }
464
505
  .chat-context-strip select:focus { outline: none; border-color: var(--blue); }
506
+ /* Italicize the "Select project" placeholder option AND the closed select
507
+ widget while the placeholder is the active value. `:has()` is needed
508
+ because native <select> rendering ignores child <option> styles for the
509
+ displayed (closed) text — we have to style the <select> itself. The
510
+ explicit `option { font-style: normal }` reset stops real project names
511
+ from inheriting the parent select's italic when the dropdown is opened
512
+ while the placeholder is still selected. */
513
+ .chat-context-strip select option { font-style: normal; color: var(--text); }
514
+ .chat-context-strip select option[value=""] { font-style: italic; color: var(--muted); }
515
+ .chat-context-strip select:has(option[value=""]:checked) { font-style: italic; color: var(--muted); }
465
516
  .chat-context-strip .context-static {
466
517
  color: var(--text);
467
518
  font-weight: 500;
@@ -514,17 +565,19 @@
514
565
  .chat-send:hover { filter: brightness(1.1); }
515
566
  .chat-send:disabled { opacity: 0.5; cursor: not-allowed; }
516
567
  .chat-stop {
517
- padding: 10px 14px;
568
+ padding: 9px;
518
569
  background: var(--surface2);
519
570
  color: var(--red);
520
571
  border: 1px solid var(--border);
521
572
  border-radius: var(--radius);
522
573
  font-size: var(--text-md);
574
+ line-height: 0;
523
575
  cursor: pointer;
524
576
  align-self: flex-end;
525
577
  display: none;
526
578
  }
527
579
  .chat-stop:hover { filter: brightness(1.1); }
580
+ .chat-stop .chat-stop-icon { display: block; width: 20px; height: 20px; }
528
581
 
529
582
  /* Tool-call progress panel + thinking indicator. While streaming, the
530
583
  thinking indicator sits at the bottom-left of the response card and the
@@ -534,7 +587,7 @@
534
587
  display: flex;
535
588
  align-items: flex-start;
536
589
  gap: 12px;
537
- margin-top: 14px;
590
+ margin-top: var(--space-3);
538
591
  }
539
592
  /* Collapsed tool panel: at most 3 non-wrapping lines, scrolls (newest at
540
593
  the bottom) once there are more. Clicking opens the full list in a modal. */
@@ -614,6 +667,8 @@
614
667
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
615
668
  }
616
669
  .tile-item-meta { font-size: var(--text-base); color: var(--muted); margin-top: 3px; word-break: break-word; }
670
+ .tile-item-subtle { font-size: var(--text-sm); color: var(--muted); margin-top: 2px; word-break: break-word; }
671
+ .pr-status { display: inline-block; margin: 0 1px; text-decoration: none; }
617
672
  .tile-chip {
618
673
  flex: 0 0 auto;
619
674
  font-size: var(--text-base); font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
@@ -676,12 +731,26 @@
676
731
 
677
732
  .chat-action {
678
733
  align-self: flex-start;
679
- padding: 5px 11px;
734
+ padding: var(--space-1) var(--space-3);
680
735
  border-radius: 4px;
681
736
  font-size: var(--text-base);
682
737
  border: 1px dashed var(--border);
683
738
  color: var(--muted);
684
739
  }
740
+ /* A status line that immediately follows a message reads as that card's
741
+ output (#9): tighten the gap (negative margin trims the 12px column gap
742
+ to ~4px), drop the free-floating dashed box, and tie it to the card
743
+ above with a shared left edge + left-border continuation. */
744
+ .chat-msg + .chat-action {
745
+ margin-top: calc(-1 * var(--space-2));
746
+ border: none;
747
+ border-left: 2px solid var(--border);
748
+ border-radius: 0 var(--radius) var(--radius) 0;
749
+ padding-left: var(--space-3);
750
+ }
751
+ .chat-msg + .chat-action.ok { border-left-color: var(--green); }
752
+ .chat-msg + .chat-action.warn { border-left-color: var(--amber); }
753
+ .chat-msg + .chat-action.err { border-left-color: var(--red); }
685
754
  .chat-action.ok { color: var(--green); }
686
755
  .chat-action.warn { color: var(--amber); }
687
756
  .chat-action.err { color: var(--red); }
@@ -693,9 +762,10 @@
693
762
  }
694
763
  .chat-msg.assistant code {
695
764
  background: var(--bg);
696
- padding: 1px 4px;
765
+ padding: 2px 6px;
697
766
  border-radius: 3px;
698
767
  font-size: var(--text-code);
768
+ vertical-align: baseline;
699
769
  }
700
770
  .chat-msg.assistant pre {
701
771
  background: var(--bg);
@@ -709,13 +779,16 @@
709
779
  .chat-msg.assistant a { color: var(--blue); }
710
780
  .chat-msg.assistant strong { color: var(--text); }
711
781
 
712
- /* ── Action footer: square action buttons (one for now: Link PR) ── */
782
+ /* ── Action footer: Link PR + Pin Content ──
783
+ De-emphasized (#6, option b): ghost/secondary styling so they no longer
784
+ compete with the primary blue Send button. The 4-column grid keeps each
785
+ button at a quarter of the panel width (low visual weight). */
713
786
  .actions-buttons {
714
787
  flex-shrink: 0;
715
788
  display: grid;
716
789
  grid-template-columns: repeat(4, 1fr);
717
- gap: 8px;
718
- padding: 10px 12px 12px;
790
+ gap: var(--space-2);
791
+ padding: var(--space-3);
719
792
  border-top: 1px solid var(--border);
720
793
  width: 100%;
721
794
  max-width: 800px;
@@ -723,11 +796,11 @@
723
796
  margin-right: auto;
724
797
  }
725
798
  .link-pr-btn {
726
- background: var(--surface2);
727
- color: var(--text);
799
+ background: transparent;
800
+ color: var(--muted);
728
801
  border: 1px solid var(--border);
729
802
  border-radius: var(--radius);
730
- padding: 10px 12px;
803
+ padding: var(--space-2) var(--space-3);
731
804
  cursor: pointer;
732
805
  font-size: var(--text-md);
733
806
  font-family: inherit;
@@ -793,9 +866,16 @@
793
866
  table inside the IIFE. */
794
867
  .cockpit-grid {
795
868
  display: grid;
796
- grid-template-columns: repeat(2, 1fr);
869
+ grid-template-columns: repeat(6, 1fr);
797
870
  gap: 8px;
798
871
  }
872
+ /* Row 2: Queued | Dispatches | PRs — three equal columns (2 of 6 each). */
873
+ .cockpit-grid > [data-tile="queued"],
874
+ .cockpit-grid > [data-tile="dispatches"],
875
+ .cockpit-grid > [data-tile="prs"] { grid-column: span 2; }
876
+ /* Row 3: Watches | Pinned — two equal columns (3 of 6 each). */
877
+ .cockpit-grid > [data-tile="watches"],
878
+ .cockpit-grid > [data-tile="pinned"] { grid-column: span 3; }
799
879
  .cockpit-tile {
800
880
  background: var(--surface2);
801
881
  border: 1px solid var(--border);
@@ -805,22 +885,37 @@
805
885
  flex-direction: column;
806
886
  gap: 4px;
807
887
  transition: border-color 0.2s, background 0.2s;
888
+ /* Per-state flash color (rgb triplet, no rgba wrapper) consumed by
889
+ `cockpit-flash` so the change-flash ring matches the tile's lit state
890
+ (amber for working dispatches, orange for queued, red for failing
891
+ PRs, etc.). Each lit-* class below overrides this. Default is blue
892
+ for unlit / generic tiles. */
893
+ --flash-color: 88, 166, 255;
808
894
  }
809
895
  .cockpit-tile.lit-blue {
810
896
  border-color: rgba(88, 166, 255, 0.6);
811
897
  background: rgba(88, 166, 255, 0.06);
898
+ --flash-color: 88, 166, 255;
812
899
  }
813
900
  .cockpit-tile.lit-green {
814
901
  border-color: rgba(63, 185, 80, 0.6);
815
902
  background: rgba(63, 185, 80, 0.06);
903
+ --flash-color: 63, 185, 80;
816
904
  }
817
905
  .cockpit-tile.lit-amber {
818
906
  border-color: rgba(210, 153, 34, 0.6);
819
907
  background: rgba(210, 153, 34, 0.06);
908
+ --flash-color: 210, 153, 34;
909
+ }
910
+ .cockpit-tile.lit-orange {
911
+ border-color: rgba(234, 88, 12, 0.6);
912
+ background: rgba(234, 88, 12, 0.08);
913
+ --flash-color: 234, 88, 12;
820
914
  }
821
915
  .cockpit-tile.lit-red {
822
916
  border-color: rgba(248, 81, 73, 0.6);
823
917
  background: rgba(248, 81, 73, 0.08);
918
+ --flash-color: 248, 81, 73;
824
919
  }
825
920
  .cockpit-label {
826
921
  font-size: var(--text-base);
@@ -842,6 +937,7 @@
842
937
  .lit-blue .cockpit-dot { background: var(--blue); box-shadow: 0 0 6px var(--blue); }
843
938
  .lit-green .cockpit-dot { background: var(--green); box-shadow: 0 0 6px var(--green); }
844
939
  .lit-amber .cockpit-dot { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
940
+ .lit-orange .cockpit-dot { background: var(--orange); box-shadow: 0 0 6px var(--orange); }
845
941
  .lit-red .cockpit-dot { background: var(--red); box-shadow: 0 0 6px var(--red); }
846
942
  .cockpit-value {
847
943
  font-size: var(--text-stat);
@@ -856,10 +952,59 @@
856
952
  line-height: 1.3;
857
953
  min-height: 14px;
858
954
  }
955
+ /* Engine tile: full-width, compact single-line horizontal strip. */
956
+ .cockpit-tile--full {
957
+ grid-column: 1 / -1;
958
+ flex-direction: row;
959
+ align-items: center;
960
+ gap: 16px;
961
+ padding: 7px 12px;
962
+ }
963
+ .cockpit-tile--full .cockpit-value { font-size: var(--text-xl); }
964
+ .cockpit-tile--full .cockpit-detail {
965
+ min-height: 0;
966
+ margin-left: auto;
967
+ text-align: right;
968
+ }
859
969
  /* Cockpit tiles are clickable -> detail modal. */
860
970
  .cockpit-tile { cursor: pointer; position: relative; }
861
971
  .cockpit-tile:hover { border-color: var(--blue); }
862
972
  .cockpit-tile:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
973
+ /* Active Dispatches tile pulses while there is work in flight — same
974
+ cadence as the per-member "working" badge in the Team section, so the
975
+ eye instantly ties "things are happening" in both regions. The pulse
976
+ only runs in the lit-amber state (dispatches > 0); empty state is
977
+ static. Respects prefers-reduced-motion. */
978
+ .cockpit-tile[data-tile="dispatches"].lit-amber {
979
+ animation: slim-pulse 1.6s ease-in-out infinite;
980
+ }
981
+ /* Change-flash: brief one-shot ring around a tile whose value just
982
+ changed, used by status.js for the `dispatches` and `prs` tiles. Layered
983
+ on a pseudo-element so it composes cleanly with `slim-pulse` above
984
+ (running both on the tile itself would force a single `animation:` rule
985
+ and clobber one of them). The ring expands then fades — fully visible
986
+ even when the underlying tile is already lit. Respects
987
+ prefers-reduced-motion. */
988
+ .cockpit-tile::after {
989
+ content: '';
990
+ position: absolute;
991
+ inset: -1px;
992
+ border-radius: inherit;
993
+ pointer-events: none;
994
+ box-shadow: 0 0 0 0 rgba(var(--flash-color), 0);
995
+ }
996
+ .cockpit-tile.flash-change::after {
997
+ animation: cockpit-flash 0.75s ease-out;
998
+ }
999
+ @keyframes cockpit-flash {
1000
+ 0% { box-shadow: 0 0 0 0 rgba(var(--flash-color), 0); }
1001
+ 30% { box-shadow: 0 0 0 6px rgba(var(--flash-color), 0.7); }
1002
+ 100% { box-shadow: 0 0 0 0 rgba(var(--flash-color), 0); }
1003
+ }
1004
+ @media (prefers-reduced-motion: reduce) {
1005
+ .cockpit-tile[data-tile="dispatches"].lit-amber { animation: none; }
1006
+ .cockpit-tile.flash-change::after { animation: none; }
1007
+ }
863
1008
 
864
1009
  /* ── Status panel sub-divisions: Team cards over System tiles,
865
1010
  separated by a divider line (no text headers). ──── */
package/dashboard.js CHANGED
@@ -8865,13 +8865,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8865
8865
  // `currentProject` hint so CC can default action.project without the
8866
8866
  // user naming it every turn. Validate against the configured project
8867
8867
  // list before injecting — body.currentProject is untrusted input.
8868
+ // When slim has no project explicitly selected (the "Select project"
8869
+ // indicator), it instead sends `noProjectSelected: true` so we inject an
8870
+ // explicit "do not default — ask the user" preamble rather than silently
8871
+ // letting CC pick projects[0] (W-mqayzsj3). The classic dashboard sends
8872
+ // neither field, so its prompt stays byte-identical (empty part).
8868
8873
  const _knownProjectNames = (PROJECTS || []).map(p => p && p.name).filter(Boolean);
8869
8874
  const _currentProject = (body.currentProject && _knownProjectNames.includes(body.currentProject))
8870
8875
  ? body.currentProject
8871
8876
  : null;
8872
8877
  const projectContextPart = _currentProject
8873
8878
  ? `[Project Context] The user is currently working in project "${_currentProject}". Default any action's "project" field to this project unless the user explicitly names a different one.`
8874
- : '';
8879
+ : (body.noProjectSelected
8880
+ ? `[Project Context] No project is selected. Do NOT default any action's "project" field — ask the user to pick a project before creating or dispatching project-scoped work.`
8881
+ : '');
8875
8882
  const prompt = _joinCcPromptParts(preamble, resumeGuard, carryover, turnHeader, projectContextPart, body.message);
8876
8883
 
8877
8884
  const { trackEngineUsage: trackUsage, trackEngineError: trackErr, withCcTurnTimeout: withTimeout } = require('./engine/llm');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2176",
3
+ "version": "0.1.2177",
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"