@cccarv82/freya 3.0.0 → 3.2.0

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/cli/web-ui.css CHANGED
@@ -1279,134 +1279,6 @@ textarea:focus {
1279
1279
  background: rgba(0, 0, 0, .18);
1280
1280
  }
1281
1281
 
1282
- /* ── Image Lightbox Modal ── */
1283
- .img-lightbox {
1284
- display: none;
1285
- position: fixed;
1286
- inset: 0;
1287
- z-index: 10000;
1288
- background: rgba(0, 0, 0, 0.85);
1289
- backdrop-filter: blur(8px);
1290
- -webkit-backdrop-filter: blur(8px);
1291
- justify-content: center;
1292
- align-items: center;
1293
- cursor: zoom-out;
1294
- animation: lbFadeIn 0.15s ease;
1295
- }
1296
-
1297
- .img-lightbox.active {
1298
- display: flex;
1299
- }
1300
-
1301
- @keyframes lbFadeIn {
1302
- from { opacity: 0; }
1303
- to { opacity: 1; }
1304
- }
1305
-
1306
- .img-lightbox img {
1307
- max-width: 92vw;
1308
- max-height: 88vh;
1309
- border-radius: 10px;
1310
- box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
1311
- object-fit: contain;
1312
- cursor: default;
1313
- animation: lbZoomIn 0.2s ease;
1314
- }
1315
-
1316
- @keyframes lbZoomIn {
1317
- from { transform: scale(0.9); opacity: 0; }
1318
- to { transform: scale(1); opacity: 1; }
1319
- }
1320
-
1321
- .img-lightbox-close {
1322
- position: absolute;
1323
- top: 16px;
1324
- right: 20px;
1325
- width: 36px;
1326
- height: 36px;
1327
- border-radius: 50%;
1328
- background: rgba(255, 255, 255, 0.12);
1329
- border: 1px solid rgba(255, 255, 255, 0.2);
1330
- color: #fff;
1331
- font-size: 20px;
1332
- cursor: pointer;
1333
- display: flex;
1334
- align-items: center;
1335
- justify-content: center;
1336
- transition: background 0.15s;
1337
- }
1338
-
1339
- .img-lightbox-close:hover {
1340
- background: rgba(255, 255, 255, 0.25);
1341
- }
1342
-
1343
- .img-lightbox-name {
1344
- position: absolute;
1345
- bottom: 16px;
1346
- left: 50%;
1347
- transform: translateX(-50%);
1348
- color: rgba(255, 255, 255, 0.7);
1349
- font-size: 12px;
1350
- background: rgba(0, 0, 0, 0.4);
1351
- padding: 4px 14px;
1352
- border-radius: 20px;
1353
- max-width: 80vw;
1354
- overflow: hidden;
1355
- text-overflow: ellipsis;
1356
- white-space: nowrap;
1357
- }
1358
-
1359
- /* ── Paste Preview Strip ── */
1360
- .paste-thumb {
1361
- position: relative;
1362
- display: inline-block;
1363
- }
1364
-
1365
- .paste-thumb-remove {
1366
- position: absolute;
1367
- top: -6px;
1368
- right: -6px;
1369
- width: 18px;
1370
- height: 18px;
1371
- border-radius: 50%;
1372
- background: var(--danger, #ef4444);
1373
- color: #fff;
1374
- border: none;
1375
- font-size: 12px;
1376
- line-height: 18px;
1377
- text-align: center;
1378
- cursor: pointer;
1379
- padding: 0;
1380
- box-shadow: 0 1px 3px rgba(0,0,0,0.3);
1381
- }
1382
-
1383
- .paste-thumb-remove:hover {
1384
- background: #dc2626;
1385
- transform: scale(1.1);
1386
- }
1387
-
1388
- /* ── Bubble Attachments (images in chat) ── */
1389
- .bubble-attachments {
1390
- margin-top: 8px;
1391
- display: flex;
1392
- gap: 6px;
1393
- flex-wrap: wrap;
1394
- }
1395
-
1396
- .bubble-img {
1397
- max-height: 120px;
1398
- max-width: 200px;
1399
- border-radius: 8px;
1400
- border: 1px solid var(--border);
1401
- cursor: pointer;
1402
- transition: transform 0.15s ease;
1403
- }
1404
-
1405
- .bubble-img:hover {
1406
- transform: scale(1.05);
1407
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
1408
- }
1409
-
1410
1282
  .chatComposer {
1411
1283
  display: none;
1412
1284
  }
@@ -1741,10 +1613,6 @@ textarea:focus {
1741
1613
  font-size: 11px;
1742
1614
  }
1743
1615
 
1744
- * {
1745
- border-radius: 0 !important;
1746
- }
1747
-
1748
1616
  /* ── Kanban Board ── */
1749
1617
  .kanban-board {
1750
1618
  display: grid;
package/cli/web-ui.js CHANGED
@@ -21,7 +21,7 @@
21
21
  timelineTag: '',
22
22
  chatSessionId: null,
23
23
  chatLoaded: false,
24
- pastedImages: [] // { file: File, dataUrl: string, name: string }
24
+ pendingImage: null // { filename, url, mimeType } from clipboard paste
25
25
  };
26
26
 
27
27
  function applyDarkTheme() {
@@ -196,11 +196,12 @@
196
196
  var num = 0;
197
197
 
198
198
  // Match append_daily_log / appenddailylog actions
199
- var logRe = /"type"\s*:\s*"append_?daily_?log"\s*,\s*"text"\s*:\s*"([^"]{1,2000})/gi;
199
+ var logRe = /"type"\s*:\s*"append_?daily_?log"\s*,\s*"text"\s*:\s*"([^"]{1,300})/gi;
200
200
  var m;
201
201
  while ((m = logRe.exec(text)) !== null) {
202
202
  num++;
203
- lines.push(num + '. \u{1F4DD} **Registrar no log:** ' + m[1]);
203
+ var t = m[1].slice(0, 140);
204
+ lines.push(num + '. \u{1F4DD} **Registrar no log:** ' + t + (m[1].length > 140 ? '...' : ''));
204
205
  }
205
206
 
206
207
  // Match create_task actions
@@ -246,8 +247,8 @@
246
247
  var num = i + 1;
247
248
 
248
249
  if (type === 'appenddailylog') {
249
- var t = String(a.text || '');
250
- return num + '. ' + icon + ' **Registrar no log:** ' + t;
250
+ var t = String(a.text || '').slice(0, 140);
251
+ return num + '. ' + icon + ' **Registrar no log:** ' + t + (String(a.text || '').length > 140 ? '...' : '');
251
252
  }
