@yemi33/minions 0.1.2212 → 0.1.2214

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.
@@ -17,6 +17,158 @@ var _ccSending = false; // true if active tab is sending (UI indicator only)
17
17
  // Clear stale sending state on page load — SSE streams don't survive refresh
18
18
  try { localStorage.removeItem('cc-sending'); } catch {}
19
19
 
20
+ // ── P-2a6d8e74 — Image attachments (paste / drag-drop) ──────────────────────
21
+ // Pending images for the NEXT CC turn. Each entry: { mimeType, dataBase64,
22
+ // filename, dataUrl }. dataBase64 is the raw (prefix-stripped) base64 sent to
23
+ // the server; dataUrl (full data: URI) is for the local thumbnail only.
24
+ // Cleared after every dispatch (v1 sends images for the current turn only).
25
+ var _ccAttachments = [];
26
+ // Client-side limits MIRROR the server (dashboard.js _validateCcImages) — they
27
+ // are UX-only pre-screening; the server remains the single trust boundary.
28
+ var CC_IMG_MAX_COUNT = 4;
29
+ var CC_IMG_MAX_BYTES = 5 * 1024 * 1024; // 5 MB decoded, per image
30
+ var CC_IMG_MIME_ALLOW = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
31
+
32
+ // Read one File/Blob into the attachment shape. Resolves { error } on a
33
+ // client-side limit miss so the caller can toast and skip it.
34
+ function _ccReadImageFile(file) {
35
+ return new Promise(function(resolve) {
36
+ var mime = String(file && file.type || '').toLowerCase();
37
+ if (CC_IMG_MIME_ALLOW.indexOf(mime) === -1) { resolve({ error: 'Unsupported image type' + (mime ? ': ' + mime : '') }); return; }
38
+ if (file.size > CC_IMG_MAX_BYTES) { resolve({ error: (file.name || 'image') + ' is too large (max 5 MB)' }); return; }
39
+ var reader = new FileReader();
40
+ reader.onload = function() {
41
+ var result = String(reader.result || '');
42
+ var comma = result.indexOf(',');
43
+ var dataBase64 = comma >= 0 ? result.slice(comma + 1) : '';
44
+ if (!dataBase64) { resolve({ error: 'Could not read image data' }); return; }
45
+ resolve({ mimeType: mime, dataBase64: dataBase64, filename: file.name || 'pasted-image.png', dataUrl: result });
46
+ };
47
+ reader.onerror = function() { resolve({ error: 'Could not read ' + (file.name || 'image') }); };
48
+ reader.readAsDataURL(file);
49
+ });
50
+ }
51
+
52
+ // Add image files (from paste, drop, or the command bar). Non-images are
53
+ // ignored; over-limit / oversized files are toasted and skipped. Exposed so
54
+ // command-input.js can forward clipboard images into the CC drawer.
55
+ async function ccAddImageFiles(fileList) {
56
+ var files = [];
57
+ for (var i = 0; i < (fileList ? fileList.length : 0); i++) {
58
+ var f = fileList[i];
59
+ if (f && String(f.type || '').indexOf('image/') === 0) files.push(f);
60
+ }
61
+ if (!files.length) return false;
62
+ var added = 0;
63
+ for (var j = 0; j < files.length; j++) {
64
+ if (_ccAttachments.length >= CC_IMG_MAX_COUNT) { showToast('cmd-toast', 'Max ' + CC_IMG_MAX_COUNT + ' images per message', false); break; }
65
+ var res = await _ccReadImageFile(files[j]);
66
+ if (res.error) { showToast('cmd-toast', res.error, false); continue; }
67
+ _ccAttachments.push(res);
68
+ added++;
69
+ }
70
+ if (added) _ccRenderAttachments();
71
+ return added > 0;
72
+ }
73
+
74
+ function ccRemoveAttachment(idx) {
75
+ if (idx >= 0 && idx < _ccAttachments.length) { _ccAttachments.splice(idx, 1); _ccRenderAttachments(); }
76
+ }
77
+
78
+ function _ccClearAttachments() {
79
+ if (!_ccAttachments.length) return;
80
+ _ccAttachments = [];
81
+ _ccRenderAttachments();
82
+ }
83
+
84
+ // Render the thumbnail chips above the input. Built with createElement +
85
+ // textContent / .src (no innerHTML) so untrusted filenames and base64 data
86
+ // can never become an XSS sink and the no-unsanitized lint gate stays clean.
87
+ function _ccRenderAttachments() {
88
+ var box = document.getElementById('cc-attachments');
89
+ if (!box) return;
90
+ box.textContent = '';
91
+ if (!_ccAttachments.length) { box.style.display = 'none'; return; }
92
+ box.style.display = 'flex';
93
+ _ccAttachments.forEach(function(att, i) {
94
+ var chip = document.createElement('div');
95
+ chip.className = 'cc-attach-chip';
96
+ var img = document.createElement('img');
97
+ img.className = 'cc-attach-thumb';
98
+ img.src = att.dataUrl;
99
+ img.alt = att.filename;
100
+ var name = document.createElement('span');
101
+ name.className = 'cc-attach-name';
102
+ name.textContent = att.filename;
103
+ name.title = att.filename;
104
+ var rm = document.createElement('button');
105
+ rm.className = 'cc-attach-remove';
106
+ rm.type = 'button';
107
+ rm.title = 'Remove';
108
+ rm.textContent = '×';
109
+ rm.onclick = function() { ccRemoveAttachment(i); };
110
+ chip.appendChild(img);
111
+ chip.appendChild(name);
112
+ chip.appendChild(rm);
113
+ box.appendChild(chip);
114
+ });
115
+ }
116
+
117
+ // Snapshot the pending attachments as the server payload shape
118
+ // ([{ mimeType, dataBase64, filename }] — no local-only dataUrl).
119
+ function _ccImagesPayload() {
120
+ return _ccAttachments.map(function(a) { return { mimeType: a.mimeType, dataBase64: a.dataBase64, filename: a.filename }; });
121
+ }
122
+
123
+ // Small "📎 N image(s) attached" note appended to the user bubble so a sent
124
+ // turn visibly records its attachments. n is a number — safe to interpolate.
125
+ function _ccAttachmentBadge(n) {
126
+ if (!n) return '';
127
+ return '<div class="cc-msg-attach-note">📎 ' + n + ' image' + (n === 1 ? '' : 's') + ' attached</div>';
128
+ }
129
+
130
+ // Inline event handlers wired from layout.html. Paste intercepts image
131
+ // clipboard items (and lets normal text paste through); drag/drop accepts
132
+ // image files dropped anywhere on the drawer.
133
+ function ccHandlePaste(e) {
134
+ var items = (e.clipboardData && e.clipboardData.items) || [];
135
+ var imgs = [];
136
+ for (var i = 0; i < items.length; i++) {
137
+ if (items[i] && items[i].kind === 'file' && String(items[i].type || '').indexOf('image/') === 0) {
138
+ var f = items[i].getAsFile();
139
+ if (f) imgs.push(f);
140
+ }
141
+ }
142
+ if (!imgs.length) return; // no image — allow the default text paste
143
+ e.preventDefault();
144
+ ccAddImageFiles(imgs);
145
+ }
146
+
147
+ function ccHandleDragOver(e) {
148
+ var types = (e.dataTransfer && e.dataTransfer.types) || [];
149
+ if (Array.prototype.indexOf.call(types, 'Files') === -1) return;
150
+ e.preventDefault();
151
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
152
+ var dz = document.getElementById('cc-drawer');
153
+ if (dz) dz.classList.add('cc-dragover');
154
+ }
155
+
156
+ function ccHandleDragLeave() {
157
+ var dz = document.getElementById('cc-drawer');
158
+ if (dz) dz.classList.remove('cc-dragover');
159
+ }
160
+
161
+ function ccHandleDrop(e) {
162
+ var dz = document.getElementById('cc-drawer');
163
+ if (dz) dz.classList.remove('cc-dragover');
164
+ var files = (e.dataTransfer && e.dataTransfer.files) || [];
165
+ var hasImage = false;
166
+ for (var i = 0; i < files.length; i++) { if (String(files[i].type || '').indexOf('image/') === 0) { hasImage = true; break; } }
167
+ if (!hasImage) return; // let non-image drops fall through
168
+ e.preventDefault();
169
+ ccAddImageFiles(files);
170
+ }
171
+
20
172
  function _ccStripActionBlockFromText(value) {
21
173
  var text = value || '';
22
174
  if (!text) return text;
@@ -947,6 +1099,11 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
947
1099
  var activeTabId = forceTabId || _ccActiveTabId;
948
1100
  var activeTab = _ccTabs.find(function(t) { return t.id === activeTabId; }) || _ccActiveTab();
949
1101
  if (!activeTab) return;
1102
+ // P-2a6d8e74 — snapshot + clear pending image attachments for this turn.
1103
+ // Only fresh user sends carry images (retries/programmatic sends set
1104
+ // skipUserMsg and reuse the already-dispatched text). v1 = current turn only.
1105
+ var sendImages = (!skipUserMsg && _ccAttachments.length) ? _ccImagesPayload() : [];
1106
+ if (sendImages.length) _ccClearAttachments();
950
1107
  activeTab._sending = true;
951
1108
  activeTab._sendStartedAt = Date.now();
952
1109
  activeTab._abortController = new AbortController();
@@ -959,7 +1116,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
959
1116
  // Scoped helper — always targets the originating tab, even if user switches tabs
960
1117
  function addMsg(role, html, skipSave, meta) { ccAddMessage(role, html, skipSave, activeTabId, meta); }
961
1118
 
962
- if (!skipUserMsg) addMsg('user', escHtml(message));
1119
+ if (!skipUserMsg) addMsg('user', escHtml(message) + _ccAttachmentBadge(sendImages.length));
963
1120
 
964
1121
  // Remove queue indicator before processing (it'll be re-added if more queued)
965
1122
  var existingQueueEl = document.getElementById('cc-queue-indicator');
@@ -1067,7 +1224,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
1067
1224
  if (!isReconnect && res.status === 429 && (!activeTab._429retries || activeTab._429retries < 3)) {
1068
1225
  activeTab._429retries = (activeTab._429retries || 0) + 1;
1069
1226
  await new Promise(function(r) { setTimeout(r, 1500); });
1070
- return await _ccConsumeStream({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, transcript: _ccBuildTranscript(activeTab), intentMetadata: intentMetadata || null }, false);
1227
+ return await _ccConsumeStream({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, images: sendImages.length ? sendImages : undefined, transcript: _ccBuildTranscript(activeTab), intentMetadata: intentMetadata || null }, false);
1071
1228
  }
1072
1229
  activeTab._429retries = 0;
1073
1230
  var errText = await res.text();
@@ -1263,7 +1420,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
1263
1420
  while (true) {
1264
1421
  var consume = await _ccConsumeStream(
1265
1422
  reconnectAttempts === 0
1266
- ? { message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, transcript: _ccBuildTranscript(activeTab), intentMetadata: intentMetadata || null }
1423
+ ? { message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, transcript: _ccBuildTranscript(activeTab), intentMetadata: intentMetadata || null, images: sendImages.length ? sendImages : undefined }
1267
1424
  : { tabId: activeTabId, sessionId: activeTab.sessionId || null, reconnect: true },
1268
1425
  reconnectAttempts > 0
1269
1426
  );
@@ -2193,4 +2350,4 @@ if (document.readyState === 'loading') {
2193
2350
  ccInitResize();
2194
2351
  }
2195
2352
 
2196
- window.MinionsCC = { toggleCommandCenter, ccNewSession, ccNewTab, ccSwitchTab, ccCloseTab, ccRenderTabBar, ccRestoreMessages, ccSaveState, ccUpdateSessionIndicator, ccAddMessage, ccSend, ccAbort, ccExecuteAction, ccPrActionFollowup };
2353
+ window.MinionsCC = { toggleCommandCenter, ccNewSession, ccNewTab, ccSwitchTab, ccCloseTab, ccRenderTabBar, ccRestoreMessages, ccSaveState, ccUpdateSessionIndicator, ccAddMessage, ccSend, ccAbort, ccExecuteAction, ccPrActionFollowup, ccAddImageFiles, ccRemoveAttachment, ccHandlePaste, ccHandleDragOver, ccHandleDragLeave, ccHandleDrop };
@@ -146,6 +146,26 @@ async function cmdSubmit() {
146
146
  return;
147
147
  }
148
148
 
149
+ // P-2a6d8e74 — paste a screenshot into the top command bar: open the Command
150
+ // Center drawer and forward the clipboard images into its attachment tray.
151
+ // Non-image pastes fall through to the default textarea behavior.
152
+ function cmdHandlePaste(e) {
153
+ const items = (e.clipboardData && e.clipboardData.items) || [];
154
+ const imgs = [];
155
+ for (let i = 0; i < items.length; i++) {
156
+ if (items[i] && items[i].kind === 'file' && String(items[i].type || '').indexOf('image/') === 0) {
157
+ const f = items[i].getAsFile();
158
+ if (f) imgs.push(f);
159
+ }
160
+ }
161
+ if (!imgs.length) return; // text paste — leave it to the textarea
162
+ e.preventDefault();
163
+ if (!_ccOpen) toggleCommandCenter();
164
+ if (window.MinionsCC && typeof window.MinionsCC.ccAddImageFiles === 'function') {
165
+ window.MinionsCC.ccAddImageFiles(imgs);
166
+ }
167
+ }
168
+
149
169
  // Render the parsed meta chips below input
150
170
  function cmdRenderMeta() {
151
171
  const el = document.getElementById('cmd-meta');
@@ -272,4 +292,4 @@ function cmdInsertPopupItem(id) {
272
292
  cmdRenderMeta();
273
293
  }
274
294
 
275
- window.MinionsCmdInput = { cmdAutoResize, cmdUpdateHighlight, syncHighlightScroll, cmdInputChanged, cmdKeyDown, cmdSubmit, cmdRenderMeta, cmdShowMentions, cmdShowProjects, cmdHidePopup, cmdInsertPopupItem };
295
+ window.MinionsCmdInput = { cmdAutoResize, cmdUpdateHighlight, syncHighlightScroll, cmdInputChanged, cmdKeyDown, cmdSubmit, cmdRenderMeta, cmdShowMentions, cmdShowProjects, cmdHidePopup, cmdInsertPopupItem, cmdHandlePaste };
@@ -699,6 +699,9 @@ function _wiRenderDetail(item) {
699
699
  return ' <span class="pr-badge needs-attention" style="font-size:var(--text-xs);margin-left:4px" title="See &quot;Why this is blocked&quot; below">&#x26A0; Needs attention</span>';
700
700
  })() +
701
701
  '</div>';
702
+ // Work item id — shown in the modal body (not just the title) so operators can
703
+ // read/copy the canonical id (W-… or sched-…) when deeplinking in from a note.
704
+ html += field('ID', '<code style="font-size:var(--text-sm);background:var(--surface2);padding:2px 6px;border-radius:var(--radius-sm)">' + escapeHtml(item.id || '—') + '</code>');
702
705
  // W-mq08kuog001110a6 — "Why this is blocked" section, rendered FIRST so the
703
706
  // human eye lands on the unblocking reason before scrolling through agent,
704
707
  // source, dates, etc. _preDispatchEval reasons are evaluator prose and can
@@ -1073,6 +1076,12 @@ function openInboxNote(filename) {
1073
1076
  titleEl.textContent = filename + ' (archived)';
1074
1077
  // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes note content before assembling HTML; filename is set via textContent
1075
1078
  bodyEl.innerHTML = '<div style="font-size:var(--text-md);line-height:1.7;color:var(--muted)">' + renderMd(content) + '</div>';
1079
+ // Wire Doc Chat for the deeplinked archive note, mirroring the live-inbox
1080
+ // openModal() path (render-prs.js). Without this the archive branch opened
1081
+ // the note as plain text with no Q&A panel, so deeplinking into a note
1082
+ // from a (completed) work item sometimes silently dropped doc-chat.
1083
+ _modalDocContext = { title: filename + ' (archived)', content: content, selection: '' };
1084
+ _modalFilePath = 'notes/archive/' + filename; showModalQa();
1076
1085
  document.getElementById('modal').classList.add('open');
1077
1086
  // P-34fa5d79 — Source-WI chip for archive notes. Same convention as the
1078
1087
  // live-inbox branch above; parsed off the freshly-fetched content.
@@ -44,7 +44,7 @@
44
44
 
45
45
  <!-- Command Center Drawer -->
46
46
  <div id="cc-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:340" onclick="toggleCommandCenter()"></div>
47
- <div id="cc-drawer" style="display:none;position:fixed;top:0;right:0;bottom:0;width:420px;background:var(--surface);border-left:1px solid var(--border);z-index:350;flex-direction:column;overscroll-behavior:contain">
47
+ <div id="cc-drawer" style="display:none;position:fixed;top:0;right:0;bottom:0;width:420px;background:var(--surface);border-left:1px solid var(--border);z-index:350;flex-direction:column;overscroll-behavior:contain" ondragover="ccHandleDragOver(event)" ondragleave="ccHandleDragLeave(event)" ondrop="ccHandleDrop(event)">
48
48
  <div id="cc-resize-handle" class="cc-resize-handle"></div>
49
49
  <div style="padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between">
50
50
  <div style="display:flex;align-items:center;gap:8px">
@@ -59,11 +59,13 @@
59
59
  <div id="cc-tab-bar" style="display:flex;gap:8px;padding:4px 12px;border-bottom:1px solid var(--border);overflow:hidden;align-items:center;flex-shrink:0"></div>
60
60
  <div id="cc-messages" style="flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:10px"></div>
61
61
  <div style="padding:12px 16px;border-top:1px solid var(--border)">
62
+ <!-- P-2a6d8e74 — image attachment thumbnail chips (paste / drag-drop); hidden until images are attached -->
63
+ <div id="cc-attachments" style="display:none;flex-wrap:wrap;gap:6px;margin-bottom:8px"></div>
62
64
  <div style="display:flex;gap:8px">
63
- <textarea id="cc-input" rows="2" placeholder="Ask anything or give a command..." style="flex:1;padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:var(--text-md);resize:none;font-family:inherit" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();ccSend()}"></textarea>
65
+ <textarea id="cc-input" rows="2" placeholder="Ask anything or give a command... (paste or drop an image)" style="flex:1;padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:var(--text-md);resize:none;font-family:inherit" onpaste="ccHandlePaste(event)" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();ccSend()}"></textarea>
64
66
  <button onclick="ccSend()" style="background:var(--blue);color:#fff;border:none;border-radius:6px;padding:8px 14px;font-size:var(--text-md);font-weight:600;cursor:pointer;align-self:flex-end">Send</button>
65
67
  </div>
66
- <div id="cc-powered-by" style="font-size:var(--text-xs);color:var(--muted);margin-top:4px">Full minions context. Enter to send, Shift+Enter for newline.</div>
68
+ <div id="cc-powered-by" style="font-size:var(--text-xs);color:var(--muted);margin-top:4px">Full minions context. Enter to send, Shift+Enter for newline. Paste or drop images to attach.</div>
67
69
  </div>
68
70
  </div>
69
71
 
@@ -3,7 +3,7 @@
3
3
  <div class="cmd-input-wrap" id="cmd-input-wrap">
4
4
  <div class="cmd-highlight-layer" id="cmd-highlight" aria-hidden="true"></div>
5
5
  <textarea id="cmd-input" rows="1" placeholder='What do you need? e.g. "Fix the auth bug @dallas", "explain the dispatch flow", or "/note always use feature flags"'
6
- oninput="cmdInputChanged()" onkeydown="cmdKeyDown(event)" onscroll="syncHighlightScroll()"></textarea>
6
+ oninput="cmdInputChanged()" onkeydown="cmdKeyDown(event)" onscroll="syncHighlightScroll()" onpaste="cmdHandlePaste(event)"></textarea>
7
7
  <button class="cmd-send-btn" id="cmd-send-btn" onclick="cmdSubmit()">Send <kbd>Ctrl+Enter</kbd></button>
8
8
  </div>
9
9
  <div class="cmd-mention-popup" id="cmd-mention-popup"></div>
@@ -1146,6 +1146,20 @@
1146
1146
  .cc-resize-handle:hover::after, .cc-resize-handle.active::after { opacity: 1; background: var(--blue); }
1147
1147
  body.cc-resizing { cursor: col-resize !important; user-select: none !important; }
1148
1148
 
1149
+ /* ── P-2a6d8e74 — CC image attachment chips + drag-drop affordance ───── */
1150
+ #cc-drawer.cc-dragover { outline: 2px dashed var(--blue); outline-offset: -2px; }
1151
+ .cc-attach-chip { display: inline-flex; align-items: center; gap: 6px; max-width: 180px;
1152
+ padding: 4px 6px; background: var(--surface2); border: 1px solid var(--border);
1153
+ border-radius: var(--radius-sm); font-size: var(--text-xs); }
1154
+ .cc-attach-thumb { width: 28px; height: 28px; object-fit: cover; border-radius: 3px;
1155
+ flex-shrink: 0; background: var(--bg); }
1156
+ .cc-attach-name { color: var(--muted); white-space: nowrap; overflow: hidden;
1157
+ text-overflow: ellipsis; }
1158
+ .cc-attach-remove { flex-shrink: 0; background: none; border: none; color: var(--muted);
1159
+ cursor: pointer; font-size: var(--text-md); line-height: 1; padding: 0 2px; }
1160
+ .cc-attach-remove:hover { color: var(--red); }
1161
+ .cc-msg-attach-note { margin-top: 4px; font-size: var(--text-xs); opacity: 0.85; }
1162
+
1149
1163
  .modal-body { padding: var(--space-7) var(--space-8); overflow-y: auto; overflow-x: auto; white-space: pre-wrap; font-size: var(--text-md); line-height: 1.7; color: var(--muted); font-family: Consolas, monospace; }
1150
1164
 
1151
1165
  /* ── Settings: left-rail tabbed layout (W-mpmwxkcn000646cc) ─────────────