clementine-agent 1.18.73 → 1.18.75
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 +219 -30
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -4267,6 +4267,10 @@ export async function cmdDashboard(opts) {
|
|
|
4267
4267
|
const agentSlug = body.agent ? (String(body.agent).trim() || undefined) : undefined;
|
|
4268
4268
|
// Parse the agent's draft if present; on any parse error fall back to the
|
|
4269
4269
|
// single-step stub so the user lands in the editor with something usable.
|
|
4270
|
+
// ── KEY FIX (1.18.75): forward EVERY step-kind body the agent produces.
|
|
4271
|
+
// Previously this parser dropped mcp/cli/channel/transform/conditional/loop
|
|
4272
|
+
// configs, leaving the user with prompt-only stubs and forcing them to
|
|
4273
|
+
// rebuild every Slack-send / shell-cmd / branch by hand.
|
|
4270
4274
|
let steps = null;
|
|
4271
4275
|
if (body.draftYaml && typeof body.draftYaml === 'string') {
|
|
4272
4276
|
try {
|
|
@@ -4277,16 +4281,39 @@ export async function cmdDashboard(opts) {
|
|
|
4277
4281
|
const dependsOn = Array.isArray(r.dependsOn)
|
|
4278
4282
|
? r.dependsOn.map(String)
|
|
4279
4283
|
: (typeof r.dependsOn === 'string' ? r.dependsOn.split(',').map(s => s.trim()).filter(Boolean) : []);
|
|
4280
|
-
|
|
4284
|
+
const kind = typeof r.kind === 'string' ? r.kind : 'prompt';
|
|
4285
|
+
const step = {
|
|
4281
4286
|
id: String(id),
|
|
4282
|
-
prompt: String(r.prompt ?? ''),
|
|
4283
4287
|
dependsOn,
|
|
4284
4288
|
tier: typeof r.tier === 'number' ? r.tier : 1,
|
|
4285
4289
|
maxTurns: typeof r.maxTurns === 'number' ? r.maxTurns : 15,
|
|
4286
|
-
...(typeof r.model === 'string' ? { model: r.model } : {}),
|
|
4287
|
-
...(typeof r.workDir === 'string' ? { workDir: r.workDir } : {}),
|
|
4288
|
-
...(typeof r.kind === 'string' && r.kind !== 'prompt' ? { kind: r.kind } : {}),
|
|
4289
4290
|
};
|
|
4291
|
+
if (kind !== 'prompt')
|
|
4292
|
+
step.kind = kind;
|
|
4293
|
+
if (typeof r.model === 'string')
|
|
4294
|
+
step.model = r.model;
|
|
4295
|
+
if (typeof r.workDir === 'string')
|
|
4296
|
+
step.workDir = r.workDir;
|
|
4297
|
+
// Prompt body — present on prompt steps and optionally on others.
|
|
4298
|
+
if (r.prompt != null)
|
|
4299
|
+
step.prompt = String(r.prompt);
|
|
4300
|
+
else if (kind === 'prompt')
|
|
4301
|
+
step.prompt = '';
|
|
4302
|
+
// Non-prompt step bodies. We forward them as-is when shaped like
|
|
4303
|
+
// an object; the validator on save will catch missing required
|
|
4304
|
+
// sub-fields and surface a structured error to the chat UI.
|
|
4305
|
+
const passThrough = (key) => {
|
|
4306
|
+
const v = r[key];
|
|
4307
|
+
if (v && typeof v === 'object' && !Array.isArray(v))
|
|
4308
|
+
step[key] = v;
|
|
4309
|
+
};
|
|
4310
|
+
passThrough('mcp');
|
|
4311
|
+
passThrough('cli');
|
|
4312
|
+
passThrough('channel');
|
|
4313
|
+
passThrough('transform');
|
|
4314
|
+
passThrough('conditional');
|
|
4315
|
+
passThrough('loop');
|
|
4316
|
+
return step;
|
|
4290
4317
|
}).filter(s => s.id);
|
|
4291
4318
|
if (steps.length === 0)
|
|
4292
4319
|
steps = null;
|
|
@@ -6576,7 +6603,52 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6576
6603
|
}
|
|
6577
6604
|
jobs.splice(idx, 1);
|
|
6578
6605
|
writeCronFileAt(cronFile, parsed, jobs);
|
|
6579
|
-
|
|
6606
|
+
// Cascade cleanup unless explicitly opted out (?purge=false). Storing the
|
|
6607
|
+
// job name's safe filename form once — same sanitization the run-log and
|
|
6608
|
+
// trace writers use, so we hit the right files.
|
|
6609
|
+
const purge = String(req.query.purge ?? 'true') !== 'false';
|
|
6610
|
+
const purged = [];
|
|
6611
|
+
if (purge) {
|
|
6612
|
+
const safe = bareJobName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
6613
|
+
const runLog = path.join(BASE_DIR, 'cron', 'runs', `${safe}.jsonl`);
|
|
6614
|
+
try {
|
|
6615
|
+
if (existsSync(runLog)) {
|
|
6616
|
+
unlinkSync(runLog);
|
|
6617
|
+
purged.push('runs.jsonl');
|
|
6618
|
+
}
|
|
6619
|
+
}
|
|
6620
|
+
catch { /* non-fatal */ }
|
|
6621
|
+
try {
|
|
6622
|
+
const traceDir = path.join(BASE_DIR, 'cron', 'traces');
|
|
6623
|
+
if (existsSync(traceDir)) {
|
|
6624
|
+
const traceFiles = readdirSync(traceDir).filter(f => f.startsWith(`${safe}_`) && f.endsWith('.json'));
|
|
6625
|
+
for (const f of traceFiles) {
|
|
6626
|
+
try {
|
|
6627
|
+
unlinkSync(path.join(traceDir, f));
|
|
6628
|
+
}
|
|
6629
|
+
catch { /* skip */ }
|
|
6630
|
+
}
|
|
6631
|
+
if (traceFiles.length > 0)
|
|
6632
|
+
purged.push(`${traceFiles.length} trace${traceFiles.length === 1 ? '' : 's'}`);
|
|
6633
|
+
}
|
|
6634
|
+
}
|
|
6635
|
+
catch { /* non-fatal */ }
|
|
6636
|
+
try {
|
|
6637
|
+
const uploadsDir = path.join(BASE_DIR, 'uploads', `cron-${safe}`);
|
|
6638
|
+
if (existsSync(uploadsDir)) {
|
|
6639
|
+
rmSync(uploadsDir, { recursive: true, force: true });
|
|
6640
|
+
purged.push('attachments');
|
|
6641
|
+
}
|
|
6642
|
+
}
|
|
6643
|
+
catch { /* non-fatal */ }
|
|
6644
|
+
}
|
|
6645
|
+
// Notify other dashboard tabs so they drop the card without polling.
|
|
6646
|
+
try {
|
|
6647
|
+
broadcastEvent({ type: 'cron_deleted', data: { job: bareJobName, purged } });
|
|
6648
|
+
}
|
|
6649
|
+
catch { /* non-fatal */ }
|
|
6650
|
+
const purgeNote = purged.length > 0 ? ` (purged: ${purged.join(', ')})` : '';
|
|
6651
|
+
res.json({ ok: true, message: `Deleted cron job: ${jobName}${purgeNote}`, purged });
|
|
6580
6652
|
}
|
|
6581
6653
|
catch (err) {
|
|
6582
6654
|
res.status(500).json({ error: String(err) });
|
|
@@ -14980,6 +15052,9 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
14980
15052
|
.task-card.compact .task-card-actions .primary {
|
|
14981
15053
|
flex: 1;
|
|
14982
15054
|
}
|
|
15055
|
+
/* ── Recent history row hover (Tasks page bottom zone) ── */
|
|
15056
|
+
.history-row { transition: background 0.12s ease; }
|
|
15057
|
+
.history-row:hover { background: var(--bg-hover); }
|
|
14983
15058
|
/* ── Trick capability strip (skills + MCP + tools at a glance) ─── */
|
|
14984
15059
|
.task-cap-strip {
|
|
14985
15060
|
border-top: 1px solid var(--border-light);
|
|
@@ -16419,7 +16494,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
16419
16494
|
apiFetch('/api/routines/' + encodeURIComponent(id) + '/toggle', { method: 'POST' })
|
|
16420
16495
|
.then(function(r){ return r.json(); })
|
|
16421
16496
|
.then(function(){ R.refreshList(); })
|
|
16422
|
-
.catch(function(err){
|
|
16497
|
+
.catch(function(err){ toast('Toggle failed: ' + err, 'error'); });
|
|
16423
16498
|
},
|
|
16424
16499
|
run: function(id, approvedSideEffects) {
|
|
16425
16500
|
apiFetch('/api/routines/' + encodeURIComponent(id) + '/run', {
|
|
@@ -16435,19 +16510,19 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
16435
16510
|
}
|
|
16436
16511
|
return r.json().then(function(j){
|
|
16437
16512
|
if (j.ok) R.flash('Triggered.');
|
|
16438
|
-
else
|
|
16513
|
+
else toast('Run failed: ' + (j.error || 'unknown'), 'error');
|
|
16439
16514
|
});
|
|
16440
|
-
}).catch(function(err){
|
|
16515
|
+
}).catch(function(err){ toast('Run failed: ' + err, 'error'); });
|
|
16441
16516
|
},
|
|
16442
16517
|
// ── editor ──────────────────────────────────────────────────
|
|
16443
16518
|
openEditor: function(id) {
|
|
16444
16519
|
apiFetch('/api/routines/' + encodeURIComponent(id))
|
|
16445
16520
|
.then(function(r){ return r.json(); })
|
|
16446
16521
|
.then(function(data){
|
|
16447
|
-
if (!data || !data.routine) {
|
|
16522
|
+
if (!data || !data.routine) { toast('Failed to load workflow', 'error'); return; }
|
|
16448
16523
|
R.state.editing = { id: data.id, routine: data.routine, dirty: false, validation: data.validation };
|
|
16449
16524
|
R.showEditor();
|
|
16450
|
-
}).catch(function(err){
|
|
16525
|
+
}).catch(function(err){ toast('Open failed: ' + err, 'error'); });
|
|
16451
16526
|
},
|
|
16452
16527
|
showEditor: function() {
|
|
16453
16528
|
document.getElementById('routines-list-pane').style.display = 'none';
|
|
@@ -16699,7 +16774,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
16699
16774
|
},
|
|
16700
16775
|
removeStep: function(idx) {
|
|
16701
16776
|
if (!R.state.editing) return;
|
|
16702
|
-
if (R.state.editing.routine.steps.length <= 1) {
|
|
16777
|
+
if (R.state.editing.routine.steps.length <= 1) { toast('A workflow must have at least one step.', 'error'); return; }
|
|
16703
16778
|
if (!confirm('Remove this step?')) return;
|
|
16704
16779
|
var removed = R.state.editing.routine.steps.splice(idx, 1)[0];
|
|
16705
16780
|
// Strip lingering dependsOn references.
|
|
@@ -16800,7 +16875,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
16800
16875
|
(d.steps || []).forEach(function(s){ lines.push('• ' + s.description + (s.warnings.length ? '\\n ⚠ ' + s.warnings.join('; ') : '')); });
|
|
16801
16876
|
if (d.notes && d.notes.length) lines.push('\\n' + d.notes.join('\\n'));
|
|
16802
16877
|
alert(lines.join('\\n'));
|
|
16803
|
-
}).catch(function(err){
|
|
16878
|
+
}).catch(function(err){ toast('Dry-run failed: ' + err, 'error'); });
|
|
16804
16879
|
},
|
|
16805
16880
|
testCurrent: function() {
|
|
16806
16881
|
if (!R.state.editing) return;
|
|
@@ -16819,9 +16894,9 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
16819
16894
|
apiFetch('/api/routines/' + encodeURIComponent(R.state.editing.id), { method: 'DELETE' })
|
|
16820
16895
|
.then(function(r){ return r.json(); })
|
|
16821
16896
|
.then(function(j){
|
|
16822
|
-
if (j.ok) { R.state.editing = null; R.closeEditor(); R.refreshList(); }
|
|
16823
|
-
else
|
|
16824
|
-
}).catch(function(err){
|
|
16897
|
+
if (j.ok) { R.state.editing = null; R.closeEditor(); R.refreshList(); toast('Deleted.', 'success'); }
|
|
16898
|
+
else toast('Delete failed: ' + (j.error || 'unknown'), 'error');
|
|
16899
|
+
}).catch(function(err){ toast('Delete error: ' + err, 'error'); });
|
|
16825
16900
|
},
|
|
16826
16901
|
setStatus: function(msg) {
|
|
16827
16902
|
var el = document.getElementById('re-status');
|
|
@@ -16876,7 +16951,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
16876
16951
|
},
|
|
16877
16952
|
submitCreate: function() {
|
|
16878
16953
|
var name = document.getElementById('routines-create-name').value.trim();
|
|
16879
|
-
if (!name) {
|
|
16954
|
+
if (!name) { toast('Name is required', 'error'); document.getElementById('routines-create-name').focus(); return; }
|
|
16880
16955
|
var body = {
|
|
16881
16956
|
name: name,
|
|
16882
16957
|
description: document.getElementById('routines-create-description').value.trim(),
|
|
@@ -16889,11 +16964,12 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
16889
16964
|
body: JSON.stringify(body)
|
|
16890
16965
|
}).then(function(r){ return r.json().then(function(j){ return { ok: r.ok, body: j }; }); })
|
|
16891
16966
|
.then(function(res){
|
|
16892
|
-
if (!res.ok) {
|
|
16967
|
+
if (!res.ok) { toast('Create failed: ' + (res.body.error || 'unknown'), 'error'); return; }
|
|
16893
16968
|
R.closeCreate();
|
|
16894
16969
|
R.refreshList();
|
|
16895
16970
|
R.openEditor(res.body.id);
|
|
16896
|
-
|
|
16971
|
+
toast('Workflow created.', 'success');
|
|
16972
|
+
}).catch(function(err){ toast('Create error: ' + err, 'error'); });
|
|
16897
16973
|
},
|
|
16898
16974
|
// ── chat-first builder ──────────────────────────────────────
|
|
16899
16975
|
// Multi-turn conversation that asks clarifying questions and
|
|
@@ -17150,14 +17226,15 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
17150
17226
|
.then(function(res){
|
|
17151
17227
|
if (btn) { btn.textContent = 'Save workflow'; btn.disabled = false; }
|
|
17152
17228
|
if (!res.ok) {
|
|
17153
|
-
|
|
17229
|
+
toast('Save failed: ' + (res.body && res.body.error || 'unknown'), 'error');
|
|
17154
17230
|
return;
|
|
17155
17231
|
}
|
|
17232
|
+
toast('Workflow saved.', 'success');
|
|
17156
17233
|
R.closeChat();
|
|
17157
17234
|
if (res.body && res.body.id) R.openEditor(res.body.id);
|
|
17158
17235
|
}).catch(function(err){
|
|
17159
17236
|
if (btn) { btn.textContent = 'Save workflow'; btn.disabled = false; }
|
|
17160
|
-
|
|
17237
|
+
toast('Save failed: ' + err, 'error');
|
|
17161
17238
|
});
|
|
17162
17239
|
},
|
|
17163
17240
|
// ── helpers ─────────────────────────────────────────────────
|
|
@@ -19947,7 +20024,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19947
20024
|
<div class="cap-section">
|
|
19948
20025
|
<label class="cap-section-label">Tags</label>
|
|
19949
20026
|
<div class="cap-picker-chips" id="cron-tags-chips"></div>
|
|
19950
|
-
<input type="text" class="cap-tag-input" id="cron-tags-input" placeholder="Type a tag and press Enter (e.g. morning, ops)" onkeydown="handleTagInputKeydown(event)">
|
|
20027
|
+
<input type="text" class="cap-tag-input" id="cron-tags-input" placeholder="Type a tag and press Enter, Tab, or comma (e.g. morning, ops)" onkeydown="handleTagInputKeydown(event)" onblur="handleTagInputBlur()">
|
|
19951
20028
|
</div>
|
|
19952
20029
|
</div>
|
|
19953
20030
|
|
|
@@ -23205,7 +23282,7 @@ function renderRecentHistoryList(runs) {
|
|
|
23205
23282
|
var preview = String(entry.outputPreview).slice(0, 140);
|
|
23206
23283
|
errorPreview = '<div style="font-size:11px;color:var(--text-muted);margin-top:2px;word-break:break-word">' + esc(preview) + '</div>';
|
|
23207
23284
|
}
|
|
23208
|
-
rowsHtml += '<div class="history-row" data-trace-job="' + esc(jobName) + '" style="display:grid;grid-template-columns:24px minmax(180px,1.2fr) minmax(180px,1fr) 90px auto;gap:10px;align-items:start;padding:8px 14px;border-bottom:1px solid var(--border);cursor:pointer"
|
|
23285
|
+
rowsHtml += '<div class="history-row" data-trace-job="' + esc(jobName) + '" style="display:grid;grid-template-columns:24px minmax(180px,1.2fr) minmax(180px,1fr) 90px auto;gap:10px;align-items:start;padding:8px 14px;border-bottom:1px solid var(--border);cursor:pointer">'
|
|
23209
23286
|
+ '<div style="color:' + statusColor + ';font-size:14px;line-height:18px;text-align:center" title="' + esc(status) + '">' + statusIcon + '</div>'
|
|
23210
23287
|
+ '<div style="min-width:0">'
|
|
23211
23288
|
+ '<div style="font-weight:500;color:var(--text-primary);font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(jobName) + '">' + esc(jobName) + attemptLabel + '</div>'
|
|
@@ -24469,8 +24546,24 @@ function toggleAllowedToolsPanel() {
|
|
|
24469
24546
|
}
|
|
24470
24547
|
|
|
24471
24548
|
function handleTagInputKeydown(event) {
|
|
24472
|
-
|
|
24549
|
+
// Accept Enter, Tab, and comma as commit keys. Tab still moves focus
|
|
24550
|
+
// afterward when the field is empty (no preventDefault). On-blur
|
|
24551
|
+
// commit is handled by handleTagInputBlur below.
|
|
24552
|
+
if (event.key !== 'Enter' && event.key !== ',' && event.key !== 'Tab') return;
|
|
24553
|
+
var inp = document.getElementById('cron-tags-input');
|
|
24554
|
+
if (!inp) return;
|
|
24555
|
+
var val = inp.value.trim().replace(/^#+/, '');
|
|
24556
|
+
if (!val) return; // Tab on empty input → let it move focus naturally
|
|
24473
24557
|
event.preventDefault();
|
|
24558
|
+
if (_cronTags.indexOf(val) === -1) {
|
|
24559
|
+
_cronTags.push(val);
|
|
24560
|
+
renderTagsPickerChips();
|
|
24561
|
+
}
|
|
24562
|
+
inp.value = '';
|
|
24563
|
+
}
|
|
24564
|
+
|
|
24565
|
+
function handleTagInputBlur() {
|
|
24566
|
+
// Catch tags the user typed but forgot to commit before clicking elsewhere.
|
|
24474
24567
|
var inp = document.getElementById('cron-tags-input');
|
|
24475
24568
|
if (!inp) return;
|
|
24476
24569
|
var val = inp.value.trim().replace(/^#+/, '');
|
|
@@ -24666,6 +24759,9 @@ function openCreateCronModal(agentSlug) {
|
|
|
24666
24759
|
switchCronTab('configure');
|
|
24667
24760
|
onPredictableChange();
|
|
24668
24761
|
document.getElementById('cron-modal').classList.add('show');
|
|
24762
|
+
// Snapshot AFTER all defaults are populated so an immediate close with no
|
|
24763
|
+
// edits doesn't trigger the "discard changes?" prompt.
|
|
24764
|
+
setTimeout(captureCronModalSnapshot, 0);
|
|
24669
24765
|
}
|
|
24670
24766
|
|
|
24671
24767
|
function openEditCronModal(jobName) {
|
|
@@ -24727,6 +24823,7 @@ function openEditCronModal(jobName) {
|
|
|
24727
24823
|
if (previewBtn) previewBtn.removeAttribute('disabled');
|
|
24728
24824
|
switchCronTab('configure');
|
|
24729
24825
|
document.getElementById('cron-modal').classList.add('show');
|
|
24826
|
+
setTimeout(captureCronModalSnapshot, 0);
|
|
24730
24827
|
}
|
|
24731
24828
|
|
|
24732
24829
|
/**
|
|
@@ -24861,10 +24958,68 @@ function renderCronPreview(d) {
|
|
|
24861
24958
|
return html;
|
|
24862
24959
|
}
|
|
24863
24960
|
|
|
24864
|
-
|
|
24961
|
+
// Snapshot of the form values at the moment the modal opened. closeCronModal
|
|
24962
|
+
// compares against this and prompts the user before discarding edits.
|
|
24963
|
+
var _cronModalSnapshot = null;
|
|
24964
|
+
|
|
24965
|
+
function captureCronModalSnapshot() {
|
|
24966
|
+
// String concatenation is enough for a dirty check; we don't need the
|
|
24967
|
+
// structured object back. Order must match restoreCheck below.
|
|
24968
|
+
function v(id) { var el = document.getElementById(id); return el ? (el.value || '') : ''; }
|
|
24969
|
+
_cronModalSnapshot = [
|
|
24970
|
+
v('cron-name'),
|
|
24971
|
+
v('cron-schedule'),
|
|
24972
|
+
v('cron-prompt'),
|
|
24973
|
+
v('cron-context'),
|
|
24974
|
+
v('cron-tier'),
|
|
24975
|
+
v('cron-mode'),
|
|
24976
|
+
v('cron-maxhours'),
|
|
24977
|
+
v('cron-max-retries'),
|
|
24978
|
+
v('cron-after'),
|
|
24979
|
+
v('cron-workdir'),
|
|
24980
|
+
v('cron-allowed-tools'),
|
|
24981
|
+
v('cron-category'),
|
|
24982
|
+
(document.getElementById('cron-predictable') || {}).checked ? '1' : '0',
|
|
24983
|
+
JSON.stringify(_cronSelectedSkills || []),
|
|
24984
|
+
JSON.stringify(_cronSelectedMcp || []),
|
|
24985
|
+
JSON.stringify(_cronTags || []),
|
|
24986
|
+
String((_pendingAttachments || []).length),
|
|
24987
|
+
].join('\\u0001');
|
|
24988
|
+
}
|
|
24989
|
+
|
|
24990
|
+
function isCronModalDirty() {
|
|
24991
|
+
if (_cronModalSnapshot === null) return false;
|
|
24992
|
+
function v(id) { var el = document.getElementById(id); return el ? (el.value || '') : ''; }
|
|
24993
|
+
var current = [
|
|
24994
|
+
v('cron-name'),
|
|
24995
|
+
v('cron-schedule'),
|
|
24996
|
+
v('cron-prompt'),
|
|
24997
|
+
v('cron-context'),
|
|
24998
|
+
v('cron-tier'),
|
|
24999
|
+
v('cron-mode'),
|
|
25000
|
+
v('cron-maxhours'),
|
|
25001
|
+
v('cron-max-retries'),
|
|
25002
|
+
v('cron-after'),
|
|
25003
|
+
v('cron-workdir'),
|
|
25004
|
+
v('cron-allowed-tools'),
|
|
25005
|
+
v('cron-category'),
|
|
25006
|
+
(document.getElementById('cron-predictable') || {}).checked ? '1' : '0',
|
|
25007
|
+
JSON.stringify(_cronSelectedSkills || []),
|
|
25008
|
+
JSON.stringify(_cronSelectedMcp || []),
|
|
25009
|
+
JSON.stringify(_cronTags || []),
|
|
25010
|
+
String((_pendingAttachments || []).length),
|
|
25011
|
+
].join('\\u0001');
|
|
25012
|
+
return current !== _cronModalSnapshot;
|
|
25013
|
+
}
|
|
25014
|
+
|
|
25015
|
+
function closeCronModal(force) {
|
|
25016
|
+
if (force !== true && isCronModalDirty()) {
|
|
25017
|
+
if (!confirm('You have unsaved changes. Discard them?')) return;
|
|
25018
|
+
}
|
|
24865
25019
|
document.getElementById('cron-modal').classList.remove('show');
|
|
24866
25020
|
editingCronJob = null;
|
|
24867
25021
|
_cronPreviewLoadedFor = null;
|
|
25022
|
+
_cronModalSnapshot = null;
|
|
24868
25023
|
var attachList = document.getElementById('cron-attachments-list');
|
|
24869
25024
|
if (attachList) attachList.innerHTML = '';
|
|
24870
25025
|
var bannerHost = document.getElementById('cron-legacy-banner-host');
|
|
@@ -24998,10 +25153,29 @@ async function saveCronJob() {
|
|
|
24998
25153
|
const allowedTools = parseAllowedToolsRaw();
|
|
24999
25154
|
const predictable = !!document.getElementById('cron-predictable')?.checked;
|
|
25000
25155
|
|
|
25001
|
-
|
|
25002
|
-
|
|
25156
|
+
// Field-specific validation. Toasting one message per problem is more
|
|
25157
|
+
// actionable than the old "Please fill in all fields" — and we bail before
|
|
25158
|
+
// hitting the API so the user sees the issue without a round-trip.
|
|
25159
|
+
if (!name) { toast('Task name is required', 'error'); document.getElementById('cron-name').focus(); return; }
|
|
25160
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
|
|
25161
|
+
toast('Task name must start with a lowercase letter and contain only a–z, 0–9, and hyphens (max 64 chars)', 'error');
|
|
25162
|
+
document.getElementById('cron-name').focus();
|
|
25003
25163
|
return;
|
|
25004
25164
|
}
|
|
25165
|
+
if (!editingCronJob && Array.isArray(cronJobsData)) {
|
|
25166
|
+
var dup = cronJobsData.find(function(j) { return String(j.name || '').toLowerCase() === name.toLowerCase(); });
|
|
25167
|
+
if (dup) { toast('A task named "' + name + '" already exists', 'error'); document.getElementById('cron-name').focus(); return; }
|
|
25168
|
+
}
|
|
25169
|
+
if (!schedule) { toast('Schedule is required', 'error'); return; }
|
|
25170
|
+
// Light client-side cron sanity check — server uses real cron-parser.
|
|
25171
|
+
// Catches obvious garbage like "foo bar" without making a round-trip.
|
|
25172
|
+
// Note: the hyphen sits at the END of the character class to avoid being
|
|
25173
|
+
// interpreted as a range. \\\\s in source → \\s in served JS → \s in regex.
|
|
25174
|
+
if (!/^([0-9*/, -]+|@(yearly|annually|monthly|weekly|daily|hourly|reboot))$/i.test(schedule) && schedule.split(/\\s+/).length < 5) {
|
|
25175
|
+
toast('Schedule does not look like a valid cron expression', 'error');
|
|
25176
|
+
return;
|
|
25177
|
+
}
|
|
25178
|
+
if (!prompt) { toast('Prompt is required — tell the agent what to do', 'error'); document.getElementById('cron-prompt').focus(); return; }
|
|
25005
25179
|
|
|
25006
25180
|
const body = {
|
|
25007
25181
|
name, schedule, tier, prompt, enabled: true,
|
|
@@ -25023,12 +25197,18 @@ async function saveCronJob() {
|
|
|
25023
25197
|
};
|
|
25024
25198
|
|
|
25025
25199
|
var wasEditing = !!editingCronJob;
|
|
25200
|
+
// Don't celebrate before the round-trip lands. apiJson toasts on its own;
|
|
25201
|
+
// we just check the {ok} flag and bail so the modal doesn't close (and
|
|
25202
|
+
// erase the user's input) on a 400/409/500.
|
|
25203
|
+
var resp;
|
|
25026
25204
|
if (editingCronJob) {
|
|
25027
|
-
await apiJson('PUT', '/api/cron/' + encodeURIComponent(editingCronJob), body);
|
|
25205
|
+
resp = await apiJson('PUT', '/api/cron/' + encodeURIComponent(editingCronJob), body);
|
|
25206
|
+
if (!resp || resp.ok !== true) return;
|
|
25028
25207
|
if (_pendingAttachments.length > 0) await uploadPendingAttachments(editingCronJob);
|
|
25029
25208
|
} else {
|
|
25030
25209
|
var createBody = _cronAgentContext ? Object.assign({}, body, { agent: _cronAgentContext }) : body;
|
|
25031
|
-
await apiJson('POST', '/api/cron', createBody);
|
|
25210
|
+
resp = await apiJson('POST', '/api/cron', createBody);
|
|
25211
|
+
if (!resp || resp.ok !== true) return;
|
|
25032
25212
|
var attachJobName = _cronAgentContext ? (_cronAgentContext + ':' + name) : name;
|
|
25033
25213
|
if (_pendingAttachments.length > 0) await uploadPendingAttachments(attachJobName);
|
|
25034
25214
|
}
|
|
@@ -25038,11 +25218,14 @@ async function saveCronJob() {
|
|
|
25038
25218
|
// confirm what they just saved actually runs the way they intended.
|
|
25039
25219
|
// This is the close-the-loop UX move that makes Predictable Mode visible.
|
|
25040
25220
|
markCronPreviewDirty();
|
|
25221
|
+
// Re-snapshot now that the form matches what's on disk — prevents the
|
|
25222
|
+
// dirty-guard from firing if the user closes the modal without further edits.
|
|
25223
|
+
captureCronModalSnapshot();
|
|
25041
25224
|
if (wasEditing) {
|
|
25042
25225
|
toast('Saved. Showing what will run…', 'success');
|
|
25043
25226
|
switchCronTab('preview');
|
|
25044
25227
|
} else {
|
|
25045
|
-
closeCronModal();
|
|
25228
|
+
closeCronModal(true); // force; we just saved, no dirty prompt
|
|
25046
25229
|
}
|
|
25047
25230
|
}
|
|
25048
25231
|
|
|
@@ -34497,6 +34680,12 @@ try {
|
|
|
34497
34680
|
if (currentPage === 'build') refreshCron();
|
|
34498
34681
|
refreshTeamNav();
|
|
34499
34682
|
}
|
|
34683
|
+
// A delete on one tab should drop the card from every open dashboard
|
|
34684
|
+
// without waiting for the next poll. cron_toggled is similar but lighter.
|
|
34685
|
+
if (evt.type === 'cron_deleted' || evt.type === 'cron_toggled') {
|
|
34686
|
+
if (currentPage === 'build') refreshCron();
|
|
34687
|
+
refreshTeamNav();
|
|
34688
|
+
}
|
|
34500
34689
|
if (evt.type === 'agent_created' || evt.type === 'agent_updated' || evt.type === 'agent_deleted' || evt.type === 'agent_status') {
|
|
34501
34690
|
refreshTeamNav();
|
|
34502
34691
|
refreshActivity();
|