clay-server 2.7.0 → 2.7.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/public/app.js CHANGED
@@ -14,6 +14,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
14
14
  import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
15
15
  import { initProjectSettings, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, isProjectSettingsOpen, handleProjectSharedEnv, handleProjectSharedEnvSaved } from './modules/project-settings.js';
16
16
  import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modules/skills.js';
17
+ import { initScheduler, handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, openSchedulerToTab, isSchedulerOpen, closeScheduler, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './modules/scheduler.js';
17
18
 
18
19
  // --- Base path for multi-project routing ---
19
20
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -179,7 +180,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
179
180
  var ralphPhase = "idle"; // idle | wizard | crafting | approval | executing | done
180
181
  var ralphCraftingSessionId = null;
181
182
  var wizardStep = 1;
182
- var wizardData = { name: "", task: "", maxIterations: 25 };
183
+ var wizardData = { name: "", task: "", maxIterations: 25, cron: null };
183
184
  var ralphFilesReady = { promptReady: false, judgeReady: false, bothReady: false };
184
185
  var ralphPreviewContent = { prompt: "", judge: "" };
185
186
  var slashCommands = [];
@@ -560,7 +561,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
560
561
  } else if (status === "processing") {
561
562
  if (dot) { dot.classList.add("connected"); dot.classList.add("processing"); }
562
563
  processing = true;
563
- setSendBtnMode("stop");
564
+ setSendBtnMode(inputEl.value.trim() ? "send" : "stop");
564
565
  } else {
565
566
  connected = false;
566
567
  sendBtn.disabled = true;
@@ -1052,13 +1053,21 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1052
1053
  contextTurnsEl.textContent = String(contextData.turns);
1053
1054
  }
1054
1055
 
