@yemi33/minions 0.1.2172 → 0.1.2173
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/slim/body.html
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
<span class="panel-sub">Command Center & quick triggers</span>
|
|
25
25
|
</div>
|
|
26
26
|
<div class="actions-chat">
|
|
27
|
+
<div class="chat-tabs" id="chat-tabs"></div>
|
|
27
28
|
<div class="chat-messages" id="chat-messages">
|
|
28
29
|
<div class="chat-empty">No messages yet — say hi to Command Center.</div>
|
|
29
30
|
</div>
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
// ── Chatbox wired to /api/command-center/stream ──────────────
|
|
2
2
|
// Mirrors the full dashboard's command center: SSE streaming, tool-call
|
|
3
3
|
// progress, abort, and per-tab session continuity.
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// ── Shared Command-Center store (carry-over with the classic dashboard) ──
|
|
5
|
+
// Slim used to persist a private single-chat blob under its own slim-only
|
|
6
|
+
// localStorage key with its own slim-prefixed tab id. That siloed it from
|
|
7
|
+
// the classic dashboard:
|
|
8
|
+
// switching views lost the conversation AND started a fresh server-side CC
|
|
9
|
+
// session (cc-sessions.json is keyed by tabId). To honor the welcome
|
|
10
|
+
// popup's promise — "multi-tab conversations that carry over from the
|
|
11
|
+
// classic dashboard" — slim now reads/writes the SAME localStorage the
|
|
12
|
+
// classic command center owns:
|
|
13
|
+
// cc-tabs — [{id, title, sessionId, messages:[{role, html, ...}]}]
|
|
14
|
+
// cc-active-tab — id of the focused tab
|
|
15
|
+
// Slim is a single-pane chat, so it mirrors the *active* tab bidirectionally
|
|
16
|
+
// and reuses its `cc-*` id as the server session key, so the same backend
|
|
17
|
+
// session continues across either view. Messages are stored in the classic
|
|
18
|
+
// shape ({role, html}) so the classic dashboard renders them, with slim's
|
|
19
|
+
// extra fields ({text, toolCalls, severity}) carried alongside — classic
|
|
20
|
+
// ignores them, slim re-hydrates from them losslessly.
|
|
21
|
+
var CC_TABS_KEY = 'cc-tabs';
|
|
22
|
+
var CC_ACTIVE_KEY = 'cc-active-tab';
|
|
23
|
+
var SLIM_MAX_TABS = 20; // matches the classic dashboard's CC_MAX_TABS
|
|
6
24
|
var SLIM_PROJECT_KEY = 'slim-current-project-v1';
|
|
7
25
|
var SLIM_MAX_MESSAGES = 30;
|
|
8
26
|
var STREAM_TIMEOUT_MS = (60 * 60 * 1000) + 60000;
|
|
@@ -14,30 +32,115 @@
|
|
|
14
32
|
var currentProject = null;
|
|
15
33
|
try { currentProject = localStorage.getItem(SLIM_PROJECT_KEY) || null; } catch (_e) { /* private mode */ }
|
|
16
34
|
|
|
17
|
-
|
|
35
|
+
function _readCcTabs() {
|
|
18
36
|
try {
|
|
19
|
-
var
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
var raw = localStorage.getItem(CC_TABS_KEY);
|
|
38
|
+
var arr = raw ? JSON.parse(raw) : [];
|
|
39
|
+
return Array.isArray(arr) ? arr : [];
|
|
40
|
+
} catch (_e) { return []; }
|
|
41
|
+
}
|
|
42
|
+
// Persist the FULL array — never trim here. Trimming on every write would
|
|
43
|
+
// silently evict the oldest tabs (and could orphan cc-active-tab) the moment
|
|
44
|
+
// slim saves a message, dropping tabs the classic dashboard still owns. Like
|
|
45
|
+
// classic, the cap is enforced at tab-CREATION time (see _pushCappedTab).
|
|
46
|
+
function _writeCcTabs(tabs) {
|
|
47
|
+
try { localStorage.setItem(CC_TABS_KEY, JSON.stringify(tabs)); } catch (_e) { /* full */ }
|
|
48
|
+
}
|
|
49
|
+
function _findTabIdx(tabs, id) {
|
|
50
|
+
return tabs.findIndex(function(t) { return t && t.id === id; });
|
|
51
|
+
}
|
|
52
|
+
function _newTabId() {
|
|
53
|
+
return 'cc-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
54
|
+
}
|
|
55
|
+
// Append a new tab, evicting the oldest to stay within the cap (classic
|
|
56
|
+
// parity). Clears cc-active-tab if the evicted tab was the active one.
|
|
57
|
+
function _pushCappedTab(tabs, tab) {
|
|
58
|
+
while (tabs.length >= SLIM_MAX_TABS) {
|
|
59
|
+
var dropped = tabs.shift();
|
|
60
|
+
try {
|
|
61
|
+
if (dropped && dropped.id === localStorage.getItem(CC_ACTIVE_KEY)) localStorage.removeItem(CC_ACTIVE_KEY);
|
|
62
|
+
} catch (_e) { /* private mode */ }
|
|
63
|
+
}
|
|
64
|
+
tabs.push(tab);
|
|
65
|
+
return tab;
|
|
66
|
+
}
|
|
67
|
+
// Single source of truth for a fresh tab's shape — used wherever a tab is
|
|
68
|
+
// created or has to be reconstructed (load, save, new-chat).
|
|
69
|
+
function _newTab(id) {
|
|
70
|
+
return { id: id || _newTabId(), title: 'New chat', sessionId: null, messages: [] };
|
|
71
|
+
}
|
|
72
|
+
// Resolve the tab slim should mirror: the classic active tab if present,
|
|
73
|
+
// else the most-recent existing tab, else a fresh one registered into the
|
|
74
|
+
// shared store so the classic dashboard immediately sees it.
|
|
75
|
+
function _ensureActiveTab() {
|
|
76
|
+
var tabs = _readCcTabs();
|
|
77
|
+
var activeId = null;
|
|
78
|
+
try { activeId = localStorage.getItem(CC_ACTIVE_KEY); } catch (_e) { /* private mode */ }
|
|
79
|
+
var ai = activeId ? _findTabIdx(tabs, activeId) : -1;
|
|
80
|
+
var tab = ai >= 0 ? tabs[ai] : null;
|
|
81
|
+
if (!tab && tabs.length) tab = tabs[tabs.length - 1];
|
|
82
|
+
if (!tab) {
|
|
83
|
+
tab = _newTab();
|
|
84
|
+
tabs.push(tab);
|
|
85
|
+
_writeCcTabs(tabs);
|
|
26
86
|
}
|
|
27
|
-
|
|
87
|
+
try { localStorage.setItem(CC_ACTIVE_KEY, tab.id); } catch (_e) { /* private mode */ }
|
|
88
|
+
return tab;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var tabId = _ensureActiveTab().id;
|
|
28
92
|
|
|
29
93
|
var msgsEl = document.getElementById('chat-messages');
|
|
30
94
|
var inputEl = document.getElementById('chat-input');
|
|
31
95
|
var sendBtn = document.getElementById('chat-send');
|
|
32
96
|
var stopBtn = document.getElementById('chat-stop');
|
|
33
97
|
|
|
98
|
+
// Extract plain text from classic-authored message HTML (no slim `text`).
|
|
99
|
+
function _htmlToText(html) {
|
|
100
|
+
if (!html) return '';
|
|
101
|
+
var tmp = document.createElement('div');
|
|
102
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: html is read back from our own persisted store (renderMarkdown/escHtml produced it); only textContent is extracted, never re-inserted into the live DOM here
|
|
103
|
+
tmp.innerHTML = String(html);
|
|
104
|
+
return (tmp.textContent || tmp.innerText || '').trim();
|
|
105
|
+
}
|
|
106
|
+
// Stored ({role, html, ...}) → slim in-memory ({role, text, toolCalls, severity}).
|
|
107
|
+
function _storedToSlim(m) {
|
|
108
|
+
if (!m || typeof m !== 'object') return null;
|
|
109
|
+
var text = (typeof m.text === 'string') ? m.text : _htmlToText(m.html);
|
|
110
|
+
var out = { role: m.role || 'assistant', text: text };
|
|
111
|
+
if (Array.isArray(m.toolCalls)) out.toolCalls = m.toolCalls;
|
|
112
|
+
if (m.severity) out.severity = m.severity;
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
// Slim in-memory → stored. Keep slim fields AND a classic-renderable `html`
|
|
116
|
+
// so both dashboards display it. User/action/error text is escaped; the
|
|
117
|
+
// assistant body is markdown-rendered (renderMarkdown escapes first).
|
|
118
|
+
function _slimToStored(m) {
|
|
119
|
+
var role = m.role || 'assistant';
|
|
120
|
+
var html = (role === 'assistant') ? renderMarkdown(m.text || '') : escHtmlChat(m.text || '');
|
|
121
|
+
var out = { role: role, html: html, text: m.text || '' };
|
|
122
|
+
if (Array.isArray(m.toolCalls)) out.toolCalls = m.toolCalls;
|
|
123
|
+
if (m.severity) out.severity = m.severity;
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function _deriveTitle() {
|
|
127
|
+
for (var i = 0; i < messages.length; i++) {
|
|
128
|
+
if (messages[i] && messages[i].role === 'user' && messages[i].text) {
|
|
129
|
+
return messages[i].text.slice(0, 40);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return 'New chat';
|
|
133
|
+
}
|
|
134
|
+
|
|
34
135
|
function loadState() {
|
|
35
136
|
try {
|
|
36
|
-
var
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
137
|
+
var tabs = _readCcTabs();
|
|
138
|
+
var idx = _findTabIdx(tabs, tabId);
|
|
139
|
+
if (idx < 0) return;
|
|
140
|
+
var tab = tabs[idx];
|
|
141
|
+
sessionId = tab.sessionId || null;
|
|
142
|
+
var msgs = Array.isArray(tab.messages) ? tab.messages.slice(-SLIM_MAX_MESSAGES) : [];
|
|
143
|
+
messages = msgs.map(_storedToSlim).filter(Boolean);
|
|
41
144
|
} catch (_e) { /* ignore */ }
|
|
42
145
|
}
|
|
43
146
|
var saveDebounce = null;
|
|
@@ -46,10 +149,15 @@
|
|
|
46
149
|
saveDebounce = setTimeout(function() {
|
|
47
150
|
saveDebounce = null;
|
|
48
151
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
152
|
+
var tabs = _readCcTabs();
|
|
153
|
+
var idx = _findTabIdx(tabs, tabId);
|
|
154
|
+
var tab = (idx >= 0) ? tabs[idx] : _newTab(tabId);
|
|
155
|
+
tab.sessionId = sessionId;
|
|
156
|
+
tab.title = _deriveTitle();
|
|
157
|
+
tab.messages = messages.slice(-SLIM_MAX_MESSAGES).map(_slimToStored);
|
|
158
|
+
if (idx >= 0) tabs[idx] = tab; else tabs.push(tab);
|
|
159
|
+
_writeCcTabs(tabs);
|
|
160
|
+
try { localStorage.setItem(CC_ACTIVE_KEY, tabId); } catch (_e) { /* private mode */ }
|
|
53
161
|
} catch (_e) { /* localStorage full */ }
|
|
54
162
|
}, 300);
|
|
55
163
|
}
|
|
@@ -324,6 +432,89 @@
|
|
|
324
432
|
else appendBubble(m.role, m.text || '', m.toolCalls);
|
|
325
433
|
}
|
|
326
434
|
}
|
|
435
|
+
// ── Command Center tab bar (shared with the classic dashboard) ──
|
|
436
|
+
// One chip per shared cc-tabs entry, so the same conversations are visible
|
|
437
|
+
// and switchable in either view. Slim shows a single pane at a time; this
|
|
438
|
+
// bar is how you move between the tabs the classic dashboard also shows.
|
|
439
|
+
var tabsEl = document.getElementById('chat-tabs');
|
|
440
|
+
|
|
441
|
+
function _tabChipLabel(t) {
|
|
442
|
+
var s = (t && t.title) ? String(t.title) : 'New chat';
|
|
443
|
+
return s.length > 24 ? s.slice(0, 24) + '…' : s;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function renderTabBar() {
|
|
447
|
+
if (!tabsEl) return;
|
|
448
|
+
var tabs = _readCcTabs();
|
|
449
|
+
tabsEl.textContent = '';
|
|
450
|
+
tabs.forEach(function(t) {
|
|
451
|
+
if (!t || !t.id) return;
|
|
452
|
+
var chip = document.createElement('div');
|
|
453
|
+
chip.className = 'chat-tab' + (t.id === tabId ? ' active' : '');
|
|
454
|
+
chip.title = t.title || 'New chat';
|
|
455
|
+
var label = document.createElement('span');
|
|
456
|
+
label.className = 'chat-tab-label';
|
|
457
|
+
label.textContent = _tabChipLabel(t);
|
|
458
|
+
label.addEventListener('click', function() { switchSlimTab(t.id); });
|
|
459
|
+
chip.appendChild(label);
|
|
460
|
+
var close = document.createElement('button');
|
|
461
|
+
close.className = 'chat-tab-close';
|
|
462
|
+
close.type = 'button';
|
|
463
|
+
close.textContent = '×';
|
|
464
|
+
close.title = 'Close chat';
|
|
465
|
+
close.addEventListener('click', function(ev) { ev.stopPropagation(); closeSlimTab(t.id); });
|
|
466
|
+
chip.appendChild(close);
|
|
467
|
+
tabsEl.appendChild(chip);
|
|
468
|
+
});
|
|
469
|
+
var add = document.createElement('button');
|
|
470
|
+
add.className = 'chat-tab-new';
|
|
471
|
+
add.type = 'button';
|
|
472
|
+
add.textContent = '+';
|
|
473
|
+
add.title = 'New chat';
|
|
474
|
+
add.addEventListener('click', function() { slimChatNew(); });
|
|
475
|
+
tabsEl.appendChild(add);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Switch the visible pane to another shared tab. Aborts an in-flight stream
|
|
479
|
+
// first (the SSE is bound to the current tabId), then re-hydrates from the
|
|
480
|
+
// target tab's persisted state.
|
|
481
|
+
function switchSlimTab(id) {
|
|
482
|
+
if (!id || id === tabId) return;
|
|
483
|
+
if (sending) abortInFlight();
|
|
484
|
+
tabId = id;
|
|
485
|
+
try { localStorage.setItem(CC_ACTIVE_KEY, id); } catch (_e) { /* private mode */ }
|
|
486
|
+
sessionId = null;
|
|
487
|
+
messages = [];
|
|
488
|
+
loadState();
|
|
489
|
+
rerenderHistory();
|
|
490
|
+
renderTabBar();
|
|
491
|
+
inputEl.focus();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function closeSlimTab(id) {
|
|
495
|
+
if (!id) return;
|
|
496
|
+
if (id === tabId && sending) abortInFlight();
|
|
497
|
+
var tabs = _readCcTabs();
|
|
498
|
+
var idx = _findTabIdx(tabs, id);
|
|
499
|
+
if (idx < 0) return;
|
|
500
|
+
tabs.splice(idx, 1);
|
|
501
|
+
_writeCcTabs(tabs);
|
|
502
|
+
// Evict the server-side session for the closed tab (mirrors classic).
|
|
503
|
+
try { fetch('/api/cc-sessions/' + encodeURIComponent(id), { method: 'DELETE' }).catch(function() {}); } catch (_e) { /* ignore */ }
|
|
504
|
+
if (id !== tabId) { renderTabBar(); return; }
|
|
505
|
+
if (tabs.length) {
|
|
506
|
+
switchSlimTab(tabs[Math.min(idx, tabs.length - 1)].id);
|
|
507
|
+
} else {
|
|
508
|
+
slimChatNew(); // never leave the user with zero tabs
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Surface tabs created/closed by the classic dashboard in another window.
|
|
513
|
+
window.addEventListener('storage', function(ev) {
|
|
514
|
+
if (!ev || ev.key === CC_TABS_KEY || ev.key === CC_ACTIVE_KEY || ev.key === null) renderTabBar();
|
|
515
|
+
});
|
|
516
|
+
|
|
327
517
|
loadState();
|
|
328
518
|
rerenderHistory();
|
|
519
|
+
renderTabBar();
|
|
329
520
|
|
|
@@ -28,9 +28,13 @@
|
|
|
28
28
|
var text = inputEl.value.trim();
|
|
29
29
|
if (!text) return;
|
|
30
30
|
|
|
31
|
+
// The tab title is derived from the first user message; only that message
|
|
32
|
+
// changes the chip, so re-render the bar once instead of on every send.
|
|
33
|
+
var wasFirstUser = !messages.some(function(m) { return m && m.role === 'user'; });
|
|
31
34
|
appendBubble('user', text);
|
|
32
35
|
messages.push({ role: 'user', text: text });
|
|
33
36
|
saveState();
|
|
37
|
+
if (wasFirstUser) renderTabBar();
|
|
34
38
|
|
|
35
39
|
inputEl.value = '';
|
|
36
40
|
inputEl.style.height = 'auto';
|
|
@@ -161,13 +165,22 @@
|
|
|
161
165
|
|
|
162
166
|
async function slimChatNew() {
|
|
163
167
|
if (sending) abortInFlight();
|
|
168
|
+
// Register a fresh tab in the shared cc-tabs store and switch slim onto
|
|
169
|
+
// it — classic's other tabs are left intact, and the previous session
|
|
170
|
+
// keeps its own server-side cc-sessions.json entry (no DELETE).
|
|
171
|
+
try {
|
|
172
|
+
var tabs = _readCcTabs();
|
|
173
|
+
var fresh = _newTab();
|
|
174
|
+
_pushCappedTab(tabs, fresh); // enforce the tab cap at creation (classic parity)
|
|
175
|
+
_writeCcTabs(tabs);
|
|
176
|
+
tabId = fresh.id;
|
|
177
|
+
try { localStorage.setItem(CC_ACTIVE_KEY, fresh.id); } catch (_e) { /* private mode */ }
|
|
178
|
+
} catch (_e) { /* fall through to in-memory reset */ }
|
|
164
179
|
sessionId = null;
|
|
165
180
|
messages = [];
|
|
166
181
|
saveState();
|
|
167
182
|
rerenderHistory();
|
|
168
|
-
|
|
169
|
-
fetch('/api/cc-sessions/' + encodeURIComponent(tabId), { method: 'DELETE' }).catch(function() {});
|
|
170
|
-
} catch (_e) { /* ignore */ }
|
|
183
|
+
renderTabBar();
|
|
171
184
|
inputEl.focus();
|
|
172
185
|
}
|
|
173
186
|
|
|
@@ -484,11 +484,43 @@
|
|
|
484
484
|
listEl.replaceChildren(frag);
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
// /api/status was slimmed to the small engine/throttle/version envelope
|
|
488
|
+
// (issue #2949) — the heavy slices the cockpit needs (dispatch, agents,
|
|
489
|
+
// pull requests, pinned, watches) moved to dedicated endpoints. The slim
|
|
490
|
+
// poll therefore has to gather those alongside /api/status and merge them
|
|
491
|
+
// into one snapshot before applyStatus(), or every tile reads `undefined`
|
|
492
|
+
// and the dashboard never reflects work in flight.
|
|
493
|
+
async function _slimFetchJson(url) {
|
|
494
|
+
var res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
495
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
496
|
+
return res.json();
|
|
497
|
+
}
|
|
498
|
+
// Resolve to `fallback` instead of rejecting, so one transient endpoint
|
|
499
|
+
// error doesn't blank the whole cockpit — we keep the last-known slice.
|
|
500
|
+
function _slimSettle(url, fallback) {
|
|
501
|
+
return _slimFetchJson(url).then(function(v) { return v; }, function() { return fallback; });
|
|
502
|
+
}
|
|
503
|
+
|
|
487
504
|
async function pollStatusOnce() {
|
|
505
|
+
var prev = lastStatusData || {};
|
|
488
506
|
try {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
507
|
+
// /api/status is the only required fetch (drives the engine tile +
|
|
508
|
+
// timestamp); its rejection rejects the batch and we keep prior values.
|
|
509
|
+
// The slice endpoints fail soft to their previous value.
|
|
510
|
+
var results = await Promise.all([
|
|
511
|
+
_slimFetchJson('/api/status'),
|
|
512
|
+
_slimSettle('/api/dispatch', prev.dispatch),
|
|
513
|
+
_slimSettle('/api/agents', prev.agents),
|
|
514
|
+
_slimSettle('/api/pull-requests', prev.pullRequests),
|
|
515
|
+
_slimSettle('/api/pinned', prev.pinned),
|
|
516
|
+
_slimSettle('/state/engine/watches.json', prev.watches),
|
|
517
|
+
]);
|
|
518
|
+
var data = results[0] || {};
|
|
519
|
+
data.dispatch = (results[1] && typeof results[1] === 'object') ? results[1] : {};
|
|
520
|
+
data.agents = Array.isArray(results[2]) ? results[2] : [];
|
|
521
|
+
data.pullRequests = Array.isArray(results[3]) ? results[3] : [];
|
|
522
|
+
data.pinned = Array.isArray(results[4]) ? results[4] : [];
|
|
523
|
+
data.watches = Array.isArray(results[5]) ? results[5] : [];
|
|
492
524
|
applyStatus(data);
|
|
493
525
|
} catch (e) {
|
|
494
526
|
// Soft failure — leave previous values, surface in stamp
|
|
@@ -247,6 +247,66 @@
|
|
|
247
247
|
flex-direction: column;
|
|
248
248
|
min-height: 0;
|
|
249
249
|
}
|
|
250
|
+
/* Command Center tab bar — shared cc-tabs surfaced for parity with the
|
|
251
|
+
classic dashboard. Sits above the message pane. */
|
|
252
|
+
.chat-tabs {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
gap: 6px;
|
|
256
|
+
flex-wrap: wrap;
|
|
257
|
+
padding: 8px 12px 0;
|
|
258
|
+
width: 100%;
|
|
259
|
+
max-width: 800px;
|
|
260
|
+
margin-left: auto;
|
|
261
|
+
margin-right: auto;
|
|
262
|
+
}
|
|
263
|
+
.chat-tab {
|
|
264
|
+
display: inline-flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
gap: 6px;
|
|
267
|
+
max-width: 220px;
|
|
268
|
+
padding: 4px 6px 4px 10px;
|
|
269
|
+
border: 1px solid var(--border);
|
|
270
|
+
border-radius: 999px;
|
|
271
|
+
background: var(--surface2);
|
|
272
|
+
color: var(--muted);
|
|
273
|
+
font-size: var(--text-sm);
|
|
274
|
+
cursor: pointer;
|
|
275
|
+
white-space: nowrap;
|
|
276
|
+
}
|
|
277
|
+
.chat-tab.active {
|
|
278
|
+
color: var(--text);
|
|
279
|
+
border-color: var(--blue);
|
|
280
|
+
background: color-mix(in srgb, var(--blue) 14%, var(--surface2));
|
|
281
|
+
}
|
|
282
|
+
.chat-tab-label {
|
|
283
|
+
overflow: hidden;
|
|
284
|
+
text-overflow: ellipsis;
|
|
285
|
+
white-space: nowrap;
|
|
286
|
+
}
|
|
287
|
+
.chat-tab-close {
|
|
288
|
+
border: none;
|
|
289
|
+
background: transparent;
|
|
290
|
+
color: var(--muted);
|
|
291
|
+
cursor: pointer;
|
|
292
|
+
font-size: var(--text-md);
|
|
293
|
+
line-height: 1;
|
|
294
|
+
padding: 0 2px;
|
|
295
|
+
border-radius: 4px;
|
|
296
|
+
}
|
|
297
|
+
.chat-tab-close:hover { color: var(--red); background: var(--surface); }
|
|
298
|
+
.chat-tab-new {
|
|
299
|
+
border: 1px dashed var(--border);
|
|
300
|
+
background: transparent;
|
|
301
|
+
color: var(--muted);
|
|
302
|
+
cursor: pointer;
|
|
303
|
+
border-radius: 999px;
|
|
304
|
+
width: 26px;
|
|
305
|
+
height: 26px;
|
|
306
|
+
font-size: var(--text-md);
|
|
307
|
+
line-height: 1;
|
|
308
|
+
}
|
|
309
|
+
.chat-tab-new:hover { color: var(--text); border-color: var(--blue); }
|
|
250
310
|
.chat-messages {
|
|
251
311
|
flex: 1;
|
|
252
312
|
overflow-y: auto;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2173",
|
|
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"
|