spectoflow 0.21.1 → 0.22.2

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.
@@ -56,9 +56,10 @@
56
56
  <button class="tab" data-tab="backlog" data-i18n-title="nav.backlog" title="Backlog"><span class="tab-ico" data-icon="backlog"></span><span class="tab-label" data-i18n="nav.backlog">Backlog</span></button>
57
57
  <button class="tab" data-tab="workflow" data-i18n-title="nav.workflow" title="Workflow"><span class="tab-ico" data-icon="workflow"></span><span class="tab-label" data-i18n="nav.workflow">Workflow</span></button>
58
58
  <button class="tab" data-tab="team" data-i18n-title="nav.team" title="Agents &amp; Skills"><span class="tab-ico" data-icon="agents"></span><span class="tab-label" data-i18n="nav.team">Agents &amp; Skills</span></button>
59
+ <button class="tab" data-tab="files" data-i18n-title="nav.files" title="Files"><span class="tab-ico" data-icon="files"></span><span class="tab-label" data-i18n="nav.files">Files</span></button>
59
60
  <button class="tab" data-tab="info" data-i18n-title="nav.info" title="Info"><span class="tab-ico" data-icon="info"></span><span class="tab-label" data-i18n="nav.info">Info</span></button>
60
61
  <button class="tab" data-tab="docs" data-i18n-title="nav.docs" title="Documentation"><span class="tab-ico" data-icon="docs"></span><span class="tab-label" data-i18n="nav.docs">Documentation</span></button>
61
- <button class="tab" data-tab="settings" data-i18n-title="nav.settings" title="Personalize"><span class="tab-ico" data-icon="settings"></span><span class="tab-label" data-i18n="nav.settings">Personalize</span></button>
62
+ <button class="tab" data-tab="personalize" data-i18n-title="nav.settings" title="Personalize"><span class="tab-ico" data-icon="settings"></span><span class="tab-label" data-i18n="nav.settings">Personalize</span></button>
62
63
  </nav>
63
64
  <div class="top-right">
64
65
  <span class="sync" id="sync"><span class="sync-dot"></span><span id="syncLabel">live</span></span>
@@ -88,6 +89,9 @@
88
89
  </div>
89
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>
90
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>
91
95
  </div>
92
96
  <div class="board" id="board"></div>
93
97
  </div>
@@ -152,6 +156,21 @@
152
156
  <button class="fchip" data-status="blocked" data-i18n="status.blocked">Blocked</button>
153
157
  </div>
154
158
  <input type="search" id="backlogSearch" class="search" data-i18n-ph="backlog.searchPlaceholder" placeholder="Filter by id or title…" autocomplete="off" />
159
+ <button id="backlogAddBtn" class="btn primary" type="button" data-i18n="backlog.addTask">+ Add task</button>
160
+ </div>
161
+ <div class="bl-add-form" id="backlogAddForm" hidden>
162
+ <input type="text" id="blAddTitle" class="bl-add-input bl-add-title" data-i18n-ph="backlog.addTitlePh" placeholder="Task title…" autocomplete="off" />
163
+ <input type="text" id="blAddPhase" class="bl-add-input bl-add-phase" list="blPhaseList" data-i18n-ph="backlog.addPhasePh" placeholder="Phase…" autocomplete="off" />
164
+ <datalist id="blPhaseList"></datalist>
165
+ <input type="text" id="blAddOwner" class="bl-add-input bl-add-owner" data-i18n-ph="backlog.addOwnerPh" placeholder="Owner…" autocomplete="off" />
166
+ <select id="blAddLevel" class="bl-add-input">
167
+ <option value="quick">quick</option>
168
+ <option value="standard" selected>standard</option>
169
+ <option value="major">major</option>
170
+ </select>
171
+ <button id="blAddSubmit" class="btn primary" type="button" data-i18n="backlog.addSubmit">Add</button>
172
+ <button id="blAddCancel" class="btn" type="button" data-i18n="backlog.addCancel">Cancel</button>
173
+ <span class="bl-add-error" id="blAddError" hidden></span>
155
174
  </div>
156
175
  <div class="table-scroll">
157
176
  <table class="backlog-table" id="backlogTable">
@@ -194,6 +213,35 @@
194
213
  </div>
195
214
  </section>
196
215
 