1055
- function accumulateContext(cost, usage, modelUsage) {
1056
+ function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens) {
1056
1057
  if (cost != null) contextData.cost += cost;
1057
1058
  // Use latest turn values (not cumulative) since each turn's input_tokens
1058
1059
  // already includes the full conversation context up to that point
1059
1060
  if (usage) {
1060
- contextData.input = (usage.input_tokens || usage.inputTokens || 0)
1061
- + (usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0);
1061
+ // Prefer per-call input_tokens from the last stream message_start event
1062
+ // when available — result.usage.input_tokens sums all API calls in a turn,
1063
+ // inflating context usage when tools are involved.
1064
+ // Falls back to the summed value for setups that don't emit message_start.
1065
+ if (lastStreamInputTokens) {
1066
+ contextData.input = lastStreamInputTokens;
1067
+ } else {
1068
+ contextData.input = (usage.input_tokens || usage.inputTokens || 0)
1069
+ + (usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0);
1070
+ }
1062
1071
  contextData.output = usage.output_tokens || usage.outputTokens || 0;
1063
1072
  contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
1064
1073
  contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
@@ -1736,6 +1745,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1736
1745
  if (!slug || slug === currentSlug) return;
1737
1746
  resetFileBrowser();
1738
1747
  closeArchive();
1748
+ if (isSchedulerOpen()) closeScheduler();
1739
1749
  currentSlug = slug;
1740
1750
  basePath = "/p/" + slug + "/";
1741
1751
  wsPath = "/p/" + slug + "/ws";
@@ -1750,6 +1760,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1750
1760
  if (newSlug && newSlug !== currentSlug) {
1751
1761
  resetFileBrowser();
1752
1762
  closeArchive();
1763
+ if (isSchedulerOpen()) closeScheduler();
1753
1764
  currentSlug = newSlug;
1754
1765
  basePath = "/p/" + newSlug + "/";
1755
1766
  wsPath = "/p/" + newSlug + "/ws";
@@ -1880,7 +1891,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1880
1891
  replayingHistory = false;
1881
1892
  // Restore accurate context data from the last result in full history
1882
1893
  if (msg.lastUsage || msg.lastModelUsage) {
1883
- accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage);
1894
+ accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
1884
1895
  }
1885
1896
  updateContextPanel();
1886
1897
  updateUsagePanel();
@@ -2055,6 +2066,22 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2055
2066
  handleSkillUninstalled(msg);
2056
2067
  break;
2057
2068
 
2069
+ case "loop_registry_updated":
2070
+ handleLoopRegistryUpdated(msg);
2071
+ break;
2072
+
2073
+ case "schedule_run_started":
2074
+ handleScheduleRunStarted(msg);
2075
+ break;
2076
+
2077
+ case "schedule_run_finished":
2078
+ handleScheduleRunFinished(msg);
2079
+ break;
2080
+
2081
+ case "loop_scheduled":
2082
+ handleLoopScheduled(msg);
2083
+ break;
2084
+
2058
2085
  case "input_sync":
2059
2086
  handleInputSync(msg.text);
2060
2087
  break;
@@ -2302,7 +2329,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2302
2329
  finalizeAssistantBlock();
2303
2330
  addTurnMeta(msg.cost, msg.duration);
2304
2331
  accumulateUsage(msg.cost, msg.usage);
2305
- accumulateContext(msg.cost, msg.usage, msg.modelUsage);
2332
+ accumulateContext(msg.cost, msg.usage, msg.modelUsage, msg.lastStreamInputTokens);
2306
2333
  break;
2307
2334
 
2308
2335
  case "done":
@@ -2566,7 +2593,9 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2566
2593
 
2567
2594
  case "loop_iteration":
2568
2595
  loopIteration = msg.iteration;
2596
+ loopMaxIterations = msg.maxIterations;
2569
2597
  updateLoopBanner(msg.iteration, msg.maxIterations, "running");
2598
+ updateLoopButton();
2570
2599
  addSystemMessage("Ralph Loop iteration #" + msg.iteration + " started", false);
2571
2600
  break;
2572
2601
 
@@ -2615,6 +2644,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2615
2644
  ralphCraftingSessionId = msg.sessionId || activeSessionId;
2616
2645
  updateLoopButton();
2617
2646
  updateRalphBars();
2647
+ enterCraftingMode(ralphCraftingSessionId, msg.taskId || null);
2618
2648
  break;
2619
2649
 
2620
2650
  case "ralph_files_status":
@@ -2626,10 +2656,15 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2626
2656
  if (msg.bothReady && (ralphPhase === "crafting" || ralphPhase === "approval")) {
2627
2657
  ralphPhase = "approval";
2628
2658
  showRalphApprovalBar(true);
2659
+ if (isSchedulerOpen()) exitCraftingMode(msg.taskId || null);
2629
2660
  }
2630
2661
  updateRalphApprovalStatus();
2631
2662
  break;
2632
2663
 
2664
+ case "loop_registry_files_content":
2665
+ handleLoopRegistryFiles(msg);
2666
+ break;
2667
+
2633
2668
  case "ralph_files_content":
2634
2669
  ralphPreviewContent = { prompt: msg.prompt || "", judge: msg.judge || "" };
2635
2670
  openRalphPreviewModal();
@@ -2779,6 +2814,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2779
2814
  resetContextData: resetContextData,
2780
2815
  showImageModal: showImageModal,
2781
2816
  hideSuggestionChips: hideSuggestionChips,
2817
+ setSendBtnMode: setSendBtnMode,
2782
2818
  });
2783
2819
 
2784
2820
  // --- Notifications module (viewport, banners, notifications, debug, service worker) ---
@@ -2853,6 +2889,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2853
2889
  var stickyNotesSidebarBtn = $("sticky-notes-sidebar-btn");
2854
2890
  if (stickyNotesSidebarBtn) {
2855
2891
  stickyNotesSidebarBtn.addEventListener("click", function () {
2892
+ if (isSchedulerOpen()) closeScheduler();
2856
2893
  if (isArchiveOpen()) {
2857
2894
  closeArchive();
2858
2895
  } else {
@@ -2861,11 +2898,11 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2861
2898
  });
2862
2899
  }
2863
2900
 
2864
- // Close archive when switching to other sidebar panels
2901
+ // Close archive / scheduler panel when switching to other sidebar panels
2865
2902
  var fileBrowserBtn = $("file-browser-btn");
2866
2903
  var terminalSidebarBtn = $("terminal-sidebar-btn");
2867
- if (fileBrowserBtn) fileBrowserBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); });
2868
- if (terminalSidebarBtn) terminalSidebarBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); });
2904
+ if (fileBrowserBtn) fileBrowserBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
2905
+ if (terminalSidebarBtn) terminalSidebarBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
2869
2906
 
