nothumanallowed 13.5.57 → 13.5.58

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "13.5.57",
3
+ "version": "13.5.58",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4697,6 +4697,154 @@ REGOLE CRITICHE:
4697
4697
  return;
4698
4698
  }
4699
4699
 
4700
+ // POST /api/studio/webcraft/snapshot { projectName } → { ok, snapshot }
4701
+ // Creates a timestamped snapshot of all project files (excludes node_modules)
4702
+ if (pathname === '/api/studio/webcraft/snapshot' && method === 'POST') {
4703
+ const body = await parseBody(req);
4704
+ const projName = (body.projectName || '').replace(/[^a-zA-Z0-9_-]/g, '');
4705
+ if (!projName) { sendJSON(res, 400, { error: 'projectName required' }); return; }
4706
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
4707
+ const snapDir = path.join(projDir, '.snapshots');
4708
+ if (!fs.existsSync(snapDir)) fs.mkdirSync(snapDir, { recursive: true });
4709
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
4710
+ const snapPath = path.join(snapDir, ts + '.json');
4711
+ const snapshot = {};
4712
+ const walkSnap = (dir, base) => {
4713
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
4714
+ if (entry.name === 'node_modules' || entry.name === '.snapshots' || entry.name.startsWith('.')) continue;
4715
+ const rel = base ? base + '/' + entry.name : entry.name;
4716
+ if (entry.isDirectory()) { walkSnap(path.join(dir, entry.name), rel); }
4717
+ else {
4718
+ try { snapshot[rel] = fs.readFileSync(path.join(dir, entry.name), 'utf8'); } catch(_) {}
4719
+ }
4720
+ }
4721
+ };
4722
+ walkSnap(projDir, '');
4723
+ fs.writeFileSync(snapPath, JSON.stringify({ ts, files: snapshot }), 'utf8');
4724
+ // Keep only last 10 snapshots
4725
+ const allSnaps = fs.readdirSync(snapDir).filter(f => f.endsWith('.json')).sort();
4726
+ if (allSnaps.length > 10) {
4727
+ for (const old of allSnaps.slice(0, allSnaps.length - 10)) {
4728
+ try { fs.unlinkSync(path.join(snapDir, old)); } catch(_) {}
4729
+ }
4730
+ }
4731
+ sendJSON(res, 200, { ok: true, snapshot: ts, fileCount: Object.keys(snapshot).length });
4732
+ logRequest(method, pathname, 200, Date.now() - start);
4733
+ return;
4734
+ }
4735
+
4736
+ // GET /api/studio/webcraft/snapshots/:name → { snapshots: [{ts, fileCount}] }
4737
+ if (pathname.startsWith('/api/studio/webcraft/snapshots/') && method === 'GET') {
4738
+ const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/snapshots/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
4739
+ const snapDir = path.join(os.homedir(), '.nha', 'webcraft', projName, '.snapshots');
4740
+ if (!fs.existsSync(snapDir)) { sendJSON(res, 200, { snapshots: [] }); return; }
4741
+ const snaps = fs.readdirSync(snapDir).filter(f => f.endsWith('.json')).sort().reverse().map(f => {
4742
+ try {
4743
+ const data = JSON.parse(fs.readFileSync(path.join(snapDir, f), 'utf8'));
4744
+ return { ts: data.ts, fileCount: Object.keys(data.files || {}).length };
4745
+ } catch(_) { return null; }
4746
+ }).filter(Boolean);
4747
+ sendJSON(res, 200, { snapshots: snaps });
4748
+ logRequest(method, pathname, 200, Date.now() - start);
4749
+ return;
4750
+ }
4751
+
4752
+ // POST /api/studio/webcraft/restore { projectName, ts } → { ok, restored }
4753
+ if (pathname === '/api/studio/webcraft/restore' && method === 'POST') {
4754
+ const body = await parseBody(req);
4755
+ const projName = (body.projectName || '').replace(/[^a-zA-Z0-9_-]/g, '');
4756
+ const ts = (body.ts || '').replace(/[^0-9T\-]/g, '');
4757
+ if (!projName || !ts) { sendJSON(res, 400, { error: 'projectName and ts required' }); return; }
4758
+ const snapPath = path.join(os.homedir(), '.nha', 'webcraft', projName, '.snapshots', ts + '.json');
4759
+ if (!fs.existsSync(snapPath)) { sendJSON(res, 404, { error: 'snapshot not found' }); return; }
4760
+ const data = JSON.parse(fs.readFileSync(snapPath, 'utf8'));
4761
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
4762
+ let restored = 0;
4763
+ for (const [rel, content] of Object.entries(data.files || {})) {
4764
+ const fp = path.join(projDir, rel);
4765
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
4766
+ fs.writeFileSync(fp, content, 'utf8');
4767
+ restored++;
4768
+ }
4769
+ sendJSON(res, 200, { ok: true, restored });
4770
+ logRequest(method, pathname, 200, Date.now() - start);
4771
+ return;
4772
+ }
4773
+
4774
+ // POST /api/studio/webcraft/syntax-check { projectName } → { results: [{file, ok, error}] }
4775
+ if (pathname === '/api/studio/webcraft/syntax-check' && method === 'POST') {
4776
+ const body = await parseBody(req);
4777
+ const projName = (body.projectName || '').replace(/[^a-zA-Z0-9_-]/g, '');
4778
+ if (!projName) { sendJSON(res, 400, { error: 'projectName required' }); return; }
4779
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
4780
+ const { execFile } = await import('child_process');
4781
+ const { promisify } = await import('util');
4782
+ const execFileAsync = promisify(execFile);
4783
+ const jsFiles = [];
4784
+ const walkJs = (dir, base) => {
4785
+ if (!fs.existsSync(dir)) return;
4786
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
4787
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
4788
+ const rel = base ? base + '/' + entry.name : entry.name;
4789
+ if (entry.isDirectory()) { walkJs(path.join(dir, entry.name), rel); }
4790
+ else if (entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) { jsFiles.push(rel); }
4791
+ }
4792
+ };
4793
+ walkJs(projDir, '');
4794
+ const results = [];
4795
+ for (const rel of jsFiles) {
4796
+ const fp = path.join(projDir, rel);
4797
+ try {
4798
+ await execFileAsync(process.execPath, ['--check', fp], { timeout: 5000 });
4799
+ results.push({ file: rel, ok: true });
4800
+ } catch(e) {
4801
+ const errMsg = (e.stderr || e.message || '').split('\n')[0].replace(fp, rel);
4802
+ results.push({ file: rel, ok: false, error: errMsg });
4803
+ }
4804
+ }
4805
+ sendJSON(res, 200, { results });
4806
+ logRequest(method, pathname, 200, Date.now() - start);
4807
+ return;
4808
+ }
4809
+
4810
+ // POST /api/studio/webcraft/grep { projectName, query } → { matches: [{file, line, lineNum, match}] }
4811
+ if (pathname === '/api/studio/webcraft/grep' && method === 'POST') {
4812
+ const body = await parseBody(req);
4813
+ const projName = (body.projectName || '').replace(/[^a-zA-Z0-9_-]/g, '');
4814
+ const query = (body.query || '').slice(0, 200);
4815
+ if (!projName || !query) { sendJSON(res, 400, { error: 'projectName and query required' }); return; }
4816
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
4817
+ const matches = [];
4818
+ let queryRe;
4819
+ try { queryRe = new RegExp(query, 'gi'); } catch(_) { queryRe = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); }
4820
+ const walkGrep = (dir, base) => {
4821
+ if (!fs.existsSync(dir)) return;
4822
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
4823
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
4824
+ const rel = base ? base + '/' + entry.name : entry.name;
4825
+ if (entry.isDirectory()) { walkGrep(path.join(dir, entry.name), rel); }
4826
+ else {
4827
+ const ext = entry.name.split('.').pop();
4828
+ if (!['js','mjs','ts','html','css','json','md','sql','env','conf'].includes(ext)) continue;
4829
+ try {
4830
+ const lines = fs.readFileSync(path.join(dir, entry.name), 'utf8').split('\n');
4831
+ lines.forEach((line, idx) => {
4832
+ queryRe.lastIndex = 0;
4833
+ if (queryRe.test(line)) {
4834
+ matches.push({ file: rel, lineNum: idx + 1, line: line.trim().slice(0, 200) });
4835
+ if (matches.length >= 100) return;
4836
+ }
4837
+ });
4838
+ } catch(_) {}
4839
+ }
4840
+ }
4841
+ };
4842
+ walkGrep(projDir, '');
4843
+ sendJSON(res, 200, { matches: matches.slice(0, 100) });
4844
+ logRequest(method, pathname, 200, Date.now() - start);
4845
+ return;
4846
+ }
4847
+
4700
4848
  // ── Studio: Parliament deliberation (SSE streaming) ──────────────────
