clay-server 2.27.1 → 2.28.0-beta.1

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.
@@ -269,50 +269,56 @@ function attachLoop(ctx) {
269
269
 
270
270
  // --- Loop Registry (unified one-off + scheduled) ---
271
271
  var activeRegistryId = null; // track which registry record triggered current loop
272
+ var pendingTriggers = []; // queue for deferred triggers when skipIfRunning=false
273
+
274
+ function triggerFromQueue(record) {
275
+ // For schedule records, resolve the linked task to get loop files
276
+ var loopFilesId = record.id;
277
+ if (record.source === "schedule") {
278
+ if (!record.linkedTaskId) {
279
+ console.error("[loop-registry] Schedule has no linked task: " + record.name);
280
+ return;
281
+ }
282
+ loopFilesId = record.linkedTaskId;
283
+ console.log("[loop-registry] Schedule triggered: " + record.name + " -> linked task " + loopFilesId);
284
+ }
285
+
286
+ // Verify the loop directory and PROMPT.md exist
287
+ var recDir = path.join(cwd, ".claude", "loops", loopFilesId);
288
+ try {
289
+ fs.accessSync(path.join(recDir, "PROMPT.md"));
290
+ } catch (e) {
291
+ console.error("[loop-registry] PROMPT.md missing for " + loopFilesId);
292
+ return;
293
+ }
294
+ // Set the loopId to the schedule's own id (not the linked task) so sidebar groups correctly
295
+ loopState.loopId = record.id;
296
+ loopState.loopFilesId = loopFilesId;
297
+ // Restore loopMode from LOOP.json so simple loops work correctly on trigger
298
+ var _triggerCfg = {};
299
+ try { _triggerCfg = JSON.parse(fs.readFileSync(path.join(recDir, "LOOP.json"), "utf8")); } catch (e) {}
300
+ loopState.wizardData = { loopMode: _triggerCfg.loopMode || "judge" };
301
+ activeRegistryId = record.id;
302
+ console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopState.loopId + ")");
303
+ send({ type: "schedule_run_started", recordId: record.id });
304
+ startLoop({ maxIterations: record.maxIterations, name: record.name });
305
+ }
272
306
 
