clay-server 2.6.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.
Files changed (38) hide show
  1. package/bin/cli.js +53 -4
  2. package/lib/config.js +15 -6
  3. package/lib/daemon.js +47 -5
  4. package/lib/ipc.js +12 -0
  5. package/lib/notes.js +2 -2
  6. package/lib/project.js +883 -2
  7. package/lib/public/app.js +862 -14
  8. package/lib/public/css/diff.css +12 -0
  9. package/lib/public/css/filebrowser.css +1 -1
  10. package/lib/public/css/loop.css +841 -0
  11. package/lib/public/css/menus.css +5 -0
  12. package/lib/public/css/mobile-nav.css +15 -15
  13. package/lib/public/css/rewind.css +23 -0
  14. package/lib/public/css/scheduler-modal.css +546 -0
  15. package/lib/public/css/scheduler.css +944 -0
  16. package/lib/public/css/sidebar.css +1 -0
  17. package/lib/public/css/skills.css +59 -0
  18. package/lib/public/css/sticky-notes.css +486 -0
  19. package/lib/public/css/title-bar.css +83 -3
  20. package/lib/public/index.html +181 -3
  21. package/lib/public/modules/diff.js +3 -3
  22. package/lib/public/modules/filebrowser.js +169 -45
  23. package/lib/public/modules/input.js +17 -3
  24. package/lib/public/modules/markdown.js +10 -0
  25. package/lib/public/modules/qrcode.js +23 -26
  26. package/lib/public/modules/scheduler.js +1240 -0
  27. package/lib/public/modules/server-settings.js +40 -0
  28. package/lib/public/modules/sidebar.js +12 -0
  29. package/lib/public/modules/skills.js +84 -0
  30. package/lib/public/modules/sticky-notes.js +617 -52
  31. package/lib/public/modules/theme.js +9 -19
  32. package/lib/public/modules/tools.js +16 -2
  33. package/lib/public/style.css +3 -0
  34. package/lib/scheduler.js +362 -0
  35. package/lib/sdk-bridge.js +36 -0
  36. package/lib/sessions.js +9 -5
  37. package/lib/utils.js +49 -3
  38. package/package.json +1 -1
package/lib/public/app.js CHANGED
@@ -6,14 +6,15 @@ import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, add
6
6
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
7
7
  import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage } from './modules/input.js';
8
8
  import { initQrCode } from './modules/qrcode.js';
9
- import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer } from './modules/filebrowser.js';
9
+ import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer, resetFileBrowser } from './modules/filebrowser.js';
10
10
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
11
- import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted } from './modules/sticky-notes.js';
11
+ import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen } from './modules/sticky-notes.js';
12
12
  import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme } from './modules/theme.js';
13
13
  import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
14
- import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
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_-]+)/);
@@ -172,6 +173,16 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
172
173
  var highlightTimer = null;
173
174
  var activeSessionId = null;
174
175
  var sessionDrafts = {};
176
+ var loopActive = false;
177
+ var loopAvailable = false;
178
+ var loopIteration = 0;
179
+ var loopMaxIterations = 0;
180
+ var ralphPhase = "idle"; // idle | wizard | crafting | approval | executing | done
181
+ var ralphCraftingSessionId = null;
182
+ var wizardStep = 1;
183
+ var wizardData = { name: "", task: "", maxIterations: 25, cron: null };
184
+ var ralphFilesReady = { promptReady: false, judgeReady: false, bothReady: false };
185
+ var ralphPreviewContent = { prompt: "", judge: "" };
175
186
  var slashCommands = [];
176
187
  // slashActiveIdx, slashFiltered, pendingImages, pendingPastes -> modules/input.js
177
188
  // pendingPermissions -> modules/tools.js
@@ -550,7 +561,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
550
561
  } else if (status === "processing") {
551
562
  if (dot) { dot.classList.add("connected"); dot.classList.add("processing"); }
552
563
  processing = true;
553
- setSendBtnMode("stop");
564
+ setSendBtnMode(inputEl.value.trim() ? "send" : "stop");
554
565
  } else {
555
566
  connected = false;
556
567
  sendBtn.disabled = true;
@@ -981,11 +992,12 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
981
992
  };
982
993
 
983
994
  function resolveContextWindow(model, sdkValue) {
995
+ if (sdkValue) return sdkValue;
984
996
  var lc = (model || "").toLowerCase();
985
997
  for (var key in KNOWN_CONTEXT_WINDOWS) {
986
- if (lc.includes(key)) return Math.max(sdkValue || 0, KNOWN_CONTEXT_WINDOWS[key]);
998
+ if (lc.includes(key)) return KNOWN_CONTEXT_WINDOWS[key];
987
999
  }
988
- return sdkValue || 200000;
1000
+ return 200000;
989
1001
  }
990
1002
 
