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.
- package/bin/spectoflow.js +30 -3
- package/package.json +1 -1
- package/templates/dashboard/files.js +108 -0
- package/templates/dashboard/public/app.js +307 -14
- package/templates/dashboard/public/designs/console.css +35 -9
- package/templates/dashboard/public/designs/console.js +37 -0
- package/templates/dashboard/public/designs/orbit.css +5 -3
- package/templates/dashboard/public/designs/orbit.js +21 -1
- package/templates/dashboard/public/i18n.js +30 -30
- package/templates/dashboard/public/icons.js +8 -6
- package/templates/dashboard/public/index.html +86 -32
- package/templates/dashboard/public/styles.css +79 -25
- package/templates/dashboard/server.js +29 -18
- package/templates/dashboard/summarize.js +25 -6
- package/templates/lib/store.js +43 -0
|
@@ -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 & Skills"><span class="tab-ico" data-icon="agents"></span><span class="tab-label" data-i18n="nav.team">Agents & 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="
|
|
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 & 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="
|
|
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-
|
|
252
|
-
<
|
|
253
|
-
<
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
299
|
+
<div class="settings-groups">
|
|
300
|
+
<div class="settings-card">
|
|
301
|
+
<h3 class="settings-card-title" data-i18n="settings.group.execution">Agent & 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 & 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:
|
|
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:
|
|
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:
|
|
247
|
-
|
|
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
|
-
/*
|
|
345
|
-
|
|
346
|
-
|
|
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:
|
|
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:
|
|
552
|
-
.settings-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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)
|
|
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:
|
|
824
|
-
:root[data-design="neon-command"] .brand-name {
|
|
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
|
-
|
|
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
|
|
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 } —
|
|
20
|
-
// shape, without the sentinel-line streaming runner.js does (this is a one-shot
|
|
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
|
|
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
|
|
47
|
-
|
|
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
|
}
|
package/templates/lib/store.js
CHANGED
|
@@ -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
|
};
|