spectoflow 0.22.2 → 0.22.4
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 +1 -1
- package/templates/dashboard/public/app.js +149 -14
- package/templates/dashboard/public/i18n.js +12 -12
- package/templates/dashboard/public/index.html +4 -0
- package/templates/dashboard/public/styles.css +34 -3
- package/templates/dashboard/runner.js +4 -1
- package/templates/dashboard/summarize.js +9 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.4",
|
|
4
4
|
"description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spec-driven-development",
|
|
@@ -35,7 +35,7 @@ function connect(){
|
|
|
35
35
|
es.onmessage = (ev)=>{
|
|
36
36
|
let m; try{ m=JSON.parse(ev.data); }catch{ return; }
|
|
37
37
|
if(m.type==='change'||m.type==='message') return scheduleLoad(); // messages live from runtime.messages
|
|
38
|
-
if(m.type==='run-start'||m.type==='run-end') { chatState.forEach(st=>{ st.rawBlock=null; }); return; }
|
|
38
|
+
if(m.type==='run-start'||m.type==='run-end') { chatState.forEach(st=>{ st.rawBlock=null; }); sseBusy=(m.type==='run-start'); updateChatBusyUI(); return; }
|
|
39
39
|
if(m.type==='run-line') return appendRaw(m.chunk); // raw output is ephemeral (not logged)
|
|
40
40
|
};
|
|
41
41
|
es.onerror = ()=>{ $('#sync').classList.add('offline'); $('#syncLabel').textContent='offline'; };
|
|
@@ -115,7 +115,11 @@ function setChat(open){
|
|
|
115
115
|
// doRun/doOrchestrate default to the floating widget's textarea/select; the Chat tab has its own
|
|
116
116
|
// #tabRunPrompt/#tabRunAgent (an id can't be shared by two elements) and passes them explicitly —
|
|
117
117
|
// one code path, same endpoints, for both surfaces.
|
|
118
|
+
// isChatBusy() also guards the Ctrl/Cmd+Enter keyboard shortcuts, which call doRun() directly and
|
|
119
|
+
// would otherwise bypass the buttons' own disabled state.
|
|
120
|
+
function isChatBusy(){ return sseBusy || (P&&P.runtime&&P.runtime.orchestration&&P.runtime.orchestration.status==='running'); }
|
|
118
121
|
async function doRun(promptEl,agentEl){
|
|
122
|
+
if(isChatBusy()) return;
|
|
119
123
|
promptEl=promptEl||$('#runPrompt'); agentEl=agentEl||$('#runAgent');
|
|
120
124
|
const prompt=promptEl.value.trim(); if(!prompt) return;
|
|
121
125
|
const agent=agentEl.value;
|
|
@@ -123,6 +127,7 @@ async function doRun(promptEl,agentEl){
|
|
|
123
127
|
promptEl.value=''; // the prompt renders as a bubble from the message log
|
|
124
128
|
}
|
|
125
129
|
async function doOrchestrate(promptEl){
|
|
130
|
+
if(isChatBusy()) return;
|
|
126
131
|
promptEl=promptEl||$('#runPrompt');
|
|
127
132
|
const prompt=promptEl.value.trim(); if(!prompt) return;
|
|
128
133
|
await fetch('/api/orchestrate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request:prompt})});
|
|
@@ -132,6 +137,7 @@ async function approve(decision){ await fetch('/api/orchestrate/approve',{method
|
|
|
132
137
|
// ---- chat context management: condense the log via the agent, or wipe it (Chat tab only — the
|
|
133
138
|
// floating widget stays "quick access", full controls live where there's room to read them) ----
|
|
134
139
|
async function summarizeChat(agentEl){
|
|
140
|
+
if(isChatBusy()) return;
|
|
135
141
|
const agent=(agentEl||$('#tabRunAgent'))?.value;
|
|
136
142
|
flash();
|
|
137
143
|
await fetch('/api/chat/summarize',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent})});
|
|
@@ -145,6 +151,23 @@ async function addComment(id,text,action){ flash(); await fetch('/api/task/'+enc
|
|
|
145
151
|
async function toggleStep(name){ flash(); await fetch('/api/workflow/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})}); }
|
|
146
152
|
function flash(){ const s=$('#sync'); s.classList.add('saving'); $('#syncLabel').textContent='writing…'; setTimeout(()=>{ s.classList.remove('saving'); $('#syncLabel').textContent='live'; },800); }
|
|
147
153
|
|
|
154
|
+
// ---- "agent is running" state — no visible feedback used to exist between clicking Send/
|
|
155
|
+
// Orchestrate/Summarize and the result showing up (the only outward sign, on Windows, was a real
|
|
156
|
+
// console window popping up behind the agent's own spawn — now suppressed, so this replaces it):
|
|
157
|
+
// a small spinner + disabled buttons for as long as an agent run is actually in flight. Driven by
|
|
158
|
+
// two signals — SSE run-start/run-end (Send, Summarize, each individual Orchestrate step) and
|
|
159
|
+
// runtime.orchestration.status (stays 'running' across the gaps between orchestrate steps, which
|
|
160
|
+
// SSE run-start/run-end alone would flicker through). ----
|
|
161
|
+
let sseBusy=false;
|
|
162
|
+
function updateChatBusyUI(){
|
|
163
|
+
const orchStatus=P&&P.runtime&&P.runtime.orchestration&&P.runtime.orchestration.status;
|
|
164
|
+
const busy=sseBusy||orchStatus==='running';
|
|
165
|
+
document.body.classList.toggle('chat-busy',!!busy);
|
|
166
|
+
[$('#runBtn'),$('#orchBtn'),$('#widgetSummarizeBtn'),$('#tabRunBtn'),$('#tabOrchBtn'),$('#tabSummarizeBtn')].forEach(b=>{ if(b) b.disabled=!!busy; });
|
|
167
|
+
const tabStatus=$('#tabChatStatus'); if(tabStatus) tabStatus.hidden=!busy;
|
|
168
|
+
const widgetStatus=$('#widgetChatStatus'); if(widgetStatus) widgetStatus.hidden=!busy;
|
|
169
|
+
}
|
|
170
|
+
|
|
148
171
|
function render(){
|
|
149
172
|
const c = P.config||{};
|
|
150
173
|
i18nSetLang(c.language||'en'); updateStatusLabels(); // language drives the whole UI, not just agent output
|
|
@@ -165,7 +188,7 @@ function render(){
|
|
|
165
188
|
if(meter) meter.title=`${t('kpi.globalProgress')}: ${s.pct}% (${s.done}/${s.total} ${t('kpi.tasksLabel')})`;
|
|
166
189
|
renderOverview(); renderBoard(); renderBacklog(); renderWorkflow(); renderTeam();
|
|
167
190
|
renderChatLog($('#chatLog')); renderChatLog($('#chatTabLog'));
|
|
168
|
-
renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderDocs(); renderSettings(); renderFiles(); applySideHidden();
|
|
191
|
+
renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderDocs(); renderSettings(); renderFiles(); applySideHidden(); updateChatBusyUI();
|
|
169
192
|
renderCustomDashboards(); // adds/removes nav tabs + panels before applyActiveTab() below reads them
|
|
170
193
|
applyActiveTab(); // re-apply the current tab so an SSE-driven re-render never resets to Board
|
|
171
194
|
applyI18nStatic(); // re-translate the static markup (nav, headers, placeholders…) for this tick's language
|
|
@@ -1298,6 +1321,7 @@ function renderDocs(){
|
|
|
1298
1321
|
// event refreshes the TREE listing but never overwrites an open file's editor buffer, so an
|
|
1299
1322
|
// unrelated agent write elsewhere can't clobber unsaved work here. ----
|
|
1300
1323
|
let filesTreeData=null, filesOpenPath=null, filesOpenDirty=false;
|
|
1324
|
+
let filesSelectedDir=''; // '' = project root — the folder + File/+ Folder create inside
|
|
1301
1325
|
const filesOpenDirs=new Set(); // persists which tree folders are expanded across a refresh
|
|
1302
1326
|
async function loadFilesTree(){
|
|
1303
1327
|
try{
|
|
@@ -1315,7 +1339,8 @@ function renderFiles(){
|
|
|
1315
1339
|
loadFilesTree();
|
|
1316
1340
|
}
|
|
1317
1341
|
function fNode(entry){
|
|
1318
|
-
const
|
|
1342
|
+
const isTarget=entry.type==='dir'&&entry.path===filesSelectedDir;
|
|
1343
|
+
const row=el('div','f-row'+(entry.type==='dir'&&filesOpenDirs.has(entry.path)?' is-open':'')+(entry.path===filesOpenPath?' is-active':'')+(isTarget?' is-target':''));
|
|
1319
1344
|
row.tabIndex=0;
|
|
1320
1345
|
if(entry.type==='dir'){
|
|
1321
1346
|
const chev=document.createElementNS('http://www.w3.org/2000/svg','svg');
|
|
@@ -1330,10 +1355,17 @@ function fNode(entry){
|
|
|
1330
1355
|
const kids=el('div','f-children'); kids.hidden=!filesOpenDirs.has(entry.path);
|
|
1331
1356
|
(entry.children||[]).forEach(c=> kids.append(fNode(c)));
|
|
1332
1357
|
wrap.append(kids);
|
|
1358
|
+
// a click both toggles expand/collapse AND marks this folder as where +File/+Folder create —
|
|
1359
|
+
// the two are independent ideas but sharing the click keeps the tree from needing a second,
|
|
1360
|
+
// easy-to-miss gesture just to pick a target folder.
|
|
1333
1361
|
row.addEventListener('click',()=>{
|
|
1334
1362
|
const open=filesOpenDirs.has(entry.path);
|
|
1335
1363
|
if(open) filesOpenDirs.delete(entry.path); else filesOpenDirs.add(entry.path);
|
|
1336
1364
|
row.classList.toggle('is-open',!open); kids.hidden=open;
|
|
1365
|
+
filesSelectedDir=entry.path;
|
|
1366
|
+
const root=$('#filesRootRow'); if(root) root.classList.remove('is-target');
|
|
1367
|
+
$$('#filesTree .f-row.is-target').forEach(r=> r!==row && r.classList.remove('is-target'));
|
|
1368
|
+
row.classList.add('is-target');
|
|
1337
1369
|
});
|
|
1338
1370
|
} else {
|
|
1339
1371
|
row.addEventListener('click',()=> openFilesFile(entry.path));
|
|
@@ -1343,10 +1375,104 @@ function fNode(entry){
|
|
|
1343
1375
|
function renderFilesTree(){
|
|
1344
1376
|
const box=$('#filesTree'); if(!box) return;
|
|
1345
1377
|
box.innerHTML='';
|
|
1378
|
+
const root=$('#filesRootRow'); if(root) root.classList.toggle('is-target',filesSelectedDir==='');
|
|
1346
1379
|
if(!filesTreeData || !filesTreeData.length){ box.append(el('div','empty',t('files.empty'))); return; }
|
|
1347
1380
|
filesTreeData.forEach(e=> box.append(fNode(e)));
|
|
1348
1381
|
}
|
|
1349
1382
|
function filesExt(p){ const m=/\.([a-z0-9]+)$/i.exec(p||''); return m?m[1].toLowerCase():''; }
|
|
1383
|
+
|
|
1384
|
+
// ---- lightweight syntax highlighting — zero-dependency (no CodeMirror/Monaco, per the explicit
|
|
1385
|
+
// call made when this tab was designed): a single char-scanner tokenizer good enough to make code
|
|
1386
|
+
// readable at a glance in a file browser, not a language-correct parser. Anything not recognized
|
|
1387
|
+
// (or with no lang mapping) just renders as plain, unstyled text — never a rendering error. ----
|
|
1388
|
+
const FILES_HL_LANG = {
|
|
1389
|
+
js: { comments:[['//','\n'],['/*','*/']], strings:['"',"'",'`'],
|
|
1390
|
+
keywords:'const let var function return if else for while do switch case break continue new class extends super this typeof instanceof in of try catch finally throw async await yield import export default from as null undefined true false void delete'.split(' ') },
|
|
1391
|
+
json: { comments:[], strings:['"'], keywords:'true false null'.split(' ') },
|
|
1392
|
+
css: { comments:[['/*','*/']], strings:['"',"'"], keywords:[] },
|
|
1393
|
+
html: { comments:[['<!--','-->']], strings:['"',"'"], keywords:[], tags:true },
|
|
1394
|
+
py: { comments:[['#','\n']], strings:['"',"'"],
|
|
1395
|
+
keywords:'def class return if elif else for while break continue pass import from as try except finally raise with lambda yield async await None True False and or not in is del global nonlocal'.split(' ') },
|
|
1396
|
+
sh: { comments:[['#','\n']], strings:['"',"'"],
|
|
1397
|
+
keywords:'if then else elif fi for while do done case esac function return exit export local readonly'.split(' ') },
|
|
1398
|
+
yml: { comments:[['#','\n']], strings:['"',"'"], keywords:'true false null'.split(' ') },
|
|
1399
|
+
};
|
|
1400
|
+
function filesHlLang(ext){
|
|
1401
|
+
if(['js','mjs','cjs','ts','jsx','tsx'].includes(ext)) return 'js';
|
|
1402
|
+
if(ext==='json') return 'json';
|
|
1403
|
+
if(ext==='css') return 'css';
|
|
1404
|
+
if(['html','htm'].includes(ext)) return 'html';
|
|
1405
|
+
if(ext==='py') return 'py';
|
|
1406
|
+
if(['sh','bash'].includes(ext)) return 'sh';
|
|
1407
|
+
if(['yml','yaml'].includes(ext)) return 'yml';
|
|
1408
|
+
return null;
|
|
1409
|
+
}
|
|
1410
|
+
// Scans `src` one character at a time, classifying spans as it goes; returns an HTML string with
|
|
1411
|
+
// each span wrapped in a colored <span> (escaped — this is the only place raw file content becomes
|
|
1412
|
+
// markup, so nothing here may skip escHtml).
|
|
1413
|
+
function filesHighlight(src,langKey){
|
|
1414
|
+
const lang=FILES_HL_LANG[langKey];
|
|
1415
|
+
if(!lang) return escHtml(src);
|
|
1416
|
+
const n=src.length;
|
|
1417
|
+
let i=0,html='',plain='';
|
|
1418
|
+
const flushPlain=()=>{ if(plain){ html+=escHtml(plain); plain=''; } };
|
|
1419
|
+
const isWordChar=c=>/[A-Za-z0-9_$]/.test(c);
|
|
1420
|
+
while(i<n){
|
|
1421
|
+
let matched=false;
|
|
1422
|
+
// comments
|
|
1423
|
+
for(const [open,close] of lang.comments){
|
|
1424
|
+
if(src.startsWith(open,i)){
|
|
1425
|
+
const end = close==='\n' ? (src.indexOf('\n',i)===-1?n:src.indexOf('\n',i)) : (src.indexOf(close,i+open.length)===-1?n:src.indexOf(close,i+open.length)+close.length);
|
|
1426
|
+
flushPlain(); html+='<span class="hl-comment">'+escHtml(src.slice(i,end))+'</span>'; i=end; matched=true; break;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if(matched) continue;
|
|
1430
|
+
// strings
|
|
1431
|
+
if(lang.strings.includes(src[i])){
|
|
1432
|
+
const q=src[i]; let j=i+1;
|
|
1433
|
+
while(j<n && src[j]!==q){ if(src[j]==='\\') j++; j++; }
|
|
1434
|
+
j=Math.min(j+1,n);
|
|
1435
|
+
flushPlain(); html+='<span class="hl-string">'+escHtml(src.slice(i,j))+'</span>'; i=j; continue;
|
|
1436
|
+
}
|
|
1437
|
+
// html tags (bonus: <tag ...> / </tag>) — a light touch, not full attribute-vs-value parsing
|
|
1438
|
+
if(lang.tags && src[i]==='<' && /[a-zA-Z/!]/.test(src[i+1]||'')){
|
|
1439
|
+
const end=src.indexOf('>',i); const j=end===-1?n:end+1;
|
|
1440
|
+
flushPlain(); html+='<span class="hl-tag">'+escHtml(src.slice(i,j))+'</span>'; i=j; continue;
|
|
1441
|
+
}
|
|
1442
|
+
// numbers
|
|
1443
|
+
if(/[0-9]/.test(src[i]) && !isWordChar(src[i-1]||'')){
|
|
1444
|
+
let j=i; while(j<n && /[0-9.]/.test(src[j])) j++;
|
|
1445
|
+
flushPlain(); html+='<span class="hl-number">'+escHtml(src.slice(i,j))+'</span>'; i=j; continue;
|
|
1446
|
+
}
|
|
1447
|
+
// keywords
|
|
1448
|
+
if(isWordChar(src[i]) && !isWordChar(src[i-1]||'')){
|
|
1449
|
+
let j=i; while(j<n && isWordChar(src[j])) j++;
|
|
1450
|
+
const word=src.slice(i,j);
|
|
1451
|
+
if(lang.keywords.includes(word)){ flushPlain(); html+='<span class="hl-keyword">'+escHtml(word)+'</span>'; i=j; continue; }
|
|
1452
|
+
plain+=word; i=j; continue;
|
|
1453
|
+
}
|
|
1454
|
+
plain+=src[i]; i++;
|
|
1455
|
+
}
|
|
1456
|
+
flushPlain();
|
|
1457
|
+
return html;
|
|
1458
|
+
}
|
|
1459
|
+
// A textarea can't render colored text itself, so this overlays one, transparent, on top of a
|
|
1460
|
+
// highlighted <pre><code> "backdrop" showing through it (the standard technique for a highlighted
|
|
1461
|
+
// plain-text editor without a full editor component) — same font metrics on both, scroll positions
|
|
1462
|
+
// kept in lockstep, backdrop re-rendered on every keystroke.
|
|
1463
|
+
function filesCodeEditor(content,langKey,onInput){
|
|
1464
|
+
const wrap=el('div','files-code-wrap');
|
|
1465
|
+
const pre=document.createElement('pre'); pre.className='files-code-backdrop'; pre.setAttribute('aria-hidden','true');
|
|
1466
|
+
const code=document.createElement('code'); pre.append(code);
|
|
1467
|
+
const ta=el('textarea','files-editor files-code-input'); ta.spellcheck=false; ta.value=content;
|
|
1468
|
+
const paint=()=>{ code.innerHTML=filesHighlight(ta.value,langKey)+'\n'; }; // trailing \n: a final blank line still gets backdrop height
|
|
1469
|
+
const syncScroll=()=>{ pre.scrollTop=ta.scrollTop; pre.scrollLeft=ta.scrollLeft; };
|
|
1470
|
+
ta.addEventListener('input',()=>{ paint(); if(onInput) onInput(ta.value); });
|
|
1471
|
+
ta.addEventListener('scroll',syncScroll);
|
|
1472
|
+
paint();
|
|
1473
|
+
wrap.append(pre,ta);
|
|
1474
|
+
return { wrap, textarea:ta };
|
|
1475
|
+
}
|
|
1350
1476
|
async function openFilesFile(relPath){
|
|
1351
1477
|
// no native confirm() dialog (it blocks the whole tab, including our own SSE/automation) — a
|
|
1352
1478
|
// dirty editor just refuses to switch until the user explicitly saves or discards.
|
|
@@ -1409,15 +1535,19 @@ function renderFilesMd(box,actions,relPath,content){
|
|
|
1409
1535
|
const showEditor=()=>{
|
|
1410
1536
|
body.innerHTML='';
|
|
1411
1537
|
const tb=el('div','files-md-toolbar');
|
|
1412
|
-
const ta=
|
|
1413
|
-
const wrapSel=(before,after)=>{
|
|
1538
|
+
const {wrap,textarea:ta}=filesCodeEditor(content,null,(v)=>{ content=v; filesOpenDirty=true; });
|
|
1539
|
+
const wrapSel=(before,after)=>{
|
|
1540
|
+
const s=ta.selectionStart,e=ta.selectionEnd; const v=ta.value;
|
|
1541
|
+
ta.value=v.slice(0,s)+before+v.slice(s,e)+after+v.slice(e);
|
|
1542
|
+
ta.dispatchEvent(new Event('input')); // repaints the backdrop and marks dirty via the same path as typing
|
|
1543
|
+
ta.focus(); ta.selectionStart=s+before.length; ta.selectionEnd=e+before.length;
|
|
1544
|
+
};
|
|
1414
1545
|
[['B','**','**'],['I','_','_'],['H','## ',''],['Link','[','](url)']].forEach(([label,a,b])=>{
|
|
1415
1546
|
const bt=el('button',null,label); bt.type='button'; bt.addEventListener('click',()=>wrapSel(a,b)); tb.append(bt);
|
|
1416
1547
|
});
|
|
1417
1548
|
const saveBtn=el('button',null,t('files.save')); saveBtn.type='button'; saveBtn.addEventListener('click',()=>{ content=ta.value; filesSave(relPath,content,actions); });
|
|
1418
1549
|
tb.append(saveBtn,filesDiscardBtn(relPath));
|
|
1419
|
-
|
|
1420
|
-
body.append(tb,ta); ta.focus(); editBtn.textContent=t('files.preview');
|
|
1550
|
+
body.append(tb,wrap); ta.focus(); editBtn.textContent=t('files.preview');
|
|
1421
1551
|
};
|
|
1422
1552
|
editBtn.addEventListener('click',()=>{ editing=!editing; if(editing) showEditor(); else showPreview(); });
|
|
1423
1553
|
showPreview();
|
|
@@ -1438,23 +1568,21 @@ function renderFilesHtml(box,actions,relPath,content){
|
|
|
1438
1568
|
};
|
|
1439
1569
|
const showEditor=()=>{
|
|
1440
1570
|
body.innerHTML='';
|
|
1441
|
-
const ta=
|
|
1442
|
-
ta.addEventListener('input',()=>{ content=ta.value; filesOpenDirty=true; });
|
|
1571
|
+
const {wrap,textarea:ta}=filesCodeEditor(content,'html',(v)=>{ content=v; filesOpenDirty=true; });
|
|
1443
1572
|
const saveBtn=el('button','btn primary',t('files.save')); saveBtn.type='button'; saveBtn.addEventListener('click',()=>filesSave(relPath,content,actions));
|
|
1444
1573
|
const tb=el('div','files-md-toolbar'); tb.append(saveBtn,filesDiscardBtn(relPath));
|
|
1445
|
-
body.append(tb,
|
|
1574
|
+
body.append(tb,wrap); ta.focus(); editBtn.textContent=t('files.preview');
|
|
1446
1575
|
};
|
|
1447
1576
|
editBtn.addEventListener('click',()=>{ editing=!editing; if(editing) showEditor(); else showPreview(); });
|
|
1448
1577
|
showPreview();
|
|
1449
1578
|
}
|
|
1450
1579
|
// Anything else that reads as text: a plain monospace editor, editable straight away.
|
|
1451
1580
|
function renderFilesText(box,actions,relPath,content){
|
|
1452
|
-
const ta=
|
|
1453
|
-
ta.addEventListener('input',()=>{ filesOpenDirty=true; });
|
|
1581
|
+
const {wrap,textarea:ta}=filesCodeEditor(content,filesHlLang(filesExt(relPath)),()=>{ filesOpenDirty=true; });
|
|
1454
1582
|
const saveBtn=el('button','btn primary',t('files.save')); saveBtn.type='button';
|
|
1455
1583
|
saveBtn.addEventListener('click',()=>filesSave(relPath,ta.value,actions));
|
|
1456
1584
|
actions.append(saveBtn,filesDiscardBtn(relPath));
|
|
1457
|
-
box.append(
|
|
1585
|
+
box.append(wrap); ta.focus();
|
|
1458
1586
|
}
|
|
1459
1587
|
// "+ File"/"+ Folder" open the same inline form (never a native prompt()/alert() — those block the
|
|
1460
1588
|
// whole tab, including our own SSE connection, until dismissed).
|
|
@@ -1465,6 +1593,7 @@ function openFilesCreateForm(kind){
|
|
|
1465
1593
|
const input=$('#filesCreateInput');
|
|
1466
1594
|
input.placeholder = kind==='dir' ? t('files.newFolderPrompt') : t('files.newFilePrompt');
|
|
1467
1595
|
input.value='';
|
|
1596
|
+
const target=$('#filesCreateTarget'); if(target) target.textContent=t('files.creatingIn',{path:filesSelectedDir||t('files.projectRoot')});
|
|
1468
1597
|
const err=$('#filesCreateError'); if(err) err.hidden=true;
|
|
1469
1598
|
form.hidden=false; form.classList.add('is-open');
|
|
1470
1599
|
input.focus();
|
|
@@ -1477,7 +1606,7 @@ async function submitFilesCreate(){
|
|
|
1477
1606
|
const input=$('#filesCreateInput'); const err=$('#filesCreateError');
|
|
1478
1607
|
const name=(input.value||'').trim();
|
|
1479
1608
|
if(!name){ if(err){ err.textContent=t('backlog.addTitleRequired'); err.hidden=false; } return; }
|
|
1480
|
-
const rel=name.replace(/^\/+/,'');
|
|
1609
|
+
const rel=(filesSelectedDir?filesSelectedDir+'/':'')+name.replace(/^\/+/,'');
|
|
1481
1610
|
const kind=filesCreateKind;
|
|
1482
1611
|
const endpoint = kind==='dir' ? '/api/files/mkdir' : '/api/files/write';
|
|
1483
1612
|
const payload = kind==='dir' ? {path:rel} : {path:rel,content:''};
|
|
@@ -1615,6 +1744,12 @@ $$('#backlogTable thead th').forEach(th=> th.addEventListener('click', ()=>{
|
|
|
1615
1744
|
const filesNewFileBtn=$('#filesNewFile'); if(filesNewFileBtn) filesNewFileBtn.addEventListener('click',()=>openFilesCreateForm('file'));
|
|
1616
1745
|
const filesNewFolderBtn=$('#filesNewFolder'); if(filesNewFolderBtn) filesNewFolderBtn.addEventListener('click',()=>openFilesCreateForm('dir'));
|
|
1617
1746
|
const filesRefreshBtn=$('#filesRefresh'); if(filesRefreshBtn) filesRefreshBtn.addEventListener('click',loadFilesTree);
|
|
1747
|
+
const filesRootRowBtn=$('#filesRootRow');
|
|
1748
|
+
if(filesRootRowBtn) filesRootRowBtn.addEventListener('click',()=>{
|
|
1749
|
+
filesSelectedDir='';
|
|
1750
|
+
$$('#filesTree .f-row.is-target').forEach(r=> r.classList.remove('is-target'));
|
|
1751
|
+
filesRootRowBtn.classList.add('is-target');
|
|
1752
|
+
});
|
|
1618
1753
|
const filesCreateGoBtn=$('#filesCreateGo'); if(filesCreateGoBtn) filesCreateGoBtn.addEventListener('click',submitFilesCreate);
|
|
1619
1754
|
const filesCreateCancelBtn=$('#filesCreateCancel'); if(filesCreateCancelBtn) filesCreateCancelBtn.addEventListener('click',closeFilesCreateForm);
|
|
1620
1755
|
const filesCreateInputEl=$('#filesCreateInput'); if(filesCreateInputEl) filesCreateInputEl.addEventListener('keydown',e=>{ if(e.key==='Enter') submitFilesCreate(); });
|
|
@@ -85,13 +85,13 @@ en: {
|
|
|
85
85
|
'team.skillsTitle':'Skills','team.skillsSub':'Evolving procedures — the <em>how</em>.',
|
|
86
86
|
'team.standardsLabel':'standards','team.usesLabel':'uses','team.standardLabel':'standard',
|
|
87
87
|
'drawer.agent':'Agent','drawer.skill':'Skill','drawer.file':'File · {rel}',
|
|
88
|
-
'drawer.loading':'Loading…','drawer.loadError':'Could not load this file.','files.title':'Files','files.sub':'Browse the project\'s files — view Markdown & HTML, edit any text file, create new ones.','files.newFile':'+ File','files.newFolder':'+ Folder','files.pickFile':'Select a file to view it.','files.empty':'No files yet.','files.edit':'Edit','files.preview':'Preview','files.save':'Save','files.saved':'✓ saved','files.saveError':'Could not save this file.','files.loadError':'Could not load this file.','files.binary':'This file can\'t be previewed here (not text).','files.discardConfirm':'Discard unsaved changes?','files.newFilePrompt':'New file
|
|
88
|
+
'drawer.loading':'Loading…','drawer.loadError':'Could not load this file.','files.title':'Files','files.sub':'Browse the project\'s files — view Markdown & HTML, edit any text file, create new ones.','files.newFile':'+ File','files.newFolder':'+ Folder','files.pickFile':'Select a file to view it.','files.empty':'No files yet.','files.edit':'Edit','files.preview':'Preview','files.save':'Save','files.saved':'✓ saved','files.saveError':'Could not save this file.','files.loadError':'Could not load this file.','files.binary':'This file can\'t be previewed here (not text).','files.discardConfirm':'Discard unsaved changes?','files.newFilePrompt':'New file name (e.g. todo.md):','files.newFolderPrompt':'New folder name:','files.projectRoot':'project root','files.creatingIn':'Creating in: {path}','files.refresh':'Refresh','files.discard':'Discard','files.create':'Create',
|
|
89
89
|
'chat.widgetTitle':'Run an agent','chat.widgetSub':'Quick access · full view in the Chat tab',
|
|
90
90
|
'chat.tabSub':'Full conversation with the runner — the same run as the widget, more room to read it.',
|
|
91
91
|
'chat.idle':'Type a request — the agent runs headless in this project with full memory (<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.',
|
|
92
92
|
'chat.inputPlaceholder':'e.g. Add a login feature with email + password',
|
|
93
93
|
'chat.orchestrateTitle':'Walk the enabled workflow','chat.summarizeTitle':'Condense the recent activity into a summary','chat.clearTitle':'Clear the chat log',
|
|
94
|
-
'chat.warn':'⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files & run commands.',
|
|
94
|
+
'chat.warn':'⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files & run commands.','chat.running':'Agent running…',
|
|
95
95
|
'info.title':'Info','info.sub':'Project overview — configuration, counts, specs and active workflow.',
|
|
96
96
|
'info.project':'Project','info.projectType':'Project type','info.mode':'Mode','info.language':'Language',
|
|
97
97
|
'info.activeAgent':'Active agent','info.runners':'Runners','info.noRunners':'No runners configured.',
|
|
@@ -197,13 +197,13 @@ fr: {
|
|
|
197
197
|
'team.skillsTitle':'Compétences','team.skillsSub':'Procédures évolutives — le <em>comment</em>.',
|
|
198
198
|
'team.standardsLabel':'standards','team.usesLabel':'utilise','team.standardLabel':'standard',
|
|
199
199
|
'drawer.agent':'Agent','drawer.skill':'Compétence','drawer.file':'Fichier · {rel}',
|
|
200
|
-
'drawer.loading':'Chargement…','drawer.loadError':'Impossible de charger ce fichier.','files.title':'Fichiers','files.sub':'Parcourez les fichiers du projet — visualisez Markdown et HTML, modifiez tout fichier texte, créez-en de nouveaux.','files.newFile':'+ Fichier','files.newFolder':'+ Dossier','files.pickFile':'Sélectionnez un fichier pour l’afficher.','files.empty':'Aucun fichier pour l’instant.','files.edit':'Modifier','files.preview':'Aperçu','files.save':'Enregistrer','files.saved':'✓ enregistré','files.saveError':'Impossible d’enregistrer ce fichier.','files.loadError':'Impossible de charger ce fichier.','files.binary':'Ce fichier ne peut pas être prévisualisé ici (non textuel).','files.discardConfirm':'Abandonner les modifications non enregistrées ?','files.newFilePrompt':'
|
|
200
|
+
'drawer.loading':'Chargement…','drawer.loadError':'Impossible de charger ce fichier.','files.title':'Fichiers','files.sub':'Parcourez les fichiers du projet — visualisez Markdown et HTML, modifiez tout fichier texte, créez-en de nouveaux.','files.newFile':'+ Fichier','files.newFolder':'+ Dossier','files.pickFile':'Sélectionnez un fichier pour l’afficher.','files.empty':'Aucun fichier pour l’instant.','files.edit':'Modifier','files.preview':'Aperçu','files.save':'Enregistrer','files.saved':'✓ enregistré','files.saveError':'Impossible d’enregistrer ce fichier.','files.loadError':'Impossible de charger ce fichier.','files.binary':'Ce fichier ne peut pas être prévisualisé ici (non textuel).','files.discardConfirm':'Abandonner les modifications non enregistrées ?','files.newFilePrompt':'Nom du nouveau fichier (ex. todo.md) :','files.newFolderPrompt':'Nom du nouveau dossier :','files.projectRoot':'racine du projet','files.creatingIn':'Création dans : {path}','files.refresh':'Actualiser','files.discard':'Annuler','files.create':'Créer',
|
|
201
201
|
'chat.widgetTitle':'Lancer un agent','chat.widgetSub':'Accès rapide · vue complète dans l’onglet Chat',
|
|
202
202
|
'chat.tabSub':'Conversation complète avec l’exécuteur — la même exécution que le widget, avec plus de place pour la lire.',
|
|
203
203
|
'chat.idle':'Tapez une demande — l’agent s’exécute sans supervision dans ce projet avec toute sa mémoire (<code>CLAUDE.md → AGENTS.md</code>) et met le tableau à jour en direct.',
|
|
204
204
|
'chat.inputPlaceholder':'ex. Ajouter une fonctionnalité de connexion par email + mot de passe',
|
|
205
205
|
'chat.orchestrateTitle':'Parcourir le workflow activé','chat.summarizeTitle':'Condenser l’activité récente en un résumé','chat.clearTitle':'Effacer le journal de discussion',
|
|
206
|
-
'chat.warn':'⚠ Lance un agent réel (<code>config.json → runners</code>) qui peut modifier des fichiers et exécuter des commandes.',
|
|
206
|
+
'chat.warn':'⚠ Lance un agent réel (<code>config.json → runners</code>) qui peut modifier des fichiers et exécuter des commandes.','chat.running':'Agent en cours d’exécution…',
|
|
207
207
|
'info.title':'Infos','info.sub':'Vue d’ensemble du projet — configuration, comptages, specs et workflow actif.',
|
|
208
208
|
'info.project':'Projet','info.projectType':'Type de projet','info.mode':'Mode','info.language':'Langue',
|
|
209
209
|
'info.activeAgent':'Agent actif','info.runners':'Runners','info.noRunners':'Aucun runner configuré.',
|
|
@@ -309,13 +309,13 @@ es: {
|
|
|
309
309
|
'team.skillsTitle':'Habilidades','team.skillsSub':'Procedimientos en evolución — el <em>cómo</em>.',
|
|
310
310
|
'team.standardsLabel':'estándares','team.usesLabel':'usa','team.standardLabel':'estándar',
|
|
311
311
|
'drawer.agent':'Agente','drawer.skill':'Habilidad','drawer.file':'Archivo · {rel}',
|
|
312
|
-
'drawer.loading':'Cargando…','drawer.loadError':'No se pudo cargar este archivo.','files.title':'Archivos','files.sub':'Explora los archivos del proyecto — visualiza Markdown y HTML, edita cualquier archivo de texto, crea otros nuevos.','files.newFile':'+ Archivo','files.newFolder':'+ Carpeta','files.pickFile':'Selecciona un archivo para verlo.','files.empty':'Aún no hay archivos.','files.edit':'Editar','files.preview':'Vista previa','files.save':'Guardar','files.saved':'✓ guardado','files.saveError':'No se pudo guardar este archivo.','files.loadError':'No se pudo cargar este archivo.','files.binary':'Este archivo no se puede previsualizar aquí (no es texto).','files.discardConfirm':'¿Descartar los cambios sin guardar?','files.newFilePrompt':'
|
|
312
|
+
'drawer.loading':'Cargando…','drawer.loadError':'No se pudo cargar este archivo.','files.title':'Archivos','files.sub':'Explora los archivos del proyecto — visualiza Markdown y HTML, edita cualquier archivo de texto, crea otros nuevos.','files.newFile':'+ Archivo','files.newFolder':'+ Carpeta','files.pickFile':'Selecciona un archivo para verlo.','files.empty':'Aún no hay archivos.','files.edit':'Editar','files.preview':'Vista previa','files.save':'Guardar','files.saved':'✓ guardado','files.saveError':'No se pudo guardar este archivo.','files.loadError':'No se pudo cargar este archivo.','files.binary':'Este archivo no se puede previsualizar aquí (no es texto).','files.discardConfirm':'¿Descartar los cambios sin guardar?','files.newFilePrompt':'Nombre del nuevo archivo (p. ej. todo.md):','files.newFolderPrompt':'Nombre de la nueva carpeta:','files.projectRoot':'raíz del proyecto','files.creatingIn':'Creando en: {path}','files.refresh':'Actualizar','files.discard':'Descartar','files.create':'Crear',
|
|
313
313
|
'chat.widgetTitle':'Ejecutar un agente','chat.widgetSub':'Acceso rápido · vista completa en la pestaña Chat',
|
|
314
314
|
'chat.tabSub':'Conversación completa con el ejecutor — la misma ejecución que el widget, con más espacio para leerla.',
|
|
315
315
|
'chat.idle':'Escribe una solicitud — el agente se ejecuta sin supervisión en este proyecto con toda su memoria (<code>CLAUDE.md → AGENTS.md</code>) y actualiza el tablero en vivo.',
|
|
316
316
|
'chat.inputPlaceholder':'p. ej. Añadir un inicio de sesión con email + contraseña',
|
|
317
317
|
'chat.orchestrateTitle':'Recorrer el workflow activado','chat.summarizeTitle':'Condensar la actividad reciente en un resumen','chat.clearTitle':'Borrar el registro del chat',
|
|
318
|
-
'chat.warn':'⚠ Lanza un agente real (<code>config.json → runners</code>) que puede modificar archivos y ejecutar comandos.',
|
|
318
|
+
'chat.warn':'⚠ Lanza un agente real (<code>config.json → runners</code>) que puede modificar archivos y ejecutar comandos.','chat.running':'Agente en ejecución…',
|
|
319
319
|
'info.title':'Info','info.sub':'Visión general del proyecto — configuración, recuentos, specs y workflow activo.',
|
|
320
320
|
'info.project':'Proyecto','info.projectType':'Tipo de proyecto','info.mode':'Modo','info.language':'Idioma',
|
|
321
321
|
'info.activeAgent':'Agente activo','info.runners':'Runners','info.noRunners':'No hay runners configurados.',
|
|
@@ -421,13 +421,13 @@ de: {
|
|
|
421
421
|
'team.skillsTitle':'Skills','team.skillsSub':'Sich weiterentwickelnde Vorgehensweisen — das <em>Wie</em>.',
|
|
422
422
|
'team.standardsLabel':'Standards','team.usesLabel':'nutzt','team.standardLabel':'Standard',
|
|
423
423
|
'drawer.agent':'Agent','drawer.skill':'Skill','drawer.file':'Datei · {rel}',
|
|
424
|
-
'drawer.loading':'Lädt…','drawer.loadError':'Diese Datei konnte nicht geladen werden.','files.title':'Dateien','files.sub':'Durchsuche die Projektdateien — Markdown & HTML ansehen, jede Textdatei bearbeiten, neue erstellen.','files.newFile':'+ Datei','files.newFolder':'+ Ordner','files.pickFile':'Wähle eine Datei aus, um sie anzuzeigen.','files.empty':'Noch keine Dateien.','files.edit':'Bearbeiten','files.preview':'Vorschau','files.save':'Speichern','files.saved':'✓ gespeichert','files.saveError':'Diese Datei konnte nicht gespeichert werden.','files.loadError':'Diese Datei konnte nicht geladen werden.','files.binary':'Diese Datei kann hier nicht angezeigt werden (kein Text).','files.discardConfirm':'Nicht gespeicherte Änderungen verwerfen?','files.newFilePrompt':'
|
|
424
|
+
'drawer.loading':'Lädt…','drawer.loadError':'Diese Datei konnte nicht geladen werden.','files.title':'Dateien','files.sub':'Durchsuche die Projektdateien — Markdown & HTML ansehen, jede Textdatei bearbeiten, neue erstellen.','files.newFile':'+ Datei','files.newFolder':'+ Ordner','files.pickFile':'Wähle eine Datei aus, um sie anzuzeigen.','files.empty':'Noch keine Dateien.','files.edit':'Bearbeiten','files.preview':'Vorschau','files.save':'Speichern','files.saved':'✓ gespeichert','files.saveError':'Diese Datei konnte nicht gespeichert werden.','files.loadError':'Diese Datei konnte nicht geladen werden.','files.binary':'Diese Datei kann hier nicht angezeigt werden (kein Text).','files.discardConfirm':'Nicht gespeicherte Änderungen verwerfen?','files.newFilePrompt':'Name der neuen Datei (z. B. todo.md):','files.newFolderPrompt':'Name des neuen Ordners:','files.projectRoot':'Projektstamm','files.creatingIn':'Erstellen in: {path}','files.refresh':'Aktualisieren','files.discard':'Verwerfen','files.create':'Erstellen',
|
|
425
425
|
'chat.widgetTitle':'Agenten ausführen','chat.widgetSub':'Schnellzugriff · vollständige Ansicht im Chat-Tab',
|
|
426
426
|
'chat.tabSub':'Vollständiges Gespräch mit dem Runner — derselbe Lauf wie im Widget, mit mehr Platz zum Lesen.',
|
|
427
427
|
'chat.idle':'Geben Sie eine Anfrage ein — der Agent läuft eigenständig in diesem Projekt mit vollem Gedächtnis (<code>CLAUDE.md → AGENTS.md</code>) und aktualisiert das Board live.',
|
|
428
428
|
'chat.inputPlaceholder':'z. B. Login mit E-Mail + Passwort hinzufügen',
|
|
429
429
|
'chat.orchestrateTitle':'Den aktivierten Workflow durchlaufen','chat.summarizeTitle':'Die letzten Aktivitäten zu einer Zusammenfassung verdichten','chat.clearTitle':'Chat-Verlauf löschen',
|
|
430
|
-
'chat.warn':'⚠ Startet einen echten Agenten (<code>config.json → runners</code>), der Dateien ändern und Befehle ausführen kann.',
|
|
430
|
+
'chat.warn':'⚠ Startet einen echten Agenten (<code>config.json → runners</code>), der Dateien ändern und Befehle ausführen kann.','chat.running':'Agent läuft…',
|
|
431
431
|
'info.title':'Info','info.sub':'Projektübersicht — Konfiguration, Zahlen, Specs und aktiver Workflow.',
|
|
432
432
|
'info.project':'Projekt','info.projectType':'Projekttyp','info.mode':'Modus','info.language':'Sprache',
|
|
433
433
|
'info.activeAgent':'Aktiver Agent','info.runners':'Runner','info.noRunners':'Keine Runner konfiguriert.',
|
|
@@ -533,13 +533,13 @@ pt: {
|
|
|
533
533
|
'team.skillsTitle':'Habilidades','team.skillsSub':'Procedimentos em evolução — o <em>como</em>.',
|
|
534
534
|
'team.standardsLabel':'padrões','team.usesLabel':'usa','team.standardLabel':'padrão',
|
|
535
535
|
'drawer.agent':'Agente','drawer.skill':'Habilidade','drawer.file':'Ficheiro · {rel}',
|
|
536
|
-
'drawer.loading':'A carregar…','drawer.loadError':'Não foi possível carregar este ficheiro.','files.title':'Ficheiros','files.sub':'Percorra os ficheiros do projeto — veja Markdown e HTML, edite qualquer ficheiro de texto, crie novos.','files.newFile':'+ Ficheiro','files.newFolder':'+ Pasta','files.pickFile':'Selecione um ficheiro para o ver.','files.empty':'Ainda não há ficheiros.','files.edit':'Editar','files.preview':'Pré-visualizar','files.save':'Guardar','files.saved':'✓ guardado','files.saveError':'Não foi possível guardar este ficheiro.','files.loadError':'Não foi possível carregar este ficheiro.','files.binary':'Este ficheiro não pode ser pré-visualizado aqui (não é texto).','files.discardConfirm':'Descartar alterações não guardadas?','files.newFilePrompt':'
|
|
536
|
+
'drawer.loading':'A carregar…','drawer.loadError':'Não foi possível carregar este ficheiro.','files.title':'Ficheiros','files.sub':'Percorra os ficheiros do projeto — veja Markdown e HTML, edite qualquer ficheiro de texto, crie novos.','files.newFile':'+ Ficheiro','files.newFolder':'+ Pasta','files.pickFile':'Selecione um ficheiro para o ver.','files.empty':'Ainda não há ficheiros.','files.edit':'Editar','files.preview':'Pré-visualizar','files.save':'Guardar','files.saved':'✓ guardado','files.saveError':'Não foi possível guardar este ficheiro.','files.loadError':'Não foi possível carregar este ficheiro.','files.binary':'Este ficheiro não pode ser pré-visualizado aqui (não é texto).','files.discardConfirm':'Descartar alterações não guardadas?','files.newFilePrompt':'Nome do novo ficheiro (ex. todo.md):','files.newFolderPrompt':'Nome da nova pasta:','files.projectRoot':'raiz do projeto','files.creatingIn':'A criar em: {path}','files.refresh':'Atualizar','files.discard':'Descartar','files.create':'Criar',
|
|
537
537
|
'chat.widgetTitle':'Executar um agente','chat.widgetSub':'Acesso rápido · vista completa no separador Chat',
|
|
538
538
|
'chat.tabSub':'Conversa completa com o executor — a mesma execução do widget, com mais espaço para ler.',
|
|
539
539
|
'chat.idle':'Escreva um pedido — o agente corre sem supervisão neste projeto com toda a sua memória (<code>CLAUDE.md → AGENTS.md</code>) e atualiza o painel em direto.',
|
|
540
540
|
'chat.inputPlaceholder':'ex. Adicionar login com email + palavra-passe',
|
|
541
541
|
'chat.orchestrateTitle':'Percorrer o workflow ativado','chat.summarizeTitle':'Condensar a atividade recente num resumo','chat.clearTitle':'Limpar o registo do chat',
|
|
542
|
-
'chat.warn':'⚠ Inicia um agente real (<code>config.json → runners</code>) que pode alterar ficheiros e executar comandos.',
|
|
542
|
+
'chat.warn':'⚠ Inicia um agente real (<code>config.json → runners</code>) que pode alterar ficheiros e executar comandos.','chat.running':'Agente em execução…',
|
|
543
543
|
'info.title':'Info','info.sub':'Visão geral do projeto — configuração, contagens, specs e workflow ativo.',
|
|
544
544
|
'info.project':'Projeto','info.projectType':'Tipo de projeto','info.mode':'Modo','info.language':'Idioma',
|
|
545
545
|
'info.activeAgent':'Agente ativo','info.runners':'Runners','info.noRunners':'Nenhum runner configurado.',
|
|
@@ -645,13 +645,13 @@ it: {
|
|
|
645
645
|
'team.skillsTitle':'Skill','team.skillsSub':'Procedure in evoluzione — il <em>come</em>.',
|
|
646
646
|
'team.standardsLabel':'standard','team.usesLabel':'usa','team.standardLabel':'standard',
|
|
647
647
|
'drawer.agent':'Agente','drawer.skill':'Skill','drawer.file':'File · {rel}',
|
|
648
|
-
'drawer.loading':'Caricamento…','drawer.loadError':'Impossibile caricare questo file.','files.title':'File','files.sub':'Sfoglia i file del progetto — visualizza Markdown e HTML, modifica qualsiasi file di testo, creane di nuovi.','files.newFile':'+ File','files.newFolder':'+ Cartella','files.pickFile':'Seleziona un file per visualizzarlo.','files.empty':'Nessun file ancora.','files.edit':'Modifica','files.preview':'Anteprima','files.save':'Salva','files.saved':'✓ salvato','files.saveError':'Impossibile salvare questo file.','files.loadError':'Impossibile caricare questo file.','files.binary':'Questo file non può essere visualizzato qui (non è testo).','files.discardConfirm':'Scartare le modifiche non salvate?','files.newFilePrompt':'
|
|
648
|
+
'drawer.loading':'Caricamento…','drawer.loadError':'Impossibile caricare questo file.','files.title':'File','files.sub':'Sfoglia i file del progetto — visualizza Markdown e HTML, modifica qualsiasi file di testo, creane di nuovi.','files.newFile':'+ File','files.newFolder':'+ Cartella','files.pickFile':'Seleziona un file per visualizzarlo.','files.empty':'Nessun file ancora.','files.edit':'Modifica','files.preview':'Anteprima','files.save':'Salva','files.saved':'✓ salvato','files.saveError':'Impossibile salvare questo file.','files.loadError':'Impossibile caricare questo file.','files.binary':'Questo file non può essere visualizzato qui (non è testo).','files.discardConfirm':'Scartare le modifiche non salvate?','files.newFilePrompt':'Nome del nuovo file (es. todo.md):','files.newFolderPrompt':'Nome della nuova cartella:','files.projectRoot':'radice del progetto','files.creatingIn':'Creazione in: {path}','files.refresh':'Aggiorna','files.discard':'Scarta','files.create':'Crea',
|
|
649
649
|
'chat.widgetTitle':'Avvia un agente','chat.widgetSub':'Accesso rapido · vista completa nella scheda Chat',
|
|
650
650
|
'chat.tabSub':'Conversazione completa con l’esecutore — la stessa esecuzione del widget, con più spazio per leggerla.',
|
|
651
651
|
'chat.idle':'Digita una richiesta — l’agente viene eseguito senza supervisione in questo progetto con tutta la sua memoria (<code>CLAUDE.md → AGENTS.md</code>) e aggiorna la bacheca in diretta.',
|
|
652
652
|
'chat.inputPlaceholder':'es. Aggiungi un login con email + password',
|
|
653
653
|
'chat.orchestrateTitle':'Percorri il workflow attivato','chat.summarizeTitle':'Condensa l’attività recente in un riassunto','chat.clearTitle':'Cancella il registro della chat',
|
|
654
|
-
'chat.warn':'⚠ Avvia un agente reale (<code>config.json → runners</code>) che può modificare file ed eseguire comandi.',
|
|
654
|
+
'chat.warn':'⚠ Avvia un agente reale (<code>config.json → runners</code>) che può modificare file ed eseguire comandi.','chat.running':'Agente in esecuzione…',
|
|
655
655
|
'info.title':'Info','info.sub':'Panoramica del progetto — configurazione, conteggi, specs e workflow attivo.',
|
|
656
656
|
'info.project':'Progetto','info.projectType':'Tipo di progetto','info.mode':'Modalità','info.language':'Lingua',
|
|
657
657
|
'info.activeAgent':'Agente attivo','info.runners':'Runner','info.noRunners':'Nessun runner configurato.',
|
|
@@ -226,6 +226,7 @@
|
|
|
226
226
|
<button id="filesRefresh" class="btn" type="button" title="Refresh" data-i18n-title="files.refresh">⟳</button>
|
|
227
227
|
</div>
|
|
228
228
|
<div class="files-create-form" id="filesCreateForm" hidden>
|
|
229
|
+
<div class="files-create-target" id="filesCreateTarget"></div>
|
|
229
230
|
<input type="text" id="filesCreateInput" class="bl-add-input" autocomplete="off" />
|
|
230
231
|
<div class="files-create-actions">
|
|
231
232
|
<button id="filesCreateGo" class="btn primary" type="button" data-i18n="files.create">Create</button>
|
|
@@ -233,6 +234,7 @@
|
|
|
233
234
|
</div>
|
|
234
235
|
<span class="files-error-tip" id="filesCreateError" hidden></span>
|
|
235
236
|
</div>
|
|
237
|
+
<div class="f-row f-root" id="filesRootRow" tabindex="0" data-i18n="files.projectRoot">project root</div>
|
|
236
238
|
<div class="files-tree" id="filesTree"></div>
|
|
237
239
|
</div>
|
|
238
240
|
<div class="files-content-col" id="filesContent">
|
|
@@ -270,6 +272,7 @@
|
|
|
270
272
|
</div>
|
|
271
273
|
</div>
|
|
272
274
|
<p class="chat-warn" data-i18n-html="chat.warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files & run commands.</p>
|
|
275
|
+
<p class="chat-status" id="tabChatStatus" hidden><span class="spinner" aria-hidden="true"></span><span data-i18n="chat.running">Agent running…</span></p>
|
|
273
276
|
</div>
|
|
274
277
|
</section>
|
|
275
278
|
|
|
@@ -389,6 +392,7 @@
|
|
|
389
392
|
<button id="orchBtn" class="btn chat-send" data-i18n-title="chat.orchestrateTitle" data-i18n="action.orchestrate" title="Walk the enabled workflow">Orchestrate</button>
|
|
390
393
|
</div>
|
|
391
394
|
<p class="chat-warn" data-i18n-html="chat.warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files & run commands.</p>
|
|
395
|
+
<p class="chat-status" id="widgetChatStatus" hidden><span class="spinner" aria-hidden="true"></span><span data-i18n="chat.running">Agent running…</span></p>
|
|
392
396
|
</div>
|
|
393
397
|
|
|
394
398
|
<div class="drawer" id="drawer" aria-hidden="true">
|
|
@@ -294,7 +294,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
294
294
|
.team-wrap { max-width:1500px; } /* the agent/skill .cards grid fills more columns instead of sitting capped */
|
|
295
295
|
|
|
296
296
|
/* Files tab — a two-pane explorer: a collapsible tree, and the selected file's preview/editor */
|
|
297
|
-
.files-wrap { padding:26px 28px
|
|
297
|
+
.files-wrap { padding:26px 28px 16px; max-width:1600px; height:calc(100vh - 90px); display:flex; flex-direction:column; }
|
|
298
298
|
.files-body { flex:1; min-height:0; display:flex; gap:16px; margin-top:8px; }
|
|
299
299
|
.files-tree-col { width:280px; flex-shrink:0; display:flex; flex-direction:column; gap:8px; min-height:0; }
|
|
300
300
|
.files-toolbar { display:flex; gap:8px; }
|
|
@@ -304,12 +304,18 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
304
304
|
.files-create-form .bl-add-input { width:100%; }
|
|
305
305
|
.files-create-actions { display:flex; gap:8px; }
|
|
306
306
|
.files-create-actions .btn { font-size:12px; padding:6px 11px; }
|
|
307
|
-
.files-tree { flex:1; min-height:0; overflow:auto; border:1px solid var(--line); border-radius:var(--radius); padding:8px; background:var(--surface); }
|
|
307
|
+
.files-tree { flex:1; min-height:0; overflow:auto; border:1px solid var(--line); border-radius:var(--radius); padding:8px; background:var(--surface); scrollbar-width:thin; scrollbar-color:var(--line) transparent; }
|
|
308
|
+
.files-tree::-webkit-scrollbar { width:8px; }
|
|
309
|
+
.files-tree::-webkit-scrollbar-thumb { background:var(--line); border-radius:8px; }
|
|
310
|
+
.files-tree::-webkit-scrollbar-track { background:transparent; }
|
|
308
311
|
.files-content-col { flex:1; min-width:0; display:flex; flex-direction:column; min-height:0; border:1px solid var(--line); border-radius:var(--radius); background:var(--surface); overflow:hidden; }
|
|
309
312
|
.files-empty,.files-binary { padding:40px; text-align:center; color:var(--faint); font-style:italic; }
|
|
310
313
|
.f-row { display:flex; align-items:center; gap:6px; padding:4px 6px; border-radius:6px; cursor:pointer; font-size:12.5px; white-space:nowrap; color:var(--muted); }
|
|
311
314
|
.f-row:hover { background:var(--surface-2); color:var(--ink); }
|
|
312
315
|
.f-row.is-active { background:var(--surface-2); color:var(--signal); font-weight:600; }
|
|
316
|
+
.f-row.is-target { background:color-mix(in srgb,var(--signal) 14%,transparent); color:var(--ink); } /* the folder new files/folders will be created in */
|
|
317
|
+
.f-root { font-weight:600; margin-bottom:4px; padding-bottom:6px; border-bottom:1px solid var(--line); border-radius:0; }
|
|
318
|
+
.files-create-target { font-size:11px; color:var(--muted); font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
313
319
|
.f-row .f-chevron { width:11px; height:11px; flex-shrink:0; transition:transform .15s; color:var(--faint); }
|
|
314
320
|
.f-row.is-open .f-chevron { transform:rotate(90deg); }
|
|
315
321
|
.f-row .f-name { overflow:hidden; text-overflow:ellipsis; }
|
|
@@ -320,8 +326,26 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
320
326
|
.files-actions { display:flex; gap:8px; flex-shrink:0; }
|
|
321
327
|
.files-actions .btn { font-size:12px; padding:6px 11px; }
|
|
322
328
|
.files-body-col { flex:1; min-height:0; display:flex; flex-direction:column; }
|
|
329
|
+
.files-view,.files-editor { scrollbar-width:thin; scrollbar-color:var(--line) transparent; }
|
|
330
|
+
.files-view::-webkit-scrollbar,.files-editor::-webkit-scrollbar { width:8px; }
|
|
331
|
+
.files-view::-webkit-scrollbar-thumb,.files-editor::-webkit-scrollbar-thumb { background:var(--line); border-radius:8px; }
|
|
332
|
+
.files-view::-webkit-scrollbar-track,.files-editor::-webkit-scrollbar-track { background:transparent; }
|
|
323
333
|
.files-view { flex:1; min-height:0; overflow:auto; padding:16px 20px; }
|
|
324
334
|
.files-editor { flex:1; min-height:0; width:100%; border:0; resize:none; padding:16px 20px; font-family:var(--mono); font-size:12.5px; line-height:1.6; background:var(--surface); color:var(--ink); outline:none; }
|
|
335
|
+
/* syntax-highlighted editor: a transparent textarea (real caret, real selection, real typing) sits
|
|
336
|
+
exactly on top of a highlighted <pre><code> backdrop showing through it — the standard technique
|
|
337
|
+
for highlighting without a full editor component. Both share identical font metrics so characters
|
|
338
|
+
line up; the backdrop never scrolls on its own (overflow:hidden), its scroll position is just
|
|
339
|
+
copied from the textarea on every scroll event. */
|
|
340
|
+
.files-code-wrap { position:relative; flex:1; min-height:0; }
|
|
341
|
+
.files-code-backdrop { position:absolute; inset:0; margin:0; overflow:hidden; pointer-events:none; white-space:pre-wrap; word-break:break-word; padding:16px 20px; font-family:var(--mono); font-size:12.5px; line-height:1.6; color:var(--ink); background:transparent; }
|
|
342
|
+
.files-code-backdrop code { font:inherit; background:none; }
|
|
343
|
+
.files-code-wrap .files-code-input { position:absolute; inset:0; background:transparent; color:transparent; caret-color:var(--ink); white-space:pre-wrap; word-break:break-word; }
|
|
344
|
+
.hl-comment { color:var(--faint); font-style:italic; }
|
|
345
|
+
.hl-string { color:var(--s-done); }
|
|
346
|
+
.hl-number { color:var(--cool); }
|
|
347
|
+
.hl-keyword { color:var(--signal); font-weight:600; }
|
|
348
|
+
.hl-tag { color:var(--s-in_progress); }
|
|
325
349
|
.files-iframe { flex:1; min-height:0; width:100%; border:0; background:#fff; }
|
|
326
350
|
.files-md-toolbar { display:flex; gap:6px; padding:8px 14px; border-bottom:1px solid var(--line); }
|
|
327
351
|
.files-md-toolbar button { font-family:var(--mono); font-size:12px; padding:4px 9px; border:1px solid var(--line); border-radius:6px; background:var(--surface-2); color:var(--ink); cursor:pointer; }
|
|
@@ -457,6 +481,13 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
457
481
|
.chat-ta:focus { outline:2px solid var(--cool); outline-offset:1px; }
|
|
458
482
|
.chat-send { flex:0 0 auto; align-self:auto; padding:8px 16px; }
|
|
459
483
|
.chat-warn { font-size:10.5px; color:var(--signal); margin:6px 12px 10px; }
|
|
484
|
+
body.chat-busy .chat-warn { display:none; } /* the running status below replaces it — one message at a time */
|
|
485
|
+
.chat-status { display:flex; align-items:center; gap:8px; font-size:11.5px; color:var(--signal); margin:6px 12px 10px; font-weight:600; }
|
|
486
|
+
.chat-status[hidden] { display:none; }
|
|
487
|
+
.chat-status .spinner { width:12px; height:12px; border-radius:50%; flex-shrink:0; border:2px solid color-mix(in srgb,var(--signal) 28%,transparent); border-top-color:var(--signal); animation:chat-spin .7s linear infinite; }
|
|
488
|
+
@keyframes chat-spin { to { transform:rotate(360deg); } }
|
|
489
|
+
.chat-send:disabled,.mini-btn:disabled { opacity:.5; cursor:not-allowed; }
|
|
490
|
+
@media (prefers-reduced-motion: reduce) { .chat-status .spinner { animation:none; } }
|
|
460
491
|
.approval { display:flex; flex-direction:column; gap:8px; padding:10px 12px; background:var(--surface-2); border:1px solid color-mix(in srgb,var(--signal) 40%,var(--line)); border-radius:var(--radius); box-shadow:var(--shadow); }
|
|
461
492
|
.approval .c-actions { display:flex; gap:8px; }
|
|
462
493
|
#orchBtn,#tabOrchBtn { background:var(--cool); color:#04202a; border-color:transparent; font-weight:600; }
|
|
@@ -478,7 +509,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
478
509
|
.chat-tab-input { display:flex; gap:10px; margin-top:10px; }
|
|
479
510
|
.chat-tab-input .chat-ta { flex:1; min-height:56px; max-height:180px; }
|
|
480
511
|
.chat-tab-actions { display:flex; flex-direction:column; gap:8px; flex-shrink:0; }
|
|
481
|
-
.chat-tab-wrap .chat-warn { margin:8px 0 0; }
|
|
512
|
+
.chat-tab-wrap .chat-warn,.chat-tab-wrap .chat-status { margin:8px 0 0; }
|
|
482
513
|
@media (max-width:640px){ .chat-tab-input { flex-direction:column; } .chat-tab-actions { flex-direction:row; } }
|
|
483
514
|
|
|
484
515
|
/* ---- chart & panel motion --------------------------------------------- */
|
|
@@ -77,7 +77,10 @@ function startRun(root, { prompt, agent, logPrompt = true }, emit) {
|
|
|
77
77
|
runStart(root, run); emit({ type: 'run-start', run }); emit({ type: 'change' });
|
|
78
78
|
|
|
79
79
|
let child;
|
|
80
|
-
|
|
80
|
+
// windowsHide: without it, spawning a .cmd-shimmed CLI (e.g. a global npm install of `claude` on
|
|
81
|
+
// Windows) pops up a real, empty console window on top of the browser — jarring, and pointless
|
|
82
|
+
// since stdout/stderr are already piped and captured below, never read from that window anyway.
|
|
83
|
+
try { child = spawn(parts[0], [...parts.slice(1), p], { cwd: root, env: process.env, windowsHide: true }); }
|
|
81
84
|
catch (e) {
|
|
82
85
|
runEnd(root, runId, 1);
|
|
83
86
|
emit({ type: 'run-line', runId, chunk: 'spawn error: ' + e.message + '\n' });
|
|
@@ -38,9 +38,15 @@ function runSummarize(root, { agent } = {}, emit) {
|
|
|
38
38
|
+ '\n\n' + formatLog(messages);
|
|
39
39
|
|
|
40
40
|
const parts = cmdStr.split(/\s+/).filter(Boolean);
|
|
41
|
+
const runId = 'summarize-' + Date.now().toString(36);
|
|
42
|
+
// Emitted before the spawn attempt (same order runner.js uses) so the client's "agent running"
|
|
43
|
+
// indicator lights up immediately, and so a spawn failure below still gets a matching run-end
|
|
44
|
+
// rather than leaving that indicator stuck on.
|
|
45
|
+
if (emit) emit({ type: 'run-start', run: { id: runId } });
|
|
41
46
|
let child;
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
// windowsHide: without it, spawning a .cmd-shimmed CLI on Windows pops up a real console window.
|
|
48
|
+
try { child = spawn(parts[0], [...parts.slice(1), prompt], { cwd: root, env: process.env, windowsHide: true }); }
|
|
49
|
+
catch (e) { if (emit) emit({ type: 'run-end', runId, code: 1 }); return { error: e.message }; }
|
|
44
50
|
try { child.stdin && child.stdin.end(); } catch {}
|
|
45
51
|
|
|
46
52
|
let out = '';
|
|
@@ -63,7 +69,7 @@ function runSummarize(root, { agent } = {}, emit) {
|
|
|
63
69
|
fresh.messages = (fresh.messages || []).filter((m) => !summarizedIds.has(m.id));
|
|
64
70
|
fresh.messages.push(summary);
|
|
65
71
|
store.writeRuntime(root, fresh);
|
|
66
|
-
if (emit) { emit({ type: 'message', message: summary }); emit({ type: 'change' }); }
|
|
72
|
+
if (emit) { emit({ type: 'run-end', runId, code }); emit({ type: 'message', message: summary }); emit({ type: 'change' }); }
|
|
67
73
|
});
|
|
68
74
|
return { child };
|
|
69
75
|
}
|