991
1003
  function contextPctClass(pct) {
@@ -994,8 +1006,8 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
994
1006
 
995
1007
  function updateContextPanel() {
996
1008
  if (!contextUsedEl) return;
997
- // Context window usage = input tokens (includes cache read/write) + output tokens
998
- var used = contextData.input + contextData.output;
1009
+ // Context window usage = input tokens only (includes cache read/write)
1010
+ var used = contextData.input;
999
1011
  var win = contextData.contextWindow;
1000
1012
  var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
1001
1013
  var cls = contextPctClass(pct);
@@ -1041,13 +1053,21 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1041
1053
  contextTurnsEl.textContent = String(contextData.turns);
1042
1054
  }
1043
1055
 
1044
- function accumulateContext(cost, usage, modelUsage) {
1056
+ function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens) {
1045
1057
  if (cost != null) contextData.cost += cost;
1046
1058
  // Use latest turn values (not cumulative) since each turn's input_tokens
1047
1059
  // already includes the full conversation context up to that point
1048
1060
  if (usage) {
1049
- contextData.input = (usage.input_tokens || usage.inputTokens || 0)
1050
- + (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
+ }
1051
1071
  contextData.output = usage.output_tokens || usage.outputTokens || 0;
1052
1072
  contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
1053
1073
  contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
@@ -1194,7 +1214,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1194
1214
  setActivity: function(text) { setActivity(text); },
1195
1215
  stopUrgentBlink: function() { stopUrgentBlink(); },
1196
1216
  getContextPercent: function() {
1197
- var used = contextData.input + contextData.output;
1217
+ var used = contextData.input;
1198
1218
  var win = contextData.contextWindow;
1199
1219
  return win > 0 ? Math.round((used / win) * 100) : 0;
1200
1220
  },
@@ -1723,6 +1743,9 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1723
1743
  // --- Project switching (no full reload) ---
1724
1744
  function switchProject(slug) {
1725
1745
  if (!slug || slug === currentSlug) return;
1746
+ resetFileBrowser();
1747
+ closeArchive();
1748
+ if (isSchedulerOpen()) closeScheduler();
1726
1749
  currentSlug = slug;
1727
1750
  basePath = "/p/" + slug + "/";
1728
1751
  wsPath = "/p/" + slug + "/ws";
@@ -1735,6 +1758,9 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1735
1758
  var m = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
1736
1759
  var newSlug = m ? m[1] : null;
1737
1760
  if (newSlug && newSlug !== currentSlug) {
1761
+ resetFileBrowser();
1762
+ closeArchive();
1763
+ if (isSchedulerOpen()) closeScheduler();
1738
1764
  currentSlug = newSlug;
1739
1765
  basePath = "/p/" + newSlug + "/";
1740
1766
  wsPath = "/p/" + newSlug + "/ws";
@@ -1865,7 +1891,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1865
1891
  replayingHistory = false;
1866
1892
  // Restore accurate context data from the last result in full history
1867
1893
  if (msg.lastUsage || msg.lastModelUsage) {
1868
- accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage);
1894
+ accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
1869
1895
  }
1870
1896
  updateContextPanel();
1871
1897
  updateUsagePanel();
@@ -2020,12 +2046,42 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2020
2046
 
2021
2047
  case "skill_installed":
2022
2048
  handleSkillInstalled(msg);
2049
+ // Advance ralph wizard if we were installing clay-ralph
2050
+ if (msg.skill === "clay-ralph" && ralphSkillInstalling) {
2051
+ ralphSkillInstalling = false;
2052
+ ralphSkillInstalled = true;
2053
+ if (msg.success) {
2054
+ wizardStep = 2;
2055
+ updateWizardStep();
2056
+ } else {
2057
+ var rNextBtn = document.getElementById("ralph-wizard-next");
2058
+ if (rNextBtn) { rNextBtn.disabled = false; rNextBtn.textContent = "Get Started"; }
2059
+ var rStatusEl = document.getElementById("ralph-install-status");
2060
+ if (rStatusEl) { rStatusEl.innerHTML = "Failed to install skill. Try again."; }
2061
+ }
2062
+ }
2023
2063
  break;
2024
2064
 
2025
2065
  case "skill_uninstalled":
2026
2066
  handleSkillUninstalled(msg);
2027
2067
  break;
2028
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
+
2029
2085
  case "input_sync":
2030
2086
  handleInputSync(msg.text);
2031
2087
  break;
@@ -2056,6 +2112,8 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2056
2112
  activeSessionId = msg.id;
2057
2113
  cliSessionId = msg.cliSessionId || null;
2058
2114
  resetClientState();
2115
+ updateRalphBars();
2116
+ updateLoopInputVisibility(msg.loop);
2059
2117
  // Restore draft for incoming session
2060
2118
  var draft = sessionDrafts[activeSessionId] || "";
2061
2119
  inputEl.value = draft;
@@ -2271,7 +2329,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2271
2329
  finalizeAssistantBlock();
2272
2330
  addTurnMeta(msg.cost, msg.duration);
2273
2331
  accumulateUsage(msg.cost, msg.usage);
2274
- accumulateContext(msg.cost, msg.usage, msg.modelUsage);
2332
+ accumulateContext(msg.cost, msg.usage, msg.modelUsage, msg.lastStreamInputTokens);
2275
2333
  break;
2276
2334
 
2277
2335
  case "done":
@@ -2500,9 +2558,117 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2500
2558
  handleKeepAwakeChanged(msg);
2501
2559
  break;
2502
2560
 
2561
+ case "restart_server_result":
2562
+ handleRestartResult(msg);
2563
+ break;
2564
+
2503
2565
  case "shutdown_server_result":
2504
2566
  handleShutdownResult(msg);
2505
2567
  break;
2568
+
2569
+ // --- Ralph Loop ---
2570
+ case "loop_available":
2571
+ loopAvailable = msg.available;
2572
+ loopActive = msg.active;
2573
+ loopIteration = msg.iteration || 0;
2574
+ loopMaxIterations = msg.maxIterations || 20;
2575
+ updateLoopButton();
2576
+ if (loopActive) {
2577
+ showLoopBanner(true);
2578
+ if (loopIteration > 0) {
2579
+ updateLoopBanner(loopIteration, loopMaxIterations, "running");
2580
+ }
2581
+ }
2582
+ break;
2583
+
2584
+ case "loop_started":
2585
+ loopActive = true;
2586
+ ralphPhase = "executing";
2587
+ loopIteration = 0;
2588
+ loopMaxIterations = msg.maxIterations;
2589
+ showLoopBanner(true);
2590
+ updateLoopButton();
2591
+ addSystemMessage("Ralph Loop started (max " + msg.maxIterations + " iterations)", false);
2592
+ break;
2593
+
2594
+ case "loop_iteration":
2595
+ loopIteration = msg.iteration;
2596
+ loopMaxIterations = msg.maxIterations;
2597
+ updateLoopBanner(msg.iteration, msg.maxIterations, "running");
2598
+ updateLoopButton();
2599
+ addSystemMessage("Ralph Loop iteration #" + msg.iteration + " started", false);
2600
+ break;
2601
+
2602
+ case "loop_judging":
2603
+ updateLoopBanner(loopIteration, loopMaxIterations, "judging");
2604
+ addSystemMessage("Judging iteration #" + msg.iteration + "...", false);
2605
+ break;
2606
+
2607
+ case "loop_verdict":
2608
+ addSystemMessage("Judge: " + msg.verdict.toUpperCase() + " - " + (msg.summary || ""), false);
2609
+ break;
2610
+
2611
+ case "loop_stopping":
2612
+ updateLoopBanner(loopIteration, loopMaxIterations, "stopping");
2613
+ break;
2614
+
2615
+ case "loop_finished":
2616
+ loopActive = false;
2617
+ ralphPhase = "done";
2618
+ showLoopBanner(false);
2619
+ updateLoopButton();
2620
+ var finishMsg = msg.reason === "pass"
2621
+ ? "Ralph Loop completed successfully after " + msg.iterations + " iteration(s)."
2622
+ : msg.reason === "max_iterations"
2623
+ ? "Ralph Loop reached maximum iterations (" + msg.iterations + ")."
2624
+ : msg.reason === "stopped"
2625
+ ? "Ralph Loop stopped."
2626
+ : "Ralph Loop ended with error.";
2627
+ addSystemMessage(finishMsg, false);
2628
+ break;
2629
+
2630
+ case "loop_error":
2631
+ addSystemMessage("Ralph Loop error: " + msg.text, true);
2632
+ break;
2633
+
2634
+ // --- Ralph Wizard / Crafting ---
2635
+ case "ralph_phase":
2636
+ ralphPhase = msg.phase || "idle";
2637
+ if (msg.craftingSessionId) ralphCraftingSessionId = msg.craftingSessionId;
2638
+ updateLoopButton();
2639
+ updateRalphBars();
2640
+ break;
2641
+
2642
+ case "ralph_crafting_started":
2643
+ ralphPhase = "crafting";
2644
+ ralphCraftingSessionId = msg.sessionId || activeSessionId;
2645
+ updateLoopButton();
2646
+ updateRalphBars();
2647
+ enterCraftingMode(ralphCraftingSessionId, msg.taskId || null);
2648
+ break;
2649
+
2650
+ case "ralph_files_status":
2651
+ ralphFilesReady = {
2652
+ promptReady: msg.promptReady,
2653
+ judgeReady: msg.judgeReady,
2654
+ bothReady: msg.bothReady,
2655
+ };
2656
+ if (msg.bothReady && (ralphPhase === "crafting" || ralphPhase === "approval")) {
2657
+ ralphPhase = "approval";
2658
+ showRalphApprovalBar(true);
2659
+ if (isSchedulerOpen()) exitCraftingMode(msg.taskId || null);
2660
+ }
2661
+ updateRalphApprovalStatus();
2662
+ break;
2663
+
2664
+ case "loop_registry_files_content":
2665
+ handleLoopRegistryFiles(msg);
2666
+ break;
2667
+
2668
+ case "ralph_files_content":
2669
+ ralphPreviewContent = { prompt: msg.prompt || "", judge: msg.judge || "" };
2670
+ openRalphPreviewModal();
2671
+ break;
2506
2672
  }
2507
2673
  }
2508
2674
 
@@ -2648,6 +2814,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2648
2814
  resetContextData: resetContextData,
2649
2815
  showImageModal: showImageModal,
2650
2816
  hideSuggestionChips: hideSuggestionChips,
2817
+ setSendBtnMode: setSendBtnMode,
2651
2818
  });