252
253
  if (type === 'createtask') {
253
254
  var desc = String(a.description || '').slice(0, 120);
@@ -503,32 +504,32 @@
503
504
  const tag = $('chatModeTag');
504
505
  if (tag) { tag.textContent = '🔍 oracle'; tag.style.display = ''; tag.style.color = 'var(--accent)'; tag.style.borderColor = 'var(--accent)'; }
505
506
 
506
- // Upload pasted images if any (for visual context in log)
507
- var askAttachments = [];
508
- if (state.pastedImages.length > 0) {
509
- askAttachments = await uploadPastedImages();
510
- }
511
- if (askAttachments.length) {
512
- var askHtml = escapeHtml(query).replace(/\n/g, '<br>');
513
- askHtml += '<div class="bubble-attachments">';
514
- for (var ai = 0; ai < askAttachments.length; ai++) {
515
- askHtml += '<img src="/attachments/' + askAttachments[ai].path.replace('data/attachments/', '') + '" class="bubble-img" alt="' + escapeHtml(askAttachments[ai].name) + '" />';
516
- }
517
- askHtml += '</div>';
518
- chatAppend('user', askHtml, { html: true });
507
+ // Include pasted image if present
508
+ var pendingImg = state.pendingImage;
509
+ if (pendingImg) {
510
+ chatAppend('user', escapeHtml(query) + '<br><img src="' + pendingImg.url + '" style="max-height:80px;border-radius:4px;margin-top:6px;cursor:pointer;" onclick="openLightbox(\'' + pendingImg.url + '\')" />', { html: true });
519
511
  } else {
520
512
  chatAppend('user', query);
521
513
  }
522
- clearPastedImages();
523
514
  syncChatThreadVisibility();
524
- setPill('run', askAttachments.length ? 'enviando imagem + pesquisando…' : 'pesquisando…');
515
+
516
+ // Clear image preview
517
+ var preview = document.getElementById('pastePreview');
518
+ if (preview) preview.remove();
519
+
520
+ setPill('run', 'pesquisando…');
525
521
 
526
522
  const typingId = 'typing-' + Date.now();
527
523
  chatAppend('assistant', '<div class="typing-indicator"><span></span><span></span><span></span></div>', { id: typingId, html: true });
528
524
 
529
525
  try {
530
526
  const sessionId = ensureChatSession();
531
- const r = await api('/api/chat/ask', { dir: dirOrDefault(), sessionId, query, attachments: askAttachments });
527
+ var askPayload = { dir: dirOrDefault(), sessionId, query };
528
+ if (pendingImg) {
529
+ askPayload.imagePath = 'data/attachments/' + pendingImg.filename;
530
+ }
531
+ state.pendingImage = null;
532
+ const r = await api('/api/chat/ask', askPayload);
532
533
  const answer = r && r.answer ? r.answer : 'Não encontrei registro';
533
534
 
534
535
  const el = $(typingId);
@@ -2362,42 +2363,36 @@
2362
2363
  const tag = $('chatModeTag');
2363
2364
  if (tag) { tag.textContent = '📥 inbox'; tag.style.display = ''; tag.style.color = 'var(--primary)'; tag.style.borderColor = 'var(--primary)'; }
2364
2365
 
2365
- // Upload pasted images if any
2366
- var attachments = [];
2367
- var hasImages = state.pastedImages.length > 0;
2368
- if (hasImages) {
2369
- setPill('run', 'enviando imagens…');
2370
- attachments = await uploadPastedImages();
2371
- }
2372
-
2373
- // Show user message with image thumbnails
2374
- var userHtml = escapeHtml(text).replace(/\n/g, '<br>');
2375
- if (attachments.length) {
2376
- userHtml += '<div class="bubble-attachments">';
2377
- for (var ai = 0; ai < attachments.length; ai++) {
2378
- userHtml += '<img src="/attachments/' + attachments[ai].path.replace('data/attachments/', '') + '" class="bubble-img" alt="' + escapeHtml(attachments[ai].name) + '" />';
2379
- }
2380
- userHtml += '</div>';
2381
- chatAppend('user', userHtml, { html: true });
2366
+ // Include pasted image if present
2367
+ var pendingImg = state.pendingImage;
2368
+ if (pendingImg) {
2369
+ chatAppend('user', escapeHtml(text) + '<br><img src="' + pendingImg.url + '" style="max-height:80px;border-radius:4px;margin-top:6px;cursor:pointer;" onclick="openLightbox(\'' + pendingImg.url + '\')" />', { html: true });
2382
2370
  } else {
2383
2371
  chatAppend('user', text);
2384
2372
  }
2385
- clearPastedImages();
2386
2373
  syncChatThreadVisibility();
2387
2374
 
2388
- // Typing indicator while processing
2389
- var saveTypingId = 'typing-save-' + Date.now();
2390
- chatAppend('assistant', '<div class="typing-indicator"><span></span><span></span><span></span></div>', { id: saveTypingId, html: true });
2375
+ // Clear image preview
2376
+ var preview = document.getElementById('pastePreview');
2377
+ if (preview) preview.remove();
2378
+
2379
+ // Show typing indicator while processing
2380
+ var typingId = 'typing-' + Date.now();
2381
+ var typingHtml = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
2382
+ chatAppend('assistant', typingHtml, { id: typingId, html: true });
2391
2383
 
2392
2384
  setPill('run', 'salvando…');
2393
- await api('/api/inbox/add', { dir: dirOrDefault(), text, attachments });
2385
+ var inboxPayload = { dir: dirOrDefault(), text };
2386
+ if (pendingImg) inboxPayload.imagePath = 'data/attachments/' + pendingImg.filename;
2387
+ state.pendingImage = null;
2388
+ await api('/api/inbox/add', inboxPayload);
2394
2389
 
2395
- setPill('run', attachments.length ? 'processando texto + imagens…' : 'processando…');
2396
- const r = await api('/api/agents/plan', { dir: dirOrDefault(), text, attachments });
2390
+ setPill('run', 'processando…');
2391
+ const r = await api('/api/agents/plan', { dir: dirOrDefault(), text });
2397
2392
 
2398
2393
  // Remove typing indicator
2399
- var typEl = $(saveTypingId);
2400
- if (typEl) typEl.remove();
2394
+ var typingEl = $(typingId);
2395
+ if (typingEl) typingEl.remove();
2401
2396
 
2402
2397
  state.lastPlan = r.plan || '';
2403
2398
 
@@ -2416,12 +2411,8 @@
2416
2411
  }
2417
2412
 
2418
2413
  if (state.autoApply) {
2419
- var applyTypingId = 'typing-apply-' + Date.now();
2420
- chatAppend('assistant', '<div class="typing-indicator"><span></span><span></span><span></span></div>', { id: applyTypingId, html: true });
2421
- setPill('run', 'aplicando…');
2414
+ setPill('run', 'applying…');
2422
2415
  await applyPlan();
2423
- var applyEl = $(applyTypingId);
2424
- if (applyEl) applyEl.remove();
2425
2416
  const a = state.lastApplied || {};
2426
2417
  setPill('ok', `applied(${a.tasks || 0}t, ${a.blockers || 0}b)`);
2427
2418
  if (state.autoRunReports) {
@@ -2485,23 +2476,40 @@
2485
2476
  state.lastApplied = r.applied || null;
2486
2477
  const summary = r.applied || {};
2487
2478
 
2488
- // Build human-friendly summary instead of raw JSON
2479
+ // Build natural language summary instead of raw JSON
2489
2480
  var parts = [];
2490
- var tasks = Number(summary.tasks || 0);
2491
- var blockers = Number(summary.blockers || 0);
2492
- var skippedT = Number(summary.tasksSkipped || 0);
2493
- var skippedB = Number(summary.blockersSkipped || 0);
2494
-
2495
- if (tasks > 0) parts.push(' **' + tasks + ' tarefa' + (tasks > 1 ? 's' : '') + '** criada' + (tasks > 1 ? 's' : ''));
2496
- if (blockers > 0) parts.push('🚧 **' + blockers + ' blocker' + (blockers > 1 ? 's' : '') + '** registrado' + (blockers > 1 ? 's' : ''));
2497
- if (skippedT > 0) parts.push('⏭️ ' + skippedT + ' tarefa' + (skippedT > 1 ? 's' : '') + ' já existente' + (skippedT > 1 ? 's' : ''));
2498
- if (skippedB > 0) parts.push('⏭️ ' + skippedB + ' blocker' + (skippedB > 1 ? 's' : '') + ' já existente' + (skippedB > 1 ? 's' : ''));
2499
- if (tasks === 0 && blockers === 0) parts.push('📝 Informação registrada no log diário');
2500
-
2501
- let msg = parts.join('\n');
2481
+ var tc = (summary.tasks || 0), bc = (summary.blockers || 0);
2482
+ var ts = (summary.tasksSkipped || 0), bs = (summary.blockersSkipped || 0);
2483
+ if (tc > 0) parts.push('**' + tc + ' tarefa' + (tc > 1 ? 's' : '') + '** registrada' + (tc > 1 ? 's' : ''));
2484
+ if (bc > 0) parts.push('**' + bc + ' blocker' + (bc > 1 ? 's' : '') + '** registrado' + (bc > 1 ? 's' : ''));
2485
+ if (ts > 0) parts.push(ts + ' tarefa' + (ts > 1 ? 's' : '') + ' já existente' + (ts > 1 ? 's' : '') + ' (ignorada' + (ts > 1 ? 's' : '') + ')');
2486
+ if (bs > 0) parts.push(bs + ' blocker' + (bs > 1 ? 's' : '') + ' já existente' + (bs > 1 ? 's' : '') + ' (ignorado' + (bs > 1 ? 's' : '') + ')');
2487
+ if (summary.mode) parts.push('Modo: **' + summary.mode + '**');
2488
+
2489
+ var oq = summary.oracleQueries;
2490
+ if (oq && Array.isArray(oq) && oq.length > 0) {
2491
+ parts.push(oq.length + ' consulta' + (oq.length > 1 ? 's' : '') + ' realizada' + (oq.length > 1 ? 's' : '') + ' no histórico');
2492
+ }
2493
+
2494
+ let msg = '## Resultado\n\n';
2495
+ if (parts.length > 0) {
2496
+ msg += parts.join(' · ') + '\n';
2497
+ } else if (tc === 0 && bc === 0) {
2498
+ msg += 'Contexto registrado no log diário. Nenhuma tarefa ou blocker identificado.\n';
2499
+ }
2500
+
2501
+ // Show semantic duplicates detected
2502
+ if (summary && Array.isArray(summary.semanticDups) && summary.semanticDups.length > 0) {
2503
+ msg += '\n⚠️ **Duplicatas detectadas** (não criadas):\n';
2504
+ for (var di = 0; di < summary.semanticDups.length; di++) {
2505
+ var dup = summary.semanticDups[di];
2506
+ msg += '- "' + dup.newDesc + '" → já existe: "' + dup.existingDesc + '" (' + dup.similarity + ' similar)\n';
2507
+ }
2508
+ }
2502
2509
 
2503
2510
  if (summary && Array.isArray(summary.reportsSuggested) && summary.reportsSuggested.length) {
2504
- msg += '\n\n📊 **Relatórios sugeridos:** ' + summary.reportsSuggested.join(', ');
2511
+ msg += '\n**Relatórios sugeridos:** ' + summary.reportsSuggested.join(', ');
2512
+ msg += '\n\nUse: **Rodar relatórios sugeridos** (barra lateral)';
2505
2513
  }
2506
2514
 
2507
2515
  setOut(msg);
@@ -2527,6 +2535,98 @@
2527
2535
  loadLocal();
2528
2536
  wireRailNav();
2529
2537
 
2538
+ // ── Image paste (Ctrl+V) support ──
2539
+ (function setupImagePaste() {
2540
+ var ta = $('inboxText');
2541
+ if (!ta) return;
2542
+
2543
+ ta.addEventListener('paste', async function(e) {
2544
+ var items = e.clipboardData && e.clipboardData.items;
2545
+ if (!items) return;
2546
+ for (var i = 0; i < items.length; i++) {
2547
+ if (items[i].type.indexOf('image') !== -1) {
2548
+ e.preventDefault();
2549
+ var file = items[i].getAsFile();
2550
+ if (!file) return;
2551
+ var reader = new FileReader();
2552
+ reader.onload = async function(ev) {
2553
+ var base64 = ev.target.result.split(',')[1];
2554
+ var mimeType = file.type || 'image/png';
2555
+ try {
2556
+ var r = await api('/api/attachments/upload', { dir: dirOrDefault(), data: base64, mimeType: mimeType });
2557
+ if (r && r.ok) {
2558
+ state.pendingImage = { filename: r.filename, url: r.url, mimeType: mimeType };
2559
+ showImagePreview(r.url, r.filename);
2560
+ }
2561
+ } catch (err) {
2562
+ console.error('Image upload failed:', err);
2563
+ }
2564
+ };
2565
+ reader.readAsDataURL(file);
2566
+ break;
2567
+ }
2568
+ }
2569
+ });
2570
+ })();
2571
+
2572
+ function showImagePreview(url, filename) {
2573
+ var existing = document.getElementById('pastePreview');
2574
+ if (existing) existing.remove();
2575
+ var ta = $('inboxText');
2576
+ if (!ta) return;
2577
+ var container = document.createElement('div');
2578
+ container.id = 'pastePreview';
2579
+ container.style.cssText = 'display:flex; align-items:center; gap:8px; padding:6px 10px; background:var(--surface); border:1px solid var(--border); border-radius:6px; margin-top:6px;';
2580
+ var img = document.createElement('img');
2581
+ img.src = url;
2582
+ img.style.cssText = 'max-height:48px; max-width:80px; border-radius:4px; cursor:pointer;';
2583
+ img.onclick = function() { openLightbox(url); };
2584
+ var label = document.createElement('span');
2585
+ label.textContent = '📎 ' + filename;
2586
+ label.style.cssText = 'font-size:12px; color:var(--muted); flex:1;';
2587
+ var removeBtn = document.createElement('button');
2588
+ removeBtn.textContent = '✕';
2589
+ removeBtn.style.cssText = 'background:none; border:none; color:var(--muted); cursor:pointer; font-size:14px;';
2590
+ removeBtn.onclick = function() { container.remove(); state.pendingImage = null; };
2591
+ container.appendChild(img);
2592
+ container.appendChild(label);
2593
+ container.appendChild(removeBtn);
2594
+ ta.parentElement.insertBefore(container, ta.nextSibling);
2595
+ }
2596
+
2597
+ // ── Image lightbox modal ──
2598
+ function openLightbox(url) {
2599
+ var existing = document.getElementById('freyaLightbox');
2600
+ if (existing) existing.remove();
2601
+ var overlay = document.createElement('div');
2602
+ overlay.id = 'freyaLightbox';
2603
+ overlay.style.cssText = 'position:fixed; inset:0; background:rgba(0,0,0,0.85); display:flex; align-items:center; justify-content:center; z-index:99999; cursor:pointer;';
2604
+ overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
2605
+ var img = document.createElement('img');
2606
+ img.src = url;
2607
+ img.style.cssText = 'max-width:90vw; max-height:90vh; border-radius:8px; box-shadow:0 8px 40px rgba(0,0,0,0.6);';
2608
+ var closeBtn = document.createElement('button');
2609
+ closeBtn.textContent = '✕';
2610
+ closeBtn.style.cssText = 'position:absolute; top:20px; right:20px; background:rgba(255,255,255,0.15); border:none; color:white; font-size:24px; width:40px; height:40px; border-radius:50%; cursor:pointer;';
2611
+ closeBtn.onclick = function() { overlay.remove(); };
2612
+ overlay.appendChild(img);
2613
+ overlay.appendChild(closeBtn);
2614
+ document.body.appendChild(overlay);
2615
+ // ESC to close
2616
+ function onEsc(e) { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onEsc); } }
2617
+ document.addEventListener('keydown', onEsc);
2618
+ }
2619
+ window.openLightbox = openLightbox;
2620
+
2621
+ // Make all chat images clickable for lightbox (delegate)
2622
+ document.addEventListener('click', function(e) {
2623
+ var img = e.target;
2624
+ if (img.tagName === 'IMG' && img.closest && img.closest('#chatThread')) {
2625
+ e.preventDefault();
2626
+ openLightbox(img.src);
2627
+ }
2628
+ });
2629
+
2530
2630
  // Developer drawer (persist open/close)
2531
2631
  try {
2532
2632
  const d = $('devDrawer');
@@ -2616,149 +2716,8 @@
2616
2716
  refreshToday();
2617
2717
  reloadSlugRules();
2618
2718
  loadChatHistory();
2619
- initPasteHandler();
2620
-
2621
- // Click on chat images → open lightbox
2622
- var thread = $('chatThread');
2623
- if (thread) {
2624
- thread.addEventListener('click', function(e) {
2625
- if (e.target && e.target.classList && e.target.classList.contains('bubble-img')) {
2626
- openLightbox(e.target.src, e.target.alt || 'Anexo');
2627
- }
2628
- });
2629
- }
2630
2719
  })();
2631
2720
 
2632
- // ── Image Paste Handler ──────────────────────────────────────
2633
- function initPasteHandler() {
2634
- var ta = $('inboxText');
2635
- if (!ta) return;
2636
-
2637
- ta.addEventListener('paste', function(e) {
2638
- var items = e.clipboardData && e.clipboardData.items;
2639
- if (!items) return;
2640
-
2641
- for (var i = 0; i < items.length; i++) {
2642
- if (items[i].type.indexOf('image/') === 0) {
2643
- e.preventDefault();
2644
- var file = items[i].getAsFile();
2645
- if (!file) continue;
2646
-
2647
- var reader = new FileReader();
2648
- reader.onload = function(ev) {
2649
- var ext = file.type.split('/')[1] || 'png';
2650
- if (ext === 'jpeg') ext = 'jpg';
2651
- var ts = Date.now();
2652
- var name = 'paste-' + ts + '.' + ext;
2653
- state.pastedImages.push({ file: file, dataUrl: ev.target.result, name: name });
2654
- renderPastePreview();
2655
- };
2656
- reader.readAsDataURL(file);
2657
- break; // handle one image per paste
2658
- }
2659
- }
2660
- });
2661
- }
2662
-
2663
- function renderPastePreview() {
2664
- var strip = $('pastePreview');
2665
- var inner = $('pastePreviewInner');
2666
- if (!strip || !inner) return;
2667
-
2668
- if (!state.pastedImages.length) {
2669
- strip.style.display = 'none';
2670
- inner.innerHTML = '';
2671
- return;
2672
- }
2673
-
2674
- strip.style.display = 'block';
2675
- inner.innerHTML = '';
2676
-
2677
- for (var i = 0; i < state.pastedImages.length; i++) {
2678
- (function(idx) {
2679
- var img = state.pastedImages[idx];
2680
- var wrap = document.createElement('div');
2681
- wrap.className = 'paste-thumb';
2682
-
2683
- var thumb = document.createElement('img');
2684
- thumb.src = img.dataUrl;
2685
- thumb.alt = img.name;
2686
- thumb.style.cssText = 'max-height:60px; max-width:100px; border-radius:6px; border:1px solid var(--border); cursor:pointer;';
2687
- thumb.title = img.name;
2688
- thumb.onclick = function() {
2689
- openLightbox(img.dataUrl, img.name);
2690
- };
2691
-
2692
- var removeBtn = document.createElement('button');
2693
- removeBtn.className = 'paste-thumb-remove';
2694
- removeBtn.innerHTML = '&times;';
2695
- removeBtn.title = 'Remover';
2696
- removeBtn.onclick = function() {
2697
- state.pastedImages.splice(idx, 1);
2698
- renderPastePreview();
2699
- };
2700
-
2701
- wrap.appendChild(thumb);
2702
- wrap.appendChild(removeBtn);
2703
- inner.appendChild(wrap);
2704
- })(i);
2705
- }
2706
-
2707
- // label
2708
- var label = document.createElement('span');
2709
- label.style.cssText = 'font-size:11px; color:var(--faint); margin-left:4px;';
2710
- label.textContent = state.pastedImages.length + ' imagem(ns) anexada(s)';
2711
- inner.appendChild(label);
2712
- }
2713
-
2714
- // Upload pasted images and return array of { name, path }
2715
- async function uploadPastedImages() {
2716
- var results = [];
2717
- for (var i = 0; i < state.pastedImages.length; i++) {
2718
- var img = state.pastedImages[i];
2719
- try {
2720
- var r = await api('/api/attachments/upload', {
2721
- dir: dirOrDefault(),
2722
- data: img.dataUrl,
2723
- filename: img.name
2724
- });
2725
- if (r && r.ok) {
2726
- results.push({ name: img.name, path: r.path });
2727
- }
2728
- } catch(err) {
2729
- // best-effort, skip failed uploads
2730
- }
2731
- }
2732
- return results;
2733
- }
2734
-
2735
- function clearPastedImages() {
2736
- state.pastedImages = [];
2737
- renderPastePreview();
2738
- }
2739
-
2740
- // ── Image Lightbox ──────────────────────────────────────────
2741
- function openLightbox(src, name) {
2742
- var lb = $('imgLightbox');
2743
- var img = $('imgLightboxImg');
2744
- var label = $('imgLightboxName');
2745
- if (!lb || !img) return;
2746
- img.src = src;
2747
- if (label) label.textContent = name || '';
2748
- lb.classList.add('active');
2749
- }
2750
-
2751
- function closeLightbox(e) {
2752
- if (e) e.stopPropagation();
2753
- var lb = $('imgLightbox');
2754
- if (lb) lb.classList.remove('active');
2755
- var img = $('imgLightboxImg');
2756
- if (img) img.src = '';
2757
- }
2758
-
2759
- window.openLightbox = openLightbox;
2760
- window.closeLightbox = closeLightbox;
2761
-
2762
2721
  setPill('ok', 'pronto');
2763
2722
 
2764
2723
  /* ── Global Keyboard Shortcuts ── */
@@ -2775,13 +2734,8 @@
2775
2734
  openQuickAdd();
2776
2735
  return;
2777
2736
  }
2778
- // Escape: Close lightbox → quick-add blur
2737
+ // Escape: Close quick-add modal or blur active element
2779
2738
  if (e.key === 'Escape') {
2780
- const lb = $('imgLightbox');
2781
- if (lb && lb.classList.contains('active')) {
2782
- closeLightbox();
2783
- return;
2784
- }
2785
2739
  const overlay = $('quickAddOverlay');
2786
2740
  if (overlay && overlay.style.display !== 'none') {
2787
2741
  closeQuickAdd();
package/cli/web.js CHANGED
@@ -1192,14 +1192,9 @@ function buildHtml(safeDefault, appVersion) {
1192
1192
  </div>
1193
1193
 
1194
1194
  <!-- Textarea -->
1195
- <textarea id="inboxText" aria-label="Entrada de texto para processar ou perguntar" placeholder="Cole updates, decisões, blockers... ou faça uma pergunta à Freya.&#10;&#10;▸ Salvar & Processar → extrai tarefas e blockers do texto&#10;▸ Perguntar → consulta o histórico via busca semântica (RAG)&#10;▸ Ctrl+V com imagem → anexa screenshot ao contexto" style="resize:none; min-height: 200px; flex: 1; border-radius: 0; border-left: none; border-right: none; border-top: none; border-bottom: 1px solid var(--border); padding: 14px 16px; font-size: 13px; line-height: 1.6;"
1195
+ <textarea id="inboxText" aria-label="Entrada de texto para processar ou perguntar" placeholder="Cole updates, decisões, blockers... ou faça uma pergunta à Freya.&#10;&#10;▸ Salvar & Processar → extrai tarefas e blockers do texto&#10;▸ Perguntar → consulta o histórico via busca semântica (RAG)" style="resize:none; min-height: 200px; flex: 1; border-radius: 0; border-left: none; border-right: none; border-top: none; border-bottom: 1px solid var(--border); padding: 14px 16px; font-size: 13px; line-height: 1.6;"
1196
1196
  onkeydown="if((event.metaKey||event.ctrlKey)&&event.key==='Enter'){event.preventDefault();window.saveAndPlan();}"></textarea>
1197
1197
 
1198
- <!-- Image paste preview strip -->
1199
- <div id="pastePreview" style="display:none; padding: 8px 14px; border-bottom: 1px solid var(--border); background: rgba(0,0,0,0.05); flex-shrink:0;">
1200
- <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;" id="pastePreviewInner"></div>
1201
- </div>
1202
-
1203
1198
  <!-- Actions bar -->
1204
1199
  <div style="padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; flex-shrink:0;">
1205
1200
  <div style="display:flex; gap:8px; flex-wrap:wrap;">
@@ -1351,13 +1346,6 @@ function buildHtml(safeDefault, appVersion) {
1351
1346
  </div>
1352
1347
  </div>
1353
1348
 
1354
- <!-- Image lightbox modal -->
1355
- <div id="imgLightbox" class="img-lightbox" onclick="window.closeLightbox(event)">
1356
- <button class="img-lightbox-close" onclick="window.closeLightbox(event)" title="Fechar (Esc)">&times;</button>
1357
- <img id="imgLightboxImg" src="" alt="preview" onclick="event.stopPropagation()" />
1358
- <div id="imgLightboxName" class="img-lightbox-name"></div>
1359
- </div>
1360
-
1361
1349
  <script>
1362
1350
  window.__FREYA_DEFAULT_DIR = "${safeDefault}";
1363
1351
  </script>
@@ -2408,26 +2396,25 @@ async function cmdWeb({ port, dir, open, dev }) {
2408
2396
  return;
2409
2397
  }
2410
2398
 
2411
- // Serve attachment images (pasted screenshots)
2399
+ // Serve attachment images from workspace data/attachments/
2412
2400
  if (req.method === 'GET' && req.url.startsWith('/attachments/')) {
2413
- const relPath = decodeURIComponent(req.url.replace('/attachments/', ''));
2401
+ const fname = decodeURIComponent(req.url.slice('/attachments/'.length)).replace(/[\/\\]/g, '');
2414
2402
  const requestedDir = dir || './freya';
2415
- const wsDir = path.resolve(process.cwd(), requestedDir);
2416
- const filePath = path.join(wsDir, 'data', 'attachments', relPath);
2417
- // Security: ensure path stays within data/attachments
2418
- if (!filePath.startsWith(path.join(wsDir, 'data', 'attachments'))) {
2419
- res.writeHead(403); res.end(); return;
2420
- }
2421
- if (exists(filePath)) {
2422
- const ext = path.extname(filePath).toLowerCase();
2423
- const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp' };
2424
- const mime = mimeMap[ext] || 'application/octet-stream';
2425
- const data = fs.readFileSync(filePath);
2426
- res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': 'public, max-age=86400' });
2427
- res.end(data);
2403
+ let wsDir;
2404
+ try { wsDir = normalizeWorkspaceDir(requestedDir); } catch { wsDir = path.resolve(process.cwd(), requestedDir); }
2405
+ const filePath = path.join(wsDir, 'data', 'attachments', fname);
2406
+ if (!exists(filePath)) {
2407
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
2408
+ res.end('Not found');
2428
2409
  return;
2429
2410
  }
2430
- res.writeHead(404); res.end(); return;
2411
+ const ext = path.extname(fname).toLowerCase();
2412
+ const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp', '.svg': 'image/svg+xml' };
2413
+ const mime = mimeMap[ext] || 'application/octet-stream';
2414
+ const data = fs.readFileSync(filePath);
2415
+ res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': 'max-age=86400' });
2416
+ res.end(data);
2417
+ return;
2431
2418
  }
2432
2419
 
2433
2420
  if (req.url.startsWith('/api/')) {
@@ -3240,30 +3227,9 @@ async function cmdWeb({ port, dir, open, dev }) {
3240
3227
  }
3241
3228
  }
3242
3229
 
3243
- // ── Upload attachment (image paste) ──────────────────────────
3244
- if (req.url === '/api/attachments/upload') {
3245
- const data = String(payload.data || '').trim();
3246
- const fname = String(payload.filename || '').trim();
3247
- if (!data || !fname) return safeJson(res, 400, { error: 'Missing data or filename' });
3248
-
3249
- const d = isoDate();
3250
- const attDir = path.join(workspaceDir, 'data', 'attachments', d);
3251
- ensureDir(attDir);
3252
-
3253
- // data is base64 (may have data:image/... prefix)
3254
- const base64 = data.replace(/^data:image\/[^;]+;base64,/, '');
3255
- const buf = Buffer.from(base64, 'base64');
3256
- const filePath = path.join(attDir, fname);
3257
- fs.writeFileSync(filePath, buf);
3258
-
3259
- const relPath = `data/attachments/${d}/${fname}`;
3260
- return safeJson(res, 200, { ok: true, path: relPath });
3261
- }
3262
-
3263
3230
  if (req.url === '/api/inbox/add') {
3264
3231
  const text = String(payload.text || '').trim();
3265
- const attachments = Array.isArray(payload.attachments) ? payload.attachments : [];
3266
- if (!text && !attachments.length) return safeJson(res, 400, { error: 'Missing text' });
3232
+ if (!text) return safeJson(res, 400, { error: 'Missing text' });
3267
3233
 
3268
3234
  const d = isoDate();
3269
3235
  const file = path.join(workspaceDir, 'logs', 'daily', `${d}.md`);
@@ -3277,13 +3243,8 @@ async function cmdWeb({ port, dir, open, dev }) {
3277
3243
  const linksText = linkInfo && linkInfo.linksText ? linkInfo.linksText : '';
3278
3244
  const slugs = linkInfo && Array.isArray(linkInfo.slugs) ? linkInfo.slugs : [];
3279
3245
 
3280
- // Build attachment markdown references
3281
- let attachBlock = '';
3282
- if (attachments.length) {
3283
- attachBlock = '\n\n**Anexos:**\n' + attachments.map(a => `![${a.name || 'screenshot'}](${a.path})`).join('\n');
3284
- }
3285
-
3286
- const block = `\n\n## [${hh}:${mm}] Raw Input\n${text}${linksText}${attachBlock}\n`;
3246
+ const imgRef = payload.imagePath ? `\n![anexo](../${payload.imagePath})` : '';
3247
+ const block = `\n\n## [${hh}:${mm}] Raw Input\n${text}${imgRef}${linksText}\n`;
3287
3248
  fs.appendFileSync(file, block, 'utf8');
3288
3249
 
3289
3250
  if (slugs.length) {
@@ -3342,16 +3303,7 @@ async function cmdWeb({ port, dir, open, dev }) {
3342
3303
  ]
3343
3304
  };
3344
3305
 
3345
- // Build image references if attachments were provided
3346
- const attachments = Array.isArray(payload.attachments) ? payload.attachments : [];
3347
- let imageContext = '';
3348
- if (attachments.length) {
3349
- imageContext = '\n\nIMAGENS ANEXADAS (use @caminho para analisar):\n' +
3350
- attachments.map(a => `- @${path.join(workspaceDir, a.path)} (${a.name})`).join('\n') + '\n' +
3351
- 'Analise as imagens anexadas e inclua as informações visuais no plano.\n';
3352
- }
3353
-
3354
- const prompt = `Você é o planner do sistema F.R.E.Y.A.\n\nContexto: vamos receber um input bruto do usuário e propor ações estruturadas.\nRegras: siga os arquivos de regras abaixo.\nSaída: retorne APENAS JSON válido no formato: ${JSON.stringify(schema)}\n\nRestrições:\n- NÃO use code fences (\`\`\`)\n- NÃO inclua texto extra antes/depois do JSON\n- NÃO use quebras de linha dentro de strings (transforme em uma frase única)\n\nREGRAS:${rulesText}${imageContext}\n\nINPUT DO USUÁRIO:\n${text}\n`;
3306
+ const prompt = `Você é o planner do sistema F.R.E.Y.A.\n\nContexto: vamos receber um input bruto do usuário e propor ações estruturadas.\nRegras: siga os arquivos de regras abaixo.\nSaída: retorne APENAS JSON válido no formato: ${JSON.stringify(schema)}\n\nRestrições:\n- NÃO use code fences (\`\`\`)\n- NÃO inclua texto extra antes/depois do JSON\n- NÃO use quebras de linha dentro de strings (transforme em uma frase única)\n\nREGRAS:${rulesText}\n\nINPUT DO USUÁRIO:\n${text}\n`;
3355
3307
 
3356
3308
  // Prefer COPILOT_CMD if provided, otherwise try 'copilot'
3357
3309
  const cmd = process.env.COPILOT_CMD || 'copilot';
@@ -3361,7 +3313,7 @@ async function cmdWeb({ port, dir, open, dev }) {
3361
3313
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
3362
3314
  const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
3363
3315
  try {
3364
- const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools', '--add-dir', workspaceDir], workspaceDir, agentEnv);
3316
+ const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir, agentEnv);
3365
3317
  const out = (r.stdout + r.stderr).trim();
3366
3318
  if (r.code !== 0) {
3367
3319
  return safeJson(res, 200, {
@@ -3539,6 +3491,49 @@ async function cmdWeb({ port, dir, open, dev }) {
3539
3491
  const insertTask = dl.db.prepare(`INSERT INTO tasks (id, project_slug, description, category, status, metadata) VALUES (?, ?, ?, ?, ?, ?)`);
3540
3492
  const insertBlocker = dl.db.prepare(`INSERT INTO blockers (id, project_slug, title, severity, status, owner, next_action, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
3541
3493
 
3494
+ // ── Semantic deduplication (async, runs BEFORE the sync transaction) ──
3495
+ // Load pending tasks for cosine similarity comparison
3496
+ const pendingTasks = dl.db.prepare("SELECT id, description, project_slug FROM tasks WHERE status = 'PENDING'").all();
3497
+ let pendingEmbeddings = []; // [{ id, description, project_slug, vector }]
3498
+ try {
3499
+ const { defaultEmbedder } = require(path.join(workspaceDir, 'scripts', 'lib', 'Embedder.js'));
3500
+ for (const pt of pendingTasks) {
3501
+ try {
3502
+ const vec = await defaultEmbedder.embedText(pt.description);
3503
+ pendingEmbeddings.push({ id: pt.id, description: pt.description, project_slug: pt.project_slug, vector: vec });
3504
+ } catch { /* skip if embedding fails for individual task */ }
3505
+ }
3506
+ } catch (embErr) {
3507
+ // Embedder not available — fall back to exact-match only
3508
+ console.error('[dedup] Semantic dedup unavailable:', embErr.message);
3509
+ }
3510
+
3511
+ // Pre-compute semantic duplicates for each action
3512
+ const semanticDupMap = new Map(); // action index → existing task id (if duplicate)
3513
+ const SIMILARITY_THRESHOLD = 0.78;
3514
+ if (pendingEmbeddings.length > 0) {
3515
+ try {
3516
+ const { defaultEmbedder } = require(path.join(workspaceDir, 'scripts', 'lib', 'Embedder.js'));
3517
+ for (let ai = 0; ai < actions.length; ai++) {
3518
+ const a = actions[ai];
3519
+ if (!a || a.type !== 'create_task') continue;
3520
+ const desc = normalizeWhitespace(a.description);
3521
+ if (!desc) continue;
3522
+ try {
3523
+ const newVec = await defaultEmbedder.embedText(desc);
3524
+ let bestScore = 0, bestMatch = null;
3525
+ for (const pe of pendingEmbeddings) {
3526
+ const score = defaultEmbedder.cosineSimilarity(newVec, pe.vector);
3527
+ if (score > bestScore) { bestScore = score; bestMatch = pe; }
3528
+ }
3529
+ if (bestScore >= SIMILARITY_THRESHOLD && bestMatch) {
3530
+ semanticDupMap.set(ai, { existingId: bestMatch.id, existingDesc: bestMatch.description, score: bestScore });
3531
+ }
3532
+ } catch { /* skip */ }
3533
+ }
3534
+ } catch { /* embedder not available */ }
3535
+ }
3536
+
3542
3537
  // BUG-31: Move deduplication queries INSIDE the transaction to eliminate TOCTOU race
3543
3538
  const applyTx = dl.db.transaction((actionsToApply) => {
3544
3539
  // Query for existing keys inside the transaction for atomicity
@@ -3547,7 +3542,8 @@ async function cmdWeb({ port, dir, open, dev }) {
3547
3542
  const recentBlockers = dl.db.prepare("SELECT title FROM blockers WHERE created_at >= datetime('now', '-1 day')").all();
3548
3543
  const existingBlockerKeys24h = new Set(recentBlockers.map(b => sha1(normalizeTextForKey(b.title))));
3549
3544
 
3550
- for (const a of actionsToApply) {
3545
+ for (let ai = 0; ai < actionsToApply.length; ai++) {
3546
+ const a = actionsToApply[ai];
3551
3547
  if (!a || typeof a !== 'object') continue;
3552
3548
  const type = String(a.type || '').trim();
3553
3549
 
@@ -3557,8 +3553,25 @@ async function cmdWeb({ port, dir, open, dev }) {
3557
3553
  if (!description) continue;
3558
3554
  const projectSlug = String(a.projectSlug || '').trim() || inferProjectSlug(description, slugMap);
3559
3555
  const streamSlug = String(a.streamSlug || '').trim();
3556
+
3557
+ // Exact-match dedup (24h window)
3560
3558
  const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + description));
3561
3559
  if (existingTaskKeys24h.has(key)) { applied.tasksSkipped++; continue; }
3560
+
3561
+ // Semantic dedup (all pending tasks)
3562
+ if (semanticDupMap.has(ai)) {
3563
+ const dup = semanticDupMap.get(ai);
3564
+ applied.tasksSkipped++;
3565
+ if (!applied.semanticDups) applied.semanticDups = [];
3566
+ applied.semanticDups.push({
3567
+ newDesc: description,
3568
+ existingId: dup.existingId,
3569
+ existingDesc: dup.existingDesc,
3570
+ similarity: Math.round(dup.score * 100) + '%'
3571
+ });
3572
+ continue;
3573
+ }
3574
+
3562
3575
  const category = validTaskCats.has(String(a.category || '').trim()) ? String(a.category).trim() : 'DO_NOW';
3563
3576
  const priority = normPriority(a.priority);
3564
3577
 
@@ -3707,9 +3720,25 @@ async function cmdWeb({ port, dir, open, dev }) {
3707
3720
  return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'index rebuild failed', output: out });
3708
3721
  }
3709
3722
 
3723
+ // Upload attachment (base64 image from clipboard paste)
3724
+ if (req.url === '/api/attachments/upload') {
3725
+ const base64 = String(payload.data || '').trim();
3726
+ const mimeType = String(payload.mimeType || 'image/png').trim();
3727
+ if (!base64) return safeJson(res, 400, { error: 'Missing data' });
3728
+ const ext = mimeType === 'image/jpeg' ? '.jpg' : mimeType === 'image/gif' ? '.gif' : mimeType === 'image/webp' ? '.webp' : '.png';
3729
+ const fname = 'paste-' + Date.now() + ext;
3730
+ const attachDir = path.join(workspaceDir, 'data', 'attachments');
3731
+ if (!exists(attachDir)) fs.mkdirSync(attachDir, { recursive: true });
3732
+ const filePath = path.join(attachDir, fname);
3733
+ const buf = Buffer.from(base64, 'base64');
3734
+ fs.writeFileSync(filePath, buf);
3735
+ return safeJson(res, 200, { ok: true, filename: fname, url: '/attachments/' + fname });
3736
+ }
3737
+
3710
3738
  if (req.url === '/api/chat/ask') {
3711
3739
  const sessionId = String(payload.sessionId || '').trim();
3712
3740
  const query = String(payload.query || '').trim();
3741
+ const imagePath = payload.imagePath ? String(payload.imagePath).trim() : null;
3713
3742
  if (!query) return safeJson(res, 400, { error: 'Missing query' });
3714
3743
 
3715
3744
  const workspaceRulesBase = path.join(workspaceDir, '.agent', 'rules', 'freya');
@@ -3742,27 +3771,23 @@ async function cmdWeb({ port, dir, open, dev }) {
3742
3771
  console.error('[oracle] RAG search failed (embedder/sharp unavailable), continuing without context:', ragErr.message);
3743
3772
  }
3744
3773
 
3745
- // Build image references if attachments were provided
3746
- const askAttachments = Array.isArray(payload.attachments) ? payload.attachments : [];
3747
- let askImageContext = '';
3748
- const askHasImages = askAttachments.length > 0;
3749
- if (askHasImages) {
3750
- askImageContext = '\n\nIMAGENS ANEXADAS (use @caminho para analisar):\n' +
3751
- askAttachments.map(a => `- @${path.join(workspaceDir, a.path)} (${a.name})`).join('\n') + '\n' +
3752
- 'Analise as imagens anexadas e use-as como contexto para responder à consulta.\n';
3753
- }
3754
-
3755
- const prompt = `Você é o agente Oracle do sistema F.R.E.Y.A.\n\nSiga estritamente os arquivos de regras abaixo.\nResponda de forma analítica e consultiva.\n${ragContext}${askImageContext}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\n${query}\n`;
3774
+ const prompt = `Você é o agente Oracle do sistema F.R.E.Y.A.\n\nSiga estritamente os arquivos de regras abaixo.\nResponda de forma analítica e consultiva.\n${ragContext}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\n${query}\n`;
3756
3775
 
3757
3776
  const cmd = process.env.COPILOT_CMD || 'copilot';
3758
3777
 
3759
3778
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
3760
3779
  const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
3761
3780
  try {
3762
- // Add --add-dir when images are attached so Copilot can access them
3763
- const oracleArgs = ['-s', '--no-color', '--stream', 'off', '-p', prompt];
3764
- if (askHasImages) { oracleArgs.push('--allow-all-tools', '--add-dir', workspaceDir); }
3765
- const r = await run(cmd, oracleArgs, workspaceDir, oracleEnv);
3781
+ // Build copilot args; add image if user pasted a screenshot
3782
+ const copilotArgs = ['-s', '--no-color', '--stream', 'off'];
3783
+ if (imagePath) {
3784
+ const absImg = path.isAbsolute(imagePath) ? imagePath : path.join(workspaceDir, imagePath);
3785
+ if (exists(absImg)) {
3786
+ copilotArgs.push('--add-image', absImg);
3787
+ }
3788
+ }
3789
+ copilotArgs.push('-p', prompt);
3790
+ const r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
3766
3791
  const out = (r.stdout + r.stderr).trim();
3767
3792
  if (r.code !== 0) {
3768
3793
  return safeJson(res, 200, { ok: false, answer: 'Falha na busca do agente Oracle:\n' + (out || 'Exit code != 0'), sessionId });
@@ -3893,7 +3918,8 @@ async function cmdWeb({ port, dir, open, dev }) {
3893
3918
 
3894
3919
  const rawTasks = dl.db.prepare(query).all(...params);
3895
3920
  const tasks = rawTasks.map(t => {
3896
- const meta = t.metadata ? JSON.parse(t.metadata) : {};
3921
+ let meta = {};
3922
+ try { meta = t.metadata ? JSON.parse(t.metadata) : {}; } catch (_) {}
3897
3923
  return {
3898
3924
  id: t.id,
3899
3925
  description: t.description,
@@ -4091,7 +4117,8 @@ async function cmdWeb({ port, dir, open, dev }) {
4091
4117
 
4092
4118
  const rawBlockers = dl.db.prepare(query).all(...params);
4093
4119
  const blockers = rawBlockers.map(b => {
4094
- const meta = b.metadata ? JSON.parse(b.metadata) : {};
4120
+ let meta = {};
4121
+ try { meta = b.metadata ? JSON.parse(b.metadata) : {}; } catch (_) {}
4095
4122
  return {
4096
4123
  id: b.id,
4097
4124
  title: b.title,
@@ -4671,13 +4698,6 @@ function buildKanbanHtml(safeDefault, appVersion) {
4671
4698
  </div>
4672
4699
  </div>
4673
4700
 
4674
- <!-- Image lightbox modal -->
4675
- <div id="imgLightbox" class="img-lightbox" onclick="window.closeLightbox(event)">
4676
- <button class="img-lightbox-close" onclick="window.closeLightbox(event)" title="Fechar (Esc)">&times;</button>
4677
- <img id="imgLightboxImg" src="" alt="preview" onclick="event.stopPropagation()" />
4678
- <div id="imgLightboxName" class="img-lightbox-name"></div>
4679
- </div>
4680
-
4681
4701
  <script>
4682
4702
  window.__FREYA_DEFAULT_DIR = "${safeDefault}";
4683
4703
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js && node scripts/validate-structure.js",