clementine-agent 1.18.132 → 1.18.133
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/dist/cli/dashboard.js +249 -2
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -7047,6 +7047,57 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
7047
7047
|
res.status(500).json({ error: String(err) });
|
|
7048
7048
|
}
|
|
7049
7049
|
});
|
|
7050
|
+
// 1.18.132 — list top-level files in a registered project. Used by
|
|
7051
|
+
// the Skill Builder's Files sidebar tab to surface clickable file
|
|
7052
|
+
// paths that the user can insert into their skill body.
|
|
7053
|
+
// Hardened: the path query param MUST match a registered project's
|
|
7054
|
+
// path exactly (no path traversal, no scanning arbitrary directories).
|
|
7055
|
+
app.get('/api/projects/files', (req, res) => {
|
|
7056
|
+
try {
|
|
7057
|
+
const projPath = String(req.query.path ?? '');
|
|
7058
|
+
if (!projPath)
|
|
7059
|
+
return res.status(400).json({ ok: false, error: 'path query param required' });
|
|
7060
|
+
const projects = loadProjectsMeta();
|
|
7061
|
+
const found = projects.find((p) => p.path === projPath);
|
|
7062
|
+
if (!found)
|
|
7063
|
+
return res.status(404).json({ ok: false, error: 'path is not a registered project' });
|
|
7064
|
+
if (!existsSync(projPath))
|
|
7065
|
+
return res.json({ ok: true, files: [], note: 'project path does not exist on disk' });
|
|
7066
|
+
let entries;
|
|
7067
|
+
try {
|
|
7068
|
+
entries = readdirSync(projPath);
|
|
7069
|
+
}
|
|
7070
|
+
catch (err) {
|
|
7071
|
+
return res.status(500).json({ ok: false, error: 'failed to list directory: ' + String(err) });
|
|
7072
|
+
}
|
|
7073
|
+
// Skip hidden + node_modules + standard noise. Up to 50 entries
|
|
7074
|
+
// (the sidebar caps at 50 anyway). One level deep — the panel is
|
|
7075
|
+
// an entry point; users can dig further from their editor.
|
|
7076
|
+
const NOISE = new Set(['node_modules', '.git', '.DS_Store', 'dist', 'build', '.next', '.cache']);
|
|
7077
|
+
const out = [];
|
|
7078
|
+
for (const entry of entries.sort()) {
|
|
7079
|
+
if (entry.startsWith('.'))
|
|
7080
|
+
continue;
|
|
7081
|
+
if (NOISE.has(entry))
|
|
7082
|
+
continue;
|
|
7083
|
+
const abs = path.join(projPath, entry);
|
|
7084
|
+
let st;
|
|
7085
|
+
try {
|
|
7086
|
+
st = statSync(abs);
|
|
7087
|
+
}
|
|
7088
|
+
catch {
|
|
7089
|
+
continue;
|
|
7090
|
+
}
|
|
7091
|
+
out.push({ relPath: entry, isDir: st.isDirectory(), sizeBytes: st.isFile() ? st.size : 0 });
|
|
7092
|
+
if (out.length >= 50)
|
|
7093
|
+
break;
|
|
7094
|
+
}
|
|
7095
|
+
res.json({ ok: true, files: out });
|
|
7096
|
+
}
|
|
7097
|
+
catch (err) {
|
|
7098
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
7099
|
+
}
|
|
7100
|
+
});
|
|
7050
7101
|
// ── Available Tools ──────────────────────────────────────────
|
|
7051
7102
|
app.get('/api/available-tools', async (_req, res) => {
|
|
7052
7103
|
try {
|
|
@@ -28178,6 +28229,7 @@ async function openSkillBuilder(skillName) {
|
|
|
28178
28229
|
+ '<span id="sb-save-status" style="font-size:11px;color:var(--text-muted)"></span>'
|
|
28179
28230
|
+ '</div>'
|
|
28180
28231
|
+ '<div style="display:flex;align-items:center;gap:6px">'
|
|
28232
|
+
+ '<button onclick="sbRunSkillTest()" id="sb-test-btn" style="font-size:12px;padding:7px 12px;border:1px solid var(--green);border-radius:6px;background:transparent;color:var(--green);cursor:pointer;font-weight:500" title="Fire this skill once and stream the result inline (toast on completion)">▶ Test run</button>'
|
|
28181
28233
|
+ '<button onclick="sbSaveCurrent()" id="sb-save-btn" class="btn-primary" style="font-size:12px;padding:7px 14px;border:none;border-radius:6px;background:var(--accent);color:#fff;font-weight:500;cursor:pointer" disabled>Save (⌘S)</button>'
|
|
28182
28234
|
+ '<button onclick="closeSkillBuilder()" style="font-size:12px;padding:7px 12px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--text-primary);cursor:pointer">Close</button>'
|
|
28183
28235
|
+ '</div>'
|
|
@@ -28203,6 +28255,8 @@ async function openSkillBuilder(skillName) {
|
|
|
28203
28255
|
+ '<div style="border-left:1px solid var(--border);background:var(--bg-secondary);display:flex;flex-direction:column;min-height:0">'
|
|
28204
28256
|
+ '<div style="padding:8px 6px 0;display:flex;gap:2px;border-bottom:1px solid var(--border)">'
|
|
28205
28257
|
+ '<button onclick="sbSwitchTab(\\x27tools\\x27)" id="sb-tab-tools" class="sb-tab" style="flex:1;font-size:11px;padding:8px 4px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;font-weight:500">Tools</button>'
|
|
28258
|
+
+ '<button onclick="sbSwitchTab(\\x27files\\x27)" id="sb-tab-files" class="sb-tab" style="flex:1;font-size:11px;padding:8px 4px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;font-weight:500">Files</button>'
|
|
28259
|
+
+ '<button onclick="sbSwitchTab(\\x27memory\\x27)" id="sb-tab-memory" class="sb-tab" style="flex:1;font-size:11px;padding:8px 4px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;font-weight:500">Memory</button>'
|
|
28206
28260
|
+ '<button onclick="sbSwitchTab(\\x27skills\\x27)" id="sb-tab-skills" class="sb-tab" style="flex:1;font-size:11px;padding:8px 4px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;font-weight:500">Skills</button>'
|
|
28207
28261
|
+ '</div>'
|
|
28208
28262
|
+ '<div style="padding:6px 10px;border-bottom:1px solid var(--border)">'
|
|
@@ -28389,10 +28443,10 @@ async function sbDeleteFile(relPath) {
|
|
|
28389
28443
|
}
|
|
28390
28444
|
}
|
|
28391
28445
|
|
|
28392
|
-
// ── Sidebar tabs (Tools / Skills)
|
|
28446
|
+
// ── Sidebar tabs (Tools / Files / Memory / Skills) ──────────────────
|
|
28393
28447
|
function sbSwitchTab(tab) {
|
|
28394
28448
|
window._sbActiveTab = tab;
|
|
28395
|
-
['tools', 'skills'].forEach(function(t) {
|
|
28449
|
+
['tools', 'files', 'memory', 'skills'].forEach(function(t) {
|
|
28396
28450
|
var el = document.getElementById('sb-tab-' + t);
|
|
28397
28451
|
if (el) {
|
|
28398
28452
|
el.style.color = (t === tab) ? 'var(--accent)' : 'var(--text-muted)';
|
|
@@ -28408,6 +28462,10 @@ async function sbRenderSidebar() {
|
|
|
28408
28462
|
var q = (document.getElementById('sb-sidebar-search')?.value || '').toLowerCase().trim();
|
|
28409
28463
|
if (window._sbActiveTab === 'tools') {
|
|
28410
28464
|
await sbRenderToolsTab(listEl, q);
|
|
28465
|
+
} else if (window._sbActiveTab === 'files') {
|
|
28466
|
+
await sbRenderFilesTab(listEl, q);
|
|
28467
|
+
} else if (window._sbActiveTab === 'memory') {
|
|
28468
|
+
await sbRenderMemoryTab(listEl, q);
|
|
28411
28469
|
} else if (window._sbActiveTab === 'skills') {
|
|
28412
28470
|
await sbRenderSkillsTab(listEl, q);
|
|
28413
28471
|
}
|
|
@@ -28505,6 +28563,163 @@ async function sbRenderSkillsTab(listEl, q) {
|
|
|
28505
28563
|
listEl.innerHTML = html;
|
|
28506
28564
|
}
|
|
28507
28565
|
|
|
28566
|
+
// 1.18.132 — Files tab (Phase 2.5). Browses the user's projects.
|
|
28567
|
+
// Click a project to expand its file tree; click a file to insert
|
|
28568
|
+
// its absolute path at the cursor (so the skill body can reference
|
|
28569
|
+
// it via Read or Bash). Scoped to projectsData (already populated by
|
|
28570
|
+
// /api/projects on page load); no separate filesystem walk endpoint
|
|
28571
|
+
// needed in this iteration.
|
|
28572
|
+
async function sbRenderFilesTab(listEl, q) {
|
|
28573
|
+
var projects = (typeof projectsData !== 'undefined' && Array.isArray(projectsData)) ? projectsData : [];
|
|
28574
|
+
if (projects.length === 0) {
|
|
28575
|
+
try {
|
|
28576
|
+
var pr = await window.apiFetch('/api/projects');
|
|
28577
|
+
var pd = await pr.json();
|
|
28578
|
+
if (pd && Array.isArray(pd.projects)) projects = pd.projects;
|
|
28579
|
+
} catch (_) { /* fall through to empty */ }
|
|
28580
|
+
}
|
|
28581
|
+
if (q) {
|
|
28582
|
+
projects = projects.filter(function(p) {
|
|
28583
|
+
return ((p.name || '') + ' ' + (p.path || '')).toLowerCase().indexOf(q) > -1;
|
|
28584
|
+
});
|
|
28585
|
+
}
|
|
28586
|
+
if (projects.length === 0) {
|
|
28587
|
+
listEl.innerHTML = '<div style="padding:18px;color:var(--text-muted);font-size:11px;text-align:center">' + (q ? 'No matches.' : 'No projects yet. Add one in Settings → Projects.') + '</div>';
|
|
28588
|
+
return;
|
|
28589
|
+
}
|
|
28590
|
+
var html = '<div style="padding:6px 12px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;background:var(--bg-tertiary)">Projects (' + projects.length + ')</div>';
|
|
28591
|
+
for (var i = 0; i < projects.length; i++) {
|
|
28592
|
+
var p = projects[i];
|
|
28593
|
+
// Click on row → insert the project path. Click on icon to load
|
|
28594
|
+
// a child file list inline (lazy expand).
|
|
28595
|
+
html += '<div style="border-bottom:1px solid var(--border-light)">'
|
|
28596
|
+
+ '<div onclick="sbInsertAtCursor(\\x27' + jsStr(p.path) + '\\x27)" style="padding:7px 12px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27" title="Insert this absolute path at the cursor">'
|
|
28597
|
+
+ '<span>📁</span>'
|
|
28598
|
+
+ '<span style="font-weight:500;color:var(--text-primary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(p.name || p.path.split(\'/\').pop()) + '</span>'
|
|
28599
|
+
+ '<button onclick="event.stopPropagation();sbToggleProjectFiles(\\x27' + jsStr(p.path) + '\\x27, this)" title="Browse files inside this project" style="background:none;border:1px solid var(--border);color:var(--text-muted);font-size:10px;padding:1px 6px;border-radius:3px;cursor:pointer">▸</button>'
|
|
28600
|
+
+ '</div>'
|
|
28601
|
+
+ '<div id="sb-files-children-' + i + '" data-project-path="' + esc(p.path) + '" style="display:none;font-size:11px;background:var(--bg-secondary);padding:4px 0"></div>'
|
|
28602
|
+
+ '</div>';
|
|
28603
|
+
}
|
|
28604
|
+
listEl.innerHTML = html;
|
|
28605
|
+
}
|
|
28606
|
+
|
|
28607
|
+
// Lazy-load the child file list for a project. Hits a tiny endpoint
|
|
28608
|
+
// that returns the top-level files (max 50, no recursion) so we don't
|
|
28609
|
+
// blow the panel on huge projects.
|
|
28610
|
+
async function sbToggleProjectFiles(projectPath, btn) {
|
|
28611
|
+
// Find the children container next to this button
|
|
28612
|
+
var children = btn.parentElement.parentElement.querySelector('[data-project-path]');
|
|
28613
|
+
if (!children) return;
|
|
28614
|
+
if (children.style.display !== 'none') {
|
|
28615
|
+
children.style.display = 'none';
|
|
28616
|
+
btn.textContent = '▸';
|
|
28617
|
+
return;
|
|
28618
|
+
}
|
|
28619
|
+
children.style.display = '';
|
|
28620
|
+
btn.textContent = '▾';
|
|
28621
|
+
if (!children.dataset.loaded) {
|
|
28622
|
+
children.innerHTML = '<div style="padding:8px 24px;color:var(--text-muted);font-size:10px">Loading…</div>';
|
|
28623
|
+
try {
|
|
28624
|
+
var r = await window.apiFetch('/api/projects/files?path=' + encodeURIComponent(projectPath));
|
|
28625
|
+
var d = await r.json();
|
|
28626
|
+
if (!r.ok || !Array.isArray(d.files)) {
|
|
28627
|
+
children.innerHTML = '<div style="padding:8px 24px;color:var(--text-muted);font-size:10px">No files surfaced (or endpoint unavailable).</div>';
|
|
28628
|
+
return;
|
|
28629
|
+
}
|
|
28630
|
+
var html = '';
|
|
28631
|
+
for (var i = 0; i < Math.min(d.files.length, 50); i++) {
|
|
28632
|
+
var f = d.files[i];
|
|
28633
|
+
var icon = f.isDir ? '📁' : '📄';
|
|
28634
|
+
var fullPath = projectPath + '/' + f.relPath;
|
|
28635
|
+
html += '<div onclick="sbInsertAtCursor(\\x27' + jsStr(fullPath) + '\\x27)" style="padding:4px 24px;cursor:pointer;display:flex;align-items:center;gap:6px" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27">'
|
|
28636
|
+
+ '<span>' + icon + '</span>'
|
|
28637
|
+
+ '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary)">' + esc(f.relPath) + '</span>'
|
|
28638
|
+
+ '</div>';
|
|
28639
|
+
}
|
|
28640
|
+
if (d.files.length > 50) {
|
|
28641
|
+
html += '<div style="padding:4px 24px;color:var(--text-muted);font-size:10px;font-style:italic">+ ' + (d.files.length - 50) + ' more (open the project in your editor for the rest)</div>';
|
|
28642
|
+
}
|
|
28643
|
+
children.innerHTML = html;
|
|
28644
|
+
children.dataset.loaded = '1';
|
|
28645
|
+
} catch (err) {
|
|
28646
|
+
children.innerHTML = '<div style="padding:8px 24px;color:var(--red);font-size:10px">' + esc(String(err)) + '</div>';
|
|
28647
|
+
}
|
|
28648
|
+
}
|
|
28649
|
+
}
|
|
28650
|
+
|
|
28651
|
+
// 1.18.132 — Memory tab (Phase 2.5). Surfaces three things the agent
|
|
28652
|
+
// can reach when the skill runs:
|
|
28653
|
+
// 1. Recent extractions (last facts the auto-extractor saved)
|
|
28654
|
+
// 2. Top-level MEMORY.md sections (h2 headers)
|
|
28655
|
+
// 3. A button to insert a memory_search call template
|
|
28656
|
+
// Click any item → inserts a contextual reference at the cursor so
|
|
28657
|
+
// the skill body can document or trigger a recall.
|
|
28658
|
+
async function sbRenderMemoryTab(listEl, q) {
|
|
28659
|
+
var html = '';
|
|
28660
|
+
// 1. Recent extractions
|
|
28661
|
+
try {
|
|
28662
|
+
var r = await window.apiFetch('/api/memory/writes/recent?limit=12');
|
|
28663
|
+
var d = await r.json();
|
|
28664
|
+
var writes = (d && Array.isArray(d.writes)) ? d.writes : (d && Array.isArray(d.entries) ? d.entries : []);
|
|
28665
|
+
if (q) {
|
|
28666
|
+
writes = writes.filter(function(w) {
|
|
28667
|
+
return JSON.stringify(w).toLowerCase().indexOf(q) > -1;
|
|
28668
|
+
});
|
|
28669
|
+
}
|
|
28670
|
+
if (writes.length > 0) {
|
|
28671
|
+
html += '<div style="padding:6px 12px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;background:var(--bg-tertiary)">Recent extractions</div>';
|
|
28672
|
+
for (var i = 0; i < Math.min(writes.length, 8); i++) {
|
|
28673
|
+
var w = writes[i];
|
|
28674
|
+
var preview = (w.content_preview || w.content || w.preview || w.text || '').slice(0, 100);
|
|
28675
|
+
if (!preview) continue;
|
|
28676
|
+
var insertion = 'Reference recent fact: "' + preview.replace(/"/g, '\\\\\\\\"').slice(0, 80) + '"';
|
|
28677
|
+
html += '<div onclick="sbInsertAtCursor(\\x27' + jsStr(insertion) + '\\x27)" style="padding:7px 12px;cursor:pointer;font-size:12px;border-bottom:1px solid var(--border-light)" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27">'
|
|
28678
|
+
+ '<div style="color:var(--text-secondary);line-height:1.4;font-size:11px">' + esc(preview) + (preview.length >= 100 ? '…' : '') + '</div>'
|
|
28679
|
+
+ '</div>';
|
|
28680
|
+
}
|
|
28681
|
+
}
|
|
28682
|
+
} catch (_) { /* memory writes endpoint may not be live in early daemons */ }
|
|
28683
|
+
// 2. MEMORY.md slot headers
|
|
28684
|
+
try {
|
|
28685
|
+
var r2 = await window.apiFetch('/api/memory/md');
|
|
28686
|
+
var d2 = await r2.json();
|
|
28687
|
+
var content = (d2 && d2.content) || '';
|
|
28688
|
+
var lines = content.split('\\n');
|
|
28689
|
+
var sections = [];
|
|
28690
|
+
for (var li = 0; li < lines.length; li++) {
|
|
28691
|
+
var match = lines[li].match(/^##\\s+(.+)$/);
|
|
28692
|
+
if (match) sections.push(match[1].trim());
|
|
28693
|
+
}
|
|
28694
|
+
if (q) {
|
|
28695
|
+
sections = sections.filter(function(s) { return s.toLowerCase().indexOf(q) > -1; });
|
|
28696
|
+
}
|
|
28697
|
+
if (sections.length > 0) {
|
|
28698
|
+
html += '<div style="padding:6px 12px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;background:var(--bg-tertiary);margin-top:4px">MEMORY.md sections</div>';
|
|
28699
|
+
for (var si = 0; si < sections.length; si++) {
|
|
28700
|
+
var s = sections[si];
|
|
28701
|
+
var ins = 'Refer to MEMORY.md > "' + s + '" section.';
|
|
28702
|
+
html += '<div onclick="sbInsertAtCursor(\\x27' + jsStr(ins) + '\\x27)" style="padding:6px 12px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--border-light)" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27">'
|
|
28703
|
+
+ '<span>📄</span>'
|
|
28704
|
+
+ '<span style="color:var(--text-primary)">' + esc(s) + '</span>'
|
|
28705
|
+
+ '</div>';
|
|
28706
|
+
}
|
|
28707
|
+
}
|
|
28708
|
+
} catch (_) { /* defensive */ }
|
|
28709
|
+
// 3. Always-available memory_search insert
|
|
28710
|
+
html += '<div style="padding:6px 12px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;background:var(--bg-tertiary);margin-top:4px">Patterns</div>';
|
|
28711
|
+
html += '<div onclick="sbInsertAtCursor(\\x27Use memory_search with query: \\\\\\\"YOUR_QUERY\\\\\\\".\\x27)" style="padding:7px 12px;cursor:pointer;font-size:12px;border-bottom:1px solid var(--border-light)" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27">'
|
|
28712
|
+
+ '<span style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-primary)">memory_search</span>'
|
|
28713
|
+
+ '<span style="color:var(--text-muted);font-size:10px;margin-left:8px">Recall facts/transcripts at runtime</span>'
|
|
28714
|
+
+ '</div>';
|
|
28715
|
+
html += '<div onclick="sbInsertAtCursor(\\x27Use memory_write to save this fact: \\\\\\\"FACT\\\\\\\" with reason \\\\\\\"WHY_IT_MATTERS\\\\\\\".\\x27)" style="padding:7px 12px;cursor:pointer;font-size:12px;border-bottom:1px solid var(--border-light)" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27">'
|
|
28716
|
+
+ '<span style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-primary)">memory_write</span>'
|
|
28717
|
+
+ '<span style="color:var(--text-muted);font-size:10px;margin-left:8px">Persist a new fact</span>'
|
|
28718
|
+
+ '</div>';
|
|
28719
|
+
if (!html.trim()) html = '<div style="padding:18px;color:var(--text-muted);font-size:11px;text-align:center">No memory items surfaced.</div>';
|
|
28720
|
+
listEl.innerHTML = html;
|
|
28721
|
+
}
|
|
28722
|
+
|
|
28508
28723
|
function sbInsertAtCursor(text) {
|
|
28509
28724
|
var ed = document.getElementById('sb-editor');
|
|
28510
28725
|
if (!ed) return;
|
|
@@ -28518,6 +28733,38 @@ function sbInsertAtCursor(text) {
|
|
|
28518
28733
|
sbOnEdit();
|
|
28519
28734
|
}
|
|
28520
28735
|
|
|
28736
|
+
// 1.18.132 — Test runner. Fires the open skill once via the same
|
|
28737
|
+
// /api/cron/run/:name path that the Skills detail Run-now button
|
|
28738
|
+
// uses (which itself works for unscheduled skills via cmdCronRun's
|
|
28739
|
+
// catalog fallback). Refuses to run if the file has unsaved changes
|
|
28740
|
+
// (otherwise the user would be testing the saved version, not what
|
|
28741
|
+
// they see in the editor — confusing).
|
|
28742
|
+
async function sbRunSkillTest() {
|
|
28743
|
+
if (window._sbState.dirty) {
|
|
28744
|
+
if (!confirm('Save current changes before testing? Unsaved edits won\\x27t be in the run.')) return;
|
|
28745
|
+
await sbSaveCurrent();
|
|
28746
|
+
if (window._sbState.dirty) return; // save failed
|
|
28747
|
+
}
|
|
28748
|
+
var name = window._sbState.skillName;
|
|
28749
|
+
if (!name) return;
|
|
28750
|
+
var btn = document.getElementById('sb-test-btn');
|
|
28751
|
+
if (btn) { btn.disabled = true; btn.textContent = '⏳ Running…'; btn.style.color = 'var(--text-muted)'; btn.style.borderColor = 'var(--border)'; }
|
|
28752
|
+
try {
|
|
28753
|
+
var r = await window.apiFetch('/api/cron/run/' + encodeURIComponent(name), { method: 'POST' });
|
|
28754
|
+
if (r.status === 409) {
|
|
28755
|
+
toast('Already running. Wait for the in-flight run to finish.', 'warn');
|
|
28756
|
+
return;
|
|
28757
|
+
}
|
|
28758
|
+
var d = await r.json();
|
|
28759
|
+
if (!r.ok) { toast(d.error || 'Run failed', 'error'); return; }
|
|
28760
|
+
toast('Started "' + name + '" — output streams to chat. Close the builder to see it.', 'success');
|
|
28761
|
+
} catch (err) {
|
|
28762
|
+
toast('Failed: ' + err, 'error');
|
|
28763
|
+
} finally {
|
|
28764
|
+
if (btn) { btn.disabled = false; btn.textContent = '▶ Test run'; btn.style.color = 'var(--green)'; btn.style.borderColor = 'var(--green)'; }
|
|
28765
|
+
}
|
|
28766
|
+
}
|
|
28767
|
+
|
|
28521
28768
|
async function _openSkillModal(opts) {
|
|
28522
28769
|
opts = opts || {};
|
|
28523
28770
|
var existing = null;
|