spectoflow 0.22.0 → 0.22.3
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 +52 -3
- package/templates/dashboard/public/designs/orbit.css +5 -3
- package/templates/dashboard/public/designs/orbit.js +21 -1
- package/templates/dashboard/public/i18n.js +12 -12
- package/templates/dashboard/public/index.html +5 -0
- package/templates/dashboard/public/styles.css +29 -6
- 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.3",
|
|
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",
|
|
@@ -7,6 +7,7 @@ function updateStatusLabels(){ for(const k of Object.keys(STATUS)) STATUS[k]=t('
|
|
|
7
7
|
let P = null, openTaskId = null;
|
|
8
8
|
let filter = { status: 'all', q: '' }; // board filter state — client-side only, read-only
|
|
9
9
|
let boardView = (()=>{ try{ return localStorage.getItem('spf-board-view')||'list'; }catch{ return 'list'; } })(); // 'list' | 'kanban'
|
|
10
|
+
let sideHidden = (()=>{ try{ return localStorage.getItem('spf-side-hidden')==='1'; }catch{ return false; } })(); // right sidebar (Journal/Specs/Running) — mainly to give Kanban's own-width columns more room
|
|
10
11
|
let backlogFilter = { status: 'open', q: '' }; // backlog defaults to open (not-done) tasks
|
|
11
12
|
let backlogSort = { col: 'id', dir: 'asc' }; // backlog sort state — client-side only
|
|
12
13
|
let backlogPage = 1; const BACKLOG_PAGE = 25; // backlog pagination — client-side only
|
|
@@ -34,7 +35,7 @@ function connect(){
|
|
|
34
35
|
es.onmessage = (ev)=>{
|
|
35
36
|
let m; try{ m=JSON.parse(ev.data); }catch{ return; }
|
|
36
37
|
if(m.type==='change'||m.type==='message') return scheduleLoad(); // messages live from runtime.messages
|
|
37
|
-
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; }
|
|
38
39
|
if(m.type==='run-line') return appendRaw(m.chunk); // raw output is ephemeral (not logged)
|
|
39
40
|
};
|
|
40
41
|
es.onerror = ()=>{ $('#sync').classList.add('offline'); $('#syncLabel').textContent='offline'; };
|
|
@@ -114,7 +115,11 @@ function setChat(open){
|
|
|
114
115
|
// doRun/doOrchestrate default to the floating widget's textarea/select; the Chat tab has its own
|
|
115
116
|
// #tabRunPrompt/#tabRunAgent (an id can't be shared by two elements) and passes them explicitly —
|
|
116
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'); }
|
|
117
121
|
async function doRun(promptEl,agentEl){
|
|
122
|
+
if(isChatBusy()) return;
|
|
118
123
|
promptEl=promptEl||$('#runPrompt'); agentEl=agentEl||$('#runAgent');
|
|
119
124
|
const prompt=promptEl.value.trim(); if(!prompt) return;
|
|
120
125
|
const agent=agentEl.value;
|
|
@@ -122,6 +127,7 @@ async function doRun(promptEl,agentEl){
|
|
|
122
127
|
promptEl.value=''; // the prompt renders as a bubble from the message log
|
|
123
128
|
}
|
|
124
129
|
async function doOrchestrate(promptEl){
|
|
130
|
+
if(isChatBusy()) return;
|
|
125
131
|
promptEl=promptEl||$('#runPrompt');
|
|
126
132
|
const prompt=promptEl.value.trim(); if(!prompt) return;
|
|
127
133
|
await fetch('/api/orchestrate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request:prompt})});
|
|
@@ -131,6 +137,7 @@ async function approve(decision){ await fetch('/api/orchestrate/approve',{method
|
|
|
131
137
|
// ---- chat context management: condense the log via the agent, or wipe it (Chat tab only — the
|
|
132
138
|
// floating widget stays "quick access", full controls live where there's room to read them) ----
|
|
133
139
|
async function summarizeChat(agentEl){
|
|
140
|
+
if(isChatBusy()) return;
|
|
134
141
|
const agent=(agentEl||$('#tabRunAgent'))?.value;
|
|
135
142
|
flash();
|
|
136
143
|
await fetch('/api/chat/summarize',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent})});
|
|
@@ -144,6 +151,23 @@ async function addComment(id,text,action){ flash(); await fetch('/api/task/'+enc
|
|
|
144
151
|
async function toggleStep(name){ flash(); await fetch('/api/workflow/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})}); }
|
|
145
152
|
function flash(){ const s=$('#sync'); s.classList.add('saving'); $('#syncLabel').textContent='writing…'; setTimeout(()=>{ s.classList.remove('saving'); $('#syncLabel').textContent='live'; },800); }
|
|
146
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
|
+
|
|
147
171
|
function render(){
|
|
148
172
|
const c = P.config||{};
|
|
149
173
|
i18nSetLang(c.language||'en'); updateStatusLabels(); // language drives the whole UI, not just agent output
|
|
@@ -164,7 +188,7 @@ function render(){
|
|
|
164
188
|
if(meter) meter.title=`${t('kpi.globalProgress')}: ${s.pct}% (${s.done}/${s.total} ${t('kpi.tasksLabel')})`;
|
|
165
189
|
renderOverview(); renderBoard(); renderBacklog(); renderWorkflow(); renderTeam();
|
|
166
190
|
renderChatLog($('#chatLog')); renderChatLog($('#chatTabLog'));
|
|
167
|
-
renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderDocs(); renderSettings(); renderFiles();
|
|
191
|
+
renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderDocs(); renderSettings(); renderFiles(); applySideHidden(); updateChatBusyUI();
|
|
168
192
|
renderCustomDashboards(); // adds/removes nav tabs + panels before applyActiveTab() below reads them
|
|
169
193
|
applyActiveTab(); // re-apply the current tab so an SSE-driven re-render never resets to Board
|
|
170
194
|
applyI18nStatic(); // re-translate the static markup (nav, headers, placeholders…) for this tick's language
|
|
@@ -1540,7 +1564,22 @@ openTaskId = taskFromPath(); // deep-link straight to a task drawer
|
|
|
1540
1564
|
function applyActiveTab(){
|
|
1541
1565
|
$$('#tabs .tab').forEach(t=> t.classList.toggle('is-active', t.dataset.tab===activeTab));
|
|
1542
1566
|
$$('.panel').forEach(p=> p.classList.toggle('is-active', p.dataset.panel===activeTab));
|
|
1543
|
-
|
|
1567
|
+
fitTabs();
|
|
1568
|
+
}
|
|
1569
|
+
// Horizontal top-nav designs (Console's vertical rail and Orbit's hidden/radial nav manage their own
|
|
1570
|
+
// layout independently) can run out of room as tabs are added over time — a single px breakpoint
|
|
1571
|
+
// tuned for whatever tab count existed back then goes stale the moment a tab is added or removed
|
|
1572
|
+
// (exactly what happened when the Files tab pushed the row from 10 to 11 items: some tabs, including
|
|
1573
|
+
// Personalize, silently overflowed with no visible way to reach them). Measure the row's REAL
|
|
1574
|
+
// overflow instead of guessing from viewport width alone, so it's correct at any tab count.
|
|
1575
|
+
function fitTabs(){
|
|
1576
|
+
const tabsEl=$('#tabs'); if(!tabsEl) return;
|
|
1577
|
+
const design=document.documentElement.getAttribute('data-design');
|
|
1578
|
+
if(design==='console'||design==='orbit'){ tabsEl.classList.remove('tabs-compact'); return; }
|
|
1579
|
+
tabsEl.classList.remove('tabs-compact'); // measure at full (labelled) size first
|
|
1580
|
+
if(tabsEl.scrollWidth>tabsEl.clientWidth+1) tabsEl.classList.add('tabs-compact');
|
|
1581
|
+
}
|
|
1582
|
+
window.addEventListener('resize', (()=>{ let t=null; return ()=>{ clearTimeout(t); t=setTimeout(fitTabs,120); }; })());
|
|
1544
1583
|
$$('#tabs .tab').forEach(tab=> tab.addEventListener('click',()=> navigateTab(tab.dataset.tab)));
|
|
1545
1584
|
// mobile hamburger — toggles the tab dropdown (body.nav-open); closes on tab pick / outside / Esc
|
|
1546
1585
|
const navToggle=$('#navToggle');
|
|
@@ -1568,6 +1607,16 @@ $$('#statusChips .fchip').forEach(b=> b.addEventListener('click', ()=>{ filter.s
|
|
|
1568
1607
|
$('#search').addEventListener('input', e=>{ filter.q=e.target.value; renderBoard(); });
|
|
1569
1608
|
// board view switch — List (phase-grouped) vs Kanban (columns by status), persisted per viewer
|
|
1570
1609
|
$$('#boardViewToggle .vt-btn').forEach(b=> b.addEventListener('click', ()=>{ boardView=b.dataset.view; try{ localStorage.setItem('spf-board-view',boardView); }catch{} renderBoard(); }));
|
|
1610
|
+
// right sidebar hide/show — Kanban's own-width columns need the room, and the toggle stays
|
|
1611
|
+
// persisted per viewer like every other layout preference here
|
|
1612
|
+
function applySideHidden(){
|
|
1613
|
+
const panel=$('.panel[data-panel="board"]'); const btn=$('#sideToggle');
|
|
1614
|
+
if(panel) panel.classList.toggle('side-hidden', sideHidden);
|
|
1615
|
+
if(btn){ btn.setAttribute('aria-pressed', String(sideHidden)); btn.title=t(sideHidden?'board.showSidebar':'board.hideSidebar'); }
|
|
1616
|
+
}
|
|
1617
|
+
const sideToggleBtn=$('#sideToggle');
|
|
1618
|
+
if(sideToggleBtn) sideToggleBtn.addEventListener('click', ()=>{ sideHidden=!sideHidden; try{ localStorage.setItem('spf-side-hidden', sideHidden?'1':'0'); }catch{} applySideHidden(); });
|
|
1619
|
+
applySideHidden();
|
|
1571
1620
|
// expand / collapse all phases (List view) — keeps a big board compact by default
|
|
1572
1621
|
const phaseToggleAllBtn=$('#phaseToggleAll');
|
|
1573
1622
|
if(phaseToggleAllBtn) phaseToggleAllBtn.addEventListener('click', ()=>{
|
|
@@ -199,12 +199,14 @@ html[data-design="orbit"] .ob-item .ob-bd {
|
|
|
199
199
|
position:absolute; top:2px; right:2px; min-width:15px; height:15px; padding:0 4px; border-radius:8px;
|
|
200
200
|
background:var(--s-blocked); color:#fff; font:700 9px/15px var(--mono); text-align:center;
|
|
201
201
|
}
|
|
202
|
-
|
|
202
|
+
/* --ob-r is set inline on .ob-dial (computed per open, from the actual tab count) and inherits down
|
|
203
|
+
to every .ob-item — the 98px/72px fallbacks only cover a stylesheet loaded before orbit.js runs. */
|
|
204
|
+
@keyframes ob-orbit { to{ opacity:1; transform:rotate(var(--a)) translate(var(--ob-r,98px)) rotate(calc(-1*var(--a))) scale(1); } }
|
|
203
205
|
@media (max-width:900px) {
|
|
204
206
|
html[data-design="orbit"] .ob-item { width:46px; height:46px; margin:-23px 0 0 -23px; }
|
|
205
207
|
html[data-design="orbit"] .ob-item .ob-lb { display:none; }
|
|
206
208
|
html[data-design="orbit"] .ob-item .ob-ico { width:16px; height:16px; }
|
|
207
|
-
@keyframes ob-orbit { to{ opacity:1; transform:rotate(var(--a)) translate(72px) rotate(calc(-1*var(--a))) scale(1); } }
|
|
209
|
+
@keyframes ob-orbit { to{ opacity:1; transform:rotate(var(--a)) translate(var(--ob-r,72px)) rotate(calc(-1*var(--a))) scale(1); } }
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -220,7 +222,7 @@ html[data-design="orbit"] .ob-item .ob-bd {
|
|
|
220
222
|
html[data-design="orbit"] body.booting .card {
|
|
221
223
|
animation:none !important;
|
|
222
224
|
}
|
|
223
|
-
html[data-design="orbit"] .ob-item { opacity:1; transform:rotate(var(--a)) translate(98px) rotate(calc(-1*var(--a))); }
|
|
225
|
+
html[data-design="orbit"] .ob-item { opacity:1; transform:rotate(var(--a)) translate(var(--ob-r,98px)) rotate(calc(-1*var(--a))); }
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
/* chat FAB — amber brand accent (the teal is the dial's); readable on both sets */
|
|
@@ -71,11 +71,31 @@
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Items sit on a ring of radius R; at n evenly-spaced items the straight-line gap between two
|
|
75
|
+
// adjacent item centers is a chord of length 2R·sin(π/n). Below ~9 items the tuned default radius
|
|
76
|
+
// already clears the item's own diameter with room to spare; past that, more tabs (built-in ones
|
|
77
|
+
// like this session's new Files tab, or future custom dashboards) would pack the same fixed ring
|
|
78
|
+
// tighter and tighter until items visibly overlap. Grow the radius instead — solved for the chord
|
|
79
|
+
// to equal the item diameter plus a minimum gap — so the ring always has room for however many
|
|
80
|
+
// tabs exist, never a fixed count tuned for whatever the tab bar happened to hold at the time.
|
|
81
|
+
function ringRadius(n, itemDiameter, minGap) {
|
|
82
|
+
if (n <= 1) return itemDiameter; // a lone item has no neighbor to clear
|
|
83
|
+
var needed = (itemDiameter + minGap) / (2 * Math.sin(Math.PI / n));
|
|
84
|
+
return Math.max(itemDiameter, needed);
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
/* ---- radial overlay ---- */
|
|
75
88
|
function buildOverlay() {
|
|
76
89
|
if (ovEl) return;
|
|
77
90
|
var list = tabs(), n = list.length || 1;
|
|
78
91
|
var idx = activeIndex(list);
|
|
92
|
+
var isCompact = window.matchMedia('(max-width:900px)').matches;
|
|
93
|
+
var itemDiameter = isCompact ? 46 : 58;
|
|
94
|
+
var baseRadius = isCompact ? 72 : 98; // the tuned default for <=9 items — never shrink below it
|
|
95
|
+
// the gap accounts for the label text under each icon, which can run wider than the icon
|
|
96
|
+
// circle itself ("Agents & Skills" is the long pole) — a gap sized only to the circle left
|
|
97
|
+
// adjacent labels touching even though the circles themselves were visibly clear.
|
|
98
|
+
var radius = Math.max(baseRadius, ringRadius(n, itemDiameter, 22));
|
|
79
99
|
var items = list.map(function (tab, i) {
|
|
80
100
|
var angle = -90 + i * (360 / n);
|
|
81
101
|
var ico = tab.querySelector('.tab-ico');
|
|
@@ -91,7 +111,7 @@
|
|
|
91
111
|
ovEl = document.createElement('div');
|
|
92
112
|
ovEl.className = 'ob-ov';
|
|
93
113
|
ovEl.innerHTML =
|
|
94
|
-
'<div class="ob-dial" role="dialog" aria-label="spectoflow navigation">' +
|
|
114
|
+
'<div class="ob-dial" role="dialog" aria-label="spectoflow navigation" style="--ob-r:' + radius + 'px">' +
|
|
95
115
|
'<svg class="ob-ring" viewBox="0 0 284 284" aria-hidden="true">' +
|
|
96
116
|
'<circle class="ob-track" cx="142" cy="142" r="128" stroke-dasharray="176 25" stroke-dashoffset="-12"></circle>' +
|
|
97
117
|
'<circle class="ob-prog" cx="142" cy="142" r="128" stroke-dasharray="0 804.2"></circle>' +
|
|
@@ -34,7 +34,7 @@ en: {
|
|
|
34
34
|
'topbar.lang.title':'Output language — change from here or in Personalize',
|
|
35
35
|
'topbar.agent.title':'Active agent — change from here or in Personalize',
|
|
36
36
|
'topbar.agent.none':'No agent found',
|
|
37
|
-
'board.filterPlaceholder':'Filter tasks…','board.expandAll':'Expand all','board.collapseAll':'Collapse all',
|
|
37
|
+
'board.filterPlaceholder':'Filter tasks…','board.expandAll':'Expand all','board.collapseAll':'Collapse all','board.hideSidebar':'Hide sidebar','board.showSidebar':'Show sidebar',
|
|
38
38
|
'board.expandAllTitle':'Expand or collapse all phases','board.viewList':'List','board.viewKanban':'Kanban',
|
|
39
39
|
'board.viewList.title':'Grouped by phase','board.viewKanban.title':'Columns by status',
|
|
40
40
|
'board.noTasksMatch':'No tasks match this filter.',
|
|
@@ -91,7 +91,7 @@ en: {
|
|
|
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.',
|
|
@@ -146,7 +146,7 @@ fr: {
|
|
|
146
146
|
'topbar.lang.title':'Langue de sortie — modifiable ici ou dans Personnalisation',
|
|
147
147
|
'topbar.agent.title':'Agent actif — modifiable ici ou dans Personnalisation',
|
|
148
148
|
'topbar.agent.none':'Aucun agent trouvé',
|
|
149
|
-
'board.filterPlaceholder':'Filtrer les tâches…','board.expandAll':'Tout déplier','board.collapseAll':'Tout replier',
|
|
149
|
+
'board.filterPlaceholder':'Filtrer les tâches…','board.expandAll':'Tout déplier','board.collapseAll':'Tout replier','board.hideSidebar':'Masquer la barre latérale','board.showSidebar':'Afficher la barre latérale',
|
|
150
150
|
'board.expandAllTitle':'Déplier ou replier toutes les phases','board.viewList':'Liste','board.viewKanban':'Kanban',
|
|
151
151
|
'board.viewList.title':'Regroupé par phase','board.viewKanban.title':'Colonnes par statut',
|
|
152
152
|
'board.noTasksMatch':'Aucune tâche ne correspond à ce filtre.',
|
|
@@ -203,7 +203,7 @@ fr: {
|
|
|
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é.',
|
|
@@ -258,7 +258,7 @@ es: {
|
|
|
258
258
|
'topbar.lang.title':'Idioma de salida — se cambia aquí o en Personalizar',
|
|
259
259
|
'topbar.agent.title':'Agente activo — se cambia aquí o en Personalizar',
|
|
260
260
|
'topbar.agent.none':'No se encontró ningún agente',
|
|
261
|
-
'board.filterPlaceholder':'Filtrar tareas…','board.expandAll':'Expandir todo','board.collapseAll':'Colapsar todo',
|
|
261
|
+
'board.filterPlaceholder':'Filtrar tareas…','board.expandAll':'Expandir todo','board.collapseAll':'Colapsar todo','board.hideSidebar':'Ocultar barra lateral','board.showSidebar':'Mostrar barra lateral',
|
|
262
262
|
'board.expandAllTitle':'Expandir o colapsar todas las fases','board.viewList':'Lista','board.viewKanban':'Kanban',
|
|
263
263
|
'board.viewList.title':'Agrupado por fase','board.viewKanban.title':'Columnas por estado',
|
|
264
264
|
'board.noTasksMatch':'Ninguna tarea coincide con este filtro.',
|
|
@@ -315,7 +315,7 @@ es: {
|
|
|
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.',
|
|
@@ -370,7 +370,7 @@ de: {
|
|
|
370
370
|
'topbar.lang.title':'Ausgabesprache — hier oder in Personalisieren änderbar',
|
|
371
371
|
'topbar.agent.title':'Aktiver Agent — hier oder in Personalisieren änderbar',
|
|
372
372
|
'topbar.agent.none':'Kein Agent gefunden',
|
|
373
|
-
'board.filterPlaceholder':'Aufgaben filtern…','board.expandAll':'Alle ausklappen','board.collapseAll':'Alle einklappen',
|
|
373
|
+
'board.filterPlaceholder':'Aufgaben filtern…','board.expandAll':'Alle ausklappen','board.collapseAll':'Alle einklappen','board.hideSidebar':'Seitenleiste ausblenden','board.showSidebar':'Seitenleiste einblenden',
|
|
374
374
|
'board.expandAllTitle':'Alle Phasen ein- oder ausklappen','board.viewList':'Liste','board.viewKanban':'Kanban',
|
|
375
375
|
'board.viewList.title':'Nach Phase gruppiert','board.viewKanban.title':'Spalten nach Status',
|
|
376
376
|
'board.noTasksMatch':'Keine Aufgabe entspricht diesem Filter.',
|
|
@@ -427,7 +427,7 @@ de: {
|
|
|
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.',
|
|
@@ -482,7 +482,7 @@ pt: {
|
|
|
482
482
|
'topbar.lang.title':'Idioma de saída — altere aqui ou em Personalizar',
|
|
483
483
|
'topbar.agent.title':'Agente ativo — altere aqui ou em Personalizar',
|
|
484
484
|
'topbar.agent.none':'Nenhum agente encontrado',
|
|
485
|
-
'board.filterPlaceholder':'Filtrar tarefas…','board.expandAll':'Expandir tudo','board.collapseAll':'Recolher tudo',
|
|
485
|
+
'board.filterPlaceholder':'Filtrar tarefas…','board.expandAll':'Expandir tudo','board.collapseAll':'Recolher tudo','board.hideSidebar':'Ocultar barra lateral','board.showSidebar':'Mostrar barra lateral',
|
|
486
486
|
'board.expandAllTitle':'Expandir ou recolher todas as fases','board.viewList':'Lista','board.viewKanban':'Kanban',
|
|
487
487
|
'board.viewList.title':'Agrupado por fase','board.viewKanban.title':'Colunas por estado',
|
|
488
488
|
'board.noTasksMatch':'Nenhuma tarefa corresponde a este filtro.',
|
|
@@ -539,7 +539,7 @@ pt: {
|
|
|
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.',
|
|
@@ -594,7 +594,7 @@ it: {
|
|
|
594
594
|
'topbar.lang.title':'Lingua di output — modificabile qui o in Personalizza',
|
|
595
595
|
'topbar.agent.title':'Agente attivo — modificabile qui o in Personalizza',
|
|
596
596
|
'topbar.agent.none':'Nessun agente trovato',
|
|
597
|
-
'board.filterPlaceholder':'Filtra attività…','board.expandAll':'Espandi tutto','board.collapseAll':'Comprimi tutto',
|
|
597
|
+
'board.filterPlaceholder':'Filtra attività…','board.expandAll':'Espandi tutto','board.collapseAll':'Comprimi tutto','board.hideSidebar':'Nascondi barra laterale','board.showSidebar':'Mostra barra laterale',
|
|
598
598
|
'board.expandAllTitle':'Espandi o comprimi tutte le fasi','board.viewList':'Elenco','board.viewKanban':'Kanban',
|
|
599
599
|
'board.viewList.title':'Raggruppato per fase','board.viewKanban.title':'Colonne per stato',
|
|
600
600
|
'board.noTasksMatch':'Nessuna attività corrisponde a questo filtro.',
|
|
@@ -651,7 +651,7 @@ it: {
|
|
|
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.',
|
|
@@ -89,6 +89,9 @@
|
|
|
89
89
|
</div>
|
|
90
90
|
<button class="mini-btn" id="phaseToggleAll" data-i18n-title="board.expandAllTitle" data-i18n="board.expandAll" title="Expand or collapse all phases">Expand all</button>
|
|
91
91
|
<input type="search" id="search" class="search" data-i18n-ph="board.filterPlaceholder" placeholder="Filter tasks…" autocomplete="off" />
|
|
92
|
+
<button class="mini-btn" id="sideToggle" type="button" data-i18n-title="board.hideSidebar" title="Hide sidebar">
|
|
93
|
+
<svg viewBox="0 0 18 18" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="2" y="3" width="14" height="12" rx="2"/><line x1="11.5" y1="3" x2="11.5" y2="15"/></svg>
|
|
94
|
+
</button>
|
|
92
95
|
</div>
|
|
93
96
|
<div class="board" id="board"></div>
|
|
94
97
|
</div>
|
|
@@ -267,6 +270,7 @@
|
|
|
267
270
|
</div>
|
|
268
271
|
</div>
|
|
269
272
|
<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>
|
|
273
|
+
<p class="chat-status" id="tabChatStatus" hidden><span class="spinner" aria-hidden="true"></span><span data-i18n="chat.running">Agent running…</span></p>
|
|
270
274
|
</div>
|
|
271
275
|
</section>
|
|
272
276
|
|
|
@@ -386,6 +390,7 @@
|
|
|
386
390
|
<button id="orchBtn" class="btn chat-send" data-i18n-title="chat.orchestrateTitle" data-i18n="action.orchestrate" title="Walk the enabled workflow">Orchestrate</button>
|
|
387
391
|
</div>
|
|
388
392
|
<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>
|
|
393
|
+
<p class="chat-status" id="widgetChatStatus" hidden><span class="spinner" aria-hidden="true"></span><span data-i18n="chat.running">Agent running…</span></p>
|
|
389
394
|
</div>
|
|
390
395
|
|
|
391
396
|
<div class="drawer" id="drawer" aria-hidden="true">
|
|
@@ -73,9 +73,14 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
73
73
|
.panel-sub { color:var(--muted); font-size:12.5px; margin:0 0 16px; }
|
|
74
74
|
.count { font-family:var(--mono); font-size:10px; color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:1px 6px; }
|
|
75
75
|
|
|
76
|
-
/* main column + right sidebar
|
|
76
|
+
/* main column + right sidebar — hideable (mainly to give Kanban's own-width columns more room
|
|
77
|
+
instead of forcing horizontal scroll on top of the sidebar already eating 300px) */
|
|
77
78
|
.main { min-width:0; display:flex; flex-direction:column; }
|
|
78
79
|
.side { border-left:1px solid var(--line); padding:18px 16px; display:flex; flex-direction:column; gap:22px; }
|
|
80
|
+
.panel[data-panel="board"].side-hidden { grid-template-columns:1fr; }
|
|
81
|
+
.panel[data-panel="board"].side-hidden .side { display:none; }
|
|
82
|
+
#sideToggle { display:inline-flex; align-items:center; justify-content:center; padding:5px 9px; }
|
|
83
|
+
#sideToggle[aria-pressed="true"] { color:var(--signal); border-color:var(--signal); }
|
|
79
84
|
.rail-title { font-size:11px; text-transform:uppercase; letter-spacing:.1em; color:var(--faint); margin:0 0 8px; font-weight:700; display:flex; gap:8px; align-items:center; }
|
|
80
85
|
.flatlist { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:5px; font-size:12.5px; }
|
|
81
86
|
.flatlist li { background:var(--surface); border:1px solid var(--line); border-radius:7px; padding:6px 9px; color:var(--muted); }
|
|
@@ -299,7 +304,10 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
299
304
|
.files-create-form .bl-add-input { width:100%; }
|
|
300
305
|
.files-create-actions { display:flex; gap:8px; }
|
|
301
306
|
.files-create-actions .btn { font-size:12px; padding:6px 11px; }
|
|
302
|
-
.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; }
|
|
303
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; }
|
|
304
312
|
.files-empty,.files-binary { padding:40px; text-align:center; color:var(--faint); font-style:italic; }
|
|
305
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); }
|
|
@@ -315,6 +323,10 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
315
323
|
.files-actions { display:flex; gap:8px; flex-shrink:0; }
|
|
316
324
|
.files-actions .btn { font-size:12px; padding:6px 11px; }
|
|
317
325
|
.files-body-col { flex:1; min-height:0; display:flex; flex-direction:column; }
|
|
326
|
+
.files-view,.files-editor { scrollbar-width:thin; scrollbar-color:var(--line) transparent; }
|
|
327
|
+
.files-view::-webkit-scrollbar,.files-editor::-webkit-scrollbar { width:8px; }
|
|
328
|
+
.files-view::-webkit-scrollbar-thumb,.files-editor::-webkit-scrollbar-thumb { background:var(--line); border-radius:8px; }
|
|
329
|
+
.files-view::-webkit-scrollbar-track,.files-editor::-webkit-scrollbar-track { background:transparent; }
|
|
318
330
|
.files-view { flex:1; min-height:0; overflow:auto; padding:16px 20px; }
|
|
319
331
|
.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; }
|
|
320
332
|
.files-iframe { flex:1; min-height:0; width:100%; border:0; background:#fff; }
|
|
@@ -389,9 +401,13 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
389
401
|
.btn.primary { background:var(--signal); color:var(--on-accent); border-color:transparent; font-weight:600; }
|
|
390
402
|
.empty { color:var(--faint); font-style:italic; font-size:12.5px; }
|
|
391
403
|
:focus-visible { outline:2px solid var(--cool); outline-offset:2px; }
|
|
392
|
-
/*
|
|
393
|
-
|
|
394
|
-
|
|
404
|
+
/* A fixed px breakpoint here would go stale the moment a tab is added or removed (exactly what
|
|
405
|
+
happened when the Files tab pushed the row from 10 to 11 items) — fitTabs() in app.js measures
|
|
406
|
+
the tab row's REAL overflow instead and adds this class, so it's correct at any tab count and any
|
|
407
|
+
viewport width, not just the count it happened to be tuned against once. Narrow-viewport padding
|
|
408
|
+
still tightens up below ~1180px regardless — smaller tabs simply need less to overflow. */
|
|
409
|
+
@media (max-width:1180px){ .tabs .tab{ padding:7px 8px; } }
|
|
410
|
+
.tabs-compact .tab-label { display:none; }
|
|
395
411
|
@media (max-width:900px){
|
|
396
412
|
.panel[data-panel="board"].is-active { grid-template-columns:1fr; }
|
|
397
413
|
.side { border-left:0; border-top:1px solid var(--line); flex-direction:row; flex-wrap:wrap; gap:16px 28px; }
|
|
@@ -448,6 +464,13 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
448
464
|
.chat-ta:focus { outline:2px solid var(--cool); outline-offset:1px; }
|
|
449
465
|
.chat-send { flex:0 0 auto; align-self:auto; padding:8px 16px; }
|
|
450
466
|
.chat-warn { font-size:10.5px; color:var(--signal); margin:6px 12px 10px; }
|
|
467
|
+
body.chat-busy .chat-warn { display:none; } /* the running status below replaces it — one message at a time */
|
|
468
|
+
.chat-status { display:flex; align-items:center; gap:8px; font-size:11.5px; color:var(--signal); margin:6px 12px 10px; font-weight:600; }
|
|
469
|
+
.chat-status[hidden] { display:none; }
|
|
470
|
+
.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; }
|
|
471
|
+
@keyframes chat-spin { to { transform:rotate(360deg); } }
|
|
472
|
+
.chat-send:disabled,.mini-btn:disabled { opacity:.5; cursor:not-allowed; }
|
|
473
|
+
@media (prefers-reduced-motion: reduce) { .chat-status .spinner { animation:none; } }
|
|
451
474
|
.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); }
|
|
452
475
|
.approval .c-actions { display:flex; gap:8px; }
|
|
453
476
|
#orchBtn,#tabOrchBtn { background:var(--cool); color:#04202a; border-color:transparent; font-weight:600; }
|
|
@@ -469,7 +492,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
469
492
|
.chat-tab-input { display:flex; gap:10px; margin-top:10px; }
|
|
470
493
|
.chat-tab-input .chat-ta { flex:1; min-height:56px; max-height:180px; }
|
|
471
494
|
.chat-tab-actions { display:flex; flex-direction:column; gap:8px; flex-shrink:0; }
|
|
472
|
-
.chat-tab-wrap .chat-warn { margin:8px 0 0; }
|
|
495
|
+
.chat-tab-wrap .chat-warn,.chat-tab-wrap .chat-status { margin:8px 0 0; }
|
|
473
496
|
@media (max-width:640px){ .chat-tab-input { flex-direction:column; } .chat-tab-actions { flex-direction:row; } }
|
|
474
497
|
|
|
475
498
|
/* ---- 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
|
}
|