clementine-agent 1.18.78 → 1.18.79

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.
@@ -15119,6 +15119,11 @@ 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
+ }
15122
15127
  /* ── Trick capability strip (skills + MCP + tools at a glance) ─── */
15123
15128
  .task-cap-strip {
15124
15129
  border-top: 1px solid var(--border-light);
@@ -19903,6 +19908,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19903
19908
  <div class="cron-tabs" role="tablist">
19904
19909
  <button type="button" class="cron-tab-btn active" data-cron-tab="configure" onclick="switchCronTab('configure')">Configure</button>
19905
19910
  <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>
19911
+ <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
19912
  </div>
19907
19913
  <div class="modal-body">
19908
19914
  <!-- ── Tab: Configure ─────────────────────────────────────────── -->
@@ -20222,10 +20228,23 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20222
20228
  </div>
20223
20229
  </div>
20224
20230
  </div><!-- /cron-tab-preview -->
20231
+
20232
+ <!-- ── Tab: Last run ── PRD Phase 1.2: "Run task once" inline output. -->
20233
+ <div class="cron-tab-pane" id="cron-tab-lastrun">
20234
+ <div id="cron-lastrun-body" style="padding:0">
20235
+ <div style="padding:36px 24px;color:var(--text-muted);text-align:center;font-size:13px">
20236
+ Save the task first, then click <strong>Run task once</strong> to fire it now and watch the result here.
20237
+ </div>
20238
+ </div>
20239
+ </div><!-- /cron-tab-lastrun -->
20225
20240
  </div>
20226
20241
  <div class="modal-footer">
20227
20242
  <div style="display:flex;align-items:center;gap:8px;flex:1">
20228
20243
  <button class="btn btn-sm" id="cron-train-btn" onclick="showCronTraining()" style="font-size:11px;display:none">Train with Agent</button>
20244
+ <!-- PRD §5.1 header bullet: "Run task once" green button. Visible only
20245
+ when editing a saved task (set by openEditCronModal). Disabled
20246
+ during an in-flight run. -->
20247
+ <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
20248
  </div>
20230
20249
  <button onclick="closeCronModal()">Cancel</button>
20231
20250
  <button class="btn-primary" id="cron-modal-save" onclick="saveCronJob()">Create Task</button>
@@ -24750,8 +24769,10 @@ function switchCronTab(tab) {
24750
24769
  });
24751
24770
  var configurePane = document.getElementById('cron-tab-configure');
24752
24771
  var previewPane = document.getElementById('cron-tab-preview');
24772
+ var lastRunPane = document.getElementById('cron-tab-lastrun');
24753
24773
  if (configurePane) configurePane.classList.toggle('active', tab === 'configure');
24754
24774
  if (previewPane) previewPane.classList.toggle('active', tab === 'preview');
24775
+ if (lastRunPane) lastRunPane.classList.toggle('active', tab === 'lastrun');
24755
24776
  if (tab === 'preview') {
24756
24777
  var name = editingCronJob;
24757
24778
  if (!name) {
@@ -24760,6 +24781,10 @@ function switchCronTab(tab) {
24760
24781
  return;
24761
24782
  }
24762
24783
  if (_cronPreviewLoadedFor !== name) loadCronPreviewIntoTab(name);
24784
+ } else if (tab === 'lastrun') {
24785
+ // Re-render in case run-state changed since the modal opened.
24786
+ var jobLR = (typeof cronJobsData !== 'undefined' ? cronJobsData : []).find(function(j) { return j.name === editingCronJob; });
24787
+ if (jobLR) renderCronLastRunPane(jobLR);
24763
24788
  }
24764
24789
  }
24765
24790
 
