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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.22.0",
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
- @keyframes ob-orbit { to{ opacity:1; transform:rotate(var(--a)) translate(98px) rotate(calc(-1*var(--a))) scale(1); } }
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 &amp; run commands.',
94
+ 'chat.warn':'⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files &amp; 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 &amp; 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 &amp; 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
- /* Below ~1180px the 8 labelled tabs no longer fit alongside brand + right cluster,
393
- so tabs go icon-only (and scroll horizontally inside their own strip if still tight). */
394
- @media (max-width:1180px){ .tabs .tab{ padding:7px 8px; } .tab-label{ display:none; } }
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
- try { child = spawn(parts[0], [...parts.slice(1), p], { cwd: root, env: process.env }); }
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
- try { child = spawn(parts[0], [...parts.slice(1), prompt], { cwd: root, env: process.env }); }
43
- catch (e) { return { error: e.message }; }
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
  }