nothumanallowed 13.2.92 → 13.2.94

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.2.92",
3
+ "version": "13.2.94",
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": {
@@ -189,11 +189,11 @@ function sendHTML(res, html) {
189
189
  res.end(html);
190
190
  }
191
191
 
192
- function parseBody(req) {
192
+ function parseBody(req, maxBytes) {
193
193
  return new Promise((resolve, reject) => {
194
194
  const chunks = [];
195
195
  let size = 0;
196
- const MAX = 1_048_576; // 1 MB
196
+ const MAX = maxBytes || 1_048_576; // 1 MB default
197
197
  req.on('data', chunk => {
198
198
  size += chunk.length;
199
199
  if (size > MAX) { reject(new Error('Body too large')); req.destroy(); return; }
@@ -2814,7 +2814,7 @@ export async function cmdUI(args) {
2814
2814
 
2815
2815
  // ── Studio: run single step (SSE streaming) ──────────────────────
2816
2816
  if (pathname === '/api/studio/run' && method === 'POST') {
2817
- const body = await parseBody(req);
2817
+ const body = await parseBody(req, 4_194_304); // 4MB — context can be up to 120KB + task + PDF
2818
2818
  const { agent, task, context, stepDef } = body;
2819
2819
  const stepPdfBase64 = body.pdfBase64 || null;
2820
2820
  const stepPdfName = body.pdfName || null;
@@ -3284,7 +3284,11 @@ ${context ? `## OUTPUT FROM PREVIOUS AGENTS (use only what is RELEVANT to the wo
3284
3284
  let inThinkBlock = false;
3285
3285
  let thinkBuf = '';
3286
3286
  sendToken(isCanvasAgent ? 'Generating visual report...' : '');
3287
- const llmTimeout = isCanvasAgent ? 120000 : 90000;
3287
+ const llmTimeout = isCanvasAgent ? 180000 : 120000;
3288
+ // Canvas: no thinking (needs full tokens for HTML). Specialists: cap thinking to 2048 to leave room for answer.
3289
+ const stepLlmOpts = isCanvasAgent
3290
+ ? { max_tokens: 12288, thinking: 'off' }
3291
+ : { max_tokens: 8192, thinking_budget: 2048 };
3288
3292
  try {
3289
3293
  await withTimeout(
3290
3294
  callLLMStream(config, sysPrompt, userMsg,
@@ -3323,7 +3327,7 @@ ${context ? `## OUTPUT FROM PREVIOUS AGENTS (use only what is RELEVANT to the wo
3323
3327
  if (stripped) sendToken(stripped);
3324
3328
  }
3325
3329
  },
3326
- { max_tokens: 8192 },
3330
+ stepLlmOpts,
3327
3331
  ),
3328
3332
  llmTimeout
3329
3333
  );
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.2.92';
8
+ export const VERSION = '13.2.94';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -502,21 +502,33 @@ export async function callLLMStream(config, systemPrompt, userMessage, onToken,
502
502
  .replace(/\|\|\(/g, '||(')
503
503
  .replace(/\)\|\|/g, ')||');
504
504
 
505
+ // opts.thinking === 'off' forces thinking off regardless of config
506
+ // opts.max_tokens overrides the default token budget
507
+ const forceThinkingOff = opts.thinking === 'off' || opts.thinking === false;
508
+
505
509
  let thinkingEnabled = false;
506
- try {
507
- const fs2 = await import('fs');
508
- const path2 = await import('path');
509
- const os2 = await import('os');
510
- const cfgFile2 = path2.default.join(os2.default.homedir(), '.nha', 'config.json');
511
- if (fs2.default.existsSync(cfgFile2)) {
512
- const cfg2 = JSON.parse(fs2.default.readFileSync(cfgFile2, 'utf-8'));
513
- thinkingEnabled = cfg2.thinking === true || cfg2.thinking === 'on' || cfg2.thinking === 'true';
514
- }
515
- } catch {}
510
+ if (!forceThinkingOff) {
511
+ try {
512
+ const fs2 = await import('fs');
513
+ const path2 = await import('path');
514
+ const os2 = await import('os');
515
+ const cfgFile2 = path2.default.join(os2.default.homedir(), '.nha', 'config.json');
516
+ if (fs2.default.existsSync(cfgFile2)) {
517
+ const cfg2 = JSON.parse(fs2.default.readFileSync(cfgFile2, 'utf-8'));
518
+ thinkingEnabled = cfg2.thinking === true || cfg2.thinking === 'on' || cfg2.thinking === 'true';
519
+ }
520
+ } catch {}
521
+ }
522
+
523
+ // Determine effective max_tokens:
524
+ // 1. If opts.max_tokens explicitly set, use it
525
+ // 2. If thinking is on, default to 8192 (need room for think + answer)
526
+ // 3. Otherwise default to 8192 (full context for specialist agents)
527
+ const effectiveMaxTokens = opts.max_tokens || (thinkingEnabled ? 8192 : 8192);
516
528
 
517
529
  const nhaBody = {
518
530
  model: model || '/opt/models/qwen3-32b',
519
- max_tokens: thinkingEnabled ? 8192 : 4096,
531
+ max_tokens: effectiveMaxTokens,
520
532
  messages: [
521
533
  { role: 'system', content: sanitize(systemPrompt) },
522
534
  { role: 'user', content: sanitize(userMessage) },
@@ -3395,14 +3395,36 @@ function studioScrollToAgent(agentLabel) {
3395
3395
  if (!logEl) return;
3396
3396
  var entries = logEl.querySelectorAll('.studio-log-entry');
3397
3397
  var target = null;
3398
+ var labelLow = agentLabel.toLowerCase();
3399
+ // Pass 1: exact match
3398
3400
  for (var i2 = 0; i2 < entries.length; i2++) {
3399
3401
  var agentSpan = entries[i2].querySelector('.studio-log-entry__agent');
3400
3402
  if (agentSpan && agentSpan.textContent.trim() === agentLabel) { target = entries[i2]; break; }
3401
3403
  }
3404
+ // Pass 2: startsWith (handles Parliament label that changes to "ATHENA ⇄ ...")
3405
+ if (!target) {
3406
+ for (var i3 = 0; i3 < entries.length; i3++) {
3407
+ var sp = entries[i3].querySelector('.studio-log-entry__agent');
3408
+ if (sp && (sp.textContent.trim().toLowerCase().indexOf(labelLow) === 0 || labelLow.indexOf(sp.textContent.trim().toLowerCase()) === 0)) { target = entries[i3]; break; }
3409
+ }
3410
+ }
3411
+ // Pass 3: contains (for Parliament: "Parlamento" matches any log entry with that word)
3412
+ if (!target) {
3413
+ for (var i4 = 0; i4 < entries.length; i4++) {
3414
+ var sp2 = entries[i4].querySelector('.studio-log-entry__agent');
3415
+ if (sp2 && sp2.textContent.trim().toLowerCase().indexOf(labelLow.slice(0,8)) >= 0) { target = entries[i4]; break; }
3416
+ }
3417
+ }
3402
3418
  if (target) {
3403
- target.scrollIntoView({behavior:'smooth', block:'start'});
3419
+ var logContainer = document.querySelector('.studio-log');
3420
+ if (logContainer) {
3421
+ var entryTop = target.offsetTop - logContainer.offsetTop;
3422
+ logContainer.scrollTo({top: entryTop - 8, behavior: 'smooth'});
3423
+ } else {
3424
+ target.scrollIntoView({behavior:'smooth', block:'start'});
3425
+ }
3404
3426
  target.style.outline = '2px solid var(--green)';
3405
- setTimeout(function(){ target.style.outline = ''; }, 1500);
3427
+ setTimeout(function(){ target.style.outline = ''; }, 1800);
3406
3428
  }
3407
3429
  }
3408
3430
 
@@ -3655,7 +3677,8 @@ function downloadStudioPDF() {
3655
3677
  '</div>' +
3656
3678
  '</body></html>';
3657
3679
 
3658
- // Generate PDF using jsPDF + html2canvas (loaded from CDN on demand)
3680
+ // Generate PDF injects print-safe CSS then uses html2canvas at 2.5x for crisp output.
3681
+ // Page breaks are avoided inside .section/.card/.priority-item by injecting break-inside:avoid.
3659
3682
  var pdfFileName = (studioState.task || 'NHA Studio Report').slice(0, 60).replace(/[^a-z0-9\s]/gi,'').trim().replace(/\s+/g,'-') + '.pdf';
3660
3683
  function doGeneratePdf() {
3661
3684
  var btn2 = document.getElementById('studioInlinePdfBtn');
@@ -3665,33 +3688,67 @@ function downloadStudioPDF() {
3665
3688
  if (dlBtn2) { dlBtn2.disabled = b; dlBtn2.textContent = b ? 'Generando PDF...' : '\u2913 Download PDF'; }
3666
3689
  }
3667
3690
  setBusy(true);
3668
- // Build iframe to render the report HTML at full width
3691
+
3692
+ // Inject page-break-safe CSS into the HTML before rendering
3693
+ var printCss = '<style>body{padding:20px!important;max-width:800px!important;margin:0 auto!important}' +
3694
+ '.section,.card,.priority-item,.source-item,.bar-row{break-inside:avoid;page-break-inside:avoid}' +
3695
+ '.header{break-after:avoid;page-break-after:avoid}' +
3696
+ 'h1,h2,h3,h4{break-after:avoid;page-break-after:avoid}' +
3697
+ '@media print{body{background:#fff!important;color:#111!important}' +
3698
+ '.header{background:linear-gradient(135deg,#4f46e5,#06b6d4)!important;-webkit-print-color-adjust:exact}' +
3699
+ '.section{background:#f8f9fa!important;border:1px solid #e0e0e0!important}' +
3700
+ '.section-title{color:#4f46e5!important}}</style>';
3701
+ var pdfHtml = html.replace('</head>', printCss + '</head>');
3702
+
3703
+ // Build hidden iframe at 800px (A4 equivalent at 96dpi)
3669
3704
  var iframe = document.createElement('iframe');
3670
- iframe.style.cssText = 'position:fixed;left:-9999px;top:0;width:900px;height:1px;border:none;visibility:hidden';
3705
+ iframe.style.cssText = 'position:fixed;left:-9999px;top:0;width:800px;height:1px;border:none;visibility:hidden';
3671
3706
  document.body.appendChild(iframe);
3672
3707
  var ifrDoc = iframe.contentDocument || iframe.contentWindow.document;
3673
- ifrDoc.open(); ifrDoc.write(html); ifrDoc.close();
3708
+ ifrDoc.open(); ifrDoc.write(pdfHtml); ifrDoc.close();
3674
3709
  iframe.onload = function() {
3675
3710
  var body = ifrDoc.body;
3676
3711
  var totalH = Math.max(body.scrollHeight, body.offsetHeight, ifrDoc.documentElement.scrollHeight);
3677
3712
  iframe.style.height = totalH + 'px';
3713
+ // Render at 2.5x scale for crisp text
3678
3714
  window.html2canvas(body, {
3679
- scale: 1.5, useCORS: true, allowTaint: true,
3680
- width: 900, windowWidth: 900,
3715
+ scale: 2.5, useCORS: true, allowTaint: true,
3716
+ backgroundColor: '#0d0d14',
3717
+ width: 800, windowWidth: 800,
3681
3718
  scrollX: 0, scrollY: 0,
3682
3719
  ignoreElements: function(el){ return el.tagName === 'SCRIPT'; }
3683
3720
  }).then(function(canvas) {
3684
- var imgData = canvas.toDataURL('image/jpeg', 0.85);
3721
+ var imgData = canvas.toDataURL('image/png'); // PNG for crisp text
3685
3722
  var pdf = new window.jspdf.jsPDF({orientation:'portrait', unit:'pt', format:'a4', compress:true});
3686
- var pageW = pdf.internal.pageSize.getWidth();
3687
- var pageH = pdf.internal.pageSize.getHeight();
3688
- var imgW = pageW;
3689
- var imgH = (canvas.height * pageW) / canvas.width;
3690
- var y = 0;
3691
- while (y < imgH) {
3692
- if (y > 0) pdf.addPage();
3693
- pdf.addImage(imgData, 'JPEG', 0, -y, imgW, imgH, '', 'FAST');
3694
- y += pageH;
3723
+ var pageW = pdf.internal.pageSize.getWidth(); // 595pt
3724
+ var pageH = pdf.internal.pageSize.getHeight(); // 842pt
3725
+ var margin = 24; // pt
3726
+ var usableW = pageW - margin * 2;
3727
+ var usableH = pageH - margin * 2;
3728
+ // Scale image to fit page width
3729
+ var imgW = usableW;
3730
+ var imgH = (canvas.height * usableW) / canvas.width;
3731
+
3732
+ // Smart paging: find safe cut points every ~usableH
3733
+ // Strategy: render slice by slice, each slice fits one page
3734
+ var sliceH = Math.floor(canvas.height * (usableH / imgH)); // canvas px per page
3735
+ var yCanvas = 0;
3736
+ var pageNum = 0;
3737
+ while (yCanvas < canvas.height) {
3738
+ if (pageNum > 0) pdf.addPage();
3739
+ var remaining = canvas.height - yCanvas;
3740
+ var thisSlice = Math.min(sliceH, remaining);
3741
+ // Create a temporary canvas for this slice
3742
+ var sliceCanvas = document.createElement('canvas');
3743
+ sliceCanvas.width = canvas.width;
3744
+ sliceCanvas.height = thisSlice;
3745
+ var ctx = sliceCanvas.getContext('2d');
3746
+ ctx.drawImage(canvas, 0, yCanvas, canvas.width, thisSlice, 0, 0, canvas.width, thisSlice);
3747
+ var sliceData = sliceCanvas.toDataURL('image/png');
3748
+ var sliceImgH = (thisSlice * usableW) / canvas.width;
3749
+ pdf.addImage(sliceData, 'PNG', margin, margin, usableW, sliceImgH, '', 'FAST');
3750
+ yCanvas += thisSlice;
3751
+ pageNum++;
3695
3752
  }
3696
3753
  pdf.save(pdfFileName);
3697
3754
  document.body.removeChild(iframe);
@@ -3781,6 +3838,8 @@ async function runStudio() {
3781
3838
  studioState.running = true;
3782
3839
  studioState.planned = false;
3783
3840
  // Keep attachmentContext — it was loaded before hitting Run
3841
+ var parlBlockEl = document.getElementById('studioParliamentBlock');
3842
+ if (parlBlockEl) parlBlockEl.style.display = 'none';
3784
3843
  renderStudioNodes();
3785
3844
  renderStudioLog();
3786
3845
  renderStudioResult();
@@ -3907,6 +3966,38 @@ async function runStudio() {
3907
3966
  var parlNodeIdx = studioState.nodes.length;
3908
3967
  studioState.nodes.push({icon:\x27&#x2656;\x27, agent:\x27Parliament\x27, label:\x27Parlamento\x27, status:\x27running\x27, output:\x27\x27, _rendered:false});
3909
3968
  renderStudioNodes();
3969
+
3970
+ // ── Parliament visual block ──────────────────────────────────────
3971
+ // Track active R2 agent for visual block
3972
+ var parlActiveAgent = null;
3973
+ var parlDoneAgents = {};
3974
+
3975
+ function renderParlBlock(phase, activeLabel, convergence) {
3976
+ var pb = document.getElementById(\x27studioParliamentBlock\x27);
3977
+ if (!pb) return;
3978
+ pb.style.display = \x27block\x27;
3979
+ var agentCircles = proposals.map(function(p) {
3980
+ var lbl = p.label || p.agent;
3981
+ var isDone = !!parlDoneAgents[lbl];
3982
+ var isActive = lbl === activeLabel;
3983
+ var circCls = \x27studio-parliament__agent-circle\x27 + (isActive ? \x27 studio-parliament__agent-circle--reading\x27 : isDone ? \x27 studio-parliament__agent-circle--done\x27 : \x27\x27);
3984
+ var readingTxt = isActive
3985
+ ? (\x27<div class="studio-parliament__reading">\u21c4 legge ' + proposals.filter(function(x){return (x.label||x.agent)!==lbl;}).map(function(x){return x.label||x.agent;}).slice(0,2).join(\x27, \x27) + \x27</div>\x27)
3986
+ : \x27\x27;
3987
+ return \x27<div class="studio-parliament__agent"><div class="\x27 + circCls + \x27">\x27 + esc(p.icon||String.fromCharCode(9632)) + \x27</div><div class="studio-parliament__agent-label">\x27 + esc(lbl.slice(0,14)) + \x27</div>\x27 + readingTxt + \x27</div>\x27;
3988
+ }).join(\x27<div class="studio-parliament__bidir">\u21c4</div>\x27);
3989
+
3990
+ var phaseLabel = phase === \x27r1\x27 ? \x27Round 1\x27 : phase === \x27r2\x27 ? \x27Round 2 — Cross-reading\x27 : phase === \x27r3\x27 ? \x27Round 3 — Mediazione\x27 : \x27Completato\x27;
3991
+ var roundEl = convergence != null
3992
+ ? \x27<div class="studio-parliament__convergence">&#x2714; Convergenza: \x27 + convergence + \x27% &mdash; deliberazione completata</div>\x27
3993
+ : \x27<span class="studio-parliament__round">\x27 + phaseLabel + \x27</span>\x27;
3994
+
3995
+ pb.innerHTML = \x27<div class="studio-parliament"><div class="studio-parliament__header"><div class="studio-parliament__master"><span class="studio-parliament__master-icon">&#x2666;</span><span>MASTER AGENT &mdash; Orchestrazione</span></div><div class="studio-parliament__title">&#x2656; PARLIAMENT</div>\x27 + (convergence == null ? roundEl : \x27\x27) + \x27</div><div class="studio-parliament__agents">\x27 + agentCircles + \x27</div>\x27 + (convergence != null ? roundEl : \x27\x27) + \x27</div>\x27;
3996
+ }
3997
+
3998
+ // Show initial R1 block
3999
+ renderParlBlock(\x27r1\x27, null, null);
4000
+
3910
4001
  var deliberateBody = JSON.stringify({task: task, proposals: proposals, language: document.getElementById(\x27langSelect\x27) ? document.getElementById(\x27langSelect\x27).value : \x27it\x27});
3911
4002
  try {
3912
4003
  var delRes = await fetch(\x27/api/studio/deliberate\x27, {method:\x27POST\x27, headers:{\x27Content-Type\x27:\x27application/json\x27}, body: deliberateBody, signal: studioAbortController ? studioAbortController.signal : undefined});
@@ -3928,12 +4019,11 @@ async function runStudio() {
3928
4019
  try {
3929
4020
  var dev = JSON.parse(dd);
3930
4021
  if (dev.token) {
3931
- // Status tokens — check for Round 2 start to add new animated log entry
3932
4022
  var r2StartM = dev.token.match(/^\\[Round 2: (.+?)\\]\\s*$/);
3933
4023
  var r2LiveM = dev.token.match(/^\\[Round 2 (.+?): (\\d+) token\\]\\s*$/);
3934
4024
  if (r2StartM) {
3935
- // New R2 agent starting — add a new log entry with thinking animation
3936
4025
  var r2Label = r2StartM[1];
4026
+ parlActiveAgent = r2Label;
3937
4027
  studioLog(r2Label, \x27&#x2656;\x27, \x27\x27, \x27agent\x27, false);
3938
4028
  var delEnts2 = document.querySelectorAll(\x27.studio-log-entry\x27);
3939
4029
  var delL2 = delEnts2[delEnts2.length - 1];
@@ -3942,7 +4032,7 @@ async function runStudio() {
3942
4032
  var delTb2 = delL2.querySelector(\x27.studio-log-entry__text\x27);
3943
4033
  if (delTb2) delTb2.innerHTML = \x27<span style="color:var(--green);font-family:var(--mono);font-size:10px">&#x2656; Deliberando Round 2<span class="thinking-dots"><span></span><span></span><span></span></span></span>\x27;
3944
4034
  }
3945
- // Update Parliament pipeline node — show R2 agent + who it reads
4035
+ renderParlBlock(\x27r2\x27, r2Label, null);
3946
4036
  if (studioState.nodes[parlNodeIdx]) {
3947
4037
  var otherLabels = proposals.filter(function(p){ return (p.label || p.agent) !== r2Label; }).map(function(p){ return p.label || p.agent; });
3948
4038
  var readingStr = otherLabels.slice(0,2).join(\x27 + \x27) + (otherLabels.length > 2 ? \x27 +\x27 + (otherLabels.length-2) : \x27\x27);
@@ -3951,7 +4041,6 @@ async function runStudio() {
3951
4041
  renderStudioNodes();
3952
4042
  }
3953
4043
  } else if (r2LiveM) {
3954
- // Live token count update for the current R2 agent
3955
4044
  var r2AgentName = r2LiveM[1];
3956
4045
  var r2Toks = parseInt(r2LiveM[2], 10);
3957
4046
  var delAllEnts = document.querySelectorAll(\x27[data-r2-agent]\x27);
@@ -3959,35 +4048,31 @@ async function runStudio() {
3959
4048
  for (var rei = delAllEnts.length - 1; rei >= 0; rei--) {
3960
4049
  if (delAllEnts[rei].getAttribute(\x27data-r2-agent\x27) === r2AgentName) { r2Entry = delAllEnts[rei]; break; }
3961
4050
  }
3962
- if (!r2Entry) {
3963
- var delAllE = document.querySelectorAll(\x27.studio-log-entry\x27);
3964
- r2Entry = delAllE[delAllE.length - 1];
3965
- }
4051
+ if (!r2Entry) { var delAllE = document.querySelectorAll(\x27.studio-log-entry\x27); r2Entry = delAllE[delAllE.length - 1]; }
3966
4052
  if (r2Entry) {
3967
4053
  var r2Tb = r2Entry.querySelector(\x27.studio-log-entry__text\x27);
3968
4054
  if (r2Tb) r2Tb.innerHTML = \x27<span style="color:var(--green);font-family:var(--mono);font-size:10px">&#x2656; Deliberando Round 2 \u2014 \x27 + r2Toks + \x27 token<span class="thinking-dots"><span></span><span></span><span></span></span></span>\x27;
3969
4055
  }
3970
- studioAddTokens(0, 20); // approx chunk size
4056
+ studioAddTokens(0, 20);
3971
4057
  } else {
3972
- // Other status tokens — update last log entry
3973
4058
  var delEntries = document.querySelectorAll(\x27.studio-log-entry\x27);
3974
4059
  var delLast = delEntries[delEntries.length - 1];
3975
4060
  if (delLast) { var delTb = delLast.querySelector(\x27.studio-log-entry__text\x27); if (delTb) delTb.textContent = dev.token.replace(new RegExp(\x27[\\r\\n]+\x27,\x27g\x27),\x27 \x27); }
3976
4061
  }
3977
4062
  } else if (dev.deliberation_r2) {
3978
4063
  var r2d = dev.deliberation_r2;
3979
- // Full output in log — no truncation
3980
4064
  studioLog(r2d.label || r2d.agent, \x27&#x2656;\x27, \x27[R2] \x27 + (r2d.output || \x27\x27), \x27agent\x27, true);
3981
4065
  var ni2 = studioState.nodes.findIndex(function(x){return x.agent===r2d.agent;});
3982
- if (ni2 >= 0) {
3983
- studioState.nodes[ni2].output = r2d.output;
3984
- studioState.nodes[ni2].status = \x27done\x27;
3985
- }
3986
- // Estimate tokens for R2 (approx 1 token ≈ 4 chars)
4066
+ if (ni2 >= 0) { studioState.nodes[ni2].output = r2d.output; studioState.nodes[ni2].status = \x27done\x27; }
3987
4067
  studioAddTokens(0, Math.ceil((r2d.output||'').length / 4));
4068
+ // Mark this agent done in parliament block
4069
+ parlDoneAgents[r2d.label || r2d.agent] = true;
4070
+ parlActiveAgent = null;
4071
+ renderParlBlock(\x27r2\x27, null, null);
3988
4072
  renderStudioNodes();
3989
4073
  context = r2d.output || context;
3990
4074
  } else if (dev.deliberation_r3) {
4075
+ renderParlBlock(\x27r3\x27, null, null);
3991
4076
  studioLog(\x27HERALD\x27, \x27&#128295;\x27, \x27[Mediazione] \x27 + (dev.deliberation_r3.output || \x27\x27), \x27system\x27, true);
3992
4077
  studioAddTokens(0, Math.ceil((dev.deliberation_r3.output||'').length / 4));
3993
4078
  context = dev.deliberation_r3.output || context;
@@ -3995,6 +4080,7 @@ async function runStudio() {
3995
4080
  var r2Conv = Math.round((dev.r2_convergence || 0) * 100);
3996
4081
  studioLog(\x27Parlamento\x27, \x27&#x2656;\x27, \x27Deliberazione completa — convergenza R2: \x27 + r2Conv + \x27%\x27, \x27system\x27);
3997
4082
  if (dev.mediation) { context = dev.mediation; }
4083
+ renderParlBlock(\x27done\x27, null, r2Conv);
3998
4084
  if (studioState.nodes[parlNodeIdx]) {
3999
4085
  studioState.nodes[parlNodeIdx].status = \x27done\x27;
4000
4086
  studioState.nodes[parlNodeIdx].label = \x27Parlamento (\x27 + r2Conv + \x27%)\x27;
@@ -4011,6 +4097,7 @@ async function runStudio() {
4011
4097
  } catch(e3) {
4012
4098
  if (e3.name !== \x27AbortError\x27) {
4013
4099
  studioLog(\x27Parlamento\x27, \x27&#x2656;\x27, \x27Deliberazione non disponibile: \x27 + (e3.message || String(e3)), \x27error\x27);
4100
+ var pb2 = document.getElementById(\x27studioParliamentBlock\x27); if (pb2) pb2.style.display = \x27none\x27;
4014
4101
  }
4015
4102
  }
4016
4103
  }
@@ -4046,6 +4133,7 @@ function saveStudioSession(task, nodes, log, result) {
4046
4133
  task: task,
4047
4134
  nodes: nodes.map(function(n){return {label:n.label,icon:n.icon,agent:n.agent};}),
4048
4135
  result: result,
4136
+ canvas: studioState.canvas || null,
4049
4137
  log: log.map(function(e){return {agent:e.agent,icon:e.icon,text:e.text,type:e.type,time:e.time};}),
4050
4138
  ts: new Date().toLocaleString()
4051
4139
  });
@@ -4089,10 +4177,28 @@ function restoreStudioSession(idx) {
4089
4177
  studioState.nodes = s.nodes.map(function(n){return {icon:n.icon,agent:n.agent,label:n.label,status:'done'};});
4090
4178
  studioState.log = s.log;
4091
4179
  studioState.result = s.result;
4180
+ studioState.canvas = s.canvas || null;
4092
4181
  studioState.running = false;
4093
4182
  var ta = document.getElementById('studioTaskInput');
4094
4183
  if (ta) ta.value = s.task;
4095
4184
  renderStudioNodes(); renderStudioLog(); renderStudioResult();
4185
+ // Restore canvas if present
4186
+ if (s.canvas) {
4187
+ var cf = document.getElementById('canvasFrame');
4188
+ var cp = document.getElementById('canvasPanel');
4189
+ if (cf) cf.srcdoc = s.canvas;
4190
+ if (cp) cp.classList.add('open');
4191
+ var ct = document.getElementById('canvasTitle');
4192
+ if (ct) ct.textContent = 'Studio Report';
4193
+ var scb = document.getElementById('studioCanvasBtn');
4194
+ if (scb) scb.style.display = '';
4195
+ // Also store in allCanvasData so canvas panel nav works
4196
+ var convId = activeConvId || 'studio';
4197
+ if (!allCanvasData[convId]) allCanvasData[convId] = {canvases:[], browsers:[]};
4198
+ allCanvasData[convId].canvases.push({html: s.canvas, title: s.task.slice(0,60), ts: s.ts});
4199
+ canvasIdx = allCanvasData[convId].canvases.length - 1;
4200
+ canvasMode = 'canvas';
4201
+ }
4096
4202
  showToast('success', 'Session restored', s.task.slice(0, 60), 3000);
4097
4203
  }
4098
4204
 
@@ -4152,7 +4258,7 @@ function runStudioStep(idx, node, task, context, stepDef, signal) {
4152
4258
  // Inject attachment into first step only — pass PDF/image as dedicated fields,
4153
4259
  // NOT as raw base64 in context (would cause 100k+ token overflow for any real PDF).
4154
4260
  // Cap accumulated context to ~40KB to avoid token overflow — keep the most recent content
4155
- var cappedContext = context && context.length > 40000 ? context.slice(-40000) : context;
4261
+ var cappedContext = context && context.length > 120000 ? context.slice(-120000) : context;
4156
4262
  var bodyObj = {stepIdx: idx, agent: node.agent, task: task, context: cappedContext, stepDef: stepDef};
4157
4263
  if (idx === 0 && studioState.attachmentContext) {
4158
4264
  var ac = studioState.attachmentContext;
@@ -4395,6 +4501,7 @@ function renderStudio(el) {
4395
4501
  '<button id="studioCanvasBtn" onclick="openCanvasPanel()" title="' + t('canvas_open') + '" style="font-size:12px;padding:5px 14px;background:none;border:1px solid var(--border);border-radius:6px;color:var(--dim);cursor:pointer;font-weight:700;transition:all .2s">&#9632; Canvas</button>' +
4396
4502
  '</div>' +
4397
4503
  '<div class="studio-canvas" id="studioNodes"></div>' +
4504
+ '<div id="studioParliamentBlock" style="display:none"></div>' +
4398
4505
  '<div class="studio-log" id="studioLog" style="display:none"></div>' +
4399
4506
  '<div id="studioResult"></div>' +
4400
4507
  '<div id="studioSessionsBar" style="margin-top:16px;display:none"></div>' +
@@ -4940,6 +5047,24 @@ input:focus,textarea:focus{border-color:var(--green3)}
4940
5047
  .studio-arrow{display:flex;align-items:center;color:var(--border2);font-size:18px;padding:0 8px;flex-shrink:0;margin-bottom:30px;transition:color .4s}
4941
5048
  .studio-arrow--active{color:var(--green3);animation:stFlow .5s ease-in-out infinite alternate}
4942
5049
  .studio-arrow--done{color:#22c55e}
5050
+ .studio-parliament{background:linear-gradient(135deg,#0e0e1c 0%,#12122a 100%);border:1.5px solid #6366f1;border-radius:12px;padding:18px 20px;margin-bottom:16px;animation:stNodeIn .4s ease forwards}
5051
+ .studio-parliament__header{display:flex;align-items:center;gap:10px;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #6366f133}
5052
+ .studio-parliament__master{display:flex;align-items:center;gap:8px;background:linear-gradient(90deg,#4f46e5,#7c3aed);border-radius:8px;padding:7px 12px;font-size:11px;font-weight:700;color:#fff;letter-spacing:.5px}
5053
+ .studio-parliament__master-icon{font-size:18px}
5054
+ .studio-parliament__title{font-size:10px;color:#a5b4fc;text-transform:uppercase;letter-spacing:1.2px;font-weight:700;margin-left:auto}
5055
+ .studio-parliament__round{font-size:10px;color:#6366f1;background:rgba(99,102,241,.12);border-radius:20px;padding:3px 10px;font-weight:700;animation:stPulse .9s ease-in-out infinite}
5056
+ .studio-parliament__agents{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;padding:4px 0}
5057
+ .studio-parliament__agent{display:flex;flex-direction:column;align-items:center;gap:5px;min-width:80px}
5058
+ .studio-parliament__agent-circle{width:44px;height:44px;border-radius:10px;border:1.5px solid #6366f1;background:#1a1a2e;display:flex;align-items:center;justify-content:center;font-size:18px;transition:all .3s}
5059
+ .studio-parliament__agent-circle--reading{border-color:#22d3ee;box-shadow:0 0 0 6px rgba(34,211,238,.15),0 0 16px rgba(34,211,238,.2);animation:stRing 1.4s ease-out infinite}
5060
+ .studio-parliament__agent-circle--done{border-color:#22c55e;background:rgba(34,197,94,.08)}
5061
+ .studio-parliament__agent-label{font-size:9px;color:#a5b4fc;text-align:center;font-weight:600;max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
5062
+ .studio-parliament__conn{display:flex;flex-direction:column;align-items:center;gap:2px;margin-bottom:16px}
5063
+ .studio-parliament__conn-line{width:2px;height:18px;background:linear-gradient(180deg,#6366f1,#22d3ee);border-radius:2px;animation:stFlow .7s ease-in-out infinite alternate}
5064
+ .studio-parliament__bidir{color:#6366f1;font-size:14px;animation:stPulse .6s ease-in-out infinite}
5065
+ .studio-parliament__reading{font-size:9px;color:#22d3ee;text-align:center;margin-top:6px;font-family:var(--mono);background:rgba(34,211,238,.07);border-radius:6px;padding:3px 8px}
5066
+ .studio-parliament__convergence{text-align:center;margin-top:12px;padding:8px;background:rgba(34,197,94,.07);border:1px solid rgba(34,197,94,.2);border-radius:8px;font-size:11px;color:#22c55e;font-weight:700}
5067
+ @keyframes stOrbit{0%{transform:translateX(0) scale(1)}50%{transform:translateX(4px) scale(1.04)}100%{transform:translateX(0) scale(1)}}
4943
5068
  .studio-log{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:16px;max-height:380px;overflow-y:auto;font-size:11.5px;line-height:1.65}
4944
5069
  .studio-log-entry{margin-bottom:12px;padding:10px 12px;border-radius:8px;background:var(--bg3);border:1px solid var(--border)}
4945
5070
  .studio-log-entry:last-child{margin-bottom:0}