2870
2907
  // --- Ralph Loop UI ---
2871
2908
  function updateLoopInputVisibility(loop) {
@@ -2879,39 +2916,72 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2879
2916
  }
2880
2917
 
2881
2918
  function updateLoopButton() {
2882
- var existing = document.getElementById("loop-start-btn");
2883
- if (!existing) {
2884
- var btn = document.createElement("button");
2885
- btn.id = "loop-start-btn";
2886
- btn.innerHTML = '<i data-lucide="repeat"></i> <span>Ralph Loop</span><span class="loop-experimental"><i data-lucide="flask-conical"></i> Experimental</span>';
2887
- btn.title = "Start a new Ralph Loop";
2888
- btn.addEventListener("click", function() {
2889
- var busy = loopActive || ralphPhase === "executing";
2890
- if (busy) {
2919
+ var section = document.getElementById("ralph-loop-section");
2920
+ if (!section) return;
2921
+
2922
+ var busy = loopActive || ralphPhase === "executing";
2923
+ var phase = busy ? "executing" : ralphPhase;
2924
+
2925
+ var statusHtml = "";
2926
+ var statusClass = "";
2927
+ var clickAction = "wizard"; // default
2928
+
2929
+ if (phase === "crafting") {
2930
+ statusHtml = '<span class="ralph-section-status crafting">' + iconHtml("loader", "icon-spin") + ' Crafting\u2026</span>';
2931
+ clickAction = "none";
2932
+ } else if (phase === "approval") {
2933
+ statusHtml = '<span class="ralph-section-status ready">Ready</span>';
2934
+ statusClass = "ralph-section-ready";
2935
+ clickAction = "none";
2936
+ } else if (phase === "executing") {
2937
+ var iterText = loopIteration > 0 ? "Running \u00b7 iteration " + loopIteration + "/" + loopMaxIterations : "Starting\u2026";
2938
+ statusHtml = '<span class="ralph-section-status running">' + iconHtml("loader", "icon-spin") + ' ' + iterText + '</span>';
2939
+ statusClass = "ralph-section-running";
2940
+ clickAction = "popover";
2941
+ } else if (phase === "done") {
2942
+ statusHtml = '<span class="ralph-section-status done">\u2713 Done</span>';
2943
+ statusHtml += '<a href="#" class="ralph-section-tasks-link">View in Scheduled Tasks</a>';
2944
+ statusClass = "ralph-section-done";
2945
+ clickAction = "wizard";
2946
+ } else {
2947
+ // idle
2948
+ statusHtml = '<span class="ralph-section-hint">Start a new loop</span>';
2949
+ }
2950
+
2951
+ section.className = "ralph-loop-section" + (statusClass ? " " + statusClass : "");
2952
+ section.innerHTML =
2953
+ '<div class="ralph-section-inner">' +
2954
+ '<div class="ralph-section-header">' +
2955
+ '<span class="ralph-section-icon">' + iconHtml("repeat") + '</span>' +
2956
+ '<span class="ralph-section-label">Ralph Loop</span>' +
2957
+ '<span class="loop-experimental"><i data-lucide="flask-conical"></i> experimental</span>' +
2958
+ '</div>' +
2959
+ '<div class="ralph-section-body">' + statusHtml + '</div>' +
2960
+ '</div>';
2961
+
2962
+ refreshIcons();
2963
+
2964
+ // Click handler on header
2965
+ var header = section.querySelector(".ralph-section-header");
2966
+ if (header) {
2967
+ header.style.cursor = clickAction === "none" ? "default" : "pointer";
2968
+ header.addEventListener("click", function() {
2969
+ if (clickAction === "popover") {
2891
2970
  toggleLoopPopover();
2892
- } else {
2971
+ } else if (clickAction === "wizard") {
2893
2972
  openRalphWizard();
2894
2973
  }
2895
2974
  });
2896
- var sessionActions = document.getElementById("session-actions");
2897
- if (sessionActions) sessionActions.appendChild(btn);
2898
- if (typeof lucide !== "undefined") lucide.createIcons();
2899
- existing = btn;
2900
2975
  }
2901
- var busy = loopActive || ralphPhase === "executing";
2902
- var hint = existing.querySelector(".loop-busy-hint");
2903
- if (busy) {
2904
- existing.style.opacity = "";
2905
- existing.style.pointerEvents = "";
2906
- if (!hint) {
2907
- hint = document.createElement("span");
2908
- hint.className = "loop-busy-hint";
2909
- hint.innerHTML = iconHtml("loader", "icon-spin");
2910
- existing.appendChild(hint);
2911
- refreshIcons();
2912
- }
2913
- } else {
2914
- if (hint) hint.remove();
2976
+
2977
+ // "View in Scheduled Tasks" link
2978
+ var tasksLink = section.querySelector(".ralph-section-tasks-link");
2979
+ if (tasksLink) {
2980
+ tasksLink.addEventListener("click", function(e) {
2981
+ e.preventDefault();
2982
+ e.stopPropagation();
2983
+ openSchedulerToTab("library");
2984
+ });
2915
2985
  }
2916
2986
  }
2917
2987
 
@@ -3094,20 +3164,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3094
3164
  var nextBtn = document.getElementById("ralph-wizard-next");
3095
3165
  if (backBtn) backBtn.style.visibility = wizardStep === 1 ? "hidden" : "visible";
3096
3166
  if (skipBtn) skipBtn.style.display = "none";
3097
- if (nextBtn) nextBtn.textContent = wizardStep === 3 ? "Launch" : wizardStep === 1 ? "Get Started" : "Next";
3098
-
3099
- // Build review on step 3
3100
- if (wizardStep === 3) {
3101
- collectWizardData();
3102
- var summary = document.getElementById("ralph-review-summary");
3103
- if (summary) {
3104
- summary.innerHTML =
3105
- '<div class="ralph-review-label">Name</div>' +
3106
- '<div class="ralph-review-value">' + escapeHtml(wizardData.name || "(empty)") + '</div>' +
3107
- '<div class="ralph-review-label">Task</div>' +
3108
- '<div class="ralph-review-value">' + escapeHtml(wizardData.task || "(empty)") + '</div>';
3109
- }
3110
- }
3167
+ if (nextBtn) nextBtn.textContent = wizardStep === 2 ? "Launch" : "Get Started";
3111
3168
  }
3112
3169
 
3113
3170
  function collectWizardData() {
@@ -3117,6 +3174,59 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3117
3174
  wizardData.name = nameEl ? nameEl.value.replace(/[^a-zA-Z0-9_-]/g, "").trim() : "";
3118
3175
  wizardData.task = taskEl ? taskEl.value.trim() : "";
3119
3176
  wizardData.maxIterations = iterEl ? parseInt(iterEl.value, 10) || 25 : 25;
3177
+ wizardData.cron = null;
3178
+ }
3179
+
3180
+ function buildWizardCron() {
3181
+ var repeatEl = document.getElementById("ralph-repeat");
3182
+ if (!repeatEl) return null;
3183
+ var preset = repeatEl.value;
3184
+ if (preset === "none") return null;
3185
+
3186
+ var timeEl = document.getElementById("ralph-time");
3187
+ var timeVal = timeEl ? timeEl.value : "09:00";
3188
+ var timeParts = timeVal.split(":");
3189
+ var hour = parseInt(timeParts[0], 10) || 9;
3190
+ var minute = parseInt(timeParts[1], 10) || 0;
3191
+
3192
+ if (preset === "daily") return minute + " " + hour + " * * *";
3193
+ if (preset === "weekdays") return minute + " " + hour + " * * 1-5";
3194
+ if (preset === "weekly") return minute + " " + hour + " * * " + new Date().getDay();
3195
+ if (preset === "monthly") return minute + " " + hour + " " + new Date().getDate() + " * *";
3196
+
3197
+ if (preset === "custom") {
3198
+ var unitEl = document.getElementById("ralph-repeat-unit");
3199
+ var unit = unitEl ? unitEl.value : "day";
3200
+ if (unit === "day") return minute + " " + hour + " * * *";
3201
+ if (unit === "month") return minute + " " + hour + " " + new Date().getDate() + " * *";
3202
+ // week: collect selected days
3203
+ var dowBtns = document.querySelectorAll("#ralph-custom-repeat .sched-dow-btn.active");
3204
+ var days = [];
3205
+ for (var i = 0; i < dowBtns.length; i++) {
3206
+ days.push(dowBtns[i].dataset.dow);
3207
+ }
3208
+ if (days.length === 0) days.push(String(new Date().getDay()));
3209
+ return minute + " " + hour + " * * " + days.join(",");
3210
+ }
3211
+ return null;
3212
+ }
3213
+
3214
+ function cronToHumanText(cron) {
3215
+ if (!cron) return "";
3216
+ var parts = cron.trim().split(/\s+/);
3217
+ if (parts.length !== 5) return cron;
3218
+ var m = parts[0], h = parts[1], dom = parts[2], dow = parts[4];
3219
+ var pad = function(n) { return (parseInt(n,10) < 10 ? "0" : "") + parseInt(n,10); };
3220
+ var t = pad(h) + ":" + pad(m);
3221
+ var dayNames = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
3222
+ if (dow === "*" && dom === "*") return "Every day at " + t;
3223
+ if (dow === "1-5" && dom === "*") return "Weekdays at " + t;
3224
+ if (dom !== "*" && dow === "*") return "Monthly on day " + dom + " at " + t;
3225
+ if (dow !== "*" && dom === "*") {
3226
+ var ds = dow.split(",").map(function(d) { return dayNames[parseInt(d,10)] || d; });
3227
+ return "Every " + ds.join(", ") + " at " + t;
3228
+ }
3229
+ return cron;
3120
3230
  }
3121
3231
 
3122
3232
  function wizardNext() {
@@ -3168,8 +3278,6 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3168
3278
  if (taskEl) { taskEl.focus(); taskEl.style.borderColor = "#e74c3c"; setTimeout(function() { taskEl.style.borderColor = ""; }, 2000); }
3169
3279
  return;
3170
3280
  }
3171
- }
3172
- if (wizardStep === 3) {
3173
3281
  wizardSubmit();
3174
3282
  return;
3175
3283
  }
@@ -3186,7 +3294,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3186
3294
  }
3187
3295
 
3188
3296
  function wizardSkip() {
3189
- if (wizardStep < 3) {
3297
+ if (wizardStep < 2) {
3190
3298
  wizardStep++;
3191
3299
  updateWizardStep();
3192
3300
  }
@@ -3221,6 +3329,52 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3221
3329
  });
3222
3330
  }
3223
3331
 
3332
+ // --- Repeat picker handlers ---
3333
+ var repeatSelect = document.getElementById("ralph-repeat");
3334
+ var repeatTimeRow = document.getElementById("ralph-time-row");
3335
+ var repeatCustom = document.getElementById("ralph-custom-repeat");
3336
+ var repeatUnitSelect = document.getElementById("ralph-repeat-unit");
3337
+ var repeatDowRow = document.getElementById("ralph-custom-dow-row");
3338
+ var cronPreview = document.getElementById("ralph-cron-preview");
3339
+
3340
+ function updateRepeatUI() {
3341
+ if (!repeatSelect) return;
3342
+ var val = repeatSelect.value;
3343
+ var isScheduled = val !== "none";
3344
+ if (repeatTimeRow) repeatTimeRow.style.display = isScheduled ? "" : "none";
3345
+ if (repeatCustom) repeatCustom.style.display = val === "custom" ? "" : "none";
3346
+ if (cronPreview) cronPreview.style.display = isScheduled ? "" : "none";
3347
+ if (isScheduled) {
3348
+ var cron = buildWizardCron();
3349
+ var humanEl = document.getElementById("ralph-cron-human");
3350
+ var cronEl = document.getElementById("ralph-cron-expr");
3351
+ if (humanEl) humanEl.textContent = cronToHumanText(cron);
3352
+ if (cronEl) cronEl.textContent = cron || "";
3353
+ }
3354
+ }
3355
+
3356
+ if (repeatSelect) {
3357
+ repeatSelect.addEventListener("change", updateRepeatUI);
3358
+ }
3359
+ if (repeatUnitSelect) {
3360
+ repeatUnitSelect.addEventListener("change", function () {
3361
+ if (repeatDowRow) repeatDowRow.style.display = this.value === "week" ? "" : "none";
3362
+ updateRepeatUI();
3363
+ });
3364
+ }
3365
+
3366
+ var timeInput = document.getElementById("ralph-time");
3367
+ if (timeInput) timeInput.addEventListener("change", updateRepeatUI);
3368
+
3369
+ // DOW buttons in custom repeat
3370
+ var customDowBtns = document.querySelectorAll("#ralph-custom-repeat .sched-dow-btn");
3371
+ for (var di = 0; di < customDowBtns.length; di++) {
3372
+ customDowBtns[di].addEventListener("click", function () {
3373
+ this.classList.toggle("active");
3374
+ updateRepeatUI();
3375
+ });
3376
+ }
3377
+
3224
3378
  // --- Ralph Sticky (title-bar island) ---
3225
3379
  function showRalphCraftingBar(show) {
3226
3380
  var stickyEl = document.getElementById("ralph-sticky");
@@ -3275,7 +3429,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3275
3429
  '<span class="ralph-sticky-label">Ralph</span>' +
3276
3430
  '<span class="ralph-sticky-status" id="ralph-sticky-status">Ready</span>' +
3277
3431
  '<button class="ralph-sticky-action ralph-sticky-preview" title="Preview files">' + iconHtml("eye") + '</button>' +
3278
- '<button class="ralph-sticky-action ralph-sticky-start" title="Start loop">' + iconHtml("play") + '</button>' +
3432
+ '<button class="ralph-sticky-action ralph-sticky-start" title="' + (wizardData.cron ? 'Schedule' : 'Start loop') + '">' + iconHtml(wizardData.cron ? "calendar-clock" : "play") + '</button>' +
3279
3433
  '<button class="ralph-sticky-action ralph-sticky-dismiss" title="Cancel and discard">' + iconHtml("x") + '</button>' +
3280
3434
  '</div>' +
3281
3435
  '</div>';
@@ -3412,6 +3566,15 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3412
3566
  sendTerminalCommand: function (cmd) { sendTerminalCommand(cmd); },
3413
3567
  });
