spectoflow 0.22.2 → 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.2",
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",
@@ -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
@@ -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.',
@@ -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é.',
@@ -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.',
@@ -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.',
@@ -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.',
@@ -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.',
@@ -270,6 +270,7 @@
270
270
  </div>
271
271
  </div>
272
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>
273
274
  </div>
274
275
  </section>
275
276
 
@@ -389,6 +390,7 @@
389
390
  <button id="orchBtn" class="btn chat-send" data-i18n-title="chat.orchestrateTitle" data-i18n="action.orchestrate" title="Walk the enabled workflow">Orchestrate</button>
390
391
  </div>
391
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>
392
394
  </div>
393
395
 
394
396
  <div class="drawer" id="drawer" aria-hidden="true">
@@ -304,7 +304,10 @@ 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); }
@@ -320,6 +323,10 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
320
323
  .files-actions { display:flex; gap:8px; flex-shrink:0; }
321
324
  .files-actions .btn { font-size:12px; padding:6px 11px; }
322
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; }
323
330
  .files-view { flex:1; min-height:0; overflow:auto; padding:16px 20px; }
324
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; }
325
332
  .files-iframe { flex:1; min-height:0; width:100%; border:0; background:#fff; }
@@ -457,6 +464,13 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
457
464
  .chat-ta:focus { outline:2px solid var(--cool); outline-offset:1px; }
458
465
  .chat-send { flex:0 0 auto; align-self:auto; padding:8px 16px; }
459
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; } }
460
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); }
461
475
  .approval .c-actions { display:flex; gap:8px; }
462
476
  #orchBtn,#tabOrchBtn { background:var(--cool); color:#04202a; border-color:transparent; font-weight:600; }
@@ -478,7 +492,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
478
492
  .chat-tab-input { display:flex; gap:10px; margin-top:10px; }
479
493
  .chat-tab-input .chat-ta { flex:1; min-height:56px; max-height:180px; }
480
494
  .chat-tab-actions { display:flex; flex-direction:column; gap:8px; flex-shrink:0; }
481
- .chat-tab-wrap .chat-warn { margin:8px 0 0; }
495
+ .chat-tab-wrap .chat-warn,.chat-tab-wrap .chat-status { margin:8px 0 0; }
482
496
  @media (max-width:640px){ .chat-tab-input { flex-direction:column; } .chat-tab-actions { flex-direction:row; } }
483
497
 
484
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
  }