273
307
  var loopRegistry = createLoopRegistry({
274
308
  cwd: cwd,
275
309
  onTrigger: function (record) {
276
- // Skip trigger if a loop is already active and skipIfRunning is enabled
310
+ // Skip or queue trigger if a loop is already active
277
311
  if (loopState.active || loopState.phase === "executing") {
278
312
  if (record.skipIfRunning !== false) {
279
313
  console.log("[loop-registry] Skipping trigger for " + record.name + " — loop already active (skipIfRunning)");
280
314
  return;
281
315
  }
282
- console.log("[loop-registry] Loop active but skipIfRunning disabled for " + record.name + "; deferring");
316
+ console.log("[loop-registry] Loop active, queuing trigger for " + record.name);
317
+ pendingTriggers.push(record);
283
318
  return;
284
319
  }
285
320
 
286
- // For schedule records, resolve the linked task to get loop files
287
- var loopFilesId = record.id;
288
- if (record.source === "schedule") {
289
- if (!record.linkedTaskId) {
290
- console.error("[loop-registry] Schedule has no linked task: " + record.name);
291
- return;
292
- }
293
- loopFilesId = record.linkedTaskId;
294
- console.log("[loop-registry] Schedule triggered: " + record.name + " → linked task " + loopFilesId);
295
- }
296
-
297
- // Verify the loop directory and PROMPT.md exist
298
- var recDir = path.join(cwd, ".claude", "loops", loopFilesId);
299
- try {
300
- fs.accessSync(path.join(recDir, "PROMPT.md"));
301
- } catch (e) {
302
- console.error("[loop-registry] PROMPT.md missing for " + loopFilesId);
303
- return;
304
- }
305
- // Set the loopId to the schedule's own id (not the linked task) so sidebar groups correctly
306
- loopState.loopId = record.id;
307
- loopState.loopFilesId = loopFilesId;
308
- // Restore loopMode from LOOP.json so simple loops work correctly on trigger
309
- var _triggerCfg = {};
310
- try { _triggerCfg = JSON.parse(fs.readFileSync(path.join(recDir, "LOOP.json"), "utf8")); } catch (e) {}
311
- loopState.wizardData = { loopMode: _triggerCfg.loopMode || "judge" };
312
- activeRegistryId = record.id;
313
- console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopState.loopId + ")");
314
- send({ type: "schedule_run_started", recordId: record.id });
315
- startLoop({ maxIterations: record.maxIterations, name: record.name });
321
+ triggerFromQueue(record);
316
322
  },
317
323
  onChange: function () {
318
324
  send({ type: "loop_registry_updated", records: getHubSchedules() });
@@ -383,6 +389,7 @@ function attachLoop(ctx) {
383
389
  loopState.results = [];
384
390
  loopState.stopping = false;
385
391
  loopState.name = loopOpts.name || null;
392
+ loopState.settings = loopConfig.settings || null;
386
393
  loopState.startedAt = Date.now();
387
394
  saveLoopState();
388
395
 
@@ -500,6 +507,7 @@ function attachLoop(ctx) {
500
507
  sendToSession(session.localId, { type: "status", status: "processing" });
501
508
  session.acceptEditsAfterStart = true;
502
509
  session.singleTurn = true;
510
+ if (loopState.settings) session.loopSettings = loopState.settings;
503
511
  sdk.startQuery(session, loopState.promptText, undefined, getLinuxUserForSession(session));
504
512
  }
505
513
 
@@ -616,6 +624,7 @@ function attachLoop(ctx) {
616
624
  judgeSession.sentToolResults = {};
617
625
  judgeSession.acceptEditsAfterStart = true;
618
626
  judgeSession.singleTurn = true;
627
+ if (loopState.settings) judgeSession.loopSettings = loopState.settings;
619
628
  sdk.startQuery(judgeSession, judgePrompt, undefined, getLinuxUserForSession(judgeSession));
620
629
  }
621
630
 
@@ -643,6 +652,16 @@ function attachLoop(ctx) {
643
652
 
644
653
  function finishLoop(reason) {
645
654
  console.log("[ralph-loop] finishLoop called, reason: " + reason + ", iteration: " + loopState.iteration);
655
+
656
+ // Unlock the last coder session so users can continue interacting with it
657
+ if (loopState.currentSessionId) {
658
+ var lastCoderSession = sm.sessions.get(loopState.currentSessionId);
659
+ if (lastCoderSession) {
660
+ lastCoderSession.singleTurn = false;
661
+ lastCoderSession.loop.active = false;
662
+ }
663
+ }
664
+
646
665
  loopState.active = false;
647
666
  loopState.phase = "done";
648
667
  loopState.stopping = false;
@@ -690,6 +709,15 @@ function attachLoop(ctx) {
690
709
  sessionId: loopState.currentSessionId,
691
710
  });
692
711
  }
712
+
713
+ // Process next queued trigger if any
714
+ if (pendingTriggers.length > 0) {
715
+ var next = pendingTriggers.shift();
716
+ console.log("[loop-registry] Processing queued trigger: " + next.name);
717
+ setTimeout(function () {
718
+ triggerFromQueue(next);
719
+ }, 1000);
720
+ }
693
721
  }
694
722
 
695
723
  function resumeLoop() {
@@ -774,6 +802,17 @@ function attachLoop(ctx) {
774
802
  send({ type: "loop_scheduled", recordId: loopState.loopId, cron: loopState.wizardData.cron });
775
803
  return true;
776
804
  }
805
+ // Save per-loop settings to LOOP.json if provided
806
+ if (msg.settings && Object.keys(msg.settings).length > 0) {
807
+ var lDir3 = loopDir();
808
+ if (lDir3) {
809
+ var ljPath = path.join(lDir3, "LOOP.json");
810
+ var lj = {};
811
+ try { lj = JSON.parse(fs.readFileSync(ljPath, "utf8")); } catch (e) {}
812
+ lj.settings = msg.settings;
813
+ fs.writeFileSync(ljPath, JSON.stringify(lj, null, 2), "utf8");
814
+ }
815
+ }
777
816
  startLoop({ maxIterations: msg.maxIterations });
778
817
  return true;
779
818
  }
@@ -789,6 +828,7 @@ function attachLoop(ctx) {
789
828
  var wizardCron = wData.cron || null;
790
829
  var newLoopId = generateLoopId();
791
830
  loopState.loopId = newLoopId;
831
+ var recordSource = wData.source === "task" ? null : "ralph";
792
832
  loopState.wizardData = {
793
833
  name: wData.name || wData.task || "Untitled",
794
834
  task: wData.task || "",
@@ -797,13 +837,13 @@ function attachLoop(ctx) {
797
837
  loopMode: wData.loopMode || "judge",
798
838
  promptAuthor: wData.promptAuthor || "clay",
799
839
  judgeAuthor: wData.judgeAuthor || null,
840
+ source: recordSource,
800
841
  };
801
842
  loopState.phase = "crafting";
802
843
  loopState.startedAt = Date.now();
803
844
  saveLoopState();
804
845
 
805
846
  // Register in loop registry
806
- var recordSource = wData.source === "task" ? null : "ralph";
807
847
  loopRegistry.register({
808
848
  id: newLoopId,
809
849
  name: loopState.wizardData.name,
@@ -963,17 +1003,59 @@ function attachLoop(ctx) {
963
1003
  var lDir = path.join(cwd, ".claude", "loops", recId);
964
1004
  var promptContent = "";
965
1005
  var judgeContent = "";
1006
+ var loopSettings = null;
966
1007
  try { promptContent = fs.readFileSync(path.join(lDir, "PROMPT.md"), "utf8"); } catch (e) {}
967
1008
  try { judgeContent = fs.readFileSync(path.join(lDir, "JUDGE.md"), "utf8"); } catch (e) {}
1009
+ try {
1010
+ var loopJson = JSON.parse(fs.readFileSync(path.join(lDir, "LOOP.json"), "utf8"));
1011
+ loopSettings = loopJson.settings || null;
1012
+ } catch (e) {}
968
1013
  send({
969
1014
  type: "loop_registry_files_content",
970
1015
  id: recId,
971
1016
  prompt: promptContent,
972
1017
  judge: judgeContent,
1018
+ settings: loopSettings,
973
1019
  });
974
1020
  return true;
975
1021
  }
976
1022
 
1023
+ if (msg.type === "loop_registry_save_files") {
1024
+ var recId2 = msg.id;
1025
+ var lDir2 = path.join(cwd, ".claude", "loops", recId2);
1026
+ try {
1027
+ fs.mkdirSync(lDir2, { recursive: true });
1028
+ if (msg.prompt !== undefined) {
1029
+ fs.writeFileSync(path.join(lDir2, "PROMPT.md"), msg.prompt, "utf8");
1030
+ }
1031
+ if (msg.judge !== undefined) {
1032
+ fs.writeFileSync(path.join(lDir2, "JUDGE.md"), msg.judge, "utf8");
1033
+ }
1034
+ if (msg.settings !== undefined) {
1035
+ var loopJsonPath2 = path.join(lDir2, "LOOP.json");
1036
+ var loopJson2 = {};
1037
+ try { loopJson2 = JSON.parse(fs.readFileSync(loopJsonPath2, "utf8")); } catch (e) {}
1038
+ loopJson2.settings = msg.settings;
1039
+ fs.writeFileSync(loopJsonPath2, JSON.stringify(loopJson2, null, 2), "utf8");
1040
+ }
1041
+ send({ type: "loop_registry_save_files_result", id: recId2, ok: true });
1042
+ // Re-send updated content so the UI refreshes
1043
+ var updatedPrompt = "";
1044
+ var updatedJudge = "";
1045
+ var updatedSettings = null;
1046
+ try { updatedPrompt = fs.readFileSync(path.join(lDir2, "PROMPT.md"), "utf8"); } catch (e) {}
1047
+ try { updatedJudge = fs.readFileSync(path.join(lDir2, "JUDGE.md"), "utf8"); } catch (e) {}
1048
+ try {
1049
+ var uj = JSON.parse(fs.readFileSync(path.join(lDir2, "LOOP.json"), "utf8"));
1050
+ updatedSettings = uj.settings || null;
1051
+ } catch (e) {}
1052
+ send({ type: "loop_registry_files_content", id: recId2, prompt: updatedPrompt, judge: updatedJudge, settings: updatedSettings });
1053
+ } catch (e) {
1054
+ send({ type: "loop_registry_save_files_result", id: recId2, ok: false, error: e.message });
1055
+ }
1056
+ return true;
1057
+ }
1058
+
977
1059
  if (msg.type === "ralph_preview_files") {
978
1060
  var promptContent = "";
979
1061
  var judgeContent = "";
@@ -1174,7 +1256,7 @@ function attachLoop(ctx) {
1174
1256
 
1175
1257
  // Ralph phase state
1176
1258
  // Derive source from wizardData for reconnect (so client can distinguish ralph vs task)
1177
- var _connSource = (loopState.wizardData && loopState.wizardData.source === "task") ? null : "ralph";
1259
+ var _connSource = loopState.wizardData ? (loopState.wizardData.source || null) : null;
1178
1260
  sendTo(ws, {
1179
1261
  type: "ralph_phase",
1180
1262
  phase: loopState.phase,
@@ -230,9 +230,11 @@ function attachUserMessage(ctx) {
230
230
 
231
231
  // --- Scheduled tasks permission gate ---
232
232
  if (msg.type === "loop_start" || msg.type === "loop_stop" || msg.type === "loop_registry_files" ||
233
- msg.type === "loop_registry_list" || msg.type === "loop_registry_update" || msg.type === "loop_registry_rename" ||
234
- msg.type === "loop_registry_remove" || msg.type === "loop_registry_convert" || msg.type === "loop_registry_toggle" ||
235
- msg.type === "loop_registry_rerun" || msg.type === "schedule_create" || msg.type === "schedule_move") {
233
+ msg.type === "loop_registry_save_files" || msg.type === "loop_registry_list" ||
234
+ msg.type === "loop_registry_update" || msg.type === "loop_registry_rename" ||
235
+ msg.type === "loop_registry_remove" || msg.type === "loop_registry_convert" ||
236
+ msg.type === "loop_registry_toggle" || msg.type === "loop_registry_rerun" ||
237
+ msg.type === "schedule_create" || msg.type === "schedule_move") {
236
238
  if (ws._clayUser) {
237
239
  var schPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
238
240
  if (!schPerms.scheduledTasks) {
@@ -598,6 +598,161 @@
598
598
  text-align: center;
599
599
  }
600
600
 
601
+ #sched-create-desc-row.hidden { display: none; }
602
+
603
+ .sched-create-row-icon-spacer {
604
+ width: 15px;
605
+ flex-shrink: 0;
606
+ }
607
+
608
+ .sched-review-toggle {
609
+ display: flex;
610
+ align-items: center;
611
+ gap: 8px;
612
+ margin-top: 10px;
613
+ font-size: 13px;
614
+ color: var(--text-secondary);
615
+ cursor: pointer;
616
+ }
617
+
618
+ .sched-review-toggle input[type="checkbox"] {
619
+ accent-color: var(--accent);
620
+ }
621
+
622
+ .sched-review-label {
623
+ flex-shrink: 0;
624
+ }
625
+
626
+ .sched-review-count {
627
+ display: inline-flex;
628
+ align-items: center;
629
+ gap: 2px;
630
+ }
631
+
632
+ .sched-review-count.hidden {
633
+ display: none;
634
+ }
635
+
636
+ .sched-create-desc-wrap {
637
+ margin-top: 10px;
638
+ }
639
+
640
+ .sched-create-desc-wrap .sched-create-row-textarea {
641
+ width: 100%;
642
+ }
643
+
644
+ /* --- Accordion sections --- */
645
+ .sched-accordion-divider {
646
+ height: 1px;
647
+ background: var(--border);
648
+ margin: 4px 14px 0;
649
+ opacity: 0.5;
650
+ }
651
+
652
+ .sched-accordion-header {
653
+ display: flex;
654
+ align-items: center;
655
+ justify-content: space-between;
656
+ width: 100%;
657
+ padding: 8px 14px;
658
+ background: none;
659
+ border: none;
660
+ cursor: pointer;
661
+ color: var(--text);
662
+ transition: background 0.12s;
663
+ text-align: left;
664
+ }
665
+
666
+ .sched-accordion-header:hover {
667
+ background: var(--hover);
668
+ }
669
+
670
+ .sched-accordion-header-left {
671
+ display: flex;
672
+ align-items: center;
673
+ gap: 10px;
674
+ }
675
+
676
+ .sched-accordion-icon {
677
+ width: 15px;
678
+ height: 15px;
679
+ color: var(--text-dimmer);
680
+ flex-shrink: 0;
681
+ }
682
+
683
+ .sched-accordion.has-value .sched-accordion-icon {
684
+ color: var(--accent);
685
+ }
686
+
687
+ .sched-accordion-text {
688
+ display: flex;
689
+ flex-direction: column;
690
+ }
691
+
692
+ .sched-accordion-title {
693
+ font-size: 13px;
694
+ font-weight: 500;
695
+ line-height: 1.2;
696
+ }
697
+
698
+ .sched-accordion-subtitle {
699
+ font-size: 11px;
700
+ color: var(--text-dimmer);
701
+ line-height: 1.3;
702
+ margin-top: 1px;
703
+ }
704
+
705
+ .sched-accordion-header-right {
706
+ display: flex;
707
+ align-items: center;
708
+ gap: 6px;
709
+ }
710
+
711
+ .sched-accordion-clear {
712
+ display: inline-flex;
713
+ align-items: center;
714
+ justify-content: center;
715
+ width: 20px;
716
+ height: 20px;
717
+ border-radius: 50%;
718
+ color: var(--text-dimmer);
719
+ transition: background 0.12s, color 0.12s;
720
+ }
721
+
722
+ .sched-accordion-clear:hover {
723
+ background: var(--hover);
724
+ color: var(--text);
725
+ }
726
+
727
+ .sched-accordion-clear .lucide {
728
+ width: 12px;
729
+ height: 12px;
730
+ }
731
+
732
+ .sched-accordion-clear.hidden {
733
+ display: none;
734
+ }
735
+
736
+ .sched-accordion-chevron {
737
+ width: 14px;
738
+ height: 14px;
739
+ color: var(--text-dimmer);
740
+ flex-shrink: 0;
741
+ transition: transform 0.2s ease;
742
+ }
743
+
744
+ .sched-accordion.open .sched-accordion-chevron {
745
+ transform: rotate(90deg);
746
+ }
747
+
748
+ .sched-accordion-body {
749
+ padding: 4px 14px 10px 39px;
750
+ }
751
+
752
+ .sched-accordion-body.hidden {
753
+ display: none;
754
+ }
755
+
601
756
  /* --- Bottom bar --- */
602
757
  .sched-create-bottom {
603
758
  display: flex;
@@ -1061,7 +1216,7 @@
1061
1216
  max-width: 100px;
1062
1217
  }
1063
1218
 
1064
- .sched-custom-repeat-panel .sched-field-select {
1219
+ .sched-accordion-body .sched-field-select {
1065
1220
  padding: 5px 26px 5px 8px;
1066
1221
  font-size: 12px;
1067
1222
  border-radius: 6px;
@@ -1103,6 +1103,12 @@
1103
1103
  overflow-y: auto;
1104
1104
  padding: 20px 24px;
1105
1105
  }
1106
+
1107
+ .scheduler-detail-body:has(.scheduler-file-editor) {
1108
+ display: flex;
1109
+ flex-direction: column;
1110
+ overflow: hidden;
1111
+ }
1106
1112
  .scheduler-detail-loading {
1107
1113
  padding: 32px;
1108
1114
  text-align: center;
@@ -1114,6 +1120,81 @@
1114
1120
  line-height: 1.6;
1115
1121
  color: var(--text);
1116
1122
  }
1123
+
1124
+ .scheduler-file-toolbar {
1125
+ display: flex;
1126
+ justify-content: flex-end;
1127
+ gap: 8px;
1128
+ margin-bottom: 12px;
1129
+ }
1130
+
1131
+ .scheduler-file-edit-btn,
1132
+ .scheduler-file-save-btn,
1133
+ .scheduler-file-cancel-btn {
1134
+ display: inline-flex;
1135
+ align-items: center;
1136
+ gap: 4px;
1137
+ padding: 4px 10px;
1138
+ font-size: 12px;
1139
+ border-radius: 6px;
1140
+ border: 1px solid var(--border);
1141
+ background: var(--bg-secondary);
1142
+ color: var(--text-secondary);
1143
+ cursor: pointer;
1144
+ transition: background 0.15s, border-color 0.15s;
1145
+ }
1146
+
1147
+ .scheduler-file-edit-btn:hover,
1148
+ .scheduler-file-cancel-btn:hover {
1149
+ border-color: var(--text-dimmer);
1150
+ background: var(--hover);
1151
+ }
1152
+
1153
+ .scheduler-file-save-btn {
1154
+ background: var(--accent);
1155
+ color: #fff;
1156
+ border-color: var(--accent);
1157
+ }
1158
+
1159
+ .scheduler-file-save-btn:hover {
1160
+ opacity: 0.9;
1161
+ }
1162
+
1163
+ .scheduler-file-edit-btn .lucide,
1164
+ .scheduler-file-save-btn .lucide,
1165
+ .scheduler-file-cancel-btn .lucide {
1166
+ width: 12px;
1167
+ height: 12px;
1168
+ }
1169
+
1170
+ .scheduler-file-editor {
1171
+ width: 100%;
1172
+ flex: 1;
1173
+ min-height: 0;
1174
+ padding: 12px;
1175
+ font-family: "SF Mono", "Fira Code", "Consolas", monospace;
1176
+ font-size: 13px;
1177
+ line-height: 1.6;
1178
+ color: var(--text);
1179
+ background: var(--bg-secondary);
1180
+ border: 1px solid var(--border);
1181
+ border-radius: 8px;
1182
+ resize: none;
1183
+ outline: none;
1184
+ }
1185
+
1186
+ .scheduler-file-editor:focus {
1187
+ border-color: var(--accent);
1188
+ }
1189
+
1190
+ .scheduler-model-settings {
1191
+ max-width: 640px;
1192
+ margin: 0 auto;
1193
+ }
1194
+
1195
+ .scheduler-model-settings .settings-card {
1196
+ margin-bottom: 16px;
1197
+ }
1117
1198
  .scheduler-detail-meta {
1118
1199
  display: grid;
1119
1200
  grid-template-columns: auto 1fr;