216
+ <!-- FILES (project file explorer: tree + view/edit Markdown, HTML and any text file) -->
217
+ <section class="panel" data-panel="files">
218
+ <div class="files-wrap">
219
+ <h2 class="panel-title" data-i18n="files.title">Files</h2>
220
+ <p class="panel-sub" data-i18n="files.sub">Browse the project's files — view Markdown &amp; HTML, edit any text file, create new ones.</p>
221
+ <div class="files-body">
222
+ <div class="files-tree-col">
223
+ <div class="files-toolbar">
224
+ <button id="filesNewFile" class="btn" type="button" data-i18n="files.newFile">+ File</button>
225
+ <button id="filesNewFolder" class="btn" type="button" data-i18n="files.newFolder">+ Folder</button>
226
+ <button id="filesRefresh" class="btn" type="button" title="Refresh" data-i18n-title="files.refresh">⟳</button>
227
+ </div>
228
+ <div class="files-create-form" id="filesCreateForm" hidden>
229
+ <input type="text" id="filesCreateInput" class="bl-add-input" autocomplete="off" />
230
+ <div class="files-create-actions">
231
+ <button id="filesCreateGo" class="btn primary" type="button" data-i18n="files.create">Create</button>
232
+ <button id="filesCreateCancel" class="btn" type="button" data-i18n="backlog.addCancel">Cancel</button>
233
+ </div>
234
+ <span class="files-error-tip" id="filesCreateError" hidden></span>
235
+ </div>
236
+ <div class="files-tree" id="filesTree"></div>
237
+ </div>
238
+ <div class="files-content-col" id="filesContent">
239
+ <div class="files-empty" data-i18n="files.pickFile">Select a file to view it.</div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </section>
244
+
197
245
  <!-- CHAT (full-width chat tab — shares renderChatLog + doRun/doOrchestrate/approve with the floating widget) -->
198
246
  <section class="panel" data-panel="chat">
199
247
  <div class="chat-tab-wrap">
@@ -244,41 +292,47 @@
244
292
  </section>
245
293
 
246
294
  <!-- SETTINGS (change autonomy mode + output language → config.json) -->
247
- <section class="panel" data-panel="settings">
295
+ <section class="panel" data-panel="personalize">
248
296
  <div class="settings-wrap">
249
297
  <h2 class="panel-title" data-i18n="nav.settings">Personalize</h2>
250
298
  <p class="panel-sub" data-i18n-html="settings.sub">Change how spectoflow runs. Writes <code>.spectoflow/config.json</code> — the agent picks it up on its next run.</p>
251
- <div class="settings-card">
252
- <label class="settings-field">
253
- <span class="settings-field-label" data-i18n="info.activeAgent">Active agent</span>
254
- <select id="setAgent" class="settings-select"></select>
255
- <span class="settings-field-hint is-empty" id="setAgentHint" data-i18n="topbar.agent.none" hidden>No agent found</span>
256
- </label>
257
- <label class="settings-field">
258
- <span class="settings-field-label" data-i18n="field.design">Dashboard design</span>
259
- <select id="setDesign" class="settings-select"></select>
260
- </label>
261
- <label class="settings-field">
262
- <span class="settings-field-label" data-i18n="field.mode">Autonomy mode</span>
263
- <select id="setMode" class="settings-select">
264
- <option value="autopilot" data-i18n="mode.autopilot.desc">autopilot — run freely, ask only on policy gates</option>
265
- <option value="semi" data-i18n="mode.semi.desc">semi — autopilot + policy approvals</option>
266
- <option value="manual" data-i18n="mode.manual.desc">manual — approve every step</option>
267
- </select>
268
- </label>
269
- <label class="settings-field">
270
- <span class="settings-field-label" data-i18n="field.lang">Output language</span>
271
- <select id="setLang" class="settings-select">
272
- <option value="en">en · English</option>
273
- <option value="fr">fr · Français</option>
274
- <option value="es">es · Español</option>
275
- <option value="de">de · Deutsch</option>
276
- <option value="pt">pt · Português</option>
277
- <option value="it">it · Italiano</option>
278
- </select>
279
- </label>
280
- <div class="settings-saved" id="settingsSaved" data-i18n="settings.saved" hidden>✓ saved</div>
299
+ <div class="settings-groups">
300
+ <div class="settings-card">
301
+ <h3 class="settings-card-title" data-i18n="settings.group.execution">Agent &amp; automation</h3>
302
+ <label class="settings-field">
303
+ <span class="settings-field-label" data-i18n="info.activeAgent">Active agent</span>
304
+ <select id="setAgent" class="settings-select"></select>
305
+ <span class="settings-field-hint is-empty" id="setAgentHint" data-i18n="topbar.agent.none" hidden>No agent found</span>
306
+ </label>
307
+ <label class="settings-field">
308
+ <span class="settings-field-label" data-i18n="field.mode">Autonomy mode</span>
309
+ <select id="setMode" class="settings-select">
310
+ <option value="autopilot" data-i18n="mode.autopilot.desc">autopilot — run freely, ask only on policy gates</option>
311
+ <option value="semi" data-i18n="mode.semi.desc">semi — autopilot + policy approvals</option>
312
+ <option value="manual" data-i18n="mode.manual.desc">manual — approve every step</option>
313
+ </select>
314
+ </label>
315
+ </div>
316
+ <div class="settings-card">
317
+ <h3 class="settings-card-title" data-i18n="settings.group.appearance">Appearance &amp; language</h3>
318
+ <label class="settings-field">
319
+ <span class="settings-field-label" data-i18n="field.design">Dashboard design</span>
320
+ <select id="setDesign" class="settings-select"></select>
321
+ </label>
322
+ <label class="settings-field">
323
+ <span class="settings-field-label" data-i18n="field.lang">Output language</span>
324
+ <select id="setLang" class="settings-select">
325
+ <option value="en">en · English</option>
326
+ <option value="fr">fr · Français</option>
327
+ <option value="es">es · Español</option>
328
+ <option value="de">de · Deutsch</option>
329
+ <option value="pt">pt · Português</option>
330
+ <option value="it">it · Italiano</option>
331
+ </select>
332
+ </label>
333
+ </div>
281
334
  </div>
