clementine-agent 1.18.78 → 1.18.80
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 +375 -87
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -15119,6 +15119,49 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15119
15119
|
/* ── Recent history row hover (Tasks page bottom zone) ── */
|
|
15120
15120
|
.history-row { transition: background 0.12s ease; }
|
|
15121
15121
|
.history-row:hover { background: var(--bg-hover); }
|
|
15122
|
+
/* PRD Phase 1.2: "Run task once" running-state pulse on the Last run tab. */
|
|
15123
|
+
@keyframes pulse {
|
|
15124
|
+
0%, 100% { opacity: 0.4; transform: scale(0.85); }
|
|
15125
|
+
50% { opacity: 1; transform: scale(1); }
|
|
15126
|
+
}
|
|
15127
|
+
/* PRD Phase 1.3a: inner config-tab strip — switches which Configure
|
|
15128
|
+
section blocks are visible without changing the outer Configure /
|
|
15129
|
+
What-will-run / Last-run tabs. Visual: pill chips along the top of
|
|
15130
|
+
the Configure pane. */
|
|
15131
|
+
.cron-config-tabs {
|
|
15132
|
+
display: flex;
|
|
15133
|
+
gap: 4px;
|
|
15134
|
+
margin: 0 0 14px;
|
|
15135
|
+
border-bottom: 1px solid var(--border);
|
|
15136
|
+
padding-bottom: 0;
|
|
15137
|
+
}
|
|
15138
|
+
.cron-config-tab-btn {
|
|
15139
|
+
background: none;
|
|
15140
|
+
border: none;
|
|
15141
|
+
color: var(--text-muted);
|
|
15142
|
+
font-size: 12px;
|
|
15143
|
+
font-weight: 500;
|
|
15144
|
+
padding: 8px 12px;
|
|
15145
|
+
cursor: pointer;
|
|
15146
|
+
border-bottom: 2px solid transparent;
|
|
15147
|
+
transition: color 0.15s, border-color 0.15s;
|
|
15148
|
+
margin-bottom: -1px;
|
|
15149
|
+
}
|
|
15150
|
+
.cron-config-tab-btn:hover { color: var(--text-primary); }
|
|
15151
|
+
.cron-config-tab-btn.active {
|
|
15152
|
+
color: var(--accent);
|
|
15153
|
+
border-bottom-color: var(--accent);
|
|
15154
|
+
}
|
|
15155
|
+
/* Sections are tagged with data-config-tab; the active tab attr on the
|
|
15156
|
+
Configure pane controls visibility. .force-show overrides for power-user
|
|
15157
|
+
"show everything" later. */
|
|
15158
|
+
#cron-tab-configure[data-active-config-tab="basics"] [data-config-tab]:not([data-config-tab="basics"]),
|
|
15159
|
+
#cron-tab-configure[data-active-config-tab="prompt"] [data-config-tab]:not([data-config-tab="prompt"]),
|
|
15160
|
+
#cron-tab-configure[data-active-config-tab="tools"] [data-config-tab]:not([data-config-tab="tools"]),
|
|
15161
|
+
#cron-tab-configure[data-active-config-tab="scope"] [data-config-tab]:not([data-config-tab="scope"]),
|
|
15162
|
+
#cron-tab-configure[data-active-config-tab="limits"] [data-config-tab]:not([data-config-tab="limits"]) {
|
|
15163
|
+
display: none;
|
|
15164
|
+
}
|
|
15122
15165
|
/* ── Trick capability strip (skills + MCP + tools at a glance) ─── */
|
|
15123
15166
|
.task-cap-strip {
|
|
15124
15167
|
border-top: 1px solid var(--border-light);
|
|
@@ -19903,15 +19946,28 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19903
19946
|
<div class="cron-tabs" role="tablist">
|
|
19904
19947
|
<button type="button" class="cron-tab-btn active" data-cron-tab="configure" onclick="switchCronTab('configure')">Configure</button>
|
|
19905
19948
|
<button type="button" class="cron-tab-btn" id="cron-tab-btn-preview" data-cron-tab="preview" onclick="switchCronTab('preview')" title="See exactly what the agent will receive at fire-time">What will run</button>
|
|
19949
|
+
<button type="button" class="cron-tab-btn" id="cron-tab-btn-lastrun" data-cron-tab="lastrun" onclick="switchCronTab('lastrun')" title="Watch the most recent run — click Run task once to fire it now">Last run</button>
|
|
19906
19950
|
</div>
|
|
19907
19951
|
<div class="modal-body">
|
|
19908
19952
|
<!-- ── Tab: Configure ─────────────────────────────────────────── -->
|
|
19909
|
-
|
|
19953
|
+
<!-- PRD §5.1 right-inspector pattern lives here as inner tabs (1.3a).
|
|
19954
|
+
data-active-config-tab controls which section blocks render via
|
|
19955
|
+
CSS — no JS show/hide needed beyond setting the attribute. -->
|
|
19956
|
+
<div class="cron-tab-pane active" id="cron-tab-configure" data-active-config-tab="basics">
|
|
19910
19957
|
<!-- Legacy / mismatch banner (populated by openEditCronModal) -->
|
|
19911
19958
|
<div id="cron-legacy-banner-host"></div>
|
|
19912
19959
|
|
|
19960
|
+
<!-- Inner tab strip — pill nav swapping which section blocks show -->
|
|
19961
|
+
<div class="cron-config-tabs" role="tablist">
|
|
19962
|
+
<button type="button" class="cron-config-tab-btn active" data-config-tab-btn="basics" onclick="switchCronConfigTab('basics')">Basics</button>
|
|
19963
|
+
<button type="button" class="cron-config-tab-btn" data-config-tab-btn="prompt" onclick="switchCronConfigTab('prompt')">Prompt</button>
|
|
19964
|
+
<button type="button" class="cron-config-tab-btn" data-config-tab-btn="tools" onclick="switchCronConfigTab('tools')">Tools & MCP</button>
|
|
19965
|
+
<button type="button" class="cron-config-tab-btn" data-config-tab-btn="scope" onclick="switchCronConfigTab('scope')">Scope</button>
|
|
19966
|
+
<button type="button" class="cron-config-tab-btn" data-config-tab-btn="limits" onclick="switchCronConfigTab('limits')">Limits</button>
|
|
19967
|
+
</div>
|
|
19968
|
+
|
|
19913
19969
|
<!-- Identity: name + category + tags -->
|
|
19914
|
-
<div class="cron-section-card">
|
|
19970
|
+
<div class="cron-section-card" data-config-tab="basics">
|
|
19915
19971
|
<h4>Identity</h4>
|
|
19916
19972
|
<p class="cron-section-desc">A unique name and optional grouping for the dashboard.</p>
|
|
19917
19973
|
<div class="form-group">
|
|
@@ -19927,7 +19983,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19927
19983
|
</div>
|
|
19928
19984
|
|
|
19929
19985
|
<!-- Schedule -->
|
|
19930
|
-
<div class="cron-section-card">
|
|
19986
|
+
<div class="cron-section-card" data-config-tab="basics">
|
|
19931
19987
|
<h4>Schedule</h4>
|
|
19932
19988
|
<p class="cron-section-desc">When this task fires. Pick a frequency or write a cron expression.</p>
|
|
19933
19989
|
<div class="form-group" style="margin:0">
|
|
@@ -20016,7 +20072,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
20016
20072
|
</div>
|
|
20017
20073
|
|
|
20018
20074
|
<!-- What it does: prompt + context + reference files -->
|
|
20019
|
-
<div class="cron-section-card">
|
|
20075
|
+
<div class="cron-section-card" data-config-tab="prompt">
|
|
20020
20076
|
<h4>What it does</h4>
|
|
20021
20077
|
<p class="cron-section-desc">The instruction the agent receives. Long prompts are fine — drag the corner to resize.</p>
|
|
20022
20078
|
<div class="form-group">
|
|
@@ -20044,7 +20100,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
20044
20100
|
The single most important new field set. Without one of these, a
|
|
20045
20101
|
run "finished"; with one, a run "accomplished what it was meant
|
|
20046
20102
|
to". Banner warns (does not block) when neither is set. -->
|
|
20047
|
-
<div class="cron-section-card">
|
|
20103
|
+
<div class="cron-section-card" data-config-tab="prompt">
|
|
20048
20104
|
<h4>Goal <span style="color:var(--text-muted);font-weight:normal;font-size:12px">— how do you know this task succeeded?</span></h4>
|
|
20049
20105
|
<p class="cron-section-desc">Optional but strongly recommended. Use plain English (an evaluator agent grades the run) or a JSON Schema (validated against the agent's structured output).</p>
|
|
20050
20106
|
<div id="cron-goal-warning" style="display:none;margin-bottom:12px;padding:10px 12px;border-radius:6px;background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.30);color:var(--yellow);font-size:12px">
|
|
@@ -20067,7 +20123,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
20067
20123
|
</div>
|
|
20068
20124
|
|
|
20069
20125
|
<!-- Skills & tools: pinned skills + MCP + tools + tags -->
|
|
20070
|
-
<div class="cron-section-card">
|
|
20126
|
+
<div class="cron-section-card" data-config-tab="tools">
|
|
20071
20127
|
<h4>Skills & tools</h4>
|
|
20072
20128
|
<p class="cron-section-desc">Pin the skills and tools this task should use. Switch to the <strong>What will run</strong> tab to see exactly what the agent receives.</p>
|
|
20073
20129
|
|
|
@@ -20118,85 +20174,90 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
20118
20174
|
</div>
|
|
20119
20175
|
</div>
|
|
20120
20176
|
|
|
20121
|
-
<!--
|
|
20122
|
-
<
|
|
20123
|
-
<
|
|
20124
|
-
|
|
20125
|
-
|
|
20126
|
-
|
|
20127
|
-
|
|
20128
|
-
|
|
20129
|
-
|
|
20130
|
-
|
|
20131
|
-
|
|
20132
|
-
<option value="1">Tier 1 — Read-only (vault, search, web)</option>
|
|
20133
|
-
<option value="2">Tier 2 — Read + Write (Bash, files, sub-agents)</option>
|
|
20134
|
-
<option value="3">Tier 3 — Full access (use with caution)</option>
|
|
20135
|
-
</select>
|
|
20136
|
-
</div>
|
|
20137
|
-
<div class="form-group">
|
|
20138
|
-
<label class="form-label">Project Context <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
|
|
20139
|
-
<select id="cron-workdir">
|
|
20140
|
-
<option value="">None — runs in default context</option>
|
|
20141
|
-
</select>
|
|
20142
|
-
<div class="form-hint">Run inside a project directory. Agent gets that project's CLAUDE.md.</div>
|
|
20143
|
-
</div>
|
|
20177
|
+
<!-- ── Scope: where the task can read/write ── -->
|
|
20178
|
+
<div class="cron-section-card" data-config-tab="scope">
|
|
20179
|
+
<h4>Scope</h4>
|
|
20180
|
+
<p class="cron-section-desc">Where the agent runs and what files it can read.</p>
|
|
20181
|
+
<div class="form-row">
|
|
20182
|
+
<div class="form-group">
|
|
20183
|
+
<label class="form-label">Project Context <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
|
|
20184
|
+
<select id="cron-workdir">
|
|
20185
|
+
<option value="">None — runs in default context</option>
|
|
20186
|
+
</select>
|
|
20187
|
+
<div class="form-hint">Run inside a project directory. Agent gets that project's CLAUDE.md.</div>
|
|
20144
20188
|
</div>
|
|
20145
|
-
|
|
20146
|
-
|
|
20147
|
-
|
|
20148
|
-
|
|
20149
|
-
|
|
20150
|
-
|
|
20151
|
-
|
|
20189
|
+
</div>
|
|
20190
|
+
<!-- PRD Phase 1: read scope beyond cwd. One absolute path per line. -->
|
|
20191
|
+
<div class="form-row">
|
|
20192
|
+
<div class="form-group" style="flex:1">
|
|
20193
|
+
<label class="form-label">Additional read directories <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
|
|
20194
|
+
<textarea id="cron-add-dirs" rows="2" placeholder="/Users/me/notes /Users/me/clients/acme" style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
|
|
20195
|
+
<div class="form-hint">One absolute path per line. The agent gets read access to these in addition to the Project Context cwd.</div>
|
|
20152
20196
|
</div>
|
|
20153
|
-
|
|
20154
|
-
|
|
20155
|
-
|
|
20156
|
-
|
|
20157
|
-
|
|
20158
|
-
|
|
20159
|
-
|
|
20160
|
-
|
|
20161
|
-
|
|
20162
|
-
<
|
|
20163
|
-
|
|
20164
|
-
<
|
|
20165
|
-
|
|
20166
|
-
|
|
20167
|
-
|
|
20168
|
-
|
|
20169
|
-
<option value="8">8 hours</option>
|
|
20170
|
-
<option value="12">12 hours</option>
|
|
20171
|
-
<option value="24">24 hours</option>
|
|
20172
|
-
</select>
|
|
20173
|
-
</div>
|
|
20197
|
+
</div>
|
|
20198
|
+
</div>
|
|
20199
|
+
|
|
20200
|
+
<!-- ── Limits: budgets, mode, retries, chain triggers ── -->
|
|
20201
|
+
<div class="cron-section-card" data-config-tab="limits">
|
|
20202
|
+
<h4>Limits</h4>
|
|
20203
|
+
<p class="cron-section-desc">Permission level, runtime mode, and retry/chain rules.</p>
|
|
20204
|
+
<div class="form-row">
|
|
20205
|
+
<div class="form-group">
|
|
20206
|
+
<label class="form-label">Tier</label>
|
|
20207
|
+
<select id="cron-tier">
|
|
20208
|
+
<option value="1">Tier 1 — Read-only (vault, search, web)</option>
|
|
20209
|
+
<option value="2">Tier 2 — Read + Write (Bash, files, sub-agents)</option>
|
|
20210
|
+
<option value="3">Tier 3 — Full access (use with caution)</option>
|
|
20211
|
+
</select>
|
|
20212
|
+
<div class="form-hint">Permission level. Higher tiers can do more but require trust.</div>
|
|
20174
20213
|
</div>
|
|
20175
|
-
<div class="form-
|
|
20176
|
-
<
|
|
20177
|
-
|
|
20178
|
-
<
|
|
20179
|
-
|
|
20180
|
-
|
|
20181
|
-
|
|
20182
|
-
<option value="2">2</option>
|
|
20183
|
-
<option value="3">3</option>
|
|
20184
|
-
<option value="5">5</option>
|
|
20185
|
-
</select>
|
|
20186
|
-
<div class="form-hint">Override automatic retry count for transient errors.</div>
|
|
20187
|
-
</div>
|
|
20188
|
-
<div class="form-group">
|
|
20189
|
-
<label class="form-label">After Job <span style="color:var(--text-muted);font-weight:normal">(chain)</span></label>
|
|
20190
|
-
<select id="cron-after">
|
|
20191
|
-
<option value="">None — runs on schedule</option>
|
|
20192
|
-
</select>
|
|
20193
|
-
<div class="form-hint">Trigger after another job succeeds (ignores schedule).</div>
|
|
20194
|
-
</div>
|
|
20214
|
+
<div class="form-group">
|
|
20215
|
+
<label class="form-label">Mode</label>
|
|
20216
|
+
<select id="cron-mode" onchange="toggleUnleashedOptions()">
|
|
20217
|
+
<option value="standard">Standard</option>
|
|
20218
|
+
<option value="unleashed">Unleashed (long-running)</option>
|
|
20219
|
+
</select>
|
|
20220
|
+
<div class="form-hint">Unleashed runs in phases with checkpointing for hour-scale tasks.</div>
|
|
20195
20221
|
</div>
|
|
20196
20222
|
</div>
|
|
20197
|
-
|
|
20223
|
+
<div class="form-row">
|
|
20224
|
+
<div class="form-group" id="cron-maxhours-group" style="display:none">
|
|
20225
|
+
<label class="form-label">Max Hours</label>
|
|
20226
|
+
<select id="cron-maxhours">
|
|
20227
|
+
<option value="1">1 hour</option>
|
|
20228
|
+
<option value="2">2 hours</option>
|
|
20229
|
+
<option value="4">4 hours</option>
|
|
20230
|
+
<option value="6" selected>6 hours (default)</option>
|
|
20231
|
+
<option value="8">8 hours</option>
|
|
20232
|
+
<option value="12">12 hours</option>
|
|
20233
|
+
<option value="24">24 hours</option>
|
|
20234
|
+
</select>
|
|
20235
|
+
</div>
|
|
20236
|
+
<div class="form-group">
|
|
20237
|
+
<label class="form-label">Max Retries <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
|
|
20238
|
+
<select id="cron-max-retries">
|
|
20239
|
+
<option value="">Auto (based on error history)</option>
|
|
20240
|
+
<option value="0">0 — No retries</option>
|
|
20241
|
+
<option value="1">1</option>
|
|
20242
|
+
<option value="2">2</option>
|
|
20243
|
+
<option value="3">3</option>
|
|
20244
|
+
<option value="5">5</option>
|
|
20245
|
+
</select>
|
|
20246
|
+
<div class="form-hint">Override automatic retry count for transient errors.</div>
|
|
20247
|
+
</div>
|
|
20248
|
+
</div>
|
|
20249
|
+
<div class="form-row">
|
|
20250
|
+
<div class="form-group">
|
|
20251
|
+
<label class="form-label">After Job <span style="color:var(--text-muted);font-weight:normal">(chain)</span></label>
|
|
20252
|
+
<select id="cron-after">
|
|
20253
|
+
<option value="">None — runs on schedule</option>
|
|
20254
|
+
</select>
|
|
20255
|
+
<div class="form-hint">Trigger after another job succeeds (ignores schedule).</div>
|
|
20256
|
+
</div>
|
|
20257
|
+
</div>
|
|
20258
|
+
</div>
|
|
20198
20259
|
|
|
20199
|
-
<!-- Training Chat -->
|
|
20260
|
+
<!-- Training Chat — visible across all config tabs (it's a tool, not a field group) -->
|
|
20200
20261
|
<div class="form-group" id="cron-training-section" style="display:none">
|
|
20201
20262
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
|
|
20202
20263
|
<label class="form-label" style="margin:0">Training Chat</label>
|
|
@@ -20222,10 +20283,23 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
20222
20283
|
</div>
|
|
20223
20284
|
</div>
|
|
20224
20285
|
</div><!-- /cron-tab-preview -->
|
|
20286
|
+
|
|
20287
|
+
<!-- ── Tab: Last run ── PRD Phase 1.2: "Run task once" inline output. -->
|
|
20288
|
+
<div class="cron-tab-pane" id="cron-tab-lastrun">
|
|
20289
|
+
<div id="cron-lastrun-body" style="padding:0">
|
|
20290
|
+
<div style="padding:36px 24px;color:var(--text-muted);text-align:center;font-size:13px">
|
|
20291
|
+
Save the task first, then click <strong>Run task once</strong> to fire it now and watch the result here.
|
|
20292
|
+
</div>
|
|
20293
|
+
</div>
|
|
20294
|
+
</div><!-- /cron-tab-lastrun -->
|
|
20225
20295
|
</div>
|
|
20226
20296
|
<div class="modal-footer">
|
|
20227
20297
|
<div style="display:flex;align-items:center;gap:8px;flex:1">
|
|
20228
20298
|
<button class="btn btn-sm" id="cron-train-btn" onclick="showCronTraining()" style="font-size:11px;display:none">Train with Agent</button>
|
|
20299
|
+
<!-- PRD §5.1 header bullet: "Run task once" green button. Visible only
|
|
20300
|
+
when editing a saved task (set by openEditCronModal). Disabled
|
|
20301
|
+
during an in-flight run. -->
|
|
20302
|
+
<button class="btn btn-sm btn-success" id="cron-run-once-btn" onclick="runCronOnceFromModal()" style="display:none;font-size:12px;padding:6px 14px">▶ Run task once</button>
|
|
20229
20303
|
</div>
|
|
20230
20304
|
<button onclick="closeCronModal()">Cancel</button>
|
|
20231
20305
|
<button class="btn-primary" id="cron-modal-save" onclick="saveCronJob()">Create Task</button>
|
|
@@ -24750,8 +24824,10 @@ function switchCronTab(tab) {
|
|
|
24750
24824
|
});
|
|
24751
24825
|
var configurePane = document.getElementById('cron-tab-configure');
|
|
24752
24826
|
var previewPane = document.getElementById('cron-tab-preview');
|
|
24827
|
+
var lastRunPane = document.getElementById('cron-tab-lastrun');
|
|
24753
24828
|
if (configurePane) configurePane.classList.toggle('active', tab === 'configure');
|
|
24754
24829
|
if (previewPane) previewPane.classList.toggle('active', tab === 'preview');
|
|
24830
|
+
if (lastRunPane) lastRunPane.classList.toggle('active', tab === 'lastrun');
|
|
24755
24831
|
if (tab === 'preview') {
|
|
24756
24832
|
var name = editingCronJob;
|
|
24757
24833
|
if (!name) {
|
|
@@ -24760,6 +24836,10 @@ function switchCronTab(tab) {
|
|
|
24760
24836
|
return;
|
|
24761
24837
|
}
|
|
24762
24838
|
if (_cronPreviewLoadedFor !== name) loadCronPreviewIntoTab(name);
|
|
24839
|
+
} else if (tab === 'lastrun') {
|
|
24840
|
+
// Re-render in case run-state changed since the modal opened.
|
|
24841
|
+
var jobLR = (typeof cronJobsData !== 'undefined' ? cronJobsData : []).find(function(j) { return j.name === editingCronJob; });
|
|
24842
|
+
if (jobLR) renderCronLastRunPane(jobLR);
|
|
24763
24843
|
}
|
|
24764
24844
|
}
|
|
24765
24845
|
|
|
@@ -24784,6 +24864,181 @@ async function loadCronPreviewIntoTab(jobName) {
|
|
|
24784
24864
|
// Mark the preview as stale (call after save so next tab visit refetches).
|
|
24785
24865
|
function markCronPreviewDirty() { _cronPreviewLoadedFor = null; }
|
|
24786
24866
|
|
|
24867
|
+
// PRD Phase 1.3a: inner Configure pane tabs. Sets the data-active-config-tab
|
|
24868
|
+
// attribute on #cron-tab-configure; CSS handles section visibility via
|
|
24869
|
+
// data-config-tab attributes on each section. JS-light by design.
|
|
24870
|
+
function switchCronConfigTab(tab) {
|
|
24871
|
+
var pane = document.getElementById('cron-tab-configure');
|
|
24872
|
+
if (!pane) return;
|
|
24873
|
+
pane.setAttribute('data-active-config-tab', tab);
|
|
24874
|
+
document.querySelectorAll('.cron-config-tab-btn').forEach(function(b) {
|
|
24875
|
+
b.classList.toggle('active', b.getAttribute('data-config-tab-btn') === tab);
|
|
24876
|
+
});
|
|
24877
|
+
}
|
|
24878
|
+
|
|
24879
|
+
// ── PRD Phase 1.2: "Run task once" — inline run + Last run tab ───────────
|
|
24880
|
+
// Tracks an in-flight run triggered FROM the modal so the SSE listeners
|
|
24881
|
+
// know when a cron_complete event belongs to "the run I just kicked off"
|
|
24882
|
+
// vs a scheduled tick that fired in the background. Cleared when the run
|
|
24883
|
+
// completes or the modal closes.
|
|
24884
|
+
var _cronRunOnceInFlight = null; // { jobName: string, startedAt: number }
|
|
24885
|
+
var _cronRunOnceTickerId = null; // setInterval id for the elapsed counter
|
|
24886
|
+
|
|
24887
|
+
// Render the Last run pane from the job's most-recent JSONL entry. Called
|
|
24888
|
+
// when the modal opens for a saved task and when switchCronTab('lastrun')
|
|
24889
|
+
// reactivates the pane.
|
|
24890
|
+
function renderCronLastRunPane(job) {
|
|
24891
|
+
var pane = document.getElementById('cron-lastrun-body');
|
|
24892
|
+
if (!pane) return;
|
|
24893
|
+
// If we have an in-flight run, render the running state regardless of
|
|
24894
|
+
// what's on disk — the on-disk lastRun is from BEFORE this fire.
|
|
24895
|
+
if (_cronRunOnceInFlight && _cronRunOnceInFlight.jobName === (job && job.name)) {
|
|
24896
|
+
pane.innerHTML = renderCronRunningState(_cronRunOnceInFlight.startedAt);
|
|
24897
|
+
return;
|
|
24898
|
+
}
|
|
24899
|
+
var lr = job && job.lastRun;
|
|
24900
|
+
if (!lr) {
|
|
24901
|
+
pane.innerHTML = '<div style="padding:36px 24px;color:var(--text-muted);text-align:center;font-size:13px">No runs yet. Click <strong>Run task once</strong> below to fire it now and watch the result here.</div>';
|
|
24902
|
+
return;
|
|
24903
|
+
}
|
|
24904
|
+
pane.innerHTML = renderCronRunDetails(lr);
|
|
24905
|
+
}
|
|
24906
|
+
|
|
24907
|
+
function renderCronRunningState(startedAtMs) {
|
|
24908
|
+
var elapsed = Math.max(0, Math.round((Date.now() - startedAtMs) / 1000));
|
|
24909
|
+
return ''
|
|
24910
|
+
+ '<div style="padding:36px 24px;text-align:center">'
|
|
24911
|
+
+ '<div class="run-once-pulse" style="font-size:14px;color:var(--accent);font-weight:500;margin-bottom:8px">'
|
|
24912
|
+
+ '<span class="pulse-dot" style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);margin-right:6px;animation:pulse 1.4s ease-in-out infinite"></span>'
|
|
24913
|
+
+ 'Running…'
|
|
24914
|
+
+ '</div>'
|
|
24915
|
+
+ '<div style="font-size:12px;color:var(--text-muted)">Elapsed: <span id="cron-run-once-elapsed">' + elapsed + 's</span></div>'
|
|
24916
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:14px">Live output streaming will land here when the run completes.</div>'
|
|
24917
|
+
+ '</div>';
|
|
24918
|
+
}
|
|
24919
|
+
|
|
24920
|
+
function renderCronRunDetails(lr) {
|
|
24921
|
+
var ok = lr.status === 'ok';
|
|
24922
|
+
var statusColor = ok ? 'var(--green)' : (lr.status === 'error' ? 'var(--red)' : 'var(--yellow)');
|
|
24923
|
+
var statusIcon = ok ? '✓' : (lr.status === 'error' ? '✗' : '⏱');
|
|
24924
|
+
var dur = lr.durationMs != null ? formatDurationMs(lr.durationMs) : '—';
|
|
24925
|
+
var when = lr.finishedAt || lr.startedAt;
|
|
24926
|
+
var whenLabel = when ? new Date(when).toLocaleString() : '—';
|
|
24927
|
+
var html = ''
|
|
24928
|
+
+ '<div style="padding:24px">'
|
|
24929
|
+
+ '<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:14px">'
|
|
24930
|
+
+ '<span style="color:' + statusColor + ';font-size:18px">' + statusIcon + '</span>'
|
|
24931
|
+
+ '<span style="font-size:14px;font-weight:600;color:var(--text-primary);text-transform:capitalize">' + esc(lr.status || 'unknown') + '</span>'
|
|
24932
|
+
+ '<span style="flex:1"></span>'
|
|
24933
|
+
+ '<span style="font-size:12px;color:var(--text-muted)">' + esc(whenLabel) + ' · ' + esc(dur) + (lr.attempt && lr.attempt > 1 ? ' · attempt ' + esc(lr.attempt) : '') + '</span>'
|
|
24934
|
+
+ '</div>';
|
|
24935
|
+
if (lr.goalCheck) {
|
|
24936
|
+
var gc = lr.goalCheck;
|
|
24937
|
+
var gIcon = gc.status === 'pass' ? '🎯' : gc.status === 'fail' ? '✗' : '⚠';
|
|
24938
|
+
var gColor = gc.status === 'pass' ? 'var(--green)' : gc.status === 'fail' ? 'var(--red)' : 'var(--yellow)';
|
|
24939
|
+
var gLabel = gc.status === 'pass' ? 'Goal met' : gc.status === 'fail' ? 'Goal NOT met' : 'Goal evaluation failed';
|
|
24940
|
+
var gReason = gc.evaluatorReason || (Array.isArray(gc.schemaErrors) ? gc.schemaErrors.join('; ') : '');
|
|
24941
|
+
html += '<div style="padding:10px 14px;border-radius:6px;background:rgba(255,255,255,0.04);border-left:3px solid ' + gColor + ';margin-bottom:14px">'
|
|
24942
|
+
+ '<div style="font-size:13px;font-weight:500;color:' + gColor + '">' + gIcon + ' ' + gLabel + '</div>'
|
|
24943
|
+
+ (gReason ? '<div style="font-size:12px;color:var(--text-secondary);margin-top:4px">' + esc(gReason) + '</div>' : '')
|
|
24944
|
+
+ '</div>';
|
|
24945
|
+
}
|
|
24946
|
+
if (lr.error) {
|
|
24947
|
+
html += '<div style="margin-bottom:14px"><div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">Error</div>'
|
|
24948
|
+
+ '<div style="font-family:\\x27JetBrains Mono\\x27,monospace;font-size:11px;color:var(--red);background:rgba(239,68,68,0.06);border:1px solid rgba(239,68,68,0.2);padding:10px;border-radius:6px;white-space:pre-wrap;word-break:break-word">'
|
|
24949
|
+
+ esc(String(lr.error).slice(0, 2000)) + '</div></div>';
|
|
24950
|
+
}
|
|
24951
|
+
if (lr.outputPreview) {
|
|
24952
|
+
html += '<div style="margin-bottom:14px"><div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">Output preview</div>'
|
|
24953
|
+
+ '<div style="font-size:12px;color:var(--text-primary);background:var(--bg-secondary);border:1px solid var(--border);padding:10px;border-radius:6px;white-space:pre-wrap;word-break:break-word;max-height:300px;overflow-y:auto">'
|
|
24954
|
+
+ esc(String(lr.outputPreview).slice(0, 4000)) + '</div></div>';
|
|
24955
|
+
}
|
|
24956
|
+
if (Array.isArray(lr.skillsApplied) && lr.skillsApplied.length) {
|
|
24957
|
+
html += '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px">Skills active: ' + esc(lr.skillsApplied.map(function(s){ return s.name; }).join(', ')) + '</div>';
|
|
24958
|
+
}
|
|
24959
|
+
if (Array.isArray(lr.mcpServersApplied) && lr.mcpServersApplied.length) {
|
|
24960
|
+
html += '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px">MCP servers: ' + esc(lr.mcpServersApplied.join(', ')) + '</div>';
|
|
24961
|
+
}
|
|
24962
|
+
html += '<div style="margin-top:14px;display:flex;gap:8px"><button class="btn-sm" onclick="openTraceViewer(\\x27' + jsStr(lr.jobName || editingCronJob || '') + '\\x27)" style="font-size:11px">Open trace</button></div>';
|
|
24963
|
+
html += '</div>';
|
|
24964
|
+
return html;
|
|
24965
|
+
}
|
|
24966
|
+
|
|
24967
|
+
// Click handler for the green "Run task once" button. Triggers the existing
|
|
24968
|
+
// /api/cron/run/:job endpoint and switches to the Last run tab so the user
|
|
24969
|
+
// sees the running state. The SSE handler at the bottom of this file picks
|
|
24970
|
+
// up cron_complete and re-renders the pane with the result.
|
|
24971
|
+
async function runCronOnceFromModal() {
|
|
24972
|
+
if (!editingCronJob) {
|
|
24973
|
+
toast('Save the task first, then Run task once.', 'error');
|
|
24974
|
+
return;
|
|
24975
|
+
}
|
|
24976
|
+
if (_cronRunOnceInFlight) {
|
|
24977
|
+
toast('Already running — wait for the current run to finish.', 'info');
|
|
24978
|
+
return;
|
|
24979
|
+
}
|
|
24980
|
+
if (isCronModalDirty()) {
|
|
24981
|
+
if (!confirm('You have unsaved changes. Run the SAVED version (your edits stay in the form)?')) return;
|
|
24982
|
+
}
|
|
24983
|
+
var btn = document.getElementById('cron-run-once-btn');
|
|
24984
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Triggering…'; }
|
|
24985
|
+
_cronRunOnceInFlight = { jobName: editingCronJob, startedAt: Date.now() };
|
|
24986
|
+
// Show the running state and switch the pane immediately.
|
|
24987
|
+
switchCronTab('lastrun');
|
|
24988
|
+
var pane = document.getElementById('cron-lastrun-body');
|
|
24989
|
+
if (pane) pane.innerHTML = renderCronRunningState(_cronRunOnceInFlight.startedAt);
|
|
24990
|
+
// Tick the elapsed counter once a second.
|
|
24991
|
+
if (_cronRunOnceTickerId) clearInterval(_cronRunOnceTickerId);
|
|
24992
|
+
_cronRunOnceTickerId = setInterval(function() {
|
|
24993
|
+
if (!_cronRunOnceInFlight) { clearInterval(_cronRunOnceTickerId); _cronRunOnceTickerId = null; return; }
|
|
24994
|
+
var elapsedEl = document.getElementById('cron-run-once-elapsed');
|
|
24995
|
+
if (elapsedEl) {
|
|
24996
|
+
var s = Math.max(0, Math.round((Date.now() - _cronRunOnceInFlight.startedAt) / 1000));
|
|
24997
|
+
elapsedEl.textContent = s + 's';
|
|
24998
|
+
}
|
|
24999
|
+
}, 1000);
|
|
25000
|
+
try {
|
|
25001
|
+
var r = await apiFetch('/api/cron/run/' + encodeURIComponent(editingCronJob), { method: 'POST' });
|
|
25002
|
+
var d = await r.json();
|
|
25003
|
+
if (!r.ok || d.ok === false) {
|
|
25004
|
+
toast(d.error || 'Run failed to start', 'error');
|
|
25005
|
+
_cronRunOnceInFlight = null;
|
|
25006
|
+
if (_cronRunOnceTickerId) { clearInterval(_cronRunOnceTickerId); _cronRunOnceTickerId = null; }
|
|
25007
|
+
if (pane) pane.innerHTML = '<div style="padding:36px 24px;color:var(--red);text-align:center;font-size:13px">' + esc(d.error || 'Run failed to start') + '</div>';
|
|
25008
|
+
}
|
|
25009
|
+
} catch (e) {
|
|
25010
|
+
toast('Run failed to start: ' + String(e), 'error');
|
|
25011
|
+
_cronRunOnceInFlight = null;
|
|
25012
|
+
if (_cronRunOnceTickerId) { clearInterval(_cronRunOnceTickerId); _cronRunOnceTickerId = null; }
|
|
25013
|
+
} finally {
|
|
25014
|
+
if (btn) { btn.disabled = false; btn.textContent = '▶ Run task once'; }
|
|
25015
|
+
}
|
|
25016
|
+
}
|
|
25017
|
+
|
|
25018
|
+
// Called from the SSE handler when cron_complete fires. The same SSE handler
|
|
25019
|
+
// also schedules refreshCron() which updates cronJobsData with the fresh
|
|
25020
|
+
// lastRun. We just wait a beat for that, then re-render the pane.
|
|
25021
|
+
function handleCronRunOnceComplete(jobName) {
|
|
25022
|
+
if (!_cronRunOnceInFlight || _cronRunOnceInFlight.jobName !== jobName) return;
|
|
25023
|
+
if (_cronRunOnceTickerId) { clearInterval(_cronRunOnceTickerId); _cronRunOnceTickerId = null; }
|
|
25024
|
+
_cronRunOnceInFlight = null;
|
|
25025
|
+
// refreshCron is racing with us; give it ~600ms to land the new entry
|
|
25026
|
+
// into cronJobsData before we read. The SSE handler at line 34927 already
|
|
25027
|
+
// kicks it off when this event arrives.
|
|
25028
|
+
setTimeout(function() {
|
|
25029
|
+
var pane = document.getElementById('cron-lastrun-body');
|
|
25030
|
+
if (!pane) return;
|
|
25031
|
+
var fresh = (Array.isArray(cronJobsData) ? cronJobsData : []).find(function(j) { return j.name === jobName; });
|
|
25032
|
+
var lr = fresh && fresh.lastRun;
|
|
25033
|
+
if (lr) {
|
|
25034
|
+
pane.innerHTML = renderCronRunDetails(lr);
|
|
25035
|
+
toast('Run finished — ' + (lr.status === 'ok' ? 'success' : lr.status), lr.status === 'ok' ? 'success' : 'error');
|
|
25036
|
+
} else {
|
|
25037
|
+
pane.innerHTML = '<div style="padding:36px 24px;color:var(--text-muted);text-align:center;font-size:13px">Run finished but the result is still propagating. Refresh the dashboard to see it.</div>';
|
|
25038
|
+
}
|
|
25039
|
+
}, 600);
|
|
25040
|
+
}
|
|
25041
|
+
|
|
24787
25042
|
// ── Predictable mode: visual card sync + legacy banner ───────────
|
|
24788
25043
|
function onPredictableChange() {
|
|
24789
25044
|
var predEl = document.getElementById('cron-predictable');
|
|
@@ -24899,12 +25154,18 @@ function openCreateCronModal(agentSlug) {
|
|
|
24899
25154
|
// No saved state to preview when creating — disable the Preview tab.
|
|
24900
25155
|
var previewBtn = document.getElementById('cron-tab-btn-preview');
|
|
24901
25156
|
if (previewBtn) previewBtn.setAttribute('disabled', 'disabled');
|
|
25157
|
+
// Last run + Run-task-once button only make sense for saved tasks.
|
|
25158
|
+
var lastRunBtn = document.getElementById('cron-tab-btn-lastrun');
|
|
25159
|
+
if (lastRunBtn) lastRunBtn.setAttribute('disabled', 'disabled');
|
|
25160
|
+
var runOnceBtn = document.getElementById('cron-run-once-btn');
|
|
25161
|
+
if (runOnceBtn) runOnceBtn.style.display = 'none';
|
|
24902
25162
|
var host = document.getElementById('cron-legacy-banner-host');
|
|
24903
25163
|
if (host) host.innerHTML = '';
|
|
24904
25164
|
// Reset the "Use a cron expression" link in case it was hidden last time.
|
|
24905
25165
|
var schedLink = document.getElementById('sched-cron-link');
|
|
24906
25166
|
if (schedLink) schedLink.style.display = '';
|
|
24907
25167
|
switchCronTab('configure');
|
|
25168
|
+
switchCronConfigTab('basics'); // PRD 1.3a: always start on Basics for new tasks
|
|
24908
25169
|
onPredictableChange();
|
|
24909
25170
|
document.getElementById('cron-modal').classList.add('show');
|
|
24910
25171
|
// Snapshot AFTER all defaults are populated so an immediate close with no
|
|
@@ -24990,10 +25251,20 @@ function openEditCronModal(jobName) {
|
|
|
24990
25251
|
renderTagsPickerChips();
|
|
24991
25252
|
_pendingAttachments = [];
|
|
24992
25253
|
loadCronAttachments(jobName);
|
|
24993
|
-
// Existing job has saved state, enable Preview
|
|
25254
|
+
// Existing job has saved state, enable Preview + Last run tabs.
|
|
24994
25255
|
var previewBtn = document.getElementById('cron-tab-btn-preview');
|
|
24995
25256
|
if (previewBtn) previewBtn.removeAttribute('disabled');
|
|
25257
|
+
var lastRunBtnEdit = document.getElementById('cron-tab-btn-lastrun');
|
|
25258
|
+
if (lastRunBtnEdit) lastRunBtnEdit.removeAttribute('disabled');
|
|
25259
|
+
// Show "Run task once" only for saved tasks.
|
|
25260
|
+
var runOnceBtnEdit = document.getElementById('cron-run-once-btn');
|
|
25261
|
+
if (runOnceBtnEdit) runOnceBtnEdit.style.display = '';
|
|
25262
|
+
// Render the most recent run from the loaded job into the Last run tab so
|
|
25263
|
+
// the user sees something the moment they switch to it (rather than a
|
|
25264
|
+
// dead empty pane). The pane updates live when Run task once fires.
|
|
25265
|
+
renderCronLastRunPane(job);
|
|
24996
25266
|
switchCronTab('configure');
|
|
25267
|
+
switchCronConfigTab('basics'); // PRD 1.3a: always start on Basics
|
|
24997
25268
|
document.getElementById('cron-modal').classList.add('show');
|
|
24998
25269
|
setTimeout(captureCronModalSnapshot, 0);
|
|
24999
25270
|
}
|
|
@@ -25200,6 +25471,10 @@ function closeCronModal(force) {
|
|
|
25200
25471
|
editingCronJob = null;
|
|
25201
25472
|
_cronPreviewLoadedFor = null;
|
|
25202
25473
|
_cronModalSnapshot = null;
|
|
25474
|
+
// Clear any pending Run-task-once watch so SSE events for a different job
|
|
25475
|
+
// don't accidentally re-render the (now closed) Last run pane.
|
|
25476
|
+
if (_cronRunOnceTickerId) { clearInterval(_cronRunOnceTickerId); _cronRunOnceTickerId = null; }
|
|
25477
|
+
_cronRunOnceInFlight = null;
|
|
25203
25478
|
var attachList = document.getElementById('cron-attachments-list');
|
|
25204
25479
|
if (attachList) attachList.innerHTML = '';
|
|
25205
25480
|
var bannerHost = document.getElementById('cron-legacy-banner-host');
|
|
@@ -25336,26 +25611,34 @@ async function saveCronJob() {
|
|
|
25336
25611
|
// Field-specific validation. Toasting one message per problem is more
|
|
25337
25612
|
// actionable than the old "Please fill in all fields" — and we bail before
|
|
25338
25613
|
// hitting the API so the user sees the issue without a round-trip.
|
|
25339
|
-
|
|
25614
|
+
// Helper: focus a field on a possibly-hidden inner config tab. Switches
|
|
25615
|
+
// to the tab that owns the field first so the user sees the cursor land.
|
|
25616
|
+
function focusOnConfigTab(fieldId, configTab) {
|
|
25617
|
+
if (typeof switchCronConfigTab === 'function') switchCronConfigTab(configTab);
|
|
25618
|
+
var el = document.getElementById(fieldId);
|
|
25619
|
+
if (el) el.focus();
|
|
25620
|
+
}
|
|
25621
|
+
if (!name) { toast('Task name is required', 'error'); focusOnConfigTab('cron-name', 'basics'); return; }
|
|
25340
25622
|
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
|
|
25341
25623
|
toast('Task name must start with a lowercase letter and contain only a–z, 0–9, and hyphens (max 64 chars)', 'error');
|
|
25342
|
-
|
|
25624
|
+
focusOnConfigTab('cron-name', 'basics');
|
|
25343
25625
|
return;
|
|
25344
25626
|
}
|
|
25345
25627
|
if (!editingCronJob && Array.isArray(cronJobsData)) {
|
|
25346
25628
|
var dup = cronJobsData.find(function(j) { return String(j.name || '').toLowerCase() === name.toLowerCase(); });
|
|
25347
|
-
if (dup) { toast('A task named "' + name + '" already exists', 'error');
|
|
25629
|
+
if (dup) { toast('A task named "' + name + '" already exists', 'error'); focusOnConfigTab('cron-name', 'basics'); return; }
|
|
25348
25630
|
}
|
|
25349
|
-
if (!schedule) { toast('Schedule is required', 'error'); return; }
|
|
25631
|
+
if (!schedule) { toast('Schedule is required', 'error'); switchCronConfigTab('basics'); return; }
|
|
25350
25632
|
// Light client-side cron sanity check — server uses real cron-parser.
|
|
25351
25633
|
// Catches obvious garbage like "foo bar" without making a round-trip.
|
|
25352
25634
|
// Note: the hyphen sits at the END of the character class to avoid being
|
|
25353
25635
|
// interpreted as a range. \\\\s in source → \\s in served JS → \s in regex.
|
|
25354
25636
|
if (!/^([0-9*/, -]+|@(yearly|annually|monthly|weekly|daily|hourly|reboot))$/i.test(schedule) && schedule.split(/\\s+/).length < 5) {
|
|
25355
25637
|
toast('Schedule does not look like a valid cron expression', 'error');
|
|
25638
|
+
switchCronConfigTab('basics');
|
|
25356
25639
|
return;
|
|
25357
25640
|
}
|
|
25358
|
-
if (!prompt) { toast('Prompt is required — tell the agent what to do', 'error');
|
|
25641
|
+
if (!prompt) { toast('Prompt is required — tell the agent what to do', 'error'); focusOnConfigTab('cron-prompt', 'prompt'); return; }
|
|
25359
25642
|
|
|
25360
25643
|
// PRD Phase 1 goal fields. successCriteriaText is freeform; successSchema
|
|
25361
25644
|
// is parsed JSON. Validate JSON early so the user gets a clean error before
|
|
@@ -25368,12 +25651,12 @@ async function saveCronJob() {
|
|
|
25368
25651
|
successSchema = JSON.parse(successSchemaRaw);
|
|
25369
25652
|
if (!successSchema || typeof successSchema !== 'object' || Array.isArray(successSchema)) {
|
|
25370
25653
|
toast('Success schema must be a JSON object', 'error');
|
|
25371
|
-
|
|
25654
|
+
focusOnConfigTab('cron-success-schema', 'prompt');
|
|
25372
25655
|
return;
|
|
25373
25656
|
}
|
|
25374
25657
|
} catch (e) {
|
|
25375
25658
|
toast('Success schema is not valid JSON: ' + (e.message || String(e)), 'error');
|
|
25376
|
-
|
|
25659
|
+
focusOnConfigTab('cron-success-schema', 'prompt');
|
|
25377
25660
|
return;
|
|
25378
25661
|
}
|
|
25379
25662
|
}
|
|
@@ -34891,6 +35174,11 @@ try {
|
|
|
34891
35174
|
refreshActivity();
|
|
34892
35175
|
if (currentPage === 'build') refreshCron();
|
|
34893
35176
|
refreshTeamNav();
|
|
35177
|
+
// PRD Phase 1.2: if the user just clicked "Run task once" in the
|
|
35178
|
+
// modal, re-render the Last run pane with the fresh result.
|
|
35179
|
+
if (evt.type === 'cron_complete' && evt.data && evt.data.job && typeof handleCronRunOnceComplete === 'function') {
|
|
35180
|
+
try { handleCronRunOnceComplete(evt.data.job); } catch (err) { /* non-fatal */ }
|
|
35181
|
+
}
|
|
34894
35182
|
}
|
|
34895
35183
|
// A delete on one tab should drop the card from every open dashboard
|
|
34896
35184
|
// without waiting for the next poll. cron_toggled is similar but lighter.
|