@@ -24784,6 +24809,169 @@ async function loadCronPreviewIntoTab(jobName) {
24784
24809
  // Mark the preview as stale (call after save so next tab visit refetches).
24785
24810
  function markCronPreviewDirty() { _cronPreviewLoadedFor = null; }
24786
24811
 
24812
+ // ── PRD Phase 1.2: "Run task once" — inline run + Last run tab ───────────
24813
+ // Tracks an in-flight run triggered FROM the modal so the SSE listeners
24814
+ // know when a cron_complete event belongs to "the run I just kicked off"
24815
+ // vs a scheduled tick that fired in the background. Cleared when the run
24816
+ // completes or the modal closes.
24817
+ var _cronRunOnceInFlight = null; // { jobName: string, startedAt: number }
24818
+ var _cronRunOnceTickerId = null; // setInterval id for the elapsed counter
24819
+
24820
+ // Render the Last run pane from the job's most-recent JSONL entry. Called
24821
+ // when the modal opens for a saved task and when switchCronTab('lastrun')
24822
+ // reactivates the pane.
24823
+ function renderCronLastRunPane(job) {
24824
+ var pane = document.getElementById('cron-lastrun-body');
24825
+ if (!pane) return;
24826
+ // If we have an in-flight run, render the running state regardless of
24827
+ // what's on disk — the on-disk lastRun is from BEFORE this fire.
24828
+ if (_cronRunOnceInFlight && _cronRunOnceInFlight.jobName === (job && job.name)) {
24829
+ pane.innerHTML = renderCronRunningState(_cronRunOnceInFlight.startedAt);
24830
+ return;
24831
+ }
24832
+ var lr = job && job.lastRun;
24833
+ if (!lr) {
24834
+ 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>';
24835
+ return;
24836
+ }
24837
+ pane.innerHTML = renderCronRunDetails(lr);
24838
+ }
24839
+
24840
+ function renderCronRunningState(startedAtMs) {
24841
+ var elapsed = Math.max(0, Math.round((Date.now() - startedAtMs) / 1000));
24842
+ return ''
24843
+ + '<div style="padding:36px 24px;text-align:center">'
24844
+ + '<div class="run-once-pulse" style="font-size:14px;color:var(--accent);font-weight:500;margin-bottom:8px">'
24845
+ + '<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>'
24846
+ + 'Running…'
24847
+ + '</div>'
24848
+ + '<div style="font-size:12px;color:var(--text-muted)">Elapsed: <span id="cron-run-once-elapsed">' + elapsed + 's</span></div>'
24849
+ + '<div style="font-size:11px;color:var(--text-muted);margin-top:14px">Live output streaming will land here when the run completes.</div>'
24850
+ + '</div>';
24851
+ }
24852
+
24853
+ function renderCronRunDetails(lr) {
24854
+ var ok = lr.status === 'ok';
24855
+ var statusColor = ok ? 'var(--green)' : (lr.status === 'error' ? 'var(--red)' : 'var(--yellow)');
24856
+ var statusIcon = ok ? '✓' : (lr.status === 'error' ? '✗' : '⏱');
24857
+ var dur = lr.durationMs != null ? formatDurationMs(lr.durationMs) : '—';
24858
+ var when = lr.finishedAt || lr.startedAt;
24859
+ var whenLabel = when ? new Date(when).toLocaleString() : '—';
24860
+ var html = ''
24861
+ + '<div style="padding:24px">'
24862
+ + '<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:14px">'
24863
+ + '<span style="color:' + statusColor + ';font-size:18px">' + statusIcon + '</span>'
24864
+ + '<span style="font-size:14px;font-weight:600;color:var(--text-primary);text-transform:capitalize">' + esc(lr.status || 'unknown') + '</span>'
24865
+ + '<span style="flex:1"></span>'
24866
+ + '<span style="font-size:12px;color:var(--text-muted)">' + esc(whenLabel) + ' · ' + esc(dur) + (lr.attempt && lr.attempt > 1 ? ' · attempt ' + esc(lr.attempt) : '') + '</span>'
24867
+ + '</div>';
24868
+ if (lr.goalCheck) {
24869
+ var gc = lr.goalCheck;
24870
+ var gIcon = gc.status === 'pass' ? '🎯' : gc.status === 'fail' ? '✗' : '⚠';
24871
+ var gColor = gc.status === 'pass' ? 'var(--green)' : gc.status === 'fail' ? 'var(--red)' : 'var(--yellow)';
24872
+ var gLabel = gc.status === 'pass' ? 'Goal met' : gc.status === 'fail' ? 'Goal NOT met' : 'Goal evaluation failed';
24873
+ var gReason = gc.evaluatorReason || (Array.isArray(gc.schemaErrors) ? gc.schemaErrors.join('; ') : '');
24874
+ html += '<div style="padding:10px 14px;border-radius:6px;background:rgba(255,255,255,0.04);border-left:3px solid ' + gColor + ';margin-bottom:14px">'
24875
+ + '<div style="font-size:13px;font-weight:500;color:' + gColor + '">' + gIcon + ' ' + gLabel + '</div>'
24876
+ + (gReason ? '<div style="font-size:12px;color:var(--text-secondary);margin-top:4px">' + esc(gReason) + '</div>' : '')
24877
+ + '</div>';
24878
+ }
24879
+ if (lr.error) {
24880
+ 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>'
24881
+ + '<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">'
24882
+ + esc(String(lr.error).slice(0, 2000)) + '</div></div>';
24883
+ }
24884
+ if (lr.outputPreview) {
24885
+ 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>'
24886
+ + '<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">'
24887
+ + esc(String(lr.outputPreview).slice(0, 4000)) + '</div></div>';
24888
+ }
24889
+ if (Array.isArray(lr.skillsApplied) && lr.skillsApplied.length) {
24890
+ 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>';
24891
+ }
24892
+ if (Array.isArray(lr.mcpServersApplied) && lr.mcpServersApplied.length) {
24893
+ html += '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px">MCP servers: ' + esc(lr.mcpServersApplied.join(', ')) + '</div>';
24894
+ }
24895
+ 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>';
24896
+ html += '</div>';
24897
+ return html;
24898
+ }
24899
+
24900
+ // Click handler for the green "Run task once" button. Triggers the existing
24901
+ // /api/cron/run/:job endpoint and switches to the Last run tab so the user
24902
+ // sees the running state. The SSE handler at the bottom of this file picks
24903
+ // up cron_complete and re-renders the pane with the result.
24904
+ async function runCronOnceFromModal() {
24905
+ if (!editingCronJob) {
24906
+ toast('Save the task first, then Run task once.', 'error');
24907
+ return;
24908
+ }
24909
+ if (_cronRunOnceInFlight) {
24910
+ toast('Already running — wait for the current run to finish.', 'info');
24911
+ return;
24912
+ }
24913
+ if (isCronModalDirty()) {
24914
+ if (!confirm('You have unsaved changes. Run the SAVED version (your edits stay in the form)?')) return;
24915
+ }
24916
+ var btn = document.getElementById('cron-run-once-btn');
24917
+ if (btn) { btn.disabled = true; btn.textContent = 'Triggering…'; }
24918
+ _cronRunOnceInFlight = { jobName: editingCronJob, startedAt: Date.now() };
24919
+ // Show the running state and switch the pane immediately.
24920
+ switchCronTab('lastrun');
24921
+ var pane = document.getElementById('cron-lastrun-body');
24922
+ if (pane) pane.innerHTML = renderCronRunningState(_cronRunOnceInFlight.startedAt);
24923
+ // Tick the elapsed counter once a second.
24924
+ if (_cronRunOnceTickerId) clearInterval(_cronRunOnceTickerId);
24925
+ _cronRunOnceTickerId = setInterval(function() {
24926
+ if (!_cronRunOnceInFlight) { clearInterval(_cronRunOnceTickerId); _cronRunOnceTickerId = null; return; }
24927
+ var elapsedEl = document.getElementById('cron-run-once-elapsed');
24928
+ if (elapsedEl) {
24929
+ var s = Math.max(0, Math.round((Date.now() - _cronRunOnceInFlight.startedAt) / 1000));
24930
+ elapsedEl.textContent = s + 's';
24931
+ }
24932
+ }, 1000);
24933
+ try {
24934
+ var r = await apiFetch('/api/cron/run/' + encodeURIComponent(editingCronJob), { method: 'POST' });
24935
+ var d = await r.json();
24936
+ if (!r.ok || d.ok === false) {
24937
+ toast(d.error || 'Run failed to start', 'error');
24938
+ _cronRunOnceInFlight = null;
24939
+ if (_cronRunOnceTickerId) { clearInterval(_cronRunOnceTickerId); _cronRunOnceTickerId = null; }
24940
+ 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>';
24941
+ }
24942
+ } catch (e) {
24943
+ toast('Run failed to start: ' + String(e), 'error');
24944
+ _cronRunOnceInFlight = null;
24945
+ if (_cronRunOnceTickerId) { clearInterval(_cronRunOnceTickerId); _cronRunOnceTickerId = null; }
24946
+ } finally {
24947
+ if (btn) { btn.disabled = false; btn.textContent = '▶ Run task once'; }
24948
+ }
24949
+ }
24950
+
24951
+ // Called from the SSE handler when cron_complete fires. The same SSE handler
24952
+ // also schedules refreshCron() which updates cronJobsData with the fresh
24953
+ // lastRun. We just wait a beat for that, then re-render the pane.
24954
+ function handleCronRunOnceComplete(jobName) {
24955
+ if (!_cronRunOnceInFlight || _cronRunOnceInFlight.jobName !== jobName) return;
24956
+ if (_cronRunOnceTickerId) { clearInterval(_cronRunOnceTickerId); _cronRunOnceTickerId = null; }
24957
+ _cronRunOnceInFlight = null;
24958
+ // refreshCron is racing with us; give it ~600ms to land the new entry
24959
+ // into cronJobsData before we read. The SSE handler at line 34927 already
24960
+ // kicks it off when this event arrives.
24961
+ setTimeout(function() {
24962
+ var pane = document.getElementById('cron-lastrun-body');
24963
+ if (!pane) return;
24964
+ var fresh = (Array.isArray(cronJobsData) ? cronJobsData : []).find(function(j) { return j.name === jobName; });
24965
+ var lr = fresh && fresh.lastRun;
24966
+ if (lr) {
24967
+ pane.innerHTML = renderCronRunDetails(lr);
24968
+ toast('Run finished — ' + (lr.status === 'ok' ? 'success' : lr.status), lr.status === 'ok' ? 'success' : 'error');
24969
+ } else {
24970
+ 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>';
24971
+ }
24972
+ }, 600);
24973
+ }
24974
+
24787
24975
  // ── Predictable mode: visual card sync + legacy banner ───────────
24788
24976
  function onPredictableChange() {
24789
24977
  var predEl = document.getElementById('cron-predictable');
@@ -24899,6 +25087,11 @@ function openCreateCronModal(agentSlug) {
24899
25087
  // No saved state to preview when creating — disable the Preview tab.
24900
25088
  var previewBtn = document.getElementById('cron-tab-btn-preview');
24901
25089
  if (previewBtn) previewBtn.setAttribute('disabled', 'disabled');
25090
+ // Last run + Run-task-once button only make sense for saved tasks.
25091
+ var lastRunBtn = document.getElementById('cron-tab-btn-lastrun');
25092
+ if (lastRunBtn) lastRunBtn.setAttribute('disabled', 'disabled');
25093
+ var runOnceBtn = document.getElementById('cron-run-once-btn');
25094
+ if (runOnceBtn) runOnceBtn.style.display = 'none';
24902
25095
  var host = document.getElementById('cron-legacy-banner-host');
24903
25096
  if (host) host.innerHTML = '';
24904
25097
  // Reset the "Use a cron expression" link in case it was hidden last time.
@@ -24990,9 +25183,18 @@ function openEditCronModal(jobName) {
24990
25183
  renderTagsPickerChips();
24991
25184
  _pendingAttachments = [];
24992
25185
  loadCronAttachments(jobName);
24993
- // Existing job has saved state, enable Preview tab.
25186
+ // Existing job has saved state, enable Preview + Last run tabs.
24994
25187
  var previewBtn = document.getElementById('cron-tab-btn-preview');
24995
25188
  if (previewBtn) previewBtn.removeAttribute('disabled');
25189
+ var lastRunBtnEdit = document.getElementById('cron-tab-btn-lastrun');
25190
+ if (lastRunBtnEdit) lastRunBtnEdit.removeAttribute('disabled');
25191
+ // Show "Run task once" only for saved tasks.
25192
+ var runOnceBtnEdit = document.getElementById('cron-run-once-btn');
25193
+ if (runOnceBtnEdit) runOnceBtnEdit.style.display = '';
25194
+ // Render the most recent run from the loaded job into the Last run tab so
25195
+ // the user sees something the moment they switch to it (rather than a
25196
+ // dead empty pane). The pane updates live when Run task once fires.
25197
+ renderCronLastRunPane(job);
24996
25198
  switchCronTab('configure');
24997
25199
  document.getElementById('cron-modal').classList.add('show');
24998
25200
  setTimeout(captureCronModalSnapshot, 0);
@@ -25200,6 +25402,10 @@ function closeCronModal(force) {
25200
25402
  editingCronJob = null;
25201
25403
  _cronPreviewLoadedFor = null;
25202
25404
  _cronModalSnapshot = null;
25405
+ // Clear any pending Run-task-once watch so SSE events for a different job
25406
+ // don't accidentally re-render the (now closed) Last run pane.
25407
+ if (_cronRunOnceTickerId) { clearInterval(_cronRunOnceTickerId); _cronRunOnceTickerId = null; }
25408
+ _cronRunOnceInFlight = null;
25203
25409
  var attachList = document.getElementById('cron-attachments-list');
25204
25410
  if (attachList) attachList.innerHTML = '';
25205
25411
  var bannerHost = document.getElementById('cron-legacy-banner-host');
@@ -34891,6 +35097,11 @@ try {
34891
35097
  refreshActivity();
34892
35098
  if (currentPage === 'build') refreshCron();
34893
35099
  refreshTeamNav();
35100
+ // PRD Phase 1.2: if the user just clicked "Run task once" in the
35101
+ // modal, re-render the Last run pane with the fresh result.
35102
+ if (evt.type === 'cron_complete' && evt.data && evt.data.job && typeof handleCronRunOnceComplete === 'function') {
35103
+ try { handleCronRunOnceComplete(evt.data.job); } catch (err) { /* non-fatal */ }
35104
+ }
34894
35105
  }
34895
35106
  // A delete on one tab should drop the card from every open dashboard
34896
35107
  // without waiting for the next poll. cron_toggled is similar but lighter.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.78",
3
+ "version": "1.18.79",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",