335
+ <div class="settings-saved" id="settingsSaved" data-i18n="settings.saved" hidden>✓ saved</div>
282
336
  <div class="settings-readonly" id="settingsReadonly"></div>
283
337
 
284
338
  <!-- Customize: project-specific dashboards, skills and agents — described or auto-proposed,
@@ -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); }
@@ -207,7 +212,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
207
212
  .tflag.fail { color:var(--s-blocked); border:1px solid color-mix(in srgb,var(--s-blocked) 45%,var(--line)); }
208
213
 
209
214
  /* Requests tab (tasks awaiting review: to_validate / to_analyze) */
210
- .requests-wrap { padding:26px 28px 60px; max-width:900px; }
215
+ .requests-wrap { padding:26px 28px 60px; max-width:1400px; }
211
216
  .requests-list { display:flex; flex-direction:column; gap:7px; }
212
217
  .request-row { cursor:pointer; display:flex; align-items:center; gap:10px; background:var(--surface); border:1px solid var(--line); border-radius:10px; padding:9px 12px; font-size:13px; transition:border-color .15s; }
213
218
  .request-row:hover,.request-row:focus-visible { border-color:color-mix(in srgb,var(--signal) 40%,var(--line)); }
@@ -220,8 +225,15 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
220
225
  .placeholder-empty { margin-top:8px; padding:40px; border:1px dashed var(--line); border-radius:var(--radius); text-align:center; color:var(--faint); font-style:italic; }
221
226
 
222
227
  /* Backlog tab — flat, sortable, filterable table of every task */
223
- .backlog-wrap { padding:26px 28px 60px; max-width:1200px; }
228
+ .backlog-wrap { padding:26px 28px 60px; max-width:1500px; }
224
229
  .backlog-wrap .filters { padding:0 0 14px; }
230
+ .bl-add-form { display:none; align-items:center; gap:8px; flex-wrap:wrap; padding:0 0 14px; }
231
+ .bl-add-form.is-open { display:flex; }
232
+ .bl-add-input { font:inherit; font-size:12.5px; padding:6px 11px; border:1px solid var(--line); border-radius:8px; background:var(--surface); color:var(--ink); }
233
+ .bl-add-input:focus { outline:2px solid var(--signal); outline-offset:1px; }
234
+ .bl-add-title { flex:1; min-width:220px; }
235
+ .bl-add-phase,.bl-add-owner { width:150px; }
236
+ .bl-add-error { font-size:12px; color:var(--s-blocked); }
225
237
  .table-scroll { width:100%; overflow-x:auto; border:1px solid var(--line); border-radius:var(--radius); }
226
238
  .backlog-table { width:100%; min-width:720px; border-collapse:collapse; font-size:12.5px; }
227
239
  .backlog-table thead th { position:sticky; top:0; z-index:1; background:var(--surface-2); text-align:left; padding:9px 12px; font-family:var(--mono); font-size:10.5px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); white-space:nowrap; cursor:pointer; user-select:none; border-bottom:1px solid var(--line); }
@@ -243,8 +255,12 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
243
255
  @media (max-width:640px){ .backlog-wrap { padding:18px 14px 40px; } }
244
256
 
245
257
  /* Info tab (project overview — config, runners, counts, specs, active workflow) */
246
- .info-wrap { padding:26px 28px 60px; max-width:1000px; }
247
- .info-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:14px; margin-top:8px; }
258
+ .info-wrap { padding:26px 28px 60px; max-width:1500px; }
259
+ /* a fixed 2-column grid, not auto-fit: with 5 sections (2 + 1 full-span + 2), more columns would
260
+ leave the 2-item rows with genuinely empty trailing cells — the same dead space this was meant to
261
+ fix, just moved inside the grid. 2 columns always fill completely; the wider .info-wrap above is
262
+ what actually puts the freed width to use, by making each card roomier. */
263
+ .info-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:14px; margin-top:8px; align-items:start; }
248
264
  .info-section { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:16px 18px; }
249
265
  .info-section-head { display:flex; align-items:center; gap:8px; margin:0 0 12px; }
250
266
  .info-section-ico { width:16px; height:16px; flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; color:var(--cool); }
@@ -275,6 +291,43 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
275
291
  /* workflow diagram */
276
292
  .wf-wrap,.team-wrap { padding:26px 28px 60px; max-width:1000px; }
277
293
  .wf-wrap { max-width:1340px; } /* wider so the horizontal pipeline fits its 9 steps on one line */
