clay-server 2.15.0-beta.2 → 2.15.0-beta.4

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/lib/project.js CHANGED
@@ -615,9 +615,13 @@ function createProjectContext(opts) {
615
615
  var loopRegistry = createLoopRegistry({
616
616
  cwd: cwd,
617
617
  onTrigger: function (record) {
618
- // Only trigger if no loop is currently active
618
+ // Skip trigger if a loop is already active and skipIfRunning is enabled
619
619
  if (loopState.active || loopState.phase === "executing") {
620
- console.log("[loop-registry] Skipping trigger — loop already active");
620
+ if (record.skipIfRunning !== false) {
621
+ console.log("[loop-registry] Skipping trigger for " + record.name + " — loop already active (skipIfRunning)");
622
+ return;
623
+ }
624
+ console.log("[loop-registry] Loop active but skipIfRunning disabled for " + record.name + "; deferring");
621
625
  return;
622
626
  }
623
627
 
@@ -3202,6 +3206,7 @@ function createProjectContext(opts) {
3202
3206
  source: "schedule",
3203
3207
  color: sData.color || null,
3204
3208
  recurrenceEnd: sData.recurrenceEnd || null,
3209
+ skipIfRunning: sData.skipIfRunning !== undefined ? sData.skipIfRunning : true,
3205
3210
  });
3206
3211
  return;
3207
3212
  }
package/lib/public/app.js CHANGED
@@ -551,6 +551,13 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
551
551
  // --- DM Mode Functions ---
552
552
  function openDm(targetUserId) {
553
553
  if (!ws || ws.readyState !== 1) return;
554
+ // Check mate skill updates before opening mate DM
555
+ if (typeof targetUserId === "string" && targetUserId.indexOf("mate_") === 0) {
556
+ requireClayMateInterview(function () {
557
+ ws.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
558
+ });
559
+ return;
560
+ }
554
561
  ws.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
555
562
  }
556
563
 
@@ -6913,6 +6920,9 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
6913
6920
  connect();
6914
6921
  if (!currentSlug) {
6915
6922
  showHomeHub();
6923
+ } else if (location.hash === "#scheduler") {
6924
+ // Restore scheduler view after refresh
6925
+ setTimeout(function () { openSchedulerToTab("calendar"); }, 500);
6916
6926
  } else {
6917
6927
  inputEl.focus();
6918
6928
  }
@@ -596,6 +596,11 @@
596
596
  box-sizing: border-box;
597
597
  }
598
598
 
599
+ .light-theme .sched-create-date-input,
600
+ .light-theme .sched-create-time-input {
601
+ color-scheme: light;
602
+ }
603
+
599
604
  .sched-create-date-input:focus,
600
605
  .sched-create-time-input:focus {
601
606
  border-color: var(--accent);
@@ -1076,6 +1081,194 @@
1076
1081
  font-weight: 600;
1077
1082
  }
1078
1083
 
1084
+ .sched-create-interval-btn {
1085
+ background: none;
1086
+ border: 1px solid var(--border);
1087
+ border-radius: 6px;
1088
+ padding: 4px 6px;
1089
+ cursor: pointer;
1090
+ color: var(--text-muted);
1091
+ display: flex;
1092
+ align-items: center;
1093
+ transition: color 0.15s, border-color 0.15s;
1094
+ }
1095
+
1096
+ .sched-create-interval-btn:hover {
1097
+ color: var(--accent);
1098
+ border-color: var(--accent);
1099
+ }
1100
+
1101
+ .sched-create-interval-btn.has-recurrence {
1102
+ color: var(--accent);
1103
+ border-color: var(--accent);
1104
+ }
1105
+
1106
+ .sched-create-interval-btn .lucide {
1107
+ width: 14px;
1108
+ height: 14px;
1109
+ }
1110
+
1111
+ .sched-create-interval-dropdown {
1112
+ position: fixed;
1113
+ z-index: 1200;
1114
+ background: var(--bg);
1115
+ border: 1px solid var(--border);
1116
+ border-radius: 10px;
1117
+ box-shadow: 0 8px 32px rgba(0,0,0,0.18);
1118
+ min-width: 200px;
1119
+ max-width: 260px;
1120
+ overflow: hidden;
1121
+ }
1122
+
1123
+ .sched-create-interval-dropdown.hidden {
1124
+ display: none;
1125
+ }
1126
+
1127
+ .sched-interval-options {
1128
+ padding: 4px;
1129
+ display: flex;
1130
+ flex-direction: column;
1131
+ }
1132
+
1133
+ .sched-interval-option {
1134
+ background: none;
1135
+ border: none;
1136
+ padding: 8px 12px;
1137
+ text-align: left;
1138
+ cursor: pointer;
1139
+ font-size: 13px;
1140
+ border-radius: 6px;
1141
+ color: var(--text);
1142
+ }
1143
+
1144
+ .sched-interval-option:hover {
1145
+ background: var(--hover);
1146
+ }
1147
+
1148
+ .sched-interval-option.active {
1149
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
1150
+ color: var(--accent);
1151
+ font-weight: 600;
1152
+ }
1153
+
1154
+ .sched-interval-custom-row {
1155
+ display: flex;
1156
+ align-items: center;
1157
+ gap: 6px;
1158
+ padding: 4px 8px 6px;
1159
+ border-top: 1px solid var(--border);
1160
+ }
1161
+
1162
+ .sched-interval-custom-row .sched-custom-label {
1163
+ font-size: 13px;
1164
+ }
1165
+
1166
+ .sched-interval-custom-row .sched-custom-interval {
1167
+ width: 48px;
1168
+ padding: 4px 6px;
1169
+ font-size: 13px;
1170
+ border-radius: 6px;
1171
+ border: 1px solid var(--border);
1172
+ background: var(--input-bg);
1173
+ color: var(--text);
1174
+ }
1175
+
1176
+ .sched-interval-unit-toggle {
1177
+ display: flex;
1178
+ border: 1px solid var(--border);
1179
+ border-radius: 6px;
1180
+ overflow: hidden;
1181
+ }
1182
+
1183
+ .sched-interval-seg {
1184
+ padding: 4px 10px;
1185
+ font-size: 12px;
1186
+ font-weight: 500;
1187
+ border: none;
1188
+ background: var(--input-bg);
1189
+ color: var(--text-dimmer);
1190
+ cursor: pointer;
1191
+ transition: background 0.15s, color 0.15s;
1192
+ }
1193
+
1194
+ .sched-interval-seg:first-child {
1195
+ border-right: 1px solid var(--border);
1196
+ }
1197
+
1198
+ .sched-interval-seg.active {
1199
+ background: var(--accent);
1200
+ color: #fff;
1201
+ font-weight: 600;
1202
+ }
1203
+
1204
+ .sched-interval-seg:not(.active):hover {
1205
+ background: var(--hover);
1206
+ color: var(--text);
1207
+ }
1208
+
1209
+ .sched-skip-running-row {
1210
+ padding: 6px 12px;
1211
+ }
1212
+
1213
+ .sched-skip-running-row.hidden {
1214
+ display: none;
1215
+ }
1216
+
1217
+ .sched-switch-label {
1218
+ display: flex;
1219
+ align-items: center;
1220
+ gap: 8px;
1221
+ cursor: pointer;
1222
+ }
1223
+
1224
+ .sched-switch-text {
1225
+ font-size: 11px;
1226
+ color: var(--text-muted);
1227
+ user-select: none;
1228
+ }
1229
+
1230
+ .sched-switch {
1231
+ position: relative;
1232
+ width: 28px;
1233
+ height: 16px;
1234
+ flex-shrink: 0;
1235
+ }
1236
+
1237
+ .sched-switch input {
1238
+ opacity: 0;
1239
+ width: 0;
1240
+ height: 0;
1241
+ position: absolute;
1242
+ }
1243
+
1244
+ .sched-switch-slider {
1245
+ position: absolute;
1246
+ inset: 0;
1247
+ background: var(--border);
1248
+ border-radius: 8px;
1249
+ transition: background 0.2s;
1250
+ }
1251
+
1252
+ .sched-switch-slider::before {
1253
+ content: "";
1254
+ position: absolute;
1255
+ left: 2px;
1256
+ top: 2px;
1257
+ width: 12px;
1258
+ height: 12px;
1259
+ background: #fff;
1260
+ border-radius: 50%;
1261
+ transition: transform 0.2s;
1262
+ }
1263
+
1264
+ .sched-switch input:checked + .sched-switch-slider {
1265
+ background: var(--accent);
1266
+ }
1267
+
1268
+ .sched-switch input:checked + .sched-switch-slider::before {
1269
+ transform: translateX(12px);
1270
+ }
1271
+
1079
1272
  /* --- Custom repeat panel --- */
1080
1273
  .sched-custom-repeat-panel {
1081
1274
  padding: 0;
@@ -1625,6 +1625,7 @@
1625
1625
  <input type="date" class="sched-create-date-input" id="sched-create-date-picker" value="">
1626
1626
  <input type="time" class="sched-create-time-input" id="sched-create-time" value="09:00">
1627
1627
  <button class="sched-create-recurrence-btn" id="sched-create-recurrence-btn" title="Recurrence"><i data-lucide="repeat"></i></button>
1628
+ <button class="sched-create-interval-btn" id="sched-create-interval-btn" title="Interval"><i data-lucide="timer"></i></button>
1628
1629
  </div>
1629
1630
  <button class="sched-create-close-btn" id="sched-create-cancel" title="Close"><i data-lucide="x"></i></button>
1630
1631
  </div>
@@ -1666,6 +1667,15 @@
1666
1667
  <i data-lucide="align-left" class="sched-create-row-icon"></i>
1667
1668
  <textarea class="sched-create-row-textarea" id="sched-create-desc" rows="3" placeholder="Description"></textarea>
1668
1669
  </div>
1670
+ <div class="sched-skip-running-row hidden" id="sched-skip-running-row">
1671
+ <label class="sched-switch-label" for="sched-skip-running">
1672
+ <div class="sched-switch">
1673
+ <input type="checkbox" id="sched-skip-running" checked>
1674
+ <span class="sched-switch-slider"></span>
1675
+ </div>
1676
+ <span class="sched-switch-text">Skip if previous run still active</span>
1677
+ </label>
1678
+ </div>
1669
1679
  <!-- Bottom bar -->
1670
1680
  <div class="sched-create-bottom">
1671
1681
  <div class="sched-create-bottom-left">
@@ -1703,6 +1713,8 @@
1703
1713
  <span class="sched-custom-label">Every</span>
1704
1714
  <input type="number" class="sched-custom-interval" id="sched-custom-interval" value="1" min="1" max="99">
1705
1715
  <select class="sched-field-select sched-custom-unit" id="sched-custom-unit">
1716
+ <option value="minute">Minute</option>
1717
+ <option value="hour">Hour</option>
1706
1718
  <option value="day">Day</option>
1707
1719
  <option value="week" selected>Week</option>
1708
1720
  <option value="month">Month</option>
@@ -1763,6 +1775,24 @@
1763
1775
  </div>
1764
1776
  </div>
1765
1777
  </div>
1778
+ <!-- Interval dropdown -->
1779
+ <div class="sched-create-interval-dropdown hidden" id="sched-create-interval-dropdown">
1780
+ <div class="sched-interval-options" id="sched-create-interval-list">
1781
+ <button class="sched-interval-option active" data-interval="none">No interval</button>
1782
+ <button class="sched-interval-option" data-interval="1">Every minute</button>
1783
+ <button class="sched-interval-option" data-interval="5">Every 5 minutes</button>
1784
+ <button class="sched-interval-option" data-interval="15">Every 15 minutes</button>
1785
+ <button class="sched-interval-option" data-interval="30">Every 30 minutes</button>
1786
+ <button class="sched-interval-option" data-interval="60">Every hour</button>
1787
+ </div>
1788
+ <div class="sched-interval-custom-row">
1789
+ <span class="sched-custom-label">Every</span>
1790
+ <input type="number" class="sched-custom-interval" id="sched-interval-custom-value" value="10" min="1" max="999">
1791
+ <div class="sched-interval-unit-toggle" id="sched-interval-custom-unit">
1792
+ <button type="button" class="sched-interval-seg active" data-unit="minute">min</button><button type="button" class="sched-interval-seg" data-unit="hour">hrs</button>
1793
+ </div>
1794
+ </div>
1795
+ </div>
1766
1796
  </div>
1767
1797
 
1768
1798
  <!-- Delete recurring event dialog -->
@@ -54,6 +54,8 @@ var createPopover = null;
54
54
  var createSelectedDate = null; // Date object for clicked calendar date
55
55
  var createRecurrence = "none"; // current recurrence selection
56
56
  var createCustomConfirmed = false; // whether custom repeat was confirmed via OK
57
+ var createInterval = "none"; // current interval selection: "none", "1", "5", "15", "30", "60", "custom"
58
+ var createIntervalCustom = null; // { value: N, unit: "minute"|"hour" } for custom interval
57
59
  var createColor = "#ffb86c"; // selected event color (default: accent)
58
60
  var createEndType = "never"; // "never" | "until" | "after"
59
61
  var createEndDate = null; // Date for "until" end type
@@ -368,6 +370,11 @@ function openScheduler() {
368
370
 
369
371
  var sidebarBtn = document.getElementById("scheduler-btn");
370
372
  if (sidebarBtn) sidebarBtn.classList.add("active");
373
+
374
+ // Persist scheduler state in URL hash
375
+ if (location.hash !== "#scheduler") {
376
+ history.replaceState(null, "", location.pathname + "#scheduler");
377
+ }
371
378
  }
372
379
 
373
380
  export function closeScheduler() {
@@ -403,6 +410,11 @@ export function closeScheduler() {
403
410
  // Un-mark sidebar button
404
411
  var sidebarBtn = document.getElementById("scheduler-btn");
405
412
  if (sidebarBtn) sidebarBtn.classList.remove("active");
413
+
414
+ // Remove scheduler hash from URL
415
+ if (location.hash === "#scheduler") {
416
+ history.replaceState(null, "", location.pathname);
417
+ }
406
418
  }
407
419
 
408
420
  // Reset state on project switch (SPA navigation, no full reload)
@@ -995,6 +1007,15 @@ function renderWeekView() {
995
1007
  }
996
1008
  for (var e = 0; e < events.length; e++) {
997
1009
  var ev = events[e];
1010
+ if (ev.intervalBadge) {
1011
+ var badgeStyle = "";
1012
+ if (ev.color) badgeStyle = "background:" + ev.color;
1013
+ html += '<div class="scheduler-week-event interval-badge ' + (ev.enabled ? "enabled" : "disabled") + '" data-rec-id="' + ev.id + '" style="position:relative;top:0;left:0;width:85%;height:auto;' + badgeStyle + '">';
1014
+ html += '<span class="scheduler-week-event-title">' + esc(ev.name) + '</span>';
1015
+ html += '<span class="scheduler-week-event-time">' + esc(ev.timeStr) + '</span>';
1016
+ html += '</div>';
1017
+ continue;
1018
+ }
998
1019
  var topPct = ((ev.hour * 60 + ev.minute) / 1440) * 100;
999
1020
  var evColor = ev.color || "";
1000
1021
  var assign = colAssign[ev.id] || { col: 0, totalCols: 1 };
@@ -1178,15 +1199,32 @@ function getEventsForDate(date) {
1178
1199
  if (parsed.months.indexOf(month) === -1) continue;
1179
1200
  if (parsed.daysOfMonth.indexOf(dom) === -1) continue;
1180
1201
  if (parsed.daysOfWeek.indexOf(dow) === -1) continue;
1181
- for (var h = 0; h < parsed.hours.length; h++) {
1182
- for (var m = 0; m < parsed.minutes.length; m++) {
1183
- results.push({
1184
- id: r.id, name: r.name, enabled: r.enabled,
1185
- hour: parsed.hours[h], minute: parsed.minutes[m],
1186
- timeStr: pad(parsed.hours[h]) + ":" + pad(parsed.minutes[m]),
1187
- color: r.color || null,
1188
- source: r.source || null,
1189
- });
1202
+ // Detect sub-daily interval mode to prevent calendar item explosion
1203
+ var cronParts = r.cron.trim().split(/\s+/);
1204
+ var isIntervalMode = (cronParts[0].indexOf("/") !== -1 && cronParts[1] === "*")
1205
+ || (cronParts[1].indexOf("/") !== -1)
1206
+ || (parsed.minutes.length * parsed.hours.length > 24);
1207
+ if (isIntervalMode) {
1208
+ results.push({
1209
+ id: r.id, name: r.name, enabled: r.enabled,
1210
+ hour: 0, minute: 0,
1211
+ timeStr: cronToHuman(r.cron) || "Interval",
1212
+ allDay: true,
1213
+ intervalBadge: true,
1214
+ color: r.color || null,
1215
+ source: r.source || null,
1216
+ });
1217
+ } else {
1218
+ for (var h = 0; h < parsed.hours.length; h++) {
1219
+ for (var m = 0; m < parsed.minutes.length; m++) {
1220
+ results.push({
1221
+ id: r.id, name: r.name, enabled: r.enabled,
1222
+ hour: parsed.hours[h], minute: parsed.minutes[m],
1223
+ timeStr: pad(parsed.hours[h]) + ":" + pad(parsed.minutes[m]),
1224
+ color: r.color || null,
1225
+ source: r.source || null,
1226
+ });
1227
+ }
1190
1228
  }
1191
1229
  }
1192
1230
  }
@@ -1766,6 +1804,8 @@ function setupCreateModal() {
1766
1804
  e.stopPropagation();
1767
1805
  var dd = document.getElementById("sched-create-recurrence-dropdown");
1768
1806
  var btn = document.getElementById("sched-create-recurrence-btn");
1807
+ // Close interval dropdown if open
1808
+ document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
1769
1809
  if (dd) {
1770
1810
  var wasHidden = dd.classList.contains("hidden");
1771
1811
  dd.classList.toggle("hidden");
@@ -1807,6 +1847,81 @@ function setupCreateModal() {
1807
1847
  })(recOptions[i]);
1808
1848
  }
1809
1849
 
1850
+ // --- Interval button + dropdown ---
1851
+ document.getElementById("sched-create-interval-btn").addEventListener("click", function (e) {
1852
+ e.stopPropagation();
1853
+ var dd = document.getElementById("sched-create-interval-dropdown");
1854
+ var btn = document.getElementById("sched-create-interval-btn");
1855
+ // Close recurrence dropdown if open
1856
+ document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
1857
+ if (dd) {
1858
+ var wasHidden = dd.classList.contains("hidden");
1859
+ dd.classList.toggle("hidden");
1860
+ if (wasHidden && btn) {
1861
+ var bRect = btn.getBoundingClientRect();
1862
+ var ddW = 220;
1863
+ var ddLeft = bRect.left;
1864
+ if (ddLeft + ddW > window.innerWidth - 10) ddLeft = window.innerWidth - ddW - 10;
1865
+ if (ddLeft < 10) ddLeft = 10;
1866
+ dd.style.left = ddLeft + "px";
1867
+ dd.style.top = (bRect.bottom + 4) + "px";
1868
+ }
1869
+ }
1870
+ });
1871
+
1872
+ // Interval option clicks
1873
+ var intOptions = document.querySelectorAll(".sched-interval-option");
1874
+ for (var ii = 0; ii < intOptions.length; ii++) {
1875
+ (function (opt) {
1876
+ opt.addEventListener("click", function (e) {
1877
+ e.stopPropagation();
1878
+ var val = opt.dataset.interval;
1879
+ for (var j = 0; j < intOptions.length; j++) {
1880
+ intOptions[j].classList.toggle("active", intOptions[j] === opt);
1881
+ }
1882
+ createInterval = val;
1883
+ createIntervalCustom = null;
1884
+ document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
1885
+ updateIntervalBtn();
1886
+ });
1887
+ })(intOptions[ii]);
1888
+ }
1889
+
1890
+ // Interval inline custom input
1891
+ var intCustomValue = document.getElementById("sched-interval-custom-value");
1892
+ var intUnitSegs = document.querySelectorAll(".sched-interval-seg");
1893
+ function getIntervalUnit() {
1894
+ for (var s = 0; s < intUnitSegs.length; s++) {
1895
+ if (intUnitSegs[s].classList.contains("active")) return intUnitSegs[s].dataset.unit;
1896
+ }
1897
+ return "minute";
1898
+ }
1899
+ function applyInlineInterval() {
1900
+ var v = parseInt(intCustomValue.value, 10) || 1;
1901
+ var u = getIntervalUnit();
1902
+ createInterval = "custom";
1903
+ createIntervalCustom = { value: v, unit: u };
1904
+ for (var j = 0; j < intOptions.length; j++) {
1905
+ intOptions[j].classList.remove("active");
1906
+ }
1907
+ updateIntervalBtn();
1908
+ }
1909
+ intCustomValue.addEventListener("change", applyInlineInterval);
1910
+ intCustomValue.addEventListener("keydown", function (e) { e.stopPropagation(); });
1911
+ intCustomValue.addEventListener("keyup", function (e) { e.stopPropagation(); });
1912
+ intCustomValue.addEventListener("keypress", function (e) { e.stopPropagation(); });
1913
+ for (var si = 0; si < intUnitSegs.length; si++) {
1914
+ (function (seg) {
1915
+ seg.addEventListener("click", function (e) {
1916
+ e.stopPropagation();
1917
+ for (var s = 0; s < intUnitSegs.length; s++) {
1918
+ intUnitSegs[s].classList.toggle("active", intUnitSegs[s] === seg);
1919
+ }
1920
+ applyInlineInterval();
1921
+ });
1922
+ })(intUnitSegs[si]);
1923
+ }
1924
+
1810
1925
  // Custom repeat: back
1811
1926
  document.getElementById("sched-custom-back").addEventListener("click", function (e) {
1812
1927
  e.stopPropagation();
@@ -2069,6 +2184,24 @@ function updateRecurrenceBtn() {
2069
2184
  if (btn) {
2070
2185
  btn.classList.toggle("has-recurrence", createRecurrence !== "none");
2071
2186
  }
2187
+ var skipRow = document.getElementById("sched-skip-running-row");
2188
+ if (skipRow) {
2189
+ skipRow.classList.toggle("hidden", createInterval === "none");
2190
+ }
2191
+ }
2192
+
2193
+ function updateIntervalBtn() {
2194
+ var btn = document.getElementById("sched-create-interval-btn");
2195
+ if (btn) {
2196
+ btn.classList.toggle("has-recurrence", createInterval !== "none");
2197
+ }
2198
+ // Hide time picker when interval is set
2199
+ var timeInput = document.getElementById("sched-create-time");
2200
+ if (timeInput) {
2201
+ timeInput.style.display = createInterval !== "none" ? "none" : "";
2202
+ }
2203
+ // Update skip-if-running visibility
2204
+ updateRecurrenceBtn();
2072
2205
  }
2073
2206
 
2074
2207
  function removePreview() {
@@ -2211,6 +2344,10 @@ function openCreateModalWithRecord(rec, anchorEl) {
2211
2344
  }
2212
2345
  }
2213
2346
 
2347
+ // Set skip-if-running
2348
+ var skipRunningEl = document.getElementById("sched-skip-running");
2349
+ if (skipRunningEl) skipRunningEl.checked = rec.skipIfRunning !== false;
2350
+
2214
2351
  // Set run mode and iterations
2215
2352
  var editRunMode = (rec.maxIterations && rec.maxIterations > 1) ? "multi" : "single";
2216
2353
  var editRunBtns = createPopover.querySelectorAll(".sched-run-mode-btn");
@@ -2266,6 +2403,8 @@ function openCreateModal(date, hour, anchorEl) {
2266
2403
  createSelectedDate = date || new Date();
2267
2404
  createRecurrence = "none";
2268
2405
  createCustomConfirmed = false;
2406
+ createInterval = "none";
2407
+ createIntervalCustom = null;
2269
2408
  createColor = "#ffb86c";
2270
2409
 
2271
2410
  // Reset form
@@ -2357,6 +2496,16 @@ function openCreateModal(date, hour, anchorEl) {
2357
2496
  }
2358
2497
  updateRecurrenceBtn();
2359
2498
 
2499
+ // Reset interval
2500
+ var intOpts = document.querySelectorAll(".sched-interval-option");
2501
+ for (var io = 0; io < intOpts.length; io++) {
2502
+ intOpts[io].classList.toggle("active", intOpts[io].dataset.interval === "none");
2503
+ }
2504
+ document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
2505
+ var timeInput = document.getElementById("sched-create-time");
2506
+ if (timeInput) timeInput.style.display = "";
2507
+ updateIntervalBtn();
2508
+
2360
2509
  // Reset custom panel
2361
2510
  document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
2362
2511
  document.getElementById("sched-custom-repeat-panel").classList.add("hidden");
@@ -2544,17 +2693,48 @@ function buildCreateCron() {
2544
2693
  var dom = createSelectedDate.getDate();
2545
2694
  var month = createSelectedDate.getMonth() + 1;
2546
2695
 
2547
- if (createRecurrence === "none") return null;
2548
- if (createRecurrence === "daily") return m + " " + h + " * * *";
2549
- if (createRecurrence === "weekly") return m + " " + h + " * * " + dow;
2696
+ // Determine interval minutes
2697
+ var intervalMins = 0;
2698
+ if (createInterval !== "none") {
2699
+ if (createInterval === "custom" && createIntervalCustom) {
2700
+ intervalMins = createIntervalCustom.unit === "hour"
2701
+ ? createIntervalCustom.value * 60
2702
+ : createIntervalCustom.value;
2703
+ } else {
2704
+ intervalMins = parseInt(createInterval, 10) || 0;
2705
+ }
2706
+ }
2707
+
2708
+ // Interval only (no recurrence) = interval every day
2709
+ if (intervalMins > 0 && createRecurrence === "none") {
2710
+ if (intervalMins < 60) return "*/" + intervalMins + " * * * *";
2711
+ var intHrs = Math.floor(intervalMins / 60);
2712
+ return "0 */" + intHrs + " * * *";
2713
+ }
2714
+
2715
+ if (createRecurrence === "none" && intervalMins === 0) return null;
2716
+
2717
+ // Build minute/hour fields from interval or time
2718
+ var minField = String(m);
2719
+ var hourField = String(h);
2720
+ if (intervalMins > 0 && intervalMins < 60) {
2721
+ minField = "*/" + intervalMins;
2722
+ hourField = "*";
2723
+ } else if (intervalMins >= 60) {
2724
+ var intHrs2 = Math.floor(intervalMins / 60);
2725
+ minField = String(m);
2726
+ hourField = "*/" + intHrs2;
2727
+ }
2728
+
2729
+ if (createRecurrence === "daily") return minField + " " + hourField + " * * *";
2730
+ if (createRecurrence === "weekly") return minField + " " + hourField + " * * " + dow;
2550
2731
  if (createRecurrence === "biweekly") {
2551
- // Nth weekday of month — approximate with cron (run weekly, but it's the closest)
2552
2732
  var weekNum = Math.ceil(dom / 7);
2553
- return m + " " + h + " " + ((weekNum - 1) * 7 + 1) + "-" + (weekNum * 7) + " * " + dow;
2733
+ return minField + " " + hourField + " " + ((weekNum - 1) * 7 + 1) + "-" + (weekNum * 7) + " * " + dow;
2554
2734
  }
2555
- if (createRecurrence === "yearly") return m + " " + h + " " + dom + " " + month + " *";
2556
- if (createRecurrence === "monthly") return m + " " + h + " " + dom + " * *";
2557
- if (createRecurrence === "weekdays") return m + " " + h + " * * 1-5";
2735
+ if (createRecurrence === "yearly") return minField + " " + hourField + " " + dom + " " + month + " *";
2736
+ if (createRecurrence === "monthly") return minField + " " + hourField + " " + dom + " * *";
2737
+ if (createRecurrence === "weekdays") return minField + " " + hourField + " * * 1-5";
2558
2738
 
2559
2739
  if (createRecurrence === "custom" && createCustomConfirmed) {
2560
2740
  return buildCustomCron(h, m);
@@ -2656,6 +2836,12 @@ function buildCustomCron(h, m) {
2656
2836
  var interval = parseInt(document.getElementById("sched-custom-interval").value, 10) || 1;
2657
2837
  var unit = document.getElementById("sched-custom-unit").value;
2658
2838
 
2839
+ if (unit === "minute") {
2840
+ return interval === 1 ? "*/1 * * * *" : "*/" + interval + " * * * *";
2841
+ }
2842
+ if (unit === "hour") {
2843
+ return interval === 1 ? "0 */1 * * *" : "0 */" + interval + " * * *";
2844
+ }
2659
2845
  if (unit === "day") {
2660
2846
  if (interval === 1) return m + " " + h + " * * *";
2661
2847
  return m + " " + h + " */" + interval + " * *";
@@ -2715,6 +2901,9 @@ function submitCreateSchedule() {
2715
2901
  }
2716
2902
  }
2717
2903
 
2904
+ var skipRunningEl = document.getElementById("sched-skip-running");
2905
+ var skipIfRunning = skipRunningEl ? skipRunningEl.checked : true;
2906
+
2718
2907
  var activeRunMode = createPopover.querySelector(".sched-run-mode-btn.active");
2719
2908
  var runMode = activeRunMode ? activeRunMode.dataset.mode : "single";
2720
2909
  var maxIterations = 1;
@@ -2740,6 +2929,7 @@ function submitCreateSchedule() {
2740
2929
  color: createColor,
2741
2930
  recurrenceEnd: recurrenceEnd,
2742
2931
  maxIterations: maxIterations,
2932
+ skipIfRunning: skipIfRunning,
2743
2933
  },
2744
2934
  });
2745
2935
  } else {
@@ -2757,6 +2947,7 @@ function submitCreateSchedule() {
2757
2947
  color: createColor,
2758
2948
  recurrenceEnd: recurrenceEnd,
2759
2949
  maxIterations: maxIterations,
2950
+ skipIfRunning: skipIfRunning,
2760
2951
  },
2761
2952
  });
2762
2953
  }
@@ -2831,6 +3022,16 @@ function cronToHuman(cron) {
2831
3022
  if (!cron) return "";
2832
3023
  var parts = cron.trim().split(/\s+/);
2833
3024
  if (parts.length !== 5) return cron;
3025
+ // Minute interval patterns (e.g. */5 * * * *)
3026
+ if (parts[0].indexOf("/") !== -1 && parts[1] === "*" && parts[2] === "*") {
3027
+ var minStep = parseInt(parts[0].split("/")[1], 10);
3028
+ return minStep === 1 ? "Every minute" : "Every " + minStep + " minutes";
3029
+ }
3030
+ // Hour interval patterns (e.g. 0 */2 * * *)
3031
+ if (parts[1].indexOf("/") !== -1 && parts[2] === "*") {
3032
+ var hrStep = parseInt(parts[1].split("/")[1], 10);
3033
+ return hrStep === 1 ? "Every hour" : "Every " + hrStep + " hours";
3034
+ }
2834
3035
  var t = pad(parseInt(parts[1], 10)) + ":" + pad(parseInt(parts[0], 10));
2835
3036
  var dow = parts[4], dom = parts[2];
2836
3037
  if (dow === "*" && dom === "*") return "Every day at " + t;
package/lib/scheduler.js CHANGED
@@ -264,6 +264,7 @@ function createLoopRegistry(opts) {
264
264
  recurrenceEnd: data.recurrenceEnd || null,
265
265
  mode: data.mode || "loop",
266
266
  prompt: data.prompt || null,
267
+ skipIfRunning: data.skipIfRunning !== undefined ? data.skipIfRunning : true,
267
268
  runs: [],
268
269
  };
269
270
  if (rec.cron && rec.enabled) {
@@ -299,6 +300,7 @@ function createLoopRegistry(opts) {
299
300
  if (data.color !== undefined) rec.color = data.color;
300
301
  if (data.allDay !== undefined) rec.allDay = data.allDay;
301
302
  if (data.linkedTaskId !== undefined) rec.linkedTaskId = data.linkedTaskId;
303
+ if (data.skipIfRunning !== undefined) rec.skipIfRunning = data.skipIfRunning;
302
304
  rec.updatedAt = Date.now();
303
305
  if (rec.cron && rec.enabled) {
304
306
  rec.nextRunAt = nextRunTime(rec.cron);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.15.0-beta.2",
3
+ "version": "2.15.0-beta.4",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",