3414
3568
 
3569
+ // --- Scheduler ---
3570
+ initScheduler({
3571
+ get ws() { return ws; },
3572
+ get connected() { return connected; },
3573
+ get activeSessionId() { return activeSessionId; },
3574
+ basePath: basePath,
3575
+ openRalphWizard: function () { openRalphWizard(); },
3576
+ });
3577
+
3415
3578
  // --- Remove project ---
3416
3579
  function confirmRemoveProject(slug, name) {
3417
3580
  showConfirm("Remove project \"" + name + "\"?", function () {
@@ -38,7 +38,68 @@
38
38
  .loop-stop-btn:hover {
39
39
  opacity: 0.85;
40
40
  }
41
- /* .loop-start-btn is now inside #session-actions, inherits sidebar button styles */
41
+ /* Ralph Loop sidebar section */
42
+ #ralph-loop-section {
43
+ border-left: 3px solid var(--accent);
44
+ background: rgba(var(--overlay-rgb), 0.03);
45
+ padding: 8px 12px;
46
+ margin: 0 8px;
47
+ border-radius: 6px;
48
+ border-bottom: 1px solid var(--border);
49
+ margin-bottom: 4px;
50
+ }
51
+ .ralph-section-inner {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 4px;
55
+ }
56
+ .ralph-section-header {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 6px;
60
+ font-size: 12px;
61
+ font-weight: 600;
62
+ color: var(--text);
63
+ }
64
+ .ralph-section-header .ralph-section-icon { display: inline-flex; color: var(--accent); }
65
+ .ralph-section-header .ralph-section-icon .lucide { width: 13px; height: 13px; }
66
+ .ralph-section-header .ralph-section-label { flex: 1; }
67
+ .ralph-section-header .loop-experimental { margin-left: 0; font-size: 9.5px; }
68
+ .ralph-section-body {
69
+ font-size: 11.5px;
70
+ color: var(--text-muted);
71
+ padding-left: 2px;
72
+ margin-top: 2px;
73
+ }
74
+ .ralph-section-hint {
75
+ color: var(--text-muted);
76
+ }
77
+ .ralph-section-status {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: 4px;
81
+ }
82
+ .ralph-section-status .lucide { width: 11px; height: 11px; }
83
+ .ralph-section-status.crafting { color: var(--accent); }
84
+ .ralph-section-status.ready { color: var(--success, #27ae60); font-weight: 600; }
85
+ .ralph-section-status.running { color: var(--accent); }
86
+ .ralph-section-status.done { color: var(--success, #27ae60); }
87
+ .ralph-section-tasks-link {
88
+ display: block;
89
+ margin-top: 2px;
90
+ font-size: 11px;
91
+ color: var(--accent);
92
+ text-decoration: none;
93
+ cursor: pointer;
94
+ }
95
+ .ralph-section-tasks-link:hover { text-decoration: underline; }
96
+
97
+ /* Section accent states */
98
+ .ralph-section-ready { border-left-color: var(--success, #27ae60); }
99
+ .ralph-section-running { border-left-color: var(--accent); }
100
+ .ralph-section-done { border-left-color: var(--success, #27ae60); }
101
+
102
+ /* .loop-start-btn is now inside #ralph-loop-section, inherits sidebar button styles */
42
103
  .loop-experimental {
43
104
  display: inline-flex;
44
105
  align-items: center;
@@ -507,6 +507,7 @@
507
507
  }
508
508
 
509
509
  #config-chip .lucide { width: 10px; height: 10px; }
510
+ #config-chip .config-chip-icon { display: none; }
510
511
  #config-chip:hover { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.06); }
511
512
  #config-chip.active { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.06); }
512
513
 
@@ -851,6 +852,10 @@
851
852
  width: auto;
852
853
  max-height: 60vh;
853
854
  }
855
+
856
+ /* Config chip: icon-only on mobile */
857
+ #config-chip .config-chip-icon { display: inline; width: 14px; height: 14px; }
858
+ #config-chip-label { display: none; }
854
859
  }
855
860
 
856
861
 
@@ -17,11 +17,12 @@
17
17
  left: 0;
18
18
  right: 0;
19
19
  height: calc(56px + var(--safe-bottom));
20
+ padding-top: 1px;
20
21
  padding-bottom: var(--safe-bottom);
21
22
  background: var(--bg);
22
23
  border-top: 1px solid var(--border);
23
24
  display: flex;
24
- align-items: flex-start;
25
+ align-items: center;
25
26
  justify-content: space-around;
26
27
  z-index: 200;
27
28
  }
@@ -88,33 +89,32 @@
88
89
 
89
90
  .mobile-tab { position: relative; }
90
91
 
91
- /* --- Center "+" button (raised) --- */
92
+ /* --- Center "+" button --- */
92
93
  .mobile-tab-new {
93
- flex: none;
94
- width: 48px;
95
- height: 48px;
96
- border-radius: 50%;
97
- background: var(--accent);
98
- color: #fff;
94
+ flex: 1;
95
+ height: 100%;
96
+ background: none;
99
97
  border: none;
100
98
  display: flex;
101
99
  align-items: center;
102
100
  justify-content: center;
103
101
  cursor: pointer;
104
- margin-top: -12px;
105
- box-shadow: 0 2px 12px rgba(var(--shadow-rgb), 0.25);
106
102
  -webkit-tap-highlight-color: transparent;
107
- transition: transform 0.1s, box-shadow 0.15s;
108
103
  }
109
104
 
110
105
  .mobile-tab-new .lucide {
111
- width: 24px;
112
- height: 24px;
106
+ width: 20px;
107
+ height: 20px;
108
+ padding: 8px;
109
+ box-sizing: content-box;
110
+ border-radius: 50%;
111
+ background: var(--border);
112
+ color: #fff;
113
+ transition: transform 0.1s;
113
114
  }
114
115
 
115
- .mobile-tab-new:active {
116
+ .mobile-tab-new:active .lucide {
116
117
  transform: scale(0.92);
117
- box-shadow: 0 1px 6px rgba(var(--shadow-rgb), 0.2);
118
118
  }
119
119
 
120
120
  /* --- Mobile project list items (inside sidebar) --- */