nothumanallowed 13.5.152 → 13.5.154

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.152",
3
+ "version": "13.5.154",
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": {
@@ -4652,6 +4652,39 @@ ${completedHeadings ? `## SECTIONS ALREADY WRITTEN (headings only):\n${completed
4652
4652
  return;
4653
4653
  }
4654
4654
 
4655
+ // POST /api/studio/webcraft/stream { system, user, max_tokens } → SSE token stream
4656
+ // Used by WebCraft generation for live typewriter effect per file
4657
+ if (pathname === '/api/studio/webcraft/stream' && method === 'POST') {
4658
+ const body = await parseBody(req, 131072);
4659
+ if (!body.system || !body.user) {
4660
+ sendJSON(res, 400, { error: 'system and user required' });
4661
+ logRequest(method, pathname, 400, Date.now() - start);
4662
+ return;
4663
+ }
4664
+ res.writeHead(200, {
4665
+ 'Content-Type': 'text/event-stream',
4666
+ 'Cache-Control': 'no-cache',
4667
+ 'Connection': 'keep-alive',
4668
+ 'Access-Control-Allow-Origin': '*',
4669
+ });
4670
+ let fullText = '';
4671
+ let tokIn = 0, tokOut = 0;
4672
+ try {
4673
+ await callLLMStream(config, body.system, body.user, (token) => {
4674
+ fullText += token;
4675
+ tokOut++;
4676
+ res.write('data: ' + JSON.stringify({ type: 'token', token }) + '\n\n');
4677
+ }, { max_tokens: body.max_tokens || 16384, temperature: 0.3 });
4678
+ tokIn = Math.round((body.system.length + body.user.length) / 4);
4679
+ res.write('data: ' + JSON.stringify({ type: 'done', text: fullText, usage: { prompt_tokens: tokIn, completion_tokens: tokOut } }) + '\n\n');
4680
+ } catch (e) {
4681
+ res.write('data: ' + JSON.stringify({ type: 'error', message: e.message }) + '\n\n');
4682
+ }
4683
+ res.end();
4684
+ logRequest(method, pathname, 200, Date.now() - start);
4685
+ return;
4686
+ }
4687
+
4655
4688
  // ── WebCraft Projects — list and delete saved projects ──────────────────
4656
4689
  // GET /api/studio/webcraft/projects → { projects: [{name, description, fileCount, createdAt, dir}] }
4657
4690
  // DELETE /api/studio/webcraft/projects/:name → { ok }
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.152';
8
+ export const VERSION = '13.5.154';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -7384,38 +7384,32 @@ function wcGenElapsed() {
7384
7384
  function wcStartGenTimer() {
7385
7385
  if (_wcTimerInterval) clearInterval(_wcTimerInterval);
7386
7386
  _wcTimerInterval = setInterval(function() {
7387
- if (!wcState.running) { clearInterval(_wcTimerInterval); _wcTimerInterval = null; return; }
7388
- if (_wcOverlayMinimized) {
7389
- var pill = document.getElementById('wcPillLabel');
7390
- if (pill) pill.textContent = _wcGenOverlayState.name || 'Generando...';
7391
- var ov = document.getElementById('wcGenOverlay');
7392
- if (ov) { var spans = ov.querySelectorAll('span'); if (spans[1]) spans[1].textContent = wcGenElapsed(); }
7393
- } else {
7394
- wcUpdateGenOverlay(_wcGenOverlayState.fi, _wcGenOverlayState.total, _wcGenOverlayState.name);
7395
- }
7387
+ if (!wcState.running && !wcState.repairing) { clearInterval(_wcTimerInterval); _wcTimerInterval = null; return; }
7388
+ // Always update time in-place — never re-render the whole component
7389
+ wcUpdateGenOverlay(_wcGenOverlayState.fi, _wcGenOverlayState.total, _wcGenOverlayState.name);
7390
+ var repairTime = document.getElementById('wcRepairTime');
7391
+ if (repairTime) repairTime.textContent = wcGenElapsed();
7396
7392
  }, 1000);
7397
7393
  }
7398
7394
 
7399
7395
  function wcUpdateGenOverlay(fi2, total, name) {
7400
7396
  _wcGenOverlayState = { fi: fi2, total: total, name: name };
7401
- if (_wcOverlayMinimized) return;
7402
- var ov = document.getElementById('wcGenOverlay');
7403
- if (!ov) return;
7404
- var pct = Math.round((fi2 / total) * 100);
7405
- var tokLabel = (_wcTokIn + _wcTokOut) > 0
7406
- ? '<div style="font-size:10px;color:var(--dim);margin-top:4px;font-family:var(--mono)">&#8679;' + _wcTokIn.toLocaleString() + ' &#8681;' + _wcTokOut.toLocaleString() + ' tok</div>'
7407
- : '';
7408
- ov.innerHTML =
7409
- '<div style="font-size:38px;animation:wcRobotBob 1s ease-in-out infinite">&#129302;</div>' +
7410
- '<div style="font-size:13px;font-weight:700;color:var(--green);margin-top:12px">' + (name.indexOf('Retry:') === 0 ? 'Retry in corso...' : name.indexOf('Fix:') === 0 ? 'Correzione in corso...' : 'Generazione in corso...') + '</div>' +
7411
- '<div style="font-size:10px;color:var(--dim);margin-top:2px">Clicca per navigare i file</div>' +
7412
- '<div style="font-size:11px;color:'+(name.indexOf('Retry:')===0?'#fb923c':name.indexOf('Fix:')===0?'#facc15':'var(--dim)')+';font-family:var(--mono);max-width:300px;text-align:center;word-break:break-all;margin-top:8px">'+wcEsc(name)+'</div>' +
7413
- '<div style="width:220px;height:4px;background:rgba(255,255,255,0.1);border-radius:2px;overflow:hidden;margin-top:12px">' +
7414
- '<div style="height:100%;width:'+pct+'%;background:var(--green);border-radius:2px;transition:width .4s ease;animation:wcBarPulse 1.5s ease-in-out infinite"></div>' +
7415
- '</div>' +
7416
- '<div style="font-size:10px;color:var(--dim);margin-top:6px">'+fi2+' / '+total+' file &nbsp;&#183;&nbsp; '+wcGenElapsed()+'</div>' +
7417
- tokLabel +
7418
- '<div style="display:flex;gap:4px;margin-top:10px">'+[0,1,2,3,4].map(function(_,idx){ return '<div style="width:6px;height:6px;border-radius:50%;background:var(--green);animation:wcDot 1.1s ease-in-out infinite '+(idx*0.14)+'s"></div>'; }).join('')+'</div>';
7397
+ // Update only the specific DOM nodes — no innerHTML rewrite, no flicker
7398
+ var pct = total > 0 ? Math.round((fi2 / total) * 100) : 0;
7399
+ var counterEl = document.getElementById('wcGenCounter');
7400
+ var barEl = document.getElementById('wcGenBar');
7401
+ var nameEl = document.getElementById('wcGenFileName');
7402
+ var timeEl = document.getElementById('wcGenTime');
7403
+ var pillLabel = document.getElementById('wcPillLabel');
7404
+ var pillCount = document.getElementById('wcPillCount');
7405
+ var pillTime = document.getElementById('wcPillTime');
7406
+ if (counterEl) counterEl.textContent = fi2 + ' / ' + total;
7407
+ if (barEl) barEl.style.width = pct + '%';
7408
+ if (nameEl) nameEl.textContent = name || '';
7409
+ if (timeEl) timeEl.textContent = wcGenElapsed();
7410
+ if (pillLabel) pillLabel.textContent = name ? name.split(',')[0].trim() : 'Generando...';
7411
+ if (pillCount) pillCount.textContent = fi2 + '/' + total;
7412
+ if (pillTime) pillTime.textContent = wcGenElapsed();
7419
7413
  }
7420
7414
 
7421
7415
  // Skills state
@@ -7479,8 +7473,8 @@ function renderWebCraft(el) {
7479
7473
  '</div>' +
7480
7474
  (_activeFile._error ? '<div style="padding:8px 14px;background:rgba(239,68,68,0.12);border-bottom:1px solid rgba(239,68,68,0.3);font-size:11px;color:#f87171;display:flex;align-items:center;gap:6px">&#9888; Generazione fallita — chiedi al modello di rigenerare questo file</div>' :
7481
7475
  _activeFile._syntaxError ? '<div style="padding:8px 14px;background:rgba(234,179,8,0.1);border-bottom:1px solid rgba(234,179,8,0.3);font-size:11px;color:#facc15;display:flex;align-items:center;gap:6px">&#9888; Syntax error: '+wcEsc(_activeFile._syntaxError)+'</div>' : '') +
7482
- (_activeFile._pending ? '<div style="display:flex;align-items:center;justify-content:center;height:120px;color:var(--dim);font-size:12px;gap:8px">&#8987; In generazione...</div>' :
7483
- '<pre style="margin:0;padding:14px 16px;font-size:11px;line-height:1.6;color:'+(_activeFile._error?'#f87171':_activeFile._syntaxError?'#fde68a':'var(--text)')+';font-family:var(--mono);white-space:pre-wrap;word-break:break-all">'+wcEsc(_activeFile.content)+'</pre>') +
7476
+ (_activeFile._pending ? '<div id="wcLivePending" style="display:flex;align-items:center;justify-content:center;height:120px;color:var(--dim);font-size:12px;gap:8px">&#8987; In generazione...</div>' :
7477
+ '<pre id="wcLiveCode" style="margin:0;padding:14px 16px;font-size:11px;line-height:1.6;color:'+(_activeFile._error?'#f87171':_activeFile._syntaxError?'#fde68a':'var(--text)')+';font-family:var(--mono);white-space:pre-wrap;word-break:break-all">'+wcEsc(_activeFile.content)+'</pre>') +
7484
7478
  '</div>' +
7485
7479
  fileSidebarHtml +
7486
7480
  '</div>'
@@ -7591,36 +7585,47 @@ function renderWebCraft(el) {
7591
7585
  '<div data-wc-files style="position:relative;flex:1;min-width:0;background:var(--bg2);border:1px solid var(--border);border-radius:10px;display:flex;flex-direction:column;height:100%;overflow:hidden">' +
7592
7586
  (wcState.repairing ?
7593
7587
  (_wcOverlayMinimized
7594
- ? '<div id="wcRepairOverlay" onclick="wcOverlayRestore()" style="position:absolute;bottom:12px;right:12px;z-index:50;background:rgba(0,0,0,0.85);border:1px solid rgba(234,179,8,0.6);border-radius:20px;padding:5px 12px;display:flex;align-items:center;gap:7px;cursor:pointer;animation:wcBubbleIn .2s ease;backdrop-filter:blur(4px)">'
7595
- +'<span style="font-size:16px;animation:wcRobotBob .9s ease-in-out infinite">&#128295;</span>'
7596
- +'<span style="font-size:10px;color:#facc15;font-weight:700;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+wcEsc(wcState.repairCurrent || 'Correzione...')+'</span>'
7597
- +'<span style="font-size:9px;color:var(--dim)">'+wcState.repairDone+'/'+wcState.repairTotal+'</span>'
7588
+ // Minimized repair: pill bottom-right
7589
+ ? '<div id="wcRepairOverlay" onclick="wcOverlayRestore()" style="position:absolute;bottom:12px;right:12px;z-index:50;background:rgba(0,0,0,0.85);border:1px solid rgba(234,179,8,0.6);border-radius:20px;padding:5px 12px;display:flex;align-items:center;gap:7px;cursor:pointer;backdrop-filter:blur(4px)">'
7590
+ +'<span style="font-size:14px;animation:wcRobotBob .9s ease-in-out infinite">&#128295;</span>'
7591
+ +'<span style="font-size:10px;color:#facc15;font-weight:700;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" id="wcRepairFile">'+wcEsc(wcState.repairCurrent || 'Correzione...')+'</span>'
7592
+ +'<span style="font-size:9px;color:var(--dim)" id="wcRepairCounter">'+wcState.repairDone+'/'+wcState.repairTotal+'</span>'
7598
7593
  +'<span style="display:flex;gap:3px">'+[0,1,2].map(function(_,idx){ return '<span style="width:4px;height:4px;border-radius:50%;background:#facc15;animation:wcDot 1.1s ease-in-out infinite '+(idx*0.18)+'s"></span>'; }).join('')+'</span>'
7599
7594
  +'</div>'
7600
- : '<div id="wcRepairOverlay" onclick="wcOverlayMinimize()" title="Clicca per navigare i file" style="position:absolute;inset:0;background:rgba(0,0,0,0.75);backdrop-filter:blur(4px);border-radius:10px;z-index:50;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;animation:wcBubbleIn .3s ease">'
7601
- +'<div style="font-size:38px;animation:wcRobotBob 1s ease-in-out infinite">&#128295;</div>'
7602
- +'<div style="font-size:13px;font-weight:700;color:#facc15;margin-top:12px">Correzione automatica in corso...</div>'
7603
- +'<div style="font-size:10px;color:var(--dim);margin-top:4px">Clicca per navigare i file</div>'
7604
- +'<div id="wcRepairCounter" style="font-size:11px;color:var(--dim);margin-top:6px">'+wcState.repairDone+' / '+wcState.repairTotal+' file</div>'
7605
- +'<div id="wcRepairFile" style="font-size:10px;color:#fde68a;font-family:var(--mono);margin-top:4px;max-width:280px;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+wcEsc(wcState.repairCurrent)+'</div>'
7606
- +'<div style="display:flex;gap:4px;margin-top:16px">'+[0,1,2,3,4].map(function(_,idx){ return '<div style="width:6px;height:6px;border-radius:50%;background:#facc15;animation:wcDot 1.1s ease-in-out infinite '+(idx*0.14)+'s"></div>'; }).join('')+'</div>'
7595
+ // Full repair: sticky header bar code stays visible below
7596
+ : '<div id="wcRepairOverlay" style="position:absolute;top:0;left:0;right:0;z-index:50;background:rgba(20,16,0,0.92);backdrop-filter:blur(6px);border-bottom:1px solid rgba(234,179,8,0.5);padding:8px 16px;display:flex;align-items:center;gap:10px">'
7597
+ +'<span style="font-size:16px;animation:wcRobotBob .9s ease-in-out infinite;flex-shrink:0">&#128295;</span>'
7598
+ +'<span style="font-size:11px;font-weight:700;color:#facc15;flex-shrink:0">Correzione automatica</span>'
7599
+ +'<span style="font-size:10px;color:#fde68a;font-family:var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1" id="wcRepairFile">'+wcEsc(wcState.repairCurrent || '')+'</span>'
7600
+ +'<span style="font-size:10px;color:var(--dim);flex-shrink:0" id="wcRepairCounter">'+wcState.repairDone+' / '+wcState.repairTotal+' file</span>'
7601
+ +'<span onclick="wcOverlayMinimize()" style="font-size:10px;color:var(--dim);cursor:pointer;flex-shrink:0;padding:2px 6px;border:1px solid var(--border2);border-radius:4px" title="Minimizza">&#8211;</span>'
7602
+ +'<span style="display:flex;gap:3px">'+[0,1,2].map(function(_,idx){ return '<span style="width:4px;height:4px;border-radius:50%;background:#facc15;animation:wcDot 1.1s ease-in-out infinite '+(idx*0.18)+'s"></span>'; }).join('')+'</span>'
7607
7603
  +'</div>'
7608
7604
  )
7609
7605
  : wcState.running ? (
7610
7606
  _wcOverlayMinimized
7611
- // Minimized: small pill in bottom-right corner
7612
- ? '<div id="wcGenOverlay" onclick="wcOverlayRestore()" style="position:absolute;bottom:12px;right:12px;z-index:50;background:rgba(0,0,0,0.85);border:1px solid var(--green3);border-radius:20px;padding:5px 12px;display:flex;align-items:center;gap:7px;cursor:pointer;animation:wcBubbleIn .2s ease;backdrop-filter:blur(4px)">'
7613
- +'<span style="font-size:16px;animation:wcRobotBob .9s ease-in-out infinite">&#129302;</span>'
7614
- +'<span id="wcPillLabel" style="font-size:10px;color:var(--green);font-weight:700;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+wcEsc(_wcGenOverlayState.name || 'Generando...')+'</span>'
7615
- +'<span style="font-size:9px;color:var(--dim)">'+wcGenElapsed()+'</span>'
7607
+ // Minimized gen: pill bottom-right
7608
+ ? '<div id="wcGenOverlay" onclick="wcOverlayRestore()" style="position:absolute;bottom:12px;right:12px;z-index:50;background:rgba(0,0,0,0.85);border:1px solid var(--green3);border-radius:20px;padding:5px 12px;display:flex;align-items:center;gap:7px;cursor:pointer;backdrop-filter:blur(4px)">'
7609
+ +'<span style="font-size:14px;animation:wcRobotBob .9s ease-in-out infinite">&#129302;</span>'
7610
+ +'<span id="wcPillLabel" style="font-size:10px;color:var(--green);font-weight:700;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+wcEsc((_wcGenOverlayState.name||'').split(',')[0].trim() || 'Generando...')+'</span>'
7611
+ +'<span id="wcPillCount" style="font-size:9px;color:var(--dim)">'+_wcGenOverlayState.fi+'/'+_wcGenOverlayState.total+'</span>'
7612
+ +'<span id="wcPillTime" style="font-size:9px;color:var(--dim)">'+wcGenElapsed()+'</span>'
7616
7613
  +'<span style="display:flex;gap:3px">'+[0,1,2].map(function(_,idx){ return '<span style="width:4px;height:4px;border-radius:50%;background:var(--green);animation:wcDot 1.1s ease-in-out infinite '+(idx*0.18)+'s"></span>'; }).join('')+'</span>'
7617
7614
  +'</div>'
7618
- // Full overlay
7619
- : '<div id="wcGenOverlay" onclick="wcOverlayMinimize()" title="Clicca per nascondere e navigare i file" style="position:absolute;inset:0;background:rgba(0,0,0,0.75);backdrop-filter:blur(4px);border-radius:10px;z-index:50;display:flex;flex-direction:column;align-items:center;justify-content:center;animation:wcBubbleIn .3s ease;cursor:pointer">'
7620
- +'<div style="font-size:38px;animation:wcRobotBob 1s ease-in-out infinite">&#129302;</div>'
7621
- +'<div style="font-size:13px;font-weight:700;color:var(--green);margin-top:12px">Generazione in corso...</div>'
7622
- +'<div style="font-size:10px;color:var(--dim);margin-top:4px">Clicca per navigare i file</div>'
7623
- +'<div style="display:flex;gap:4px;margin-top:16px">'+[0,1,2,3,4].map(function(_,idx){ return '<div style="width:6px;height:6px;border-radius:50%;background:var(--green);animation:wcDot 1.1s ease-in-out infinite '+(idx*0.14)+'s"></div>'; }).join('')+'</div>'
7615
+ // Full gen: sticky header bar — code streams visibly below
7616
+ : '<div id="wcGenOverlay" style="position:absolute;top:0;left:0;right:0;z-index:50;background:rgba(0,14,0,0.90);backdrop-filter:blur(6px);border-bottom:1px solid var(--green3);padding:8px 16px;display:flex;flex-direction:column;gap:4px">'
7617
+ +'<div style="display:flex;align-items:center;gap:10px">'
7618
+ +'<span style="font-size:16px;animation:wcRobotBob .9s ease-in-out infinite;flex-shrink:0">&#129302;</span>'
7619
+ +'<span style="font-size:11px;font-weight:700;color:var(--green);flex-shrink:0">Generazione in corso</span>'
7620
+ +'<span id="wcGenFileName" style="font-size:10px;color:var(--dim);font-family:var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">'+wcEsc((_wcGenOverlayState.name||'').split(',')[0].trim())+'</span>'
7621
+ +'<span id="wcGenCounter" style="font-size:10px;color:var(--dim);flex-shrink:0">'+_wcGenOverlayState.fi+' / '+_wcGenOverlayState.total+'</span>'
7622
+ +'<span id="wcGenTime" style="font-size:10px;color:var(--dim);flex-shrink:0">'+wcGenElapsed()+'</span>'
7623
+ +'<span onclick="wcOverlayMinimize()" style="font-size:10px;color:var(--dim);cursor:pointer;flex-shrink:0;padding:2px 6px;border:1px solid var(--border2);border-radius:4px" title="Minimizza">&#8211;</span>'
7624
+ +'<span style="display:flex;gap:3px">'+[0,1,2].map(function(_,idx){ return '<span style="width:4px;height:4px;border-radius:50%;background:var(--green);animation:wcDot 1.1s ease-in-out infinite '+(idx*0.18)+'s"></span>'; }).join('')+'</span>'
7625
+ +'</div>'
7626
+ +'<div style="height:2px;background:rgba(255,255,255,0.07);border-radius:1px;overflow:hidden">'
7627
+ +'<div id="wcGenBar" style="height:100%;width:'+Math.round((_wcGenOverlayState.fi/_wcGenOverlayState.total||0)*100)+'%;background:var(--green);border-radius:1px;transition:width .3s ease"></div>'
7628
+ +'</div>'
7624
7629
  +'</div>'
7625
7630
  ) : '') +
7626
7631
  '<div style="display:flex;border-bottom:1px solid var(--border);flex-shrink:0">' +
@@ -8116,12 +8121,17 @@ function wcStopAll() {
8116
8121
  }
8117
8122
 
8118
8123
  function wcOverlayMinimize() {
8124
+ if (_wcOverlayTimer) clearTimeout(_wcOverlayTimer);
8119
8125
  _wcOverlayMinimized = true;
8120
8126
  renderWebCraft(document.getElementById('content'));
8121
- if (_wcOverlayTimer) clearTimeout(_wcOverlayTimer);
8127
+ // Auto-restore after 30s of inactivity so user doesn't miss progress
8122
8128
  _wcOverlayTimer = setTimeout(function() {
8123
- if (wcState.running || wcState.repairing) { _wcOverlayMinimized = false; renderWebCraft(document.getElementById('content')); }
8124
- }, 10000);
8129
+ if (wcState.running || wcState.repairing) {
8130
+ _wcOverlayMinimized = false;
8131
+ renderWebCraft(document.getElementById('content'));
8132
+ }
8133
+ _wcOverlayTimer = null;
8134
+ }, 30000);
8125
8135
  }
8126
8136
 
8127
8137
  function wcOverlayRestore() {
@@ -8864,19 +8874,20 @@ async function wcGenerate() {
8864
8874
  }
8865
8875
 
8866
8876
  // Helper: generate one file (with two-pass split for large CSS files)
8867
- async function wcGenOneFile(fp, signal) {
8877
+ // onLiveUpdate(partialContent) is called on each token for live display
8878
+ async function wcGenOneFile(fp, signal, onLiveUpdate) {
8868
8879
  var _nl2 = String.fromCharCode(10);
8869
8880
  var splitPrompts = WC_CSS_SPLIT[fp.name];
8870
8881
  if (splitPrompts) {
8871
- // Two-pass generation: call LLM twice and concatenate
8872
- var part1 = await wcCallLLM(sysPreamble, splitPrompts[0] + _nl2 + _nl2 + 'File: ' + fp.name, signal, fp.lang, 8192);
8882
+ // Two-pass generation: streaming on first pass only
8883
+ var part1 = await wcCallLLM(sysPreamble, splitPrompts[0] + _nl2 + _nl2 + 'File: ' + fp.name, signal, fp.lang, 8192, onLiveUpdate);
8873
8884
  part1 = wcStripFences(part1);
8874
8885
  if (signal && signal.aborted) return part1;
8875
- var part2 = await wcCallLLM(sysPreamble, splitPrompts[1] + _nl2 + _nl2 + 'File: ' + fp.name, signal, fp.lang, 8192);
8886
+ var part2 = await wcCallLLM(sysPreamble, splitPrompts[1] + _nl2 + _nl2 + 'File: ' + fp.name, signal, fp.lang, 8192, function(p2) { if (onLiveUpdate) onLiveUpdate(part1 + _nl2 + _nl2 + p2); });
8876
8887
  part2 = wcStripFences(part2);
8877
8888
  return part1 + _nl2 + _nl2 + part2;
8878
8889
  }
8879
- var content = await wcCallLLM(sysPreamble, fp.prompt + _nl2 + _nl2 + 'File to generate: ' + fp.name, signal, fp.lang);
8890
+ var content = await wcCallLLM(sysPreamble, fp.prompt + _nl2 + _nl2 + 'File to generate: ' + fp.name, signal, fp.lang, undefined, onLiveUpdate);
8880
8891
  return wcStripFences(content);
8881
8892
  }
8882
8893
 
@@ -8889,17 +8900,67 @@ async function wcGenerate() {
8889
8900
  wcState.activeFile = 0;
8890
8901
  renderWebCraft(document.getElementById('content'));
8891
8902
 
8903
+ // Live update: write token directly into DOM — zero re-render, zero flicker
8904
+ var _wcLiveDomTimers = {};
8905
+ function wcLiveUpdateFile(fpName, fpLang, partialContent) {
8906
+ // Always update state
8907
+ var fileIdx = -1;
8908
+ for (var li = 0; li < wcState.generatedFiles.length; li++) {
8909
+ if (wcState.generatedFiles[li].name === fpName) {
8910
+ wcState.generatedFiles[li].content = partialContent;
8911
+ wcState.generatedFiles[li]._pending = false;
8912
+ fileIdx = li;
8913
+ break;
8914
+ }
8915
+ }
8916
+ // If this file is the active one, update the <pre> directly (throttled 80ms)
8917
+ if (fileIdx !== wcState.activeFile) return;
8918
+ if (_wcLiveDomTimers[fpName]) return;
8919
+ _wcLiveDomTimers[fpName] = setTimeout(function() {
8920
+ delete _wcLiveDomTimers[fpName];
8921
+ // Replace pending placeholder with live <pre> if needed
8922
+ var pending = document.getElementById('wcLivePending');
8923
+ var pre = document.getElementById('wcLiveCode');
8924
+ var wrap = document.getElementById('wcCodeWrap');
8925
+ if (pending && wrap) {
8926
+ // First token: swap placeholder for <pre>
8927
+ var newPre = document.createElement('pre');
8928
+ newPre.id = 'wcLiveCode';
8929
+ newPre.style.cssText = 'margin:0;padding:14px 16px;font-size:11px;line-height:1.6;color:var(--text);font-family:var(--mono);white-space:pre-wrap;word-break:break-all';
8930
+ newPre.textContent = partialContent;
8931
+ pending.parentNode.replaceChild(newPre, pending);
8932
+ return;
8933
+ }
8934
+ if (pre) {
8935
+ pre.textContent = partialContent;
8936
+ // Auto-scroll to bottom so user sees latest tokens
8937
+ var codeWrap = document.getElementById('wcCodeWrap');
8938
+ if (codeWrap) codeWrap.scrollTop = codeWrap.scrollHeight;
8939
+ }
8940
+ }, 80);
8941
+ }
8942
+
8892
8943
  // Generate in parallel batches of 4 — each call is independent/fresh to Liara
8944
+ // Counter increments 1 per file as it completes, not per batch
8893
8945
  var BATCH = 4;
8894
8946
  var doneCount = 0;
8947
+ wcUpdateGenOverlay(0, filePlan.length, '');
8895
8948
  for (var bi = 0; bi < filePlan.length; bi += BATCH) {
8896
8949
  if (_wcGenAbortCtrl && _wcGenAbortCtrl.signal.aborted) break;
8897
8950
  var batch = filePlan.slice(bi, bi + BATCH);
8898
- wcUpdateGenOverlay(doneCount, filePlan.length, batch.map(function(f){ return f.name; }).join(', '));
8899
- var results = await Promise.allSettled(batch.map(function(fp) {
8900
- return wcGenOneFile(fp, _wcGenAbortCtrl ? _wcGenAbortCtrl.signal : null).then(function(c){ return { fp: fp, content: c }; });
8951
+ var results = await Promise.allSettled(batch.map(function(fp, bii) {
8952
+ // Point activeFile at the first file of this batch so tokens appear immediately
8953
+ if (bii === 0) {
8954
+ var firstIdx = wcState.generatedFiles.findIndex(function(f){ return f.name === fp.name; });
8955
+ if (firstIdx >= 0) wcState.activeFile = firstIdx;
8956
+ }
8957
+ // Show file name in overlay as soon as it starts
8958
+ wcUpdateGenOverlay(doneCount, filePlan.length, fp.name);
8959
+ var liveCallback = function(partial) { wcLiveUpdateFile(fp.name, fp.lang, partial); };
8960
+ return wcGenOneFile(fp, _wcGenAbortCtrl ? _wcGenAbortCtrl.signal : null, liveCallback).then(function(c){ return { fp: fp, content: c }; });
8901
8961
  }));
8902
- results.forEach(function(r) {
8962
+ results.forEach(function(r, ri) {
8963
+ var batchIdx = ri;
8903
8964
  if (r.status === 'fulfilled') {
8904
8965
  var fp = r.value.fp;
8905
8966
  for (var gi = 0; gi < wcState.generatedFiles.length; gi++) {
@@ -8909,20 +8970,20 @@ async function wcGenerate() {
8909
8970
  }
8910
8971
  }
8911
8972
  } else if (r.reason && r.reason.name !== 'AbortError') {
8912
- var fpName = batch[results.indexOf(r)] ? batch[results.indexOf(r)].name : '?';
8913
- // find by matching position
8914
- var batchIdx = results.indexOf(r);
8915
- if (batch[batchIdx]) fpName = batch[batchIdx].name;
8916
- for (var gi2 = 0; gi2 < wcState.generatedFiles.length; gi2++) {
8917
- if (wcState.generatedFiles[gi2].name === fpName) {
8918
- wcState.generatedFiles[gi2] = { name: fpName, content: '// Error generating this file: ' + (r.reason.message || 'unknown error'), lang: (batch[batchIdx] || {}).lang || '', _error: true };
8919
- break;
8973
+ var fpErr = batch[batchIdx];
8974
+ if (fpErr) {
8975
+ for (var gi2 = 0; gi2 < wcState.generatedFiles.length; gi2++) {
8976
+ if (wcState.generatedFiles[gi2].name === fpErr.name) {
8977
+ wcState.generatedFiles[gi2] = { name: fpErr.name, content: '// Error generating this file: ' + (r.reason.message || 'unknown error'), lang: fpErr.lang || '', _error: true };
8978
+ break;
8979
+ }
8920
8980
  }
8921
8981
  }
8922
8982
  }
8923
8983
  doneCount++;
8984
+ // Update counter immediately on each file completion — no full re-render
8985
+ wcUpdateGenOverlay(doneCount, filePlan.length, doneCount < filePlan.length ? (batch[Math.min(batchIdx+1, batch.length-1)] || batch[batchIdx] || {}).name || '' : '');
8924
8986
  });
8925
- renderWebCraft(document.getElementById('content'));
8926
8987
  }
8927
8988
 
8928
8989
  if (_wcTimerInterval) { clearInterval(_wcTimerInterval); _wcTimerInterval = null; }
@@ -9120,7 +9181,57 @@ function wcIsTruncated(content, lang) {
9120
9181
  return false;
9121
9182
  }
9122
9183
 
9123
- async function wcCallLLMRaw(sys, user, signal, maxTok) {
9184
+ async function wcCallLLMRaw(sys, user, signal, maxTok, onToken) {
9185
+ // Streaming path: use SSE endpoint so tokens appear live in the file editor
9186
+ if (onToken) {
9187
+ var streamOpts = {
9188
+ method: 'POST',
9189
+ headers: {'Content-Type':'application/json'},
9190
+ body: JSON.stringify({system: sys, user: user, max_tokens: maxTok || 16384})
9191
+ };
9192
+ if (signal) streamOpts.signal = signal;
9193
+ for (var sa = 0; sa < 3; sa++) {
9194
+ if (signal && signal.aborted) throw new DOMException('Aborted', 'AbortError');
9195
+ var sr = await fetch(API + '/api/studio/webcraft/stream', streamOpts);
9196
+ if (!sr.ok) {
9197
+ if (sr.status < 500 || sa === 2) throw new Error('LLM stream error ' + sr.status);
9198
+ await new Promise(function(resolve) { setTimeout(resolve, 2000); });
9199
+ continue;
9200
+ }
9201
+ var sreader = sr.body.getReader();
9202
+ var sdec = new TextDecoder();
9203
+ var sbuf = '';
9204
+ var fullText = '';
9205
+ while (true) {
9206
+ var sres = await sreader.read();
9207
+ if (sres.done) break;
9208
+ sbuf += sdec.decode(sres.value, {stream: true});
9209
+ var sparts = sbuf.split(String.fromCharCode(10) + String.fromCharCode(10));
9210
+ sbuf = sparts.pop();
9211
+ for (var si = 0; si < sparts.length; si++) {
9212
+ var sline = sparts[si].replace(/^data: /, '').trim();
9213
+ if (!sline) continue;
9214
+ try {
9215
+ var sev = JSON.parse(sline);
9216
+ if (sev.type === 'token') {
9217
+ fullText += sev.token;
9218
+ onToken(fullText);
9219
+ } else if (sev.type === 'done') {
9220
+ if (sev.usage) {
9221
+ _wcTokIn += (sev.usage.prompt_tokens || 0);
9222
+ _wcTokOut += (sev.usage.completion_tokens || 0);
9223
+ }
9224
+ } else if (sev.type === 'error') {
9225
+ throw new Error(sev.message || 'Stream error');
9226
+ }
9227
+ } catch(_) {}
9228
+ }
9229
+ }
9230
+ return fullText;
9231
+ }
9232
+ }
9233
+
9234
+ // Non-streaming fallback (used by repair and continuation passes)
9124
9235
  var fetchOpts = {
9125
9236
  method: 'POST',
9126
9237
  headers: {'Content-Type':'application/json'},
@@ -9132,12 +9243,10 @@ async function wcCallLLMRaw(sys, user, signal, maxTok) {
9132
9243
  var r = await fetch(API + '/api/studio/webcraft', fetchOpts);
9133
9244
  if (r.ok) {
9134
9245
  var d = await r.json();
9135
- // Accumulate token counts if the server returns usage data
9136
9246
  if (d && d.usage) {
9137
9247
  _wcTokIn += (d.usage.prompt_tokens || d.usage.input_tokens || 0);
9138
9248
  _wcTokOut += (d.usage.completion_tokens || d.usage.output_tokens || 0);
9139
9249
  } else if (d && d.text) {
9140
- // Estimate from char count (4 chars ≈ 1 token)
9141
9250
  _wcTokIn += Math.round((sys.length + user.length) / 4);
9142
9251
  _wcTokOut += Math.round((d.text || '').length / 4);
9143
9252
  }
@@ -9153,9 +9262,9 @@ async function wcCallLLMRaw(sys, user, signal, maxTok) {
9153
9262
  }
9154
9263
  }
9155
9264
 
9156
- async function wcCallLLM(sys, user, signal, lang, maxTok) {
9157
- var content = await wcCallLLMRaw(sys, user, signal, maxTok);
9158
- // Continuation loop: if response is truncated, ask model to continue
9265
+ async function wcCallLLM(sys, user, signal, lang, maxTok, onToken) {
9266
+ var content = await wcCallLLMRaw(sys, user, signal, maxTok, onToken);
9267
+ // Continuation loop: if response is truncated, ask model to continue (no streaming for continuations)
9159
9268
  var maxContinuations = 2;
9160
9269
  for (var ci = 0; ci < maxContinuations; ci++) {
9161
9270
  if (!wcIsTruncated(content, lang || 'text')) break;
@@ -9166,6 +9275,7 @@ async function wcCallLLM(sys, user, signal, lang, maxTok) {
9166
9275
  var continuation = await wcCallLLMRaw(sys, continuePrompt, signal, maxTok);
9167
9276
  if (!continuation || continuation.trim().length < 5) break;
9168
9277
  content = content + String.fromCharCode(10) + continuation;
9278
+ if (onToken) onToken(content);
9169
9279
  }
9170
9280
  return content;
9171
9281
  }