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.
- package/lib/project-loop.js +116 -34
- package/lib/project-user-message.js +5 -3
- package/lib/public/css/scheduler-modal.css +156 -1
- package/lib/public/css/scheduler.css +81 -0
- package/lib/public/index.html +83 -59
- package/lib/public/modules/app-loop-ui.js +85 -2
- package/lib/public/modules/app-messages.js +1 -0
- package/lib/public/modules/scheduler-config.js +241 -162
- package/lib/public/modules/scheduler-history.js +57 -5
- package/lib/public/modules/scheduler.js +80 -36
- package/lib/sdk-bridge.js +40 -14
- package/lib/server-mates.js +7 -2
- package/package.json +1 -1
package/lib/project-loop.js
CHANGED
|
@@ -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
|
|
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
|
|
316
|
+
console.log("[loop-registry] Loop active, queuing trigger for " + record.name);
|
|
317
|
+
pendingTriggers.push(record);
|
|
283
318
|
return;
|
|
284
319
|
}
|
|
285
320
|
|
|
286
|
-
|
|
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 =
|
|
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 === "
|
|
234
|
-
msg.type === "
|
|
235
|
-
msg.type === "
|
|
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-
|
|
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;
|