4701
4849
  // Implements the Legion DeliberationEngine protocol adapted for Studio:
4702
4850
  // Round 1 outputs already exist (from normal workflow steps).
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '13.5.57';
8
+ export const VERSION = '13.5.58';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -4586,6 +4586,286 @@ function downloadStudioPDF() {
4586
4586
  doGeneratePdf();
4587
4587
  }
4588
4588
 
4589
+ // ── Studio Export: CSV ────────────────────────────────────────────────────────
4590
+ function extractMarkdownTables(md) {
4591
+ var NL = String.fromCharCode(10);
4592
+ var lines = md.split(NL);
4593
+ var tables = [];
4594
+ var current = null;
4595
+ for (var i = 0; i < lines.length; i++) {
4596
+ var l = lines[i].trim();
4597
+ if (l.charAt(0) === '|' && l.lastIndexOf('|') > 0) {
4598
+ if (/^\|[\s\-|:]+\|$/.test(l)) { continue; } // separator
4599
+ var cells = l.split('|').slice(1,-1).map(function(c){ return c.trim(); });
4600
+ if (!current) { current = { headers: cells, rows: [] }; }
4601
+ else { current.rows.push(cells); }
4602
+ } else {
4603
+ if (current && current.rows.length > 0) { tables.push(current); }
4604
+ current = null;
4605
+ }
4606
+ }
4607
+ if (current && current.rows.length > 0) tables.push(current);
4608
+ return tables;
4609
+ }
4610
+
4611
+ function tableToCsvString(table) {
4612
+ var NL = String.fromCharCode(10);
4613
+ function escCell(v) {
4614
+ if (v === undefined || v === null) return '';
4615
+ var s = String(v).replace(new RegExp('"', 'g'), '""');
4616
+ if (s.indexOf(',') >= 0 || s.indexOf('"') >= 0 || s.indexOf(NL) >= 0) return '"' + s + '"';
4617
+ return s;
4618
+ }
4619
+ var rows = [table.headers].concat(table.rows);
4620
+ return rows.map(function(r){ return r.map(escCell).join(','); }).join(NL);
4621
+ }
4622
+
4623
+ function downloadStudioCSV() {
4624
+ var nodes = (studioState.nodes || []).filter(function(n){ return n.output && n.output !== '(no output)'; });
4625
+ var allTables = [];
4626
+ nodes.forEach(function(n) {
4627
+ var tbls = extractMarkdownTables(n.output || '');
4628
+ tbls.forEach(function(t, i){ allTables.push({ agent: (n.label||n.agent), idx: i+1, table: t }); });
4629
+ });
4630
+ // Also check synthesis result
4631
+ if (studioState.result) {
4632
+ var tbls2 = extractMarkdownTables(studioState.result);
4633
+ tbls2.forEach(function(t, i){ allTables.push({ agent: 'Synthesis', idx: i+1, table: t }); });
4634
+ }
4635
+ if (allTables.length === 0) { alert('Nessuna tabella trovata nel report. Chiedi agli agenti di produrre dati in formato tabella Markdown.'); return; }
4636
+ var NL = String.fromCharCode(10);
4637
+ var csvParts = allTables.map(function(entry) {
4638
+ return '# ' + entry.agent + (allTables.length > 1 ? ' — Tabella ' + entry.idx : '') + NL + tableToCsvString(entry.table);
4639
+ });
4640
+ var csv = csvParts.join(NL + NL);
4641
+ var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
4642
+ var fname = (studioState.task || 'NHA-Studio').slice(0,50).replace(/[^a-z0-9\s]/gi,'').trim().replace(/\s+/g,'-') + '.csv';
4643
+ var a = document.createElement('a');
4644
+ a.href = URL.createObjectURL(blob);
4645
+ a.download = fname;
4646
+ a.click();
4647
+ }
4648
+
4649
+ // ── Studio Export: Excel (XLSX via SheetJS) ───────────────────────────────────
4650
+ var _xlsxLoaded = false;
4651
+ var _xlsxLoading = false;
4652
+
4653
+ function loadXLSX(cb) {
4654
+ if (_xlsxLoaded && window.XLSX) { cb(); return; }
4655
+ if (_xlsxLoading) { setTimeout(function(){ loadXLSX(cb); }, 200); return; }
4656
+ _xlsxLoading = true;
4657
+ var s = document.createElement('script');
4658
+ s.src = 'https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js';
4659
+ s.onload = function() { _xlsxLoaded = true; _xlsxLoading = false; cb(); };
4660
+ s.onerror = function() { _xlsxLoading = false; alert('Errore caricamento SheetJS. Controlla la connessione.'); };
4661
+ document.head.appendChild(s);
4662
+ }
4663
+
4664
+ function downloadStudioXLSX() {
4665
+ loadXLSX(function() { _doGenerateXLSX(); });
4666
+ }
4667
+
4668
+ function _doGenerateXLSX() {
4669
+ var XLSX = window.XLSX;
4670
+ if (!XLSX) { alert('SheetJS non disponibile.'); return; }
4671
+
4672
+ var nodes = (studioState.nodes || []).filter(function(n){ return n.output && n.output !== '(no output)' && n.agent !== 'CanvasAgent'; });
4673
+ var task = studioState.task || 'NHA Studio Report';
4674
+ var today = new Date();
4675
+ var dateStr = today.toLocaleDateString('it-IT');
4676
+ var wb = XLSX.utils.book_new();
4677
+
4678
+ // ── ACCENT COLORS per agente ──────────────────────────────────────────────
4679
+ var AGENT_COLORS = ['4F46E5','0891B2','059669','D97706','DC2626','7C3AED','0284C7','BE185D','0D9488','CA8A04'];
4680
+
4681
+ // ── Helper: cell style fabbrica ──────────────────────────────────────────
4682
+ function headerStyle(hexFg) {
4683
+ return {
4684
+ font: { bold: true, color: { rgb: 'FFFFFF' }, sz: 11, name: 'Calibri' },
4685
+ fill: { patternType: 'solid', fgColor: { rgb: hexFg || '4F46E5' } },
4686
+ alignment: { horizontal: 'center', vertical: 'center', wrapText: true },
4687
+ border: {
4688
+ top: { style: 'thin', color: { rgb: 'CCCCCC' } },
4689
+ bottom: { style: 'thin', color: { rgb: 'CCCCCC' } },
4690
+ left: { style: 'thin', color: { rgb: 'CCCCCC' } },
4691
+ right: { style: 'thin', color: { rgb: 'CCCCCC' } }
4692
+ }
4693
+ };
4694
+ }
4695
+ function dataStyle(even) {
4696
+ return {
4697
+ font: { sz: 10, name: 'Calibri' },
4698
+ fill: even ? { patternType: 'solid', fgColor: { rgb: 'F3F4F6' } } : { patternType: 'none' },
4699
+ alignment: { vertical: 'center', wrapText: true },
4700
+ border: {
4701
+ top: { style: 'hair', color: { rgb: 'E5E7EB' } },
4702
+ bottom: { style: 'hair', color: { rgb: 'E5E7EB' } },
4703
+ left: { style: 'hair', color: { rgb: 'E5E7EB' } },
4704
+ right: { style: 'hair', color: { rgb: 'E5E7EB' } }
4705
+ }
4706
+ };
4707
+ }
4708
+ function titleStyle(hex) {
4709
+ return {
4710
+ font: { bold: true, sz: 14, name: 'Calibri', color: { rgb: hex || '4F46E5' } },
4711
+ fill: { patternType: 'solid', fgColor: { rgb: 'F8F9FC' } },
4712
+ alignment: { horizontal: 'left', vertical: 'center' }
4713
+ };
4714
+ }
4715
+ function metaStyle() {
4716
+ return { font: { sz: 10, italic: true, color: { rgb: '6B7280' }, name: 'Calibri' } };
4717
+ }
4718
+
4719
+ // ── Helper: parse numeric value ───────────────────────────────────────────
4720
+ function parseNum(v) {
4721
+ var s = String(v).replace(/[€$£%,\s]/g,'').trim();
4722
+ var n = parseFloat(s);
4723
+ return isNaN(n) ? null : n;
4724
+ }
4725
+
4726
+ // ── Helper: aggiunge un foglio tabella da markdown ────────────────────────
4727
+ function addTableSheet(sheetName, agentLabel, tables, colorHex) {
4728
+ var ws = {};
4729
+ var maxCol = 0;
4730
+ var rowNum = 0;
4731
+
4732
+ // Title row
4733
+ ws['A' + (rowNum+1)] = { v: agentLabel, t: 's', s: titleStyle(colorHex) };
4734
+ rowNum++;
4735
+ // Meta row
4736
+ ws['A' + (rowNum+1)] = { v: 'Generato da NHA Studio il ' + dateStr, t: 's', s: metaStyle() };
4737
+ ws['B' + (rowNum+1)] = { v: 'Task: ' + task.slice(0,80), t: 's', s: metaStyle() };
4738
+ rowNum++;
4739
+ rowNum++; // blank
4740
+
4741
+ tables.forEach(function(table, ti) {
4742
+ if (tables.length > 1) {
4743
+ ws['A' + (rowNum+1)] = { v: 'Tabella ' + (ti+1), t: 's', s: { font: { bold: true, sz: 11, name: 'Calibri', color: { rgb: colorHex } } } };
4744
+ rowNum++;
4745
+ }
4746
+ // Detect numeric columns
4747
+ var isNumericCol = table.headers.map(function(_, ci) {
4748
+ return table.rows.every(function(r){ return r[ci] === undefined || r[ci] === '' || parseNum(r[ci]) !== null; });
4749
+ });
4750
+ // Header row
4751
+ var colCount = table.headers.length;
4752
+ table.headers.forEach(function(h, ci) {
4753
+ var col = String.fromCharCode(65 + ci);
4754
+ ws[col + (rowNum+1)] = { v: h, t: 's', s: headerStyle(colorHex) };
4755
+ if (ci > maxCol) maxCol = ci;
4756
+ });
4757
+ rowNum++;
4758
+ // Data rows
4759
+ table.rows.forEach(function(row, ri) {
4760
+ row.forEach(function(cell, ci) {
4761
+ var col = String.fromCharCode(65 + ci);
4762
+ var num = isNumericCol[ci] ? parseNum(cell) : null;
4763
+ var addr = col + (rowNum+1);
4764
+ if (num !== null && cell !== '') {
4765
+ ws[addr] = { v: num, t: 'n', z: num % 1 !== 0 ? '#,##0.00' : '#,##0', s: dataStyle(ri % 2 === 0) };
4766
+ } else {
4767
+ ws[addr] = { v: cell || '', t: 's', s: dataStyle(ri % 2 === 0) };
4768
+ }
4769
+ });
4770
+ rowNum++;
4771
+ });
4772
+ rowNum++; // blank between tables
4773
+ });
4774
+
4775
+ // Set sheet range
4776
+ var lastCol = String.fromCharCode(65 + maxCol);
4777
+ ws['!ref'] = 'A1:' + lastCol + (rowNum + 1);
4778
+
4779
+ // Column widths (auto-estimate from content)
4780
+ var colWidths = [];
4781
+ for (var ci = 0; ci <= maxCol; ci++) {
4782
+ var maxW = 12;
4783
+ tables.forEach(function(table) {
4784
+ if (table.headers[ci]) maxW = Math.max(maxW, table.headers[ci].length + 2);
4785
+ table.rows.forEach(function(r){ if (r[ci]) maxW = Math.max(maxW, Math.min(String(r[ci]).length + 2, 50)); });
4786
+ });
4787
+ colWidths.push({ wch: maxW });
4788
+ }
4789
+ ws['!cols'] = colWidths;
4790
+
4791
+ // Row heights
4792
+ var rowH = [];
4793
+ for (var ri2 = 0; ri2 < rowNum; ri2++) rowH.push({ hpt: ri2 < 3 ? 22 : 18 });
4794
+ ws['!rows'] = rowH;
4795
+
4796
+ // Freeze top rows (title + meta + header row of first table)
4797
+ ws['!freeze'] = { xSplit: 0, ySplit: 4, topLeftCell: 'A5' };
4798
+
4799
+ XLSX.utils.book_append_sheet(wb, ws, sheetName.slice(0,31));
4800
+ }
4801
+
4802
+ // ── Foglio INDICE ─────────────────────────────────────────────────────────
4803
+ var wsIdx = {};
4804
+ wsIdx['A1'] = { v: 'NHA Studio Report', t: 's', s: titleStyle('4F46E5') };
4805
+ wsIdx['A2'] = { v: task, t: 's', s: { font: { sz: 12, name: 'Calibri', bold: true } } };
4806
+ wsIdx['A3'] = { v: 'Generato il ' + dateStr + ' con NHA Studio', t: 's', s: metaStyle() };
4807
+ wsIdx['A5'] = { v: 'Agente', t: 's', s: headerStyle('4F46E5') };
4808
+ wsIdx['B5'] = { v: 'Tabelle', t: 's', s: headerStyle('4F46E5') };
4809
+ wsIdx['C5'] = { v: 'Token In', t: 's', s: headerStyle('4F46E5') };
4810
+ wsIdx['D5'] = { v: 'Token Out', t: 's', s: headerStyle('4F46E5') };
4811
+ var idxRow = 5;
4812
+ var hasAnyTable = false;
4813
+ nodes.forEach(function(n, ni) {
4814
+ var tables = extractMarkdownTables(n.output || '');
4815
+ if (tables.length > 0) hasAnyTable = true;
4816
+ idxRow++;
4817
+ var co = AGENT_COLORS[ni % AGENT_COLORS.length];
4818
+ wsIdx['A' + idxRow] = { v: (n.label||n.agent), t: 's', s: dataStyle(ni % 2 === 0) };
4819
+ wsIdx['B' + idxRow] = { v: tables.length, t: 'n', s: dataStyle(ni % 2 === 0) };
4820
+ wsIdx['C' + idxRow] = { v: n.tokensIn || 0, t: 'n', z: '#,##0', s: dataStyle(ni % 2 === 0) };
4821
+ wsIdx['D' + idxRow] = { v: n.tokensOut || 0, t: 'n', z: '#,##0', s: dataStyle(ni % 2 === 0) };
4822
+ });
4823
+ wsIdx['!ref'] = 'A1:D' + (idxRow + 1);
4824
+ wsIdx['!cols'] = [{ wch: 28 }, { wch: 10 }, { wch: 12 }, { wch: 12 }];
4825
+ XLSX.utils.book_append_sheet(wb, wsIdx, 'Indice');
4826
+
4827
+ // ── Un foglio per ogni agente con tabelle ─────────────────────────────────
4828
+ var sheetCount = 0;
4829
+ nodes.forEach(function(n, ni) {
4830
+ var tables = extractMarkdownTables(n.output || '');
4831
+ if (tables.length === 0) return;
4832
+ var colorHex = AGENT_COLORS[ni % AGENT_COLORS.length];
4833
+ var sheetName = (n.label || n.agent).slice(0,28);
4834
+ addTableSheet(sheetName, n.label || n.agent, tables, colorHex);
4835
+ sheetCount++;
4836
+ });
4837
+
4838
+ // ── Foglio Risultato Finale (testo libero come tabella a singola colonna) ──
4839
+ if (studioState.result) {
4840
+ var synTables = extractMarkdownTables(studioState.result);
4841
+ if (synTables.length > 0) {
4842
+ addTableSheet('Sintesi', 'Sintesi Finale', synTables, '059669');
4843
+ sheetCount++;
4844
+ } else {
4845
+ // Export free text as single-column sheet with line-by-line rows
4846
+ var wsText = {};
4847
+ wsText['A1'] = { v: 'Sintesi Finale', t: 's', s: titleStyle('059669') };
4848
+ wsText['A2'] = { v: task.slice(0,80), t: 's', s: metaStyle() };
4849
+ var NL5 = String.fromCharCode(10);
4850
+ var lines = (studioState.result || '').split(NL5);
4851
+ lines.forEach(function(line, li) {
4852
+ wsText['A' + (li+4)] = { v: line.replace(new RegExp('[*_#]','g'), '').replace(new RegExp(String.fromCharCode(96),'g'), ''), t: 's', s: dataStyle(li % 2 === 0) };
4853
+ });
4854
+ wsText['!ref'] = 'A1:A' + (lines.length + 4);
4855
+ wsText['!cols'] = [{ wch: 90 }];
4856
+ XLSX.utils.book_append_sheet(wb, wsText, 'Sintesi');
4857
+ }
4858
+ }
4859
+
4860
+ if (sheetCount === 0 && !studioState.result) {
4861
+ alert('Nessuna tabella trovata. Chiedi agli agenti di produrre dati in formato tabella Markdown per generare Excel.');
4862
+ return;
4863
+ }
4864
+
4865
+ var fname = task.slice(0,50).replace(/[^a-z0-9\s]/gi,'').trim().replace(/\s+/g,'-') + '-NHAStudio.xlsx';
4866
+ XLSX.writeFile(wb, fname, { bookType: 'xlsx', type: 'binary', cellStyles: true });
4867
+ }
4868
+
4589
4869
  function renderStudioResult() {
4590
4870
  var el = document.getElementById('studioResult');
4591
4871
  if (!el) return;
@@ -4598,14 +4878,19 @@ function renderStudioResult() {
4598
4878
  var tokLine = (studioTokens && (studioTokens.in > 0 || studioTokens.out > 0))
4599
4879
  ? '<div style="margin-top:8px;font-size:11px;color:var(--dim);font-family:var(--mono)">&#x2B06; ' + (studioTokens.in||0).toLocaleString() + ' token in &nbsp;&#x2B07; ' + (studioTokens.out||0).toLocaleString() + ' token out &nbsp;&#x2022;&nbsp; <strong style="color:var(--green)">' + ((studioTokens.in||0)+(studioTokens.out||0)).toLocaleString() + '</strong> totale</div>'
4600
4880
  : '';
4601
- var dlBtn = '<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-wrap:wrap">' +
4602
- '<button onclick="downloadStudioPDF()" title="Genera e scarica il report come PDF" style="display:inline-flex;align-items:center;gap:6px;padding:8px 18px;background:linear-gradient(135deg,#4f46e5,#2563eb);border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;letter-spacing:.3px;box-shadow:0 2px 8px rgba(79,70,229,.35)">&#x2913; Download PDF</button>' +
4603
- '<span style="font-size:11px;color:var(--dim)">Scarica il report completo come file PDF</span>' +
4881
+ var dlBtn = '<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border);display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
4882
+ '<button onclick="downloadStudioPDF()" title="Report completo come PDF" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:linear-gradient(135deg,#4f46e5,#2563eb);border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;box-shadow:0 2px 8px rgba(79,70,229,.35)">&#x2913; PDF</button>' +
4883
+ '<button onclick="downloadStudioXLSX()" title="Esporta tabelle come Excel professionale (SheetJS)" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:linear-gradient(135deg,#059669,#047857);border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;box-shadow:0 2px 8px rgba(5,150,105,.35)">&#x1f4ca; Excel</button>' +
4884
+ '<button onclick="downloadStudioCSV()" title="Esporta tabelle come CSV" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:linear-gradient(135deg,#0891b2,#0369a1);border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;box-shadow:0 2px 8px rgba(8,145,178,.35)">&#x1f4cb; CSV</button>' +
4604
4885
  '</div>';
4605
4886
  el.innerHTML = '<div class="studio-result__title">&#10003; ' + t('workflow_complete') + '</div>' + body + tokLine + dlBtn;
4606
- // Show/hide inline PDF button in the prompt bar
4887
+ // Show/hide inline export buttons in the prompt bar
4607
4888
  var inlinePdfBtn = document.getElementById('studioInlinePdfBtn');
4608
4889
  if (inlinePdfBtn) inlinePdfBtn.style.display = 'inline-flex';
4890
+ var inlineXlsxBtn = document.getElementById('studioInlineXlsxBtn');
4891
+ if (inlineXlsxBtn) inlineXlsxBtn.style.display = 'inline-flex';
4892
+ var inlineCsvBtn = document.getElementById('studioInlineCsvBtn');
4893
+ if (inlineCsvBtn) inlineCsvBtn.style.display = 'inline-flex';
4609
4894
  // Update canvas button style: bright green when canvas exists, dimmed otherwise
4610
4895
  var canvasBtn = document.getElementById('studioCanvasBtn');
4611
4896
  if (canvasBtn) {
@@ -5962,7 +6247,9 @@ function renderStudio(el) {
5962
6247
  '<button onclick="document.getElementById(\\x27studioFileInput\\x27).click()" title="Attach PDF or image" style="padding:8px 10px;background:none;border:1px solid var(--border);border-radius:8px;color:var(--dim);cursor:pointer;font-size:15px" ' + (studioState.running ? 'disabled' : '') + '>&#128206;</button>' +
5963
6248
  '<button id="studioRunBtn" class="studio-run-btn" onclick="runStudio()" style="flex:1" ' + (studioState.running ? 'disabled' : '') + '>' + t('run') + '</button>' +
5964
6249
  '<button id="studioStopBtn" onclick="stopStudio()" title="' + t('stop') + '" style="padding:8px 14px;background:#7f1d1d;border:1px solid #ef4444;border-radius:8px;color:#ef4444;cursor:pointer;font-size:13px;font-weight:700;white-space:nowrap;' + (studioState.running ? '' : 'display:none') + '">&#9632; ' + t('stop') + '</button>' +
5965
- '<button id="studioInlinePdfBtn" onclick="downloadStudioPDF()" title="Genera e scarica il report come PDF" style="display:' + (studioState.result ? 'inline-flex' : 'none') + ';align-items:center;gap:5px;padding:8px 12px;background:linear-gradient(135deg,#4f46e5,#2563eb);border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;box-shadow:0 2px 6px rgba(79,70,229,.35)">&#x2913; PDF</button>' +
6250
+ '<button id="studioInlinePdfBtn" onclick="downloadStudioPDF()" title="Download PDF" style="display:' + (studioState.result ? 'inline-flex' : 'none') + ';align-items:center;gap:5px;padding:8px 12px;background:linear-gradient(135deg,#4f46e5,#2563eb);border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;box-shadow:0 2px 6px rgba(79,70,229,.35)">&#x2913; PDF</button>' +
6251
+ '<button id="studioInlineXlsxBtn" onclick="downloadStudioXLSX()" title="Export Excel" style="display:' + (studioState.result ? 'inline-flex' : 'none') + ';align-items:center;gap:5px;padding:8px 12px;background:linear-gradient(135deg,#059669,#047857);border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;box-shadow:0 2px 6px rgba(5,150,105,.35)">&#x1f4ca; Excel</button>' +
6252
+ '<button id="studioInlineCsvBtn" onclick="downloadStudioCSV()" title="Export CSV" style="display:' + (studioState.result ? 'inline-flex' : 'none') + ';align-items:center;gap:5px;padding:8px 10px;background:linear-gradient(135deg,#0891b2,#0369a1);border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;box-shadow:0 2px 6px rgba(8,145,178,.35)">CSV</button>' +
5966
6253
  '<button onclick="studioReset()" title="' + t('reset') + '" style="padding:8px 12px;background:none;border:1px solid var(--border);border-radius:8px;color:var(--dim);cursor:pointer;font-size:16px;line-height:1" ' + (studioState.running ? 'disabled' : '') + '>&#8635;</button>' +
5967
6254
  '</div>' +
5968
6255
  '</div>' +
@@ -6252,6 +6539,13 @@ var wcChatRunning = false;
6252
6539
  var wcChatAttachments = []; // [{name, mimeType, base64, size}]
6253
6540
  var _wcAutoFixAttempts = 0;
6254
6541
  var _wcAutoFixTimer = null;
6542
+ var _wcPlanPending = null; // null | { plan: string, message: string } — plan mode waiting for approval
6543
+ var _wcDiffQueue = []; // [{file, before, after}] diffs from last agent run
6544
+ var _wcGrepOpen = false; // grep panel visible
6545
+ var _wcGrepQuery = '';
6546
+ var _wcGrepResults = [];
6547
+ var _wcSyntaxResults = []; // [{file, ok, error}]
6548
+ var _wcSnapshots = []; // [{ts, fileCount}]
6255
6549
  // Skills state
6256
6550
  var wcSkills = []; // [{name, content, type}] type: 'skill'|'memory'|'provider'
6257
6551
  var wcSkillModal = null; // null | {mode:'edit'|'new', idx:number|null, name, content, type, generating}
@@ -6348,11 +6642,17 @@ function renderWebCraft(el) {
6348
6642
  '<div style="font-size:9px;color:var(--dim);margin-top:4px">'+t('wc_required_hint')+'</div>' +
6349
6643
  '</div>' +
6350
6644
  wcSkillsPanelHtml() +
6645
+ wcSnapshotsPanelHtml() +
6351
6646
  (wcState.running ?
6352
6647
  '<div style="width:100%;padding:11px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;color:var(--dim);font-size:12px;text-align:center">&#9203; '+t('wc_generating')+'...</div>'
6353
6648
  : '') +
6354
6649
  (wcState.generatedFiles.length > 0 && !wcState.running ?
6355
- '<button onclick="wcDownloadZip()" style="width:100%;padding:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-size:12px;font-weight:600;cursor:pointer">&#8681; '+t('wc_download')+'</button>' +
6650
+ '<div style="display:flex;gap:6px;flex-wrap:wrap">' +
6651
+ '<button onclick="wcDownloadZip()" style="flex:1;padding:9px;background:var(--bg3);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-size:11px;font-weight:600;cursor:pointer">&#8681; ZIP</button>' +
6652
+ '<button onclick="wcRunSyntaxCheck()" title="Controlla errori sintassi JS" style="padding:9px 10px;background:var(--bg3);border:1px solid var(--border2);border-radius:8px;color:var(--dim);font-size:11px;cursor:pointer" title="Syntax check">&#9989;</button>' +
6653
+ '<button onclick="wcToggleGrep()" title="Cerca nel codice" style="padding:9px 10px;background:'+(_wcGrepOpen?'var(--greendim)':'var(--bg3)')+';border:1px solid '+(_wcGrepOpen?'var(--green3)':'var(--border2)')+';border-radius:8px;color:'+(_wcGrepOpen?'var(--green)':'var(--dim)')+';font-size:11px;cursor:pointer">&#128269;</button>' +
6654
+ '<button onclick="wcManualSnapshot()" title="Salva snapshot" style="padding:9px 10px;background:var(--bg3);border:1px solid var(--border2);border-radius:8px;color:var(--dim);font-size:11px;cursor:pointer">&#128190;</button>' +
6655
+ '</div>' +
6356
6656
  '<button onclick="wcStartSandbox()" id="wcSandboxBtn" style="width:100%;padding:10px;background:var(--bg3);border:1px solid var(--green3);border-radius:8px;color:var(--green);font-size:12px;font-weight:600;cursor:pointer">&#9654; '+t('wc_sandbox_start')+'</button>'
6357
6657
  : '') +
6358
6658
  '</div>' +
@@ -6372,6 +6672,9 @@ function renderWebCraft(el) {
6372
6672
  '<div style="flex:1;min-height:0;overflow:hidden">' +
6373
6673
  (wcMainTab === 'projects' ? wcProjectsPanelHtml() : editorHtml) +
6374
6674
  '</div>' +
6675
+ (wcMainTab !== 'projects' ? wcPlanBannerHtml() : '') +
6676
+ (wcMainTab !== 'projects' ? wcGrepPanelHtml() : '') +
6677
+ (wcMainTab !== 'projects' ? wcDiffPanelHtml() : '') +
6375
6678
  wcChatPanelHtml() +
6376
6679
  '</div>' +
6377
6680
  wcSkillModalHtml();
@@ -6671,6 +6974,14 @@ function wcChatPanelHtml() {
6671
6974
  msg.attachments.map(function(a){ return '<span style="background:var(--bg3);border:1px solid var(--border2);border-radius:5px;padding:2px 7px;font-size:10px;color:var(--dim)">&#128206; '+wcEsc(a.name)+'</span>'; }).join('') +
6672
6975
  '</div>';
6673
6976
  }
6977
+ } else if (msg.role === 'system') {
6978
+ // System messages: compact notices (snapshot, syntax check, etc.)
6979
+ messagesHtml += '<div style="margin:2px 12px;padding:4px 10px;background:var(--bg3);border-left:2px solid var(--border2);border-radius:4px;font-size:10px;color:var(--dim)">' + (msg.text||'') + '</div>';
6980
+ if (msg.syntaxErrors && msg.syntaxErrors.length) {
6981
+ messagesHtml += '<div style="margin:2px 12px">' + msg.syntaxErrors.map(function(e2){
6982
+ return '<div style="font-size:10px;font-family:var(--mono);color:#f87171;padding:2px 0">&#10005; ' + wcEsc(e2.file) + ': ' + wcEsc(e2.error) + '</div>';
6983
+ }).join('') + '</div>';
6984
+ }
6674
6985
  } else {
6675
6986
  var toolBadges = (msg.tools || []).map(function(tool){
6676
6987
  var icon = tool.op === 'edit' ? '&#9998;' : (tool.op === 'write' ? '&#10133;' : '&#128065;');
@@ -6777,23 +7088,60 @@ async function wcChatSend() {
6777
7088
 
6778
7089
  if (!msg && wcChatAttachments.length === 0) return;
6779
7090
  if (wcChatRunning || wcState.running) return;
7091
+ if (inputEl) inputEl.value = '';
7092
+
7093
+ // Plan mode: if message starts with "/plan " or contains "plan:" keyword, ask agent for plan first
7094
+ var planMode = msg.toLowerCase().startsWith('/plan ') || msg.toLowerCase().startsWith('piano: ');
7095
+ if (planMode) {
7096
+ var planMsg = msg.replace(new RegExp('^/plan[ ]*', 'i'),'').replace(new RegExp('^piano:[ ]*', 'i'),'');
7097
+ wcChat.push({ role: 'user', text: msg });
7098
+ renderWebCraft(document.getElementById('content'));
7099
+ // Ask agent to produce only a plan, no edits
7100
+ await wcExecuteAgentCall(
7101
+ '[MODALITA PIANO] Descrivi in dettaglio cosa modificheresti per: "' + planMsg + '". ' +
7102
+ 'Elenca i file che toccheresti e cosa faresti in ciascuno. NON applicare nessuna modifica ancora. ' +
7103
+ 'Rispondi con il piano in bullet list.',
7104
+ false, planMsg
7105
+ );
7106
+ return;
7107
+ }
6780
7108
 
6781
7109
  var attachCopy = wcChatAttachments.slice();
6782
7110
  wcChatAttachments = [];
6783
- wcChatRunning = true;
6784
7111
  wcChat.push({ role: 'user', text: msg, attachments: attachCopy });
6785
- if (inputEl) inputEl.value = '';
6786
7112
  renderWebCraft(document.getElementById('content'));
6787
7113
  wcScrollChatToBottom();
6788
7114
 
7115
+ // Auto-snapshot before first agent call in a session
7116
+ if (_wcAutoFixAttempts === 0 && wcChat.filter(function(c){ return c.role==='user'; }).length === 1) {
7117
+ wcTakeSnapshot().then(function(ts) {
7118
+ if (ts) wcChat.push({ role: 'system', text: '&#128190; Snapshot automatico salvato (' + ts.slice(0,16).replace('T',' ') + ')' });
7119
+ renderWebCraft(document.getElementById('content'));
7120
+ });
7121
+ }
7122
+
7123
+ await wcExecuteAgentCall(msg, false, null, attachCopy);
7124
+ }
7125
+
7126
+ // Core agent call — separated so plan mode and normal mode share the same SSE pipeline
7127
+ async function wcExecuteAgentCall(message, isPlanExec, planOrigMsg, attachments) {
7128
+ if (wcChatRunning) return;
7129
+ wcChatRunning = true;
7130
+ renderWebCraft(document.getElementById('content'));
7131
+
7132
+ // Track file state BEFORE edits for diff
7133
+ var filesBefore = {};
7134
+ wcState.generatedFiles.forEach(function(f) { filesBefore[f.name] = f.content; });
7135
+ _wcDiffQueue = [];
7136
+
6789
7137
  try {
6790
7138
  var r = await fetch(API + '/api/studio/webcraft/agent', {
6791
7139
  method: 'POST',
6792
7140
  headers: { 'Content-Type': 'application/json' },
6793
7141
  body: JSON.stringify({
6794
7142
  projectName: wcState.projectName,
6795
- message: msg,
6796
- attachments: attachCopy.map(function(a){ return { name: a.name, mimeType: a.mimeType, base64: a.base64 }; })
7143
+ message: message,
7144
+ attachments: (attachments || []).map(function(a){ return { name: a.name, mimeType: a.mimeType, base64: a.base64 }; })
6797
7145
  })
6798
7146
  });
6799
7147
 
@@ -6811,6 +7159,7 @@ async function wcChatSend() {
6811
7159
  var reader2 = r.body.getReader();
6812
7160
  var dec = new TextDecoder();
6813
7161
  var buf = '';
7162
+ var anyEdits = false;
6814
7163
  while (true) {
6815
7164
  var res = await reader2.read();
6816
7165
  if (res.done) break;
@@ -6829,16 +7178,32 @@ async function wcChatSend() {
6829
7178
  wcScrollChatToBottom();
6830
7179
  } else if (ev.type === 'tool') {
6831
7180
  agentMsg.tools.push({ op: ev.op, path: ev.path, result: ev.result });
6832
- // Update file in generatedFiles if edited/written
6833
7181
  if ((ev.op === 'edit' || ev.op === 'write') && ev.result === 'ok') {
6834
- // Reload file from server via projects/load (simplified: mark for reload)
7182
+ anyEdits = true;
6835
7183
  wcChat[wcChat.length-1] = agentMsg;
6836
7184
  renderWebCraft(document.getElementById('content'));
6837
7185
  }
6838
7186
  } else if (ev.type === 'done') {
7187
+ // Plan mode: detect if response is a plan (no tool edits), show approval banner
7188
+ if (planOrigMsg && !anyEdits) {
7189
+ _wcPlanPending = { plan: agentMsg.text, originalMessage: planOrigMsg };
7190
+ }
6839
7191
  wcChatRunning = false;
6840
- if (ev.changed) { wcReloadProjectFiles(); }
6841
- // Persist chat to disk
7192
+ if (ev.changed) {
7193
+ // Build diffs before reloading
7194
+ var changedFiles = (agentMsg.tools || []).filter(function(t2){ return t2.op === 'edit' || t2.op === 'write'; }).map(function(t2){ return t2.path; });
7195
+ await wcReloadProjectFiles();
7196
+ // Build diffs from before/after
7197
+ changedFiles.forEach(function(fname) {
7198
+ var after = wcState.generatedFiles.find(function(f){ return f.name === fname; });
7199
+ if (after) _wcDiffQueue.push({ file: fname, before: filesBefore[fname] || '', after: after.content });
7200
+ });
7201
+ // Auto syntax-check after edits
7202
+ if (changedFiles.some(function(f){ return f.endsWith('.js') || f.endsWith('.mjs'); })) {
7203
+ setTimeout(wcRunSyntaxCheck, 500);
7204
+ }
7205
+ }
7206
+ // Persist chat
6842
7207
  fetch(API + '/api/studio/webcraft/projects/chat/save', {
6843
7208
  method: 'POST',
6844
7209
  headers: {'Content-Type':'application/json'},
@@ -7033,6 +7398,218 @@ function wcAddField() {
7033
7398
  }
7034
7399
  function wcSetFile(i) { wcState.activeFile = i; renderWebCraft(document.getElementById('content')); }
7035
7400
 
7401
+ // ── WebCraft: Diff Viewer ─────────────────────────────────────────────────────
7402
+ function wcDiffLines(before, after) {
7403
+ var NL = String.fromCharCode(10);
7404
+ var bLines = (before || '').split(NL);
7405
+ var aLines = (after || '').split(NL);
7406
+ var html = '';
7407
+ var maxLen = Math.max(bLines.length, aLines.length);
7408
+ var bi = 0, ai = 0;
7409
+ while (bi < bLines.length || ai < aLines.length) {
7410
+ var bL = bLines[bi], aL = aLines[ai];
7411
+ if (bL === aL) {
7412
+ html += '<div style="font-family:var(--mono);font-size:10px;padding:1px 8px;color:var(--dim);white-space:pre-wrap">&nbsp;' + wcEsc(bL||'') + '</div>';
7413
+ bi++; ai++;
7414
+ } else if (bi >= bLines.length) {
7415
+ html += '<div style="font-family:var(--mono);font-size:10px;padding:1px 8px;background:#0a3a1a;color:#4ade80;white-space:pre-wrap">+' + wcEsc(aL||'') + '</div>';
7416
+ ai++;
7417
+ } else if (ai >= aLines.length) {
7418
+ html += '<div style="font-family:var(--mono);font-size:10px;padding:1px 8px;background:#3a0a0a;color:#f87171;white-space:pre-wrap">-' + wcEsc(bL||'') + '</div>';
7419
+ bi++;
7420
+ } else {
7421
+ html += '<div style="font-family:var(--mono);font-size:10px;padding:1px 8px;background:#3a0a0a;color:#f87171;white-space:pre-wrap">-' + wcEsc(bL||'') + '</div>';
7422
+ html += '<div style="font-family:var(--mono);font-size:10px;padding:1px 8px;background:#0a3a1a;color:#4ade80;white-space:pre-wrap">+' + wcEsc(aL||'') + '</div>';
7423
+ bi++; ai++;
7424
+ }
7425
+ if (bi > maxLen + 50 && ai > maxLen + 50) break; // safety
7426
+ }
7427
+ return html;
7428
+ }
7429
+
7430
+ function wcDiffPanelHtml() {
7431
+ if (_wcDiffQueue.length === 0) return '';
7432
+ var items = _wcDiffQueue.map(function(d, di) {
7433
+ var addedLines = (d.after||'').split(String.fromCharCode(10)).length - (d.before||'').split(String.fromCharCode(10)).length;
7434
+ var sign = addedLines >= 0 ? '+' : '';
7435
+ return '<details style="border:1px solid var(--border);border-radius:6px;margin-bottom:6px;background:var(--bg3)">' +
7436
+ '<summary style="padding:7px 10px;cursor:pointer;font-size:11px;font-family:var(--mono);color:var(--text);list-style:none;display:flex;align-items:center;gap:8px">' +
7437
+ '<span style="color:var(--green);font-size:10px">&#9650;</span>' +
7438
+ '<span style="flex:1">' + wcEsc(d.file) + '</span>' +
7439
+ '<span style="color:' + (addedLines >= 0 ? '#4ade80' : '#f87171') + ';font-size:10px">' + sign + addedLines + ' linee</span>' +
7440
+ '</summary>' +
7441
+ '<div style="max-height:200px;overflow-y:auto;border-top:1px solid var(--border)">' + wcDiffLines(d.before, d.after) + '</div>' +
7442
+ '</details>';
7443
+ }).join('');
7444
+ return '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:12px;margin-top:8px">' +
7445
+ '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
7446
+ '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px">&#128268; Diff — ' + _wcDiffQueue.length + ' file modificati</div>' +
7447
+ '<button onclick="wcClearDiff()" style="font-size:10px;background:none;border:none;color:var(--dim);cursor:pointer">&#10005; Chiudi</button>' +
7448
+ '</div>' +
7449
+ items +
7450
+ '</div>';
7451
+ }
7452
+
7453
+ // ── WebCraft: Snapshot / Rollback ─────────────────────────────────────────────
7454
+ async function wcManualSnapshot() {
7455
+ var ts = await wcTakeSnapshot();
7456
+ if (ts) {
7457
+ wcChat.push({ role: 'system', text: '&#128190; Snapshot salvato (' + ts.slice(0,16).replace('T',' ') + ')' });
7458
+ await wcLoadSnapshots();
7459
+ renderWebCraft(document.getElementById('content'));
7460
+ }
7461
+ }
7462
+
7463
+ async function wcTakeSnapshot() {
7464
+ if (!wcState.projectName) return null;
7465
+ try {
7466
+ var r = await fetch(API + '/api/studio/webcraft/snapshot', {
7467
+ method: 'POST',
7468
+ headers: {'Content-Type':'application/json'},
7469
+ body: JSON.stringify({ projectName: wcState.projectName })
7470
+ });
7471
+ if (r.ok) { var d = await r.json(); return d.snapshot; }
7472
+ } catch(_) {}
7473
+ return null;
7474
+ }
7475
+
7476
+ async function wcLoadSnapshots() {
7477
+ if (!wcState.projectName) return;
7478
+ try {
7479
+ var r = await fetch(API + '/api/studio/webcraft/snapshots/' + encodeURIComponent(wcState.projectName));
7480
+ if (r.ok) { var d = await r.json(); _wcSnapshots = d.snapshots || []; renderWebCraft(document.getElementById('content')); }
7481
+ } catch(_) {}
7482
+ }
7483
+
7484
+ async function wcRestoreSnapshot(ts) {
7485
+ if (!confirm('Ripristinare lo snapshot del ' + ts.replace('T',' ').replace(/-/g,':').slice(0,16) + '? I file attuali verranno sovrascritti.')) return;
7486
+ try {
7487
+ var r = await fetch(API + '/api/studio/webcraft/restore', {
7488
+ method: 'POST',
7489
+ headers: {'Content-Type':'application/json'},
7490
+ body: JSON.stringify({ projectName: wcState.projectName, ts: ts })
7491
+ });
7492
+ if (r.ok) {
7493
+ wcChat.push({ role: 'agent', text: 'Snapshot ripristinato (' + ts + '). Ricarico i file...' });
7494
+ await wcReloadProjectFiles();
7495
+ }
7496
+ } catch(_) {}
7497
+ }
7498
+
7499
+ function wcSnapshotsPanelHtml() {
7500
+ if (_wcSnapshots.length === 0) return '';
7501
+ return '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:12px">' +
7502
+ '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px">&#128190; Snapshot</div>' +
7503
+ _wcSnapshots.slice(0,5).map(function(s) {
7504
+ var label = s.ts.replace('T',' ').replace(/-/g,':').slice(0,16);
7505
+ return '<div style="display:flex;align-items:center;gap:6px;padding:4px 0;border-bottom:1px solid var(--border);font-size:10px">' +
7506
+ '<span style="flex:1;color:var(--dim);font-family:var(--mono)">' + label + '</span>' +
7507
+ '<span style="color:var(--dim)">' + s.fileCount + 'f</span>' +
7508
+ '<button onclick="wcRestoreSnapshot(' + JSON.stringify(s.ts) + ')" style="padding:2px 8px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;color:var(--dim);font-size:10px;cursor:pointer">&#8635;</button>' +
7509
+ '</div>';
7510
+ }).join('') +
7511
+ '</div>';
7512
+ }
7513
+
7514
+ // ── WebCraft: Syntax Check ────────────────────────────────────────────────────
7515
+ async function wcRunSyntaxCheck() {
7516
+ if (!wcState.projectName) return;
7517
+ try {
7518
+ var r = await fetch(API + '/api/studio/webcraft/syntax-check', {
7519
+ method: 'POST',
7520
+ headers: {'Content-Type':'application/json'},
7521
+ body: JSON.stringify({ projectName: wcState.projectName })
7522
+ });
7523
+ if (r.ok) {
7524
+ var d = await r.json();
7525
+ _wcSyntaxResults = d.results || [];
7526
+ var errors = _wcSyntaxResults.filter(function(x){ return !x.ok; });
7527
+ if (errors.length > 0) {
7528
+ wcChat.push({ role: 'system', text: '&#9888; Syntax check: ' + errors.length + ' errore/i trovato/i. Clicca "Fix" per correggere automaticamente.', syntaxErrors: errors });
7529
+ } else {
7530
+ wcChat.push({ role: 'system', text: '&#10003; Syntax check: tutti i file JS sono validi.' });
7531
+ }
7532
+ renderWebCraft(document.getElementById('content'));
7533
+ wcScrollChatToBottom();
7534
+ }
7535
+ } catch(_) {}
7536
+ }
7537
+
7538
+ // ── WebCraft: Grep / Search ───────────────────────────────────────────────────
7539
+ async function wcRunGrep() {
7540
+ var el = document.getElementById('wcGrepInput');
7541
+ var q = el ? el.value.trim() : _wcGrepQuery;
7542
+ if (!q || !wcState.projectName) return;
7543
+ _wcGrepQuery = q;
7544
+ try {
7545
+ var r = await fetch(API + '/api/studio/webcraft/grep', {
7546
+ method: 'POST',
7547
+ headers: {'Content-Type':'application/json'},
7548
+ body: JSON.stringify({ projectName: wcState.projectName, query: q })
7549
+ });
7550
+ if (r.ok) { var d = await r.json(); _wcGrepResults = d.matches || []; renderWebCraft(document.getElementById('content')); }
7551
+ } catch(_) {}
7552
+ }
7553
+
7554
+ function wcGrepPanelHtml() {
7555
+ if (!_wcGrepOpen) return '';
7556
+ var resultsHtml = _wcGrepResults.length > 0
7557
+ ? _wcGrepResults.map(function(m) {
7558
+ return '<div style="padding:4px 8px;border-bottom:1px solid var(--border);cursor:pointer" onclick="wcJumpToFile(' + JSON.stringify(m.file) + ')">' +
7559
+ '<span style="font-size:10px;color:var(--green);font-family:var(--mono)">' + wcEsc(m.file) + ':' + m.lineNum + '</span>' +
7560
+ '<pre style="margin:2px 0 0;font-size:10px;color:var(--text);white-space:pre-wrap;overflow:hidden;max-height:30px">' + wcEsc(m.line) + '</pre>' +
7561
+ '</div>';
7562
+ }).join('')
7563
+ : '<div style="font-size:11px;color:var(--dim);padding:8px">Nessun risultato.</div>';
7564
+ return '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:12px;margin-top:8px">' +
7565
+ '<div style="display:flex;gap:6px;margin-bottom:8px">' +
7566
+ '<input id="wcGrepInput" value="'+wcEsc(_wcGrepQuery)+'" placeholder="Cerca nel codice..." onkeydown="wcGrepKeydown(event)" style="flex:1;padding:6px 10px;font-size:12px;border-radius:6px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);font-family:var(--mono)">' +
7567
+ '<button onclick="wcRunGrep()" style="padding:6px 12px;background:var(--green3);border:none;border-radius:6px;color:var(--bg);font-size:11px;font-weight:700;cursor:pointer">&#128269;</button>' +
7568
+ '<button onclick="wcCloseGrep()" style="padding:6px 8px;background:none;border:1px solid var(--border);border-radius:6px;color:var(--dim);cursor:pointer">&times;</button>' +
7569
+ '</div>' +
7570
+ (_wcGrepResults.length > 0 ? '<div style="font-size:9px;color:var(--dim);margin-bottom:4px">' + _wcGrepResults.length + ' risultati</div>' : '') +
7571
+ '<div style="max-height:200px;overflow-y:auto">' + resultsHtml + '</div>' +
7572
+ '</div>';
7573
+ }
7574
+
7575
+ function wcClearDiff() { _wcDiffQueue = []; renderWebCraft(document.getElementById('content')); }
7576
+ function wcCloseGrep() { _wcGrepOpen = false; renderWebCraft(document.getElementById('content')); }
7577
+ function wcGrepKeydown(e) { if (e.key === 'Enter') wcRunGrep(); }
7578
+
7579
+ function wcJumpToFile(fname) {
7580
+ var idx = wcState.generatedFiles.findIndex(function(f){ return f.name === fname; });
7581
+ if (idx >= 0) { wcState.activeFile = idx; wcRightTab = 'files'; renderWebCraft(document.getElementById('content')); }
7582
+ }
7583
+
7584
+ function wcToggleGrep() { _wcGrepOpen = !_wcGrepOpen; renderWebCraft(document.getElementById('content')); }
7585
+
7586
+ // ── WebCraft: Plan Mode ───────────────────────────────────────────────────────
7587
+ function wcPlanBannerHtml() {
7588
+ if (!_wcPlanPending) return '';
7589
+ return '<div style="background:#1a2a1a;border:1px solid var(--green3);border-radius:10px;padding:14px;margin-bottom:8px">' +
7590
+ '<div style="font-size:11px;font-weight:700;color:var(--green);margin-bottom:8px">&#128204; Piano proposto — approva per eseguire</div>' +
7591
+ '<pre style="font-size:11px;color:var(--text);white-space:pre-wrap;margin:0 0 10px;max-height:120px;overflow-y:auto;font-family:var(--mono)">' + wcEsc(_wcPlanPending.plan) + '</pre>' +
7592
+ '<div style="display:flex;gap:8px">' +
7593
+ '<button onclick="wcApprovePlan()" style="padding:7px 18px;background:var(--green3);border:none;border-radius:7px;color:var(--bg);font-size:12px;font-weight:700;cursor:pointer">&#10003; Esegui</button>' +
7594
+ '<button onclick="wcRejectPlan()" style="padding:7px 14px;background:none;border:1px solid var(--border2);border-radius:7px;color:var(--dim);font-size:12px;cursor:pointer">&#10005; Annulla</button>' +
7595
+ '</div>' +
7596
+ '</div>';
7597
+ }
7598
+
7599
+ async function wcApprovePlan() {
7600
+ if (!_wcPlanPending) return;
7601
+ var msg = _wcPlanPending.originalMessage;
7602
+ _wcPlanPending = null;
7603
+ await wcExecuteAgentCall(msg + String.fromCharCode(10) + '[Piano approvato — procedi con le modifiche]', false);
7604
+ }
7605
+
7606
+ function wcRejectPlan() {
7607
+ wcChat.push({ role: 'agent', text: 'Piano annullato. Dimmi se vuoi modificare la richiesta.' });
7608
+ _wcPlanPending = null;
7609
+ renderWebCraft(document.getElementById('content'));
7610
+ wcScrollChatToBottom();
7611
+ }
7612
+
7036
7613
  async function wcGenerate() {
7037
7614
  if (wcState.running) return;
7038
7615
  var desc = wcState.description;