2652
2819
 
2653
2820
  // --- Notifications module (viewport, banners, notifications, debug, service worker) ---
@@ -2718,6 +2885,678 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2718
2885
  get connected() { return connected; },
2719
2886
  });
2720
2887
 
2888
+ // --- Sticky Notes sidebar button (archive view) ---
2889
+ var stickyNotesSidebarBtn = $("sticky-notes-sidebar-btn");
2890
+ if (stickyNotesSidebarBtn) {
2891
+ stickyNotesSidebarBtn.addEventListener("click", function () {
2892
+ if (isSchedulerOpen()) closeScheduler();
2893
+ if (isArchiveOpen()) {
2894
+ closeArchive();
2895
+ } else {
2896
+ openArchive();
2897
+ }
2898
+ });
2899
+ }
2900
+
2901
+ // Close archive / scheduler panel when switching to other sidebar panels
2902
+ var fileBrowserBtn = $("file-browser-btn");
2903
+ var terminalSidebarBtn = $("terminal-sidebar-btn");
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(); });
2906
+
2907
+ // --- Ralph Loop UI ---
2908
+ function updateLoopInputVisibility(loop) {
2909
+ var inputArea = document.getElementById("input-area");
2910
+ if (!inputArea) return;
2911
+ if (loop && loop.active) {
2912
+ inputArea.style.display = "none";
2913
+ } else {
2914
+ inputArea.style.display = "";
2915
+ }
2916
+ }
2917
+
2918
+ function updateLoopButton() {
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") {
2970
+ toggleLoopPopover();
2971
+ } else if (clickAction === "wizard") {
2972
+ openRalphWizard();
2973
+ }
2974
+ });
2975
+ }
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
+ });
2985
+ }
2986
+ }
2987
+
2988
+ function toggleLoopPopover() {
2989
+ var existing = document.getElementById("loop-status-modal");
2990
+ if (existing) {
2991
+ existing.remove();
2992
+ return;
2993
+ }
2994
+
2995
+ var taskPreview = wizardData.task || "—";
2996
+ if (taskPreview.length > 120) taskPreview = taskPreview.substring(0, 120) + "\u2026";
2997
+ var statusText = "Iteration #" + loopIteration + " / " + loopMaxIterations;
2998
+
2999
+ var modal = document.createElement("div");
3000
+ modal.id = "loop-status-modal";
3001
+ modal.className = "loop-status-modal";
3002
+ modal.innerHTML =
3003
+ '<div class="loop-status-backdrop"></div>' +
3004
+ '<div class="loop-status-dialog">' +
3005
+ '<div class="loop-status-dialog-header">' +
3006
+ '<span class="loop-status-dialog-icon">' + iconHtml("repeat") + '</span>' +
3007
+ '<span class="loop-status-dialog-title">Ralph Loop</span>' +
3008
+ '<button class="loop-status-dialog-close" title="Close">' + iconHtml("x") + '</button>' +
3009
+ '</div>' +
3010
+ '<div class="loop-status-dialog-body">' +
3011
+ '<div class="loop-status-dialog-row">' +
3012
+ '<span class="loop-status-dialog-label">Progress</span>' +
3013
+ '<span class="loop-status-dialog-value">' + escapeHtml(statusText) + '</span>' +
3014
+ '</div>' +
3015
+ '<div class="loop-status-dialog-row">' +
3016
+ '<span class="loop-status-dialog-label">Task</span>' +
3017
+ '<span class="loop-status-dialog-value loop-status-dialog-task">' + escapeHtml(taskPreview) + '</span>' +
3018
+ '</div>' +
3019
+ '</div>' +
3020
+ '<div class="loop-status-dialog-footer">' +
3021
+ '<button class="loop-status-dialog-stop">' + iconHtml("square") + ' Stop loop</button>' +
3022
+ '</div>' +
3023
+ '</div>';
3024
+
3025
+ document.body.appendChild(modal);
3026
+ refreshIcons();
3027
+
3028
+ function closeModal() { modal.remove(); }
3029
+
3030
+ modal.querySelector(".loop-status-backdrop").addEventListener("click", closeModal);
3031
+ modal.querySelector(".loop-status-dialog-close").addEventListener("click", closeModal);
3032
+
3033
+ modal.querySelector(".loop-status-dialog-stop").addEventListener("click", function(e) {
3034
+ e.stopPropagation();
3035
+ closeModal();
3036
+ showConfirm("Stop the running Ralph Loop?", function() {
3037
+ if (ws && ws.readyState === 1) {
3038
+ ws.send(JSON.stringify({ type: "loop_stop" }));
3039
+ }
3040
+ });
3041
+ });
3042
+ }
3043
+
3044
+ function showLoopBanner(show) {
3045
+ var stickyEl = document.getElementById("ralph-sticky");
3046
+ if (!stickyEl) { updateLoopButton(); return; }
3047
+ if (!show) {
3048
+ stickyEl.classList.add("hidden");
3049
+ stickyEl.classList.remove("ralph-running");
3050
+ stickyEl.innerHTML = "";
3051
+ updateLoopButton();
3052
+ return;
3053
+ }
3054
+
3055
+ stickyEl.innerHTML =
3056
+ '<div class="ralph-sticky-inner">' +
3057
+ '<div class="ralph-sticky-header">' +
3058
+ '<span class="ralph-sticky-icon">' + iconHtml("repeat") + '</span>' +
3059
+ '<span class="ralph-sticky-label">Ralph Loop</span>' +
3060
+ '<span class="ralph-sticky-status" id="loop-status">Starting\u2026</span>' +
3061
+ '<button class="ralph-sticky-action ralph-sticky-stop" title="Stop loop">' + iconHtml("square") + '</button>' +
3062
+ '</div>' +
3063
+ '</div>';
3064
+ stickyEl.classList.remove("hidden", "ralph-ready");
3065
+ stickyEl.classList.add("ralph-running");
3066
+ refreshIcons();
3067
+
3068
+ stickyEl.querySelector(".ralph-sticky-stop").addEventListener("click", function(e) {
3069
+ e.stopPropagation();
3070
+ if (ws && ws.readyState === 1) {
3071
+ ws.send(JSON.stringify({ type: "loop_stop" }));
3072
+ }
3073
+ });
3074
+ updateLoopButton();
3075
+ }
3076
+
3077
+ function updateLoopBanner(iteration, maxIterations, phase) {
3078
+ var statusEl = document.getElementById("loop-status");
3079
+ if (!statusEl) return;
3080
+ var text = "#" + iteration + "/" + maxIterations;
3081
+ if (phase === "judging") text += " judging\u2026";
3082
+ else if (phase === "stopping") text = "Stopping\u2026";
3083
+ else text += " running";
3084
+ statusEl.textContent = text;
3085
+ }
3086
+
3087
+ function updateRalphBars() {
3088
+ var onCraftingSession = ralphCraftingSessionId && activeSessionId === ralphCraftingSessionId;
3089
+ if (ralphPhase === "crafting" && onCraftingSession) {
3090
+ showRalphCraftingBar(true);
3091
+ } else {
3092
+ showRalphCraftingBar(false);
3093
+ }
3094
+ if (ralphPhase === "approval" && onCraftingSession) {
3095
+ showRalphApprovalBar(true);
3096
+ } else {
3097
+ showRalphApprovalBar(false);
3098
+ }
3099
+ }
3100
+
3101
+ // --- Ralph Wizard ---
3102
+ var ralphSkillInstalled = false;
3103
+ var ralphSkillInstalling = false;
3104
+
3105
+ function checkRalphSkillInstalled(cb) {
3106
+ fetch(basePath + "api/installed-skills")
3107
+ .then(function (res) { return res.json(); })
3108
+ .then(function (data) {
3109
+ var installed = data.installed || {};
3110
+ ralphSkillInstalled = !!installed["clay-ralph"];
3111
+ cb(ralphSkillInstalled);
3112
+ })
3113
+ .catch(function () { cb(false); });
3114
+ }
3115
+
3116
+ function openRalphWizard() {
3117
+ wizardData = { name: "", task: "", maxIterations: 25 };
3118
+ ralphSkillInstalling = false;
3119
+ var el = document.getElementById("ralph-wizard");
3120
+ if (!el) return;
3121
+
3122
+ var nameEl = document.getElementById("ralph-name");
3123
+ if (nameEl) nameEl.value = "";
3124
+ var taskEl = document.getElementById("ralph-task");
3125
+ if (taskEl) taskEl.value = "";
3126
+ var iterEl = document.getElementById("ralph-max-iterations");
3127
+ if (iterEl) iterEl.value = "25";
3128
+
3129
+ // Check if clay-ralph skill is installed — skip onboarding if so
3130
+ checkRalphSkillInstalled(function (installed) {
3131
+ wizardStep = installed ? 2 : 1;
3132
+ el.classList.remove("hidden");
3133
+ var statusEl = document.getElementById("ralph-install-status");
3134
+ if (statusEl) { statusEl.classList.add("hidden"); statusEl.innerHTML = ""; }
3135
+ updateWizardStep();
3136
+ });
3137
+ }
3138
+
3139
+ function closeRalphWizard() {
3140
+ var el = document.getElementById("ralph-wizard");
3141
+ if (el) el.classList.add("hidden");
3142
+ }
3143
+
3144
+ function updateWizardStep() {
3145
+ var steps = document.querySelectorAll(".ralph-step");
3146
+ for (var i = 0; i < steps.length; i++) {
3147
+ var stepNum = parseInt(steps[i].getAttribute("data-step"), 10);
3148
+ if (stepNum === wizardStep) {
3149
+ steps[i].classList.add("active");
3150
+ } else {
3151
+ steps[i].classList.remove("active");
3152
+ }
3153
+ }
3154
+ var dots = document.querySelectorAll(".ralph-dot");
3155
+ for (var j = 0; j < dots.length; j++) {
3156
+ var dotStep = parseInt(dots[j].getAttribute("data-step"), 10);
3157
+ dots[j].classList.remove("active", "done");
3158
+ if (dotStep === wizardStep) dots[j].classList.add("active");
3159
+ else if (dotStep < wizardStep) dots[j].classList.add("done");
3160
+ }
3161
+
3162
+ var backBtn = document.getElementById("ralph-wizard-back");
3163
+ var skipBtn = document.getElementById("ralph-wizard-skip");
3164
+ var nextBtn = document.getElementById("ralph-wizard-next");
3165
+ if (backBtn) backBtn.style.visibility = wizardStep === 1 ? "hidden" : "visible";
3166
+ if (skipBtn) skipBtn.style.display = "none";
3167
+ if (nextBtn) nextBtn.textContent = wizardStep === 2 ? "Launch" : "Get Started";
3168
+ }
3169
+
3170
+ function collectWizardData() {
3171
+ var nameEl = document.getElementById("ralph-name");
3172
+ var taskEl = document.getElementById("ralph-task");
3173
+ var iterEl = document.getElementById("ralph-max-iterations");
3174
+ wizardData.name = nameEl ? nameEl.value.replace(/[^a-zA-Z0-9_-]/g, "").trim() : "";
3175
+ wizardData.task = taskEl ? taskEl.value.trim() : "";
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;
3230
+ }
3231
+
3232
+ function wizardNext() {
3233
+ collectWizardData();
3234
+
3235
+ // Step 1: install clay-ralph skill if needed, otherwise just advance
3236
+ if (wizardStep === 1) {
3237
+ if (ralphSkillInstalled) {
3238
+ wizardStep++;
3239
+ updateWizardStep();
3240
+ return;
3241
+ }
3242
+ if (ralphSkillInstalling) return;
3243
+ ralphSkillInstalling = true;
3244
+ var nextBtn = document.getElementById("ralph-wizard-next");
3245
+ if (nextBtn) {
3246
+ nextBtn.disabled = true;
3247
+ nextBtn.textContent = "Installing...";
3248
+ }
3249
+ var statusEl = document.getElementById("ralph-install-status");
3250
+ if (statusEl) {
3251
+ statusEl.classList.remove("hidden");
3252
+ statusEl.innerHTML = '<div class="skills-spinner small"></div> Installing clay-ralph skill...';
3253
+ }
3254
+ fetch(basePath + "api/install-skill", {
3255
+ method: "POST",
3256
+ headers: { "Content-Type": "application/json" },
3257
+ body: JSON.stringify({ url: "https://github.com/chadbyte/clay-ralph", skill: "clay-ralph", scope: "global" }),
3258
+ })
3259
+ .then(function () {
3260
+ // Wait for skill_installed websocket message to advance
3261
+ })
3262
+ .catch(function () {
3263
+ ralphSkillInstalling = false;
3264
+ if (nextBtn) { nextBtn.disabled = false; nextBtn.textContent = "Get Started"; }
3265
+ if (statusEl) { statusEl.innerHTML = "Failed to install skill. Try again."; }
3266
+ });
3267
+ return;
3268
+ }
3269
+
3270
+ if (wizardStep === 2) {
3271
+ var nameEl = document.getElementById("ralph-name");
3272
+ var taskEl = document.getElementById("ralph-task");
3273
+ if (!wizardData.name) {
3274
+ if (nameEl) { nameEl.focus(); nameEl.style.borderColor = "#e74c3c"; setTimeout(function() { nameEl.style.borderColor = ""; }, 2000); }
3275
+ return;
3276
+ }
3277
+ if (!wizardData.task) {
3278
+ if (taskEl) { taskEl.focus(); taskEl.style.borderColor = "#e74c3c"; setTimeout(function() { taskEl.style.borderColor = ""; }, 2000); }
3279
+ return;
3280
+ }
3281
+ wizardSubmit();
3282
+ return;
3283
+ }
3284
+ wizardStep++;
3285
+ updateWizardStep();
3286
+ }
3287
+
3288
+ function wizardBack() {
3289
+ if (wizardStep > 1) {
3290
+ collectWizardData();
3291
+ wizardStep--;
3292
+ updateWizardStep();
3293
+ }
3294
+ }
3295
+
3296
+ function wizardSkip() {
3297
+ if (wizardStep < 2) {
3298
+ wizardStep++;
3299
+ updateWizardStep();
3300
+ }
3301
+ }
3302
+
3303
+ function wizardSubmit() {
3304
+ collectWizardData();
3305
+ closeRalphWizard();
3306
+ if (ws && ws.readyState === 1) {
3307
+ ws.send(JSON.stringify({ type: "ralph_wizard_complete", data: wizardData }));
3308
+ }
3309
+ }
3310
+
3311
+ // Wizard button listeners
3312
+ var wizardCloseBtn = document.getElementById("ralph-wizard-close");
3313
+ var wizardBackdrop = document.querySelector(".ralph-wizard-backdrop");
3314
+ var wizardBackBtn = document.getElementById("ralph-wizard-back");
3315
+ var wizardSkipBtn = document.getElementById("ralph-wizard-skip");
3316
+ var wizardNextBtn = document.getElementById("ralph-wizard-next");
3317
+
3318
+ if (wizardCloseBtn) wizardCloseBtn.addEventListener("click", closeRalphWizard);
3319
+ if (wizardBackdrop) wizardBackdrop.addEventListener("click", closeRalphWizard);
3320
+ if (wizardBackBtn) wizardBackBtn.addEventListener("click", wizardBack);
3321
+ if (wizardSkipBtn) wizardSkipBtn.addEventListener("click", wizardSkip);
3322
+ if (wizardNextBtn) wizardNextBtn.addEventListener("click", wizardNext);
3323
+
3324
+ // Enforce alphanumeric + hyphens + underscores on name input
3325
+ var wizardNameEl = document.getElementById("ralph-name");
3326
+ if (wizardNameEl) {
3327
+ wizardNameEl.addEventListener("input", function() {
3328
+ this.value = this.value.replace(/[^a-zA-Z0-9_-]/g, "");
3329
+ });
3330
+ }
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
+
3378
+ // --- Ralph Sticky (title-bar island) ---
3379
+ function showRalphCraftingBar(show) {
3380
+ var stickyEl = document.getElementById("ralph-sticky");
3381
+ if (!stickyEl) return;
3382
+ if (!show) {
3383
+ stickyEl.classList.add("hidden");
3384
+ stickyEl.innerHTML = "";
3385
+ return;
3386
+ }
3387
+ stickyEl.innerHTML =
3388
+ '<div class="ralph-sticky-inner">' +
3389
+ '<div class="ralph-sticky-header">' +
3390
+ '<span class="ralph-sticky-icon">' + iconHtml("repeat") + '</span>' +
3391
+ '<span class="ralph-sticky-label">Ralph</span>' +
3392
+ '<span class="ralph-sticky-status">' + iconHtml("loader", "icon-spin") + ' Preparing\u2026</span>' +
3393
+ '<button class="ralph-sticky-cancel" title="Cancel">' + iconHtml("x") + '</button>' +
3394
+ '</div>' +
3395
+ '</div>';
3396
+ stickyEl.classList.remove("hidden");
3397
+ refreshIcons();
3398
+
3399
+ var cancelBtn = stickyEl.querySelector(".ralph-sticky-cancel");
3400
+ if (cancelBtn) {
3401
+ cancelBtn.addEventListener("click", function(e) {
3402
+ e.stopPropagation();
3403
+ if (ws && ws.readyState === 1) {
3404
+ ws.send(JSON.stringify({ type: "ralph_cancel_crafting" }));
3405
+ }
3406
+ showRalphCraftingBar(false);
3407
+ showRalphApprovalBar(false);
3408
+ });
3409
+ }
3410
+ }
3411
+
3412
+ // --- Ralph Approval Bar (also uses sticky island) ---
3413
+ function showRalphApprovalBar(show) {
3414
+ var stickyEl = document.getElementById("ralph-sticky");
3415
+ if (!stickyEl) return;
3416
+ if (!show) {
3417
+ // Only clear if we're in approval mode (don't clobber crafting)
3418
+ if (ralphPhase !== "crafting") {
3419
+ stickyEl.classList.add("hidden");
3420
+ stickyEl.innerHTML = "";
3421
+ }
3422
+ return;
3423
+ }
3424
+
3425
+ stickyEl.innerHTML =
3426
+ '<div class="ralph-sticky-inner">' +
3427
+ '<div class="ralph-sticky-header" id="ralph-sticky-header">' +
3428
+ '<span class="ralph-sticky-icon">' + iconHtml("repeat") + '</span>' +
3429
+ '<span class="ralph-sticky-label">Ralph</span>' +
3430
+ '<span class="ralph-sticky-status" id="ralph-sticky-status">Ready</span>' +
3431
+ '<button class="ralph-sticky-action ralph-sticky-preview" title="Preview files">' + iconHtml("eye") + '</button>' +
3432
+ '<button class="ralph-sticky-action ralph-sticky-start" title="' + (wizardData.cron ? 'Schedule' : 'Start loop') + '">' + iconHtml(wizardData.cron ? "calendar-clock" : "play") + '</button>' +
3433
+ '<button class="ralph-sticky-action ralph-sticky-dismiss" title="Cancel and discard">' + iconHtml("x") + '</button>' +
3434
+ '</div>' +
3435
+ '</div>';
3436
+ stickyEl.classList.remove("hidden");
3437
+ refreshIcons();
3438
+
3439
+ stickyEl.querySelector(".ralph-sticky-preview").addEventListener("click", function(e) {
3440
+ e.stopPropagation();
3441
+ if (ws && ws.readyState === 1) {
3442
+ ws.send(JSON.stringify({ type: "ralph_preview_files" }));
3443
+ }
3444
+ });
3445
+
3446
+ stickyEl.querySelector(".ralph-sticky-start").addEventListener("click", function(e) {
3447
+ e.stopPropagation();
3448
+ // Check for uncommitted changes before starting
3449
+ fetch(basePath + "api/git-dirty")
3450
+ .then(function (res) { return res.json(); })
3451
+ .then(function (data) {
3452
+ if (data.dirty) {
3453
+ showConfirm("You have uncommitted changes. Ralph Loop uses git diff to track progress \u2014 uncommitted files may cause unexpected results.\n\nStart anyway?", function () {
3454
+ if (ws && ws.readyState === 1) {
3455
+ ws.send(JSON.stringify({ type: "loop_start" }));
3456
+ }
3457
+ stickyEl.classList.add("hidden");
3458
+ stickyEl.innerHTML = "";
3459
+ });
3460
+ } else {
3461
+ if (ws && ws.readyState === 1) {
3462
+ ws.send(JSON.stringify({ type: "loop_start" }));
3463
+ }
3464
+ stickyEl.classList.add("hidden");
3465
+ stickyEl.innerHTML = "";
3466
+ }
3467
+ })
3468
+ .catch(function () {
3469
+ // If check fails, just start
3470
+ if (ws && ws.readyState === 1) {
3471
+ ws.send(JSON.stringify({ type: "loop_start" }));
3472
+ }
3473
+ stickyEl.classList.add("hidden");
3474
+ stickyEl.innerHTML = "";
3475
+ });
3476
+ });
3477
+
3478
+ stickyEl.querySelector(".ralph-sticky-dismiss").addEventListener("click", function(e) {
3479
+ e.stopPropagation();
3480
+ showConfirm("Discard this Ralph Loop setup?", function() {
3481
+ if (ws && ws.readyState === 1) {
3482
+ ws.send(JSON.stringify({ type: "ralph_wizard_cancel" }));
3483
+ }
3484
+ stickyEl.classList.add("hidden");
3485
+ stickyEl.classList.remove("ralph-ready");
3486
+ stickyEl.innerHTML = "";
3487
+ });
3488
+ });
3489
+
3490
+ updateRalphApprovalStatus();
3491
+ }
3492
+
3493
+ function updateRalphApprovalStatus() {
3494
+ var stickyEl = document.getElementById("ralph-sticky");
3495
+ var statusEl = document.getElementById("ralph-sticky-status");
3496
+ var startBtn = document.querySelector(".ralph-sticky-start");
3497
+ if (!statusEl) return;
3498
+
3499
+ if (ralphFilesReady.bothReady) {
3500
+ statusEl.textContent = "Ready";
3501
+ if (startBtn) startBtn.disabled = false;
3502
+ if (stickyEl) stickyEl.classList.add("ralph-ready");
3503
+ } else if (ralphFilesReady.promptReady || ralphFilesReady.judgeReady) {
3504
+ statusEl.textContent = "Partial\u2026";
3505
+ if (startBtn) startBtn.disabled = true;
3506
+ if (stickyEl) stickyEl.classList.remove("ralph-ready");
3507
+ } else {
3508
+ statusEl.textContent = "Waiting\u2026";
3509
+ if (startBtn) startBtn.disabled = true;
3510
+ if (stickyEl) stickyEl.classList.remove("ralph-ready");
3511
+ }
3512
+ }
3513
+
3514
+ // --- Ralph Preview Modal ---
3515
+ function openRalphPreviewModal() {
3516
+ var modal = document.getElementById("ralph-preview-modal");
3517
+ if (!modal) return;
3518
+ modal.classList.remove("hidden");
3519
+ showRalphPreviewTab("prompt");
3520
+ }
3521
+
3522
+ function closeRalphPreviewModal() {
3523
+ var modal = document.getElementById("ralph-preview-modal");
3524
+ if (modal) modal.classList.add("hidden");
3525
+ }
3526
+
3527
+ function showRalphPreviewTab(tab) {
3528
+ var tabs = document.querySelectorAll(".ralph-tab");
3529
+ for (var i = 0; i < tabs.length; i++) {
3530
+ if (tabs[i].getAttribute("data-tab") === tab) {
3531
+ tabs[i].classList.add("active");
3532
+ } else {
3533
+ tabs[i].classList.remove("active");
3534
+ }
3535
+ }
3536
+ var body = document.getElementById("ralph-preview-body");
3537
+ if (!body) return;
3538
+ var content = tab === "prompt" ? ralphPreviewContent.prompt : ralphPreviewContent.judge;
3539
+ if (typeof marked !== "undefined" && marked.parse) {
3540
+ body.innerHTML = DOMPurify.sanitize(marked.parse(content));
3541
+ } else {
3542
+ body.textContent = content;
3543
+ }
3544
+ }
3545
+
3546
+ // Preview modal listeners
3547
+ var previewCloseBtn = document.getElementById("ralph-preview-close");
3548
+ if (previewCloseBtn) previewCloseBtn.addEventListener("click", closeRalphPreviewModal);
3549
+
3550
+ var previewBackdrop = document.querySelector("#ralph-preview-modal .confirm-backdrop");
3551
+ if (previewBackdrop) previewBackdrop.addEventListener("click", closeRalphPreviewModal);
3552
+
3553
+ var previewTabs = document.querySelectorAll(".ralph-tab");
3554
+ for (var ti = 0; ti < previewTabs.length; ti++) {
3555
+ previewTabs[ti].addEventListener("click", function() {
3556
+ showRalphPreviewTab(this.getAttribute("data-tab"));
3557
+ });
3558
+ }
3559
+
2721
3560
  // --- Skills ---
2722
3561
  initSkills({
2723
3562
  get ws() { return ws; },
@@ -2727,6 +3566,15 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2727
3566
  sendTerminalCommand: function (cmd) { sendTerminalCommand(cmd); },
2728
3567
  });
2729
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
+
2730
3578
  // --- Remove project ---
2731
3579
  function confirmRemoveProject(slug, name) {
2732
3580
  showConfirm("Remove project \"" + name + "\"?", function () {