294
+ .team-wrap { max-width:1500px; } /* the agent/skill .cards grid fills more columns instead of sitting capped */
295
+
296
+ /* Files tab — a two-pane explorer: a collapsible tree, and the selected file's preview/editor */
297
+ .files-wrap { padding:26px 28px 60px; max-width:1600px; height:calc(100vh - 130px); display:flex; flex-direction:column; }
298
+ .files-body { flex:1; min-height:0; display:flex; gap:16px; margin-top:8px; }
299
+ .files-tree-col { width:280px; flex-shrink:0; display:flex; flex-direction:column; gap:8px; min-height:0; }
300
+ .files-toolbar { display:flex; gap:8px; }
301
+ .files-toolbar .btn { font-size:12px; padding:6px 11px; }
302
+ .files-create-form { display:none; flex-direction:column; gap:6px; padding:10px; border:1px solid var(--line); border-radius:var(--radius); background:var(--surface); }
303
+ .files-create-form.is-open { display:flex; }
304
+ .files-create-form .bl-add-input { width:100%; }
305
+ .files-create-actions { display:flex; gap:8px; }
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); }
308
+ .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
+ .files-empty,.files-binary { padding:40px; text-align:center; color:var(--faint); font-style:italic; }
310
+ .f-row { display:flex; align-items:center; gap:6px; padding:4px 6px; border-radius:6px; cursor:pointer; font-size:12.5px; white-space:nowrap; color:var(--muted); }
311
+ .f-row:hover { background:var(--surface-2); color:var(--ink); }
312
+ .f-row.is-active { background:var(--surface-2); color:var(--signal); font-weight:600; }
313
+ .f-row .f-chevron { width:11px; height:11px; flex-shrink:0; transition:transform .15s; color:var(--faint); }
314
+ .f-row.is-open .f-chevron { transform:rotate(90deg); }
315
+ .f-row .f-name { overflow:hidden; text-overflow:ellipsis; }
316
+ .f-children { margin-left:13px; border-left:1px solid var(--line); padding-left:6px; }
317
+ .f-children[hidden] { display:none; }
318
+ .files-toolbar-row { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:10px 14px; border-bottom:1px solid var(--line); }
319
+ .files-path { font-family:var(--mono); font-size:12px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
320
+ .files-actions { display:flex; gap:8px; flex-shrink:0; }
321
+ .files-actions .btn { font-size:12px; padding:6px 11px; }
322
+ .files-body-col { flex:1; min-height:0; display:flex; flex-direction:column; }
323
+ .files-view { flex:1; min-height:0; overflow:auto; padding:16px 20px; }
324
+ .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
+ .files-iframe { flex:1; min-height:0; width:100%; border:0; background:#fff; }
326
+ .files-md-toolbar { display:flex; gap:6px; padding:8px 14px; border-bottom:1px solid var(--line); }
327
+ .files-md-toolbar button { font-family:var(--mono); font-size:12px; padding:4px 9px; border:1px solid var(--line); border-radius:6px; background:var(--surface-2); color:var(--ink); cursor:pointer; }
328
+ .files-saved-tip { font-size:11.5px; color:var(--s-done); font-family:var(--mono); }
329
+ .files-error-tip { font-size:11.5px; color:var(--s-blocked); }
330
+ @media (max-width:820px){ .files-body { flex-direction:column; } .files-tree-col { width:auto; max-height:220px; } .files-wrap { height:auto; } }
278
331
  .wf-diagram { display:block; margin-top:8px; }
279
332
  .wf-step { display:flex; align-items:center; }
280
333
  .wf-node { display:flex; align-items:center; gap:8px; padding:11px 15px; border:1px solid var(--line); border-radius:var(--radius); background:var(--surface); cursor:pointer; transition:border-color .18s,transform .15s,box-shadow .15s; user-select:none; }
@@ -341,9 +394,13 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
341
394
  .btn.primary { background:var(--signal); color:var(--on-accent); border-color:transparent; font-weight:600; }
342
395
  .empty { color:var(--faint); font-style:italic; font-size:12.5px; }
343
396
  :focus-visible { outline:2px solid var(--cool); outline-offset:2px; }
344
- /* Below ~1180px the 8 labelled tabs no longer fit alongside brand + right cluster,
345
- so tabs go icon-only (and scroll horizontally inside their own strip if still tight). */
346
- @media (max-width:1180px){ .tabs .tab{ padding:7px 8px; } .tab-label{ display:none; } }
397
+ /* A fixed px breakpoint here would go stale the moment a tab is added or removed (exactly what
398
+ happened when the Files tab pushed the row from 10 to 11 items) — fitTabs() in app.js measures
399
+ the tab row's REAL overflow instead and adds this class, so it's correct at any tab count and any
400
+ viewport width, not just the count it happened to be tuned against once. Narrow-viewport padding
401
+ still tightens up below ~1180px regardless — smaller tabs simply need less to overflow. */
402
+ @media (max-width:1180px){ .tabs .tab{ padding:7px 8px; } }
403
+ .tabs-compact .tab-label { display:none; }
347
404
  @media (max-width:900px){
348
405
  .panel[data-panel="board"].is-active { grid-template-columns:1fr; }
349
406
  .side { border-left:0; border-top:1px solid var(--line); flex-direction:row; flex-wrap:wrap; gap:16px 28px; }
@@ -373,7 +430,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
373
430
  .chat-fab.is-open { box-shadow:inset 0 0 0 2px var(--signal); }
374
431
  .chat { position:fixed; right:20px; bottom:88px; z-index:18; width:min(420px,94vw); height:min(600px,78vh); background:var(--surface); border:1px solid var(--line); border-radius:calc(var(--radius) + 2px); box-shadow:var(--shadow); display:flex; flex-direction:column; overflow:hidden; transform-origin:bottom right; transition:opacity .16s,transform .16s; }
375
432
  .chat[aria-hidden="true"] { opacity:0; transform:scale(.96) translateY(8px); pointer-events:none; }
376
- .chat-head { display:flex; align-items:center; gap:10px; padding:14px 16px; border-bottom:1px solid var(--line); background:linear-gradient(180deg,color-mix(in srgb,var(--surface-2) 75%,transparent),transparent); }
433
+ .chat-head { display:flex; align-items:center; gap:10px; padding:14px 16px; border-bottom:1px solid var(--line); background:color-mix(in srgb,var(--surface-2) 40%,transparent); }
377
434
  .chat-head-text { display:flex; flex-direction:column; gap:1px; min-width:0; }
378
435
  .chat-title { font-weight:700; font-size:15px; }
379
436
  .chat-sub { font-size:10px; color:var(--faint); text-transform:uppercase; letter-spacing:.04em; }
@@ -548,8 +605,10 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
548
605
  .kpi.has-accent { border-left-width:3px; border-left-style:solid; }
549
606
 
550
607
  /* Settings tab */
551
- .settings-wrap { max-width:720px; margin:0 auto; padding:20px 22px; }
552
- .settings-card { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:18px; display:flex; flex-direction:column; gap:16px; margin:14px 0; }
608
+ .settings-wrap { max-width:1120px; margin:0 auto; padding:20px 22px; }
609
+ .settings-groups { display:grid; grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); gap:16px; margin:14px 0; }
610
+ .settings-card { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:18px; display:flex; flex-direction:column; gap:16px; margin:0; }
611
+ .settings-card-title { margin:0; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); }
553
612
  .settings-field { display:flex; flex-direction:column; gap:6px; }
554
613
  .settings-field-label { font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
555
614
  .settings-select { font-family:inherit; font-size:14px; padding:9px 11px; border:1px solid var(--line); border-radius:9px; background:var(--surface-2); color:var(--ink); cursor:pointer; }
@@ -575,7 +634,9 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
575
634
  /* ---- Customize (Settings → dashboards/skills/agents) — reuses .card/.chat-ta/.btn styling so it
576
635
  reads as part of the same system, not a bolted-on sub-app. ---- */
577
636
  .cz-wrap { margin-top:28px; }
578
- .cz-block { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:14px 16px; margin-top:12px; }
637
+ #czRoot { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:12px; margin-top:12px; }
638
+ #czRoot.has-open { grid-template-columns:1fr; } /* one block's form open — stack instead of leaving empty cells beside the other two */
639
+ .cz-block { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:14px 16px; margin-top:0; }
579
640
  .cz-head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
580
641
  .cz-head h3 { margin:0; font-size:13.5px; font-weight:700; }
581
642
  .cz-add { font-size:12.5px; padding:6px 13px; }
@@ -790,7 +851,7 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
790
851
  /* Obsidian Ops fonts: JetBrains Mono (mono + display) + IBM Plex Sans (body) */
791
852
  :root[data-design="obsidian"] { --sans:'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; --mono:'JetBrains Mono', ui-monospace, monospace; --display:'JetBrains Mono', ui-monospace, monospace; }
792
853
 
793
- /* --- Neon Command: glassmorphism, aurora violet + cyan, control-room --- */
854
+ /* --- Neon Command: violet + cyan control-room, flat surfaces (no gradients/aurora glow) --- */
794
855
  :root[data-design="neon-command"] {
795
856
  --bg:#0d0b1a; --surface:#171233; --surface-2:#1f1940; --line:#2e2857;
796
857
  --ink:#eae6ff; --muted:#a99fd6; --faint:#6f689e;
@@ -799,13 +860,7 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
799
860
  --radius:16px; --shadow:0 18px 50px rgba(0,0,0,.55);
800
861
  --sans:'Sora', system-ui, sans-serif; --display:'Space Grotesk', system-ui, sans-serif;
801
862
  }
802
- :root[data-design="neon-command"] body {
803
- background:
804
- radial-gradient(1200px 600px at 15% -5%, rgba(124,77,255,.22), transparent 60%),
805
- radial-gradient(1000px 520px at 100% 8%, rgba(0,229,255,.14), transparent 55%),
806
- var(--bg);
807
- background-attachment:fixed;
808
- }
863
+ :root[data-design="neon-command"] body { background:var(--bg); }
809
864
  :root[data-design="neon-command"] .kpi,
810
865
  :root[data-design="neon-command"] .ocard,
811
866
  :root[data-design="neon-command"] .card,
@@ -814,14 +869,13 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
814
869
  :root[data-design="neon-command"] .settings-readonly,
815
870
  :root[data-design="neon-command"] .wf-pop,
816
871
  :root[data-design="neon-command"] .approval {
817
- background:linear-gradient(160deg, color-mix(in srgb,var(--surface) 80%,transparent), color-mix(in srgb,var(--surface-2) 50%,transparent));
872
+ background:var(--surface);
818
873
  border-color:color-mix(in srgb,var(--signal) 22%,var(--line));
819
- backdrop-filter:blur(10px);
820
874
  }
821
- :root[data-design="neon-command"] .topbar { background:color-mix(in srgb,var(--bg) 68%,transparent); border-bottom-color:color-mix(in srgb,var(--signal) 22%,var(--line)); }
875
+ :root[data-design="neon-command"] .topbar { background:color-mix(in srgb,var(--bg) 92%,transparent); border-bottom-color:color-mix(in srgb,var(--signal) 22%,var(--line)); }
822
876
  :root[data-design="neon-command"] .btn.primary,
823
- :root[data-design="neon-command"] .run-btn { background:linear-gradient(120deg,var(--signal),var(--cool)); color:#fff; border-color:transparent; box-shadow:0 6px 20px color-mix(in srgb,var(--signal) 45%,transparent); }
824
- :root[data-design="neon-command"] .brand-name { background:linear-gradient(120deg,var(--signal),var(--cool)); -webkit-background-clip:text; background-clip:text; color:transparent; }
877
+ :root[data-design="neon-command"] .run-btn { background:var(--signal); color:#fff; border-color:transparent; }
878
+ :root[data-design="neon-command"] .brand-name { color:var(--signal); }
825
879
  :root[data-design="neon-command"] .tab.is-active { background:color-mix(in srgb,var(--signal) 20%,transparent); box-shadow:inset 0 -2px 0 var(--cool); }
826
880
  :root[data-design="neon-command"] .wf-circle { backdrop-filter:blur(6px); }
827
881
  :root[data-design="neon-command"][data-theme="light"] {
@@ -13,6 +13,7 @@ const { startRun } = require('./runner');
13
13
  const { runSummarize } = require('./summarize');
14
14
  const orchestrator = require('./orchestrator');
15
15
  const agentsRegistry = require('../lib/agents-registry');
16
+ const files = require('./files');
16
17
 
17
18
  const PORT = process.env.SPECTOFLOW_PORT ? Number(process.env.SPECTOFLOW_PORT) : 4319;
18
19
  const PUBLIC = path.join(__dirname, 'public');
@@ -71,26 +72,9 @@ function writeConfig(patch){
71
72
  fs.writeFileSync(cp, JSON.stringify(cfg, null, 2) + '\n');
72
73
  return cfg;
73
74
  }
74
- // Next free T-### id across every plan file (absolute paths from store.readPlans).
75
- function nextTaskId(){
76
- let max = 0;
77
- for (const pl of store.readPlans(ROOT)) {
78
- try { const t = fs.readFileSync(pl.file, 'utf8'); const re = /\bT-(\d+)/g; let m; while ((m = re.exec(t))) max = Math.max(max, Number(m[1])); } catch {}
79
- }
80
- return 'T-' + String(max + 1).padStart(3, '0');
81
- }
82
75
  // Promote an attention item into a real checkbox task under an `## Attention` phase.
83
76
  function promoteAttention(item){
84
- const plans = store.readPlans(ROOT);
85
- let file = plans[0] && plans[0].file;
86
- if (!file) { file = path.join(ROOT, 'plans', 'inbox.md'); fs.mkdirSync(path.dirname(file), { recursive: true }); if (!fs.existsSync(file)) fs.writeFileSync(file, '# Inbox\n'); }
87
- const id = nextTaskId();
88
- let text = fs.readFileSync(file, 'utf8');
89
- const line = `- [ ] ${id} ${String(item.text).replace(/\s+/g, ' ').trim()} @user ~standard`;
90
- if (/^##\s+Attention\s*$/m.test(text)) text = text.replace(/^(##\s+Attention\s*)$/m, `$1\n${line}`);
91
- else { if (!text.endsWith('\n')) text += '\n'; text += `\n## Attention\n${line}\n`; }
92
- fs.writeFileSync(file, text);
93
- return { id, file };
77
+ return store.addTask(ROOT, { phase: 'Attention', title: item.text, owner: 'user' });
94
78
  }
95
79
 
96
80
  function watch(dir){ try{ fs.watch(dir,{recursive:false},()=>emit({type:'change'})); }catch(_){} }
@@ -131,12 +115,39 @@ const server = http.createServer(async (req,res)=>{
131
115
  return sendJSON(res, 200, { content: fs.readFileSync(real, 'utf8') });
132
116
  }
133
117
 
118
+ // ---- File Explorer: browse/read/write/create anywhere under the project root ----
119
+ if (p === '/api/files/tree' && req.method === 'GET') return sendJSON(res, 200, { tree: files.tree(ROOT) });
120
+ if (p === '/api/files/read' && req.method === 'GET') {
121
+ const rel = new URL(req.url, 'http://x').searchParams.get('path') || '';
122
+ const r = files.readFile(ROOT, rel);
123
+ return sendJSON(res, r.error ? 400 : 200, r);
124
+ }
125
+ if (p === '/api/files/write' && req.method === 'POST') {
126
+ const { path: rel, content } = await body(req);
127
+ const r = files.writeFile(ROOT, rel, content);
128
+ if (r.error) return sendJSON(res, 400, r);
129
+ emit({ type: 'change' }); return sendJSON(res, 200, r);
130
+ }
131
+ if (p === '/api/files/mkdir' && req.method === 'POST') {
132
+ const { path: rel } = await body(req);
133
+ const r = files.mkdir(ROOT, rel);
134
+ if (r.error) return sendJSON(res, 400, r);
135
+ emit({ type: 'change' }); return sendJSON(res, 200, r);
136
+ }
137
+
134
138
  if(p==='/api/events'){
135
139
  res.writeHead(200,{'Content-Type':'text/event-stream','Cache-Control':'no-cache',Connection:'keep-alive'});
136
140
  res.write('data: '+JSON.stringify({type:'hello'})+'\n\n');
137
141
  clients.add(res); req.on('close',()=>clients.delete(res)); return;
138
142
  }
139
143
 
144
+ // ---- manual task creation: add a checkbox task straight to a plan, no agent involved ----
145
+ if(p==='/api/task'&&req.method==='POST'){
146
+ const { title, phase, file, owner, level } = await body(req);
147
+ if(!title||!String(title).trim()) return sendJSON(res,400,{error:'A title is required.'});
148
+ const t = store.addTask(ROOT,{ title:String(title).trim(), phase, file, owner, level });
149
+ emit({type:'change'}); return sendJSON(res,200,{task:t});
150
+ }
140
151
  if(p.startsWith('/api/task/')&&req.method==='PATCH'){
141
152
  const id=decodeURIComponent(p.split('/')[3]||''); const patch=await body(req);
142
153
  const file=findPlanFileForTask(id); if(!file) return sendJSON(res,404,{error:`Task ${id} not found.`});
@@ -1,8 +1,10 @@
1
1
  'use strict';
2
2
  /*
3
- * Chat "Summarize" — condenses the recent group-chat log into one digest message, via the same
3
+ * Chat "Summarize" — condenses the group-chat log down to one digest message, via the same
4
4
  * configured agent runner (kept separate from runner.js since it captures the child's raw stdout as
5
5
  * one summary, not sentinel-parsed lines; unit-testable without an HTTP server, same as runner.js).
6
+ * Deliberately REPLACES runtime.messages rather than appending: a digest that leaves the full history
7
+ * sitting right below it doesn't condense anything — it just adds noise on top of noise.
6
8
  */
7
9
  const { spawn } = require('child_process');
8
10
  const store = require('../lib/store');
@@ -16,8 +18,9 @@ function formatLog(messages, limit = DEFAULT_LIMIT) {
16
18
  }
17
19
 
18
20
  // Summarizes runtime.messages (excluding prior summaries, so re-summarizing doesn't compound) into
19
- // one new message of kind 'summary'. Returns { child } on success or { error } — mirrors startRun's
20
- // shape, without the sentinel-line streaming runner.js does (this is a one-shot digest, not a task run).
21
+ // one new message of kind 'summary', REPLACING the log. Returns { child } on success or { error } —
22
+ // mirrors startRun's shape, without the sentinel-line streaming runner.js does (this is a one-shot
23
+ // digest, not a task run).
21
24
  function runSummarize(root, { agent } = {}, emit) {
22
25
  const cfg = store.readConfig(root);
23
26
  const which = agent || cfg.agent || 'claude';
@@ -27,9 +30,11 @@ function runSummarize(root, { agent } = {}, emit) {
27
30
  const rt = store.readRuntime(root);
28
31
  const messages = (rt.messages || []).filter((m) => m.kind !== 'summary');
29
32
  if (!messages.length) return { error: 'Nothing to summarize yet.' };
33
+ const summarizedIds = new Set(messages.map((m) => m.id));
30
34
 
31
35
  const prompt = 'Summarize this project\'s recent activity log in 3-6 concise bullet points — '
32
- + 'what was built, decided, or is blocked. Reply with the summary only, no preamble or sentinel lines.'
36
+ + 'what was built, decided, or is blocked. Reply with the summary text only: no preamble, no '
37
+ + 'sentinel lines, and do not read, search or edit any files — answer using only the log below.'
33
38
  + '\n\n' + formatLog(messages);
34
39
 
35
40
  const parts = cmdStr.split(/\s+/).filter(Boolean);
@@ -43,8 +48,22 @@ function runSummarize(root, { agent } = {}, emit) {
43
48
  child.stderr && child.stderr.on('data', (d) => { out += d.toString(); });
44
49
  child.on('close', (code) => {
45
50
  const text = out.trim() || (code === 0 ? '(no output)' : `summarize failed (exit ${code})`);
46
- const m = store.appendMessage(root, { role: which, kind: 'summary', text, agent: which });
47
- if (emit) { emit({ type: 'message', message: m }); emit({ type: 'change' }); }
51
+ const summary = {
52
+ id: 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
53
+ at: new Date().toISOString(),
54
+ kind: 'summary',
55
+ role: which,
56
+ agent: which,
57
+ text,
58
+ };
59
+ // Re-read fresh (not the earlier snapshot): drop exactly the messages that went INTO this
60
+ // summary, keep anything that arrived while the agent was running — condensing the log must
61
+ // never silently lose a message someone sent in the meantime.
62
+ const fresh = store.readRuntime(root);
63
+ fresh.messages = (fresh.messages || []).filter((m) => !summarizedIds.has(m.id));
64
+ fresh.messages.push(summary);
65
+ store.writeRuntime(root, fresh);
66
+ if (emit) { emit({ type: 'message', message: summary }); emit({ type: 'change' }); }
48
67
  });
49
68
  return { child };
50
69
  }
@@ -139,6 +139,48 @@ function addTaskComment(projectRoot, file, id, text, author) {
139
139
  return false;
140
140
  }
141
141
 
142
+ // Next free T-### id across every plan file — scans for the highest existing T-<n> and returns n+1.
143
+ function nextTaskId(projectRoot) {
144
+ let max = 0;
145
+ for (const pl of readPlans(projectRoot)) {
146
+ for (const ph of pl.phases) {
147
+ for (const t of ph.tasks) {
148
+ const m = /^T-(\d+)$/.exec(t.id);
149
+ if (m) max = Math.max(max, Number(m[1]));
150
+ }
151
+ }
152
+ }
153
+ return 'T-' + String(max + 1).padStart(3, '0');
154
+ }
155
+
156
+ // Add a new checkbox task, generating its id. `file` (a basename from readPlans, e.g. "0001-x.md")
157
+ // picks which plan; when omitted, the first existing plan file is used, or `plans/inbox.md` is
158
+ // created if the project has no plans yet. `phase` is the "## " heading the task is filed under —
159
+ // created at the end of the file if it doesn't already exist. Returns { id, file }.
160
+ function addTask(projectRoot, { file, phase, title, owner, level, status } = {}) {
161
+ const dirName = resolvePlansDir(projectRoot, readConfig(projectRoot));
162
+ const dir = path.join(projectRoot, dirName);
163
+ const plans = readPlans(projectRoot);
164
+ let fp;
165
+ if (file && plans.some((pl) => pl.file === file)) fp = path.join(dir, file);
166
+ else if (plans[0]) fp = path.join(dir, plans[0].file);
167
+ else { fs.mkdirSync(dir, { recursive: true }); fp = path.join(dir, 'inbox.md'); if (!fs.existsSync(fp)) writeAtomic(fp, '# Inbox\n'); }
168
+
169
+ const id = nextTaskId(projectRoot);
170
+ const phaseName = (phase && String(phase).trim()) || 'Backlog';
171
+ const line = buildTaskLine({
172
+ indent: '', id, title: String(title).replace(/\s+/g, ' ').trim(),
173
+ owner: (owner && String(owner).trim()) || null, level: level || 'standard', status: status || 'todo',
174
+ });
175
+
176
+ let text = fs.readFileSync(fp, 'utf8');
177
+ const headingRe = new RegExp(`^##\\s+${phaseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm');
178
+ if (headingRe.test(text)) text = text.replace(headingRe, (m) => `${m}\n${line}`);
179
+ else { if (!text.endsWith('\n')) text += '\n'; text += `\n## ${phaseName}\n${line}\n`; }
180
+ writeAtomic(fp, text);
181
+ return { id, file: path.basename(fp) };
182
+ }
183
+
142
184
  function writeAtomic(fp, content) {
143
185
  const tmp = fp + '.tmp';
144
186
  fs.writeFileSync(tmp, content, 'utf8');
@@ -331,6 +373,7 @@ function readSkills(projectRoot) {
331
373
 
332
374
  module.exports = {
333
375
  parseTaskLine, buildTaskLine, parsePlan, readPlans, readSpecs, updateTaskLine, addTaskComment,
376
+ nextTaskId, addTask,
334
377
  readRuntime, writeRuntime, parseAgentLine, appendMessage, readConfig, readWorkflow, readProject,
335
378
  readAgents, readSkills, readCustomDashboards, recordSnapshot, resolvePlansDir, resolveSpecsDir,
336
379
  };