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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/public/app.js CHANGED
@@ -25,6 +25,7 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
25
25
  import { initSessionSearch, toggleSearch, closeSearch, isSearchOpen, handleFindInSessionResults, onHistoryPrepended as onSessionSearchHistoryPrepended } from './modules/session-search.js';
26
26
  import { initTooltips, registerTooltip } from './modules/tooltip.js';
27
27
  import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } from './modules/mate-wizard.js';
28
+ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
28
29
 
29
30
  // --- Base path for multi-project routing ---
30
31
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -1133,6 +1134,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
1133
1134
  isProcessing: p.isProcessing,
1134
1135
  onlineUsers: p.onlineUsers || [],
1135
1136
  unread: p.unread || 0,
1137
+ pendingPermissions: p.pendingPermissions || 0,
1136
1138
  isWorktree: p.isWorktree || false,
1137
1139
  parentSlug: p.parentSlug || null,
1138
1140
  branch: p.branch || null,
@@ -1245,7 +1247,9 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
1245
1247
  var loopMaxIterations = 0;
1246
1248
  var ralphPhase = "idle"; // idle | wizard | crafting | approval | executing | done
1247
1249
  var ralphCraftingSessionId = null;
1250
+ var ralphCraftingSource = null; // "ralph" or null (task)
1248
1251
  var wizardStep = 1;
1252
+ var wizardSource = "ralph"; // "ralph" or "task"
1249
1253
  var wizardData = { name: "", task: "", maxIterations: 3, cron: null };
1250
1254
  var ralphFilesReady = { promptReady: false, judgeReady: false, bothReady: false };
1251
1255
  var ralphPreviewContent = { prompt: "", judge: "" };
@@ -1473,7 +1477,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
1473
1477
  switchProject: function (slug) { switchProject(slug); },
1474
1478
  openTerminal: function () { openTerminal(); },
1475
1479
  showHomeHub: function () { showHomeHub(); },
1476
- openRalphWizard: function () { openRalphWizard(); },
1480
+ openRalphWizard: function (source) { openRalphWizard(source); },
1477
1481
  getUpcomingSchedules: getUpcomingSchedules,
1478
1482
  get multiUser() { return isMultiUserMode; },
1479
1483
  get myUserId() { return myUserId; },
@@ -1484,6 +1488,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
1484
1488
  sendWs: function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
1485
1489
  onDmRemoveUser: function (userId) { dmRemovedUsers[userId] = true; },
1486
1490
  getHistoryFrom: function () { return historyFrom; },
1491
+ get permissions() { return myPermissions; },
1487
1492
  };
1488
1493
  initSidebar(sidebarCtx);
1489
1494
  initIconStrip(sidebarCtx);
@@ -1494,6 +1499,32 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
1494
1499
  function (mate) { handleMateCreatedInApp(mate); }
1495
1500
  );
1496
1501
 
1502
+ initCommandPalette({
1503
+ switchProject: function (slug) { switchProject(slug); },
1504
+ currentSlug: function () { return currentSlug; },
1505
+ projectList: function () { return cachedProjects || []; },
1506
+ matesList: function () { return cachedMatesList || []; },
1507
+ allUsers: function () { return cachedAllUsers || []; },
1508
+ dmConversations: function () { return cachedDmConversations || []; },
1509
+ myUserId: function () { return myUserId; },
1510
+ selectSession: function (id) {
1511
+ if (ws && ws.readyState === 1) {
1512
+ ws.send(JSON.stringify({ type: "switch_session", id: id }));
1513
+ }
1514
+ },
1515
+ openDm: function (userId) { openDm(userId); },
1516
+ runAction: function (action) {
1517
+ switch (action) {
1518
+ case "createMate": openMateWizard(); break;
1519
+ case "openSettings":
1520
+ var sb = document.getElementById("server-settings-btn");
1521
+ if (sb) sb.click();
1522
+ break;
1523
+ case "showHome": showHomeHub(); break;
1524
+ }
1525
+ },
1526
+ });
1527
+
1497
1528
  // --- Connect overlay (animated ASCII logo) ---
1498
1529
  var asciiLogoCanvas = $("ascii-logo-canvas");
1499
1530
  initAsciiLogo(asciiLogoCanvas);
@@ -3109,7 +3140,9 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
3109
3140
  disconnectNotifTimer = null;
3110
3141
  }
3111
3142
  // Only show "restored" notification if "lost" was actually shown
3112
- if (wasConnected && disconnectNotifShown && !document.hasFocus() && "serviceWorker" in navigator) {
3143
+ var isMobileDevice = /Mobi|Android|iPad|iPhone|iPod/.test(navigator.userAgent) ||
3144
+ (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
3145
+ if (wasConnected && disconnectNotifShown && !isMobileDevice && isNotifAlertEnabled() && !document.hasFocus() && "serviceWorker" in navigator) {
3113
3146
  navigator.serviceWorker.ready.then(function (reg) {
3114
3147
  reg.showNotification("Clay", {
3115
3148
  body: "Server connection restored",
@@ -3159,7 +3192,9 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
3159
3192
  disconnectNotifTimer = setTimeout(function () {
3160
3193
  disconnectNotifTimer = null;
3161
3194
  disconnectNotifShown = true;
3162
- if (!document.hasFocus() && "serviceWorker" in navigator) {
3195
+ var isMobileDevice = /Mobi|Android|iPad|iPhone|iPod/.test(navigator.userAgent) ||
3196
+ (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
3197
+ if (!isMobileDevice && isNotifAlertEnabled() && !document.hasFocus() && "serviceWorker" in navigator) {
3163
3198
  navigator.serviceWorker.ready.then(function (reg) {
3164
3199
  reg.showNotification("Clay", {
3165
3200
  body: "Server connection lost",
@@ -3260,8 +3295,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
3260
3295
  if (tbProjectName) tbProjectName.textContent = msg.title || projectName;
3261
3296
  updatePageTitle();
3262
3297
  if (msg.version) {
3263
- var vEl = $("footer-version");
3264
- if (vEl) vEl.textContent = "v" + msg.version;
3298
+ setPaletteVersion(msg.version);
3265
3299
  }
3266
3300
  if (msg.projectOwnerId !== undefined) currentProjectOwnerId = msg.projectOwnerId;
3267
3301
  if (msg.osUsers !== undefined) isOsUsers = !!msg.osUsers;
@@ -3429,6 +3463,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
3429
3463
 
3430
3464
  case "session_list":
3431
3465
  renderSessionList(msg.sessions || []);
3466
+ handlePaletteSessionSwitch();
3432
3467
  break;
3433
3468
 
3434
3469
  case "session_presence":
@@ -4168,6 +4203,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4168
4203
  case "ralph_phase":
4169
4204
  ralphPhase = msg.phase || "idle";
4170
4205
  if (msg.craftingSessionId) ralphCraftingSessionId = msg.craftingSessionId;
4206
+ if (msg.source !== undefined) ralphCraftingSource = msg.source;
4171
4207
  updateLoopButton();
4172
4208
  updateRalphBars();
4173
4209
  break;
@@ -4175,6 +4211,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4175
4211
  case "ralph_crafting_started":
4176
4212
  ralphPhase = "crafting";
4177
4213
  ralphCraftingSessionId = msg.sessionId || activeSessionId;
4214
+ ralphCraftingSource = msg.source || null;
4178
4215
  updateLoopButton();
4179
4216
  updateRalphBars();
4180
4217
  if (msg.source !== "ralph") {
@@ -4192,7 +4229,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4192
4229
  };
4193
4230
  if (msg.bothReady && (ralphPhase === "crafting" || ralphPhase === "approval")) {
4194
4231
  ralphPhase = "approval";
4195
- if (isSchedulerOpen()) {
4232
+ if (ralphCraftingSource !== "ralph" || isSchedulerOpen()) {
4196
4233
  // Task crafting in scheduler: switch from crafting chat to detail view showing files
4197
4234
  exitCraftingMode(msg.taskId);
4198
4235
  } else {
@@ -4495,11 +4532,40 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4495
4532
  initAdmin({
4496
4533
  get projectList() { return cachedProjects; },
4497
4534
  });
4535
+ var myPermissions = null; // null = single-user, all allowed
4498
4536
  fetch("/api/me").then(function (r) { return r.json(); }).then(function (d) {
4499
4537
  if (d.multiUser) isMultiUserMode = true;
4500
4538
  if (d.user && d.user.id) myUserId = d.user.id;
4539
+ if (d.permissions) myPermissions = d.permissions;
4501
4540
  if (d.mustChangePin) showForceChangePinOverlay();
4502
4541
  initCursorToggle();
4542
+ // Apply RBAC UI gating
4543
+ if (myPermissions) {
4544
+ if (!myPermissions.terminal) {
4545
+ var termBtn = document.getElementById("terminal-toggle-btn");
4546
+ if (termBtn) termBtn.style.display = "none";
4547
+ var termSideBtn = document.getElementById("terminal-sidebar-btn");
4548
+ if (termSideBtn) termSideBtn.style.display = "none";
4549
+ }
4550
+ if (!myPermissions.fileBrowser) {
4551
+ var fbBtn = document.getElementById("file-browser-btn");
4552
+ if (fbBtn) fbBtn.style.display = "none";
4553
+ }
4554
+ if (!myPermissions.skills) {
4555
+ var sBtn = document.getElementById("skills-btn");
4556
+ if (sBtn) sBtn.style.display = "none";
4557
+ var msBtn = document.getElementById("mate-skills-btn");
4558
+ if (msBtn) msBtn.style.display = "none";
4559
+ }
4560
+ if (!myPermissions.scheduledTasks) {
4561
+ var schBtn = document.getElementById("scheduler-btn");
4562
+ if (schBtn) schBtn.style.display = "none";
4563
+ }
4564
+ if (!myPermissions.createProject) {
4565
+ var addProjBtn = document.getElementById("icon-strip-add");
4566
+ if (addProjBtn) addProjBtn.style.display = "none";
4567
+ }
4568
+ }
4503
4569
  }).catch(function () {});
4504
4570
  // Hide server settings and update controls for non-admin users in multi-user mode
4505
4571
  checkAdminAccess().then(function (isAdmin) {
@@ -4799,19 +4865,28 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4799
4865
  }
4800
4866
 
4801
4867
  function updateRalphBars() {
4868
+ // Task source uses the scheduler panel, not the sticky bar
4869
+ var isTaskSource = ralphCraftingSource !== "ralph";
4802
4870
  var onCraftingSession = ralphCraftingSessionId && activeSessionId === ralphCraftingSessionId;
4803
4871
  // If approval phase but no craftingSessionId (recovered after server restart), show bar anyway
4804
4872
  var recoveredApproval = ralphPhase === "approval" && !ralphCraftingSessionId;
4805
- if (ralphPhase === "crafting" && onCraftingSession) {
4873
+ if (!isTaskSource && ralphPhase === "crafting" && onCraftingSession) {
4806
4874
  showRalphCraftingBar(true);
4807
4875
  } else {
4808
4876
  showRalphCraftingBar(false);
4809
4877
  }
4810
- if (ralphPhase === "approval" && (onCraftingSession || recoveredApproval)) {
4878
+ if (!isTaskSource && ralphPhase === "approval" && (onCraftingSession || recoveredApproval)) {
4811
4879
  showRalphApprovalBar(true);
4812
4880
  } else {
4813
4881
  showRalphApprovalBar(false);
4814
4882
  }
4883
+ // Restore running loop banner on session switch
4884
+ if (loopActive && ralphPhase === "executing") {
4885
+ showLoopBanner(true);
4886
+ if (loopIteration > 0) {
4887
+ updateLoopBanner(loopIteration, loopMaxIterations, "running");
4888
+ }
4889
+ }
4815
4890
  }
4816
4891
 
4817
4892
  // --- Skill install dialog (generic) ---
@@ -4829,18 +4904,35 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4829
4904
  var knownInstalledSkills = {}; // client-side cache of installed skills
4830
4905
 
4831
4906
  function renderSkillInstallDialog(opts, missing) {
4832
- skillInstallTitle.textContent = opts.title || "Skill Installation Required";
4833
- skillInstallReason.textContent = opts.reason || "";
4907
+ var hasOutdated = false;
4908
+ var hasMissing = false;
4909
+ for (var c = 0; c < missing.length; c++) {
4910
+ if (missing[c].status === "outdated") hasOutdated = true;
4911
+ else hasMissing = true;
4912
+ }
4913
+ var defaultTitle = hasMissing ? "Skill Installation Required" : "Skill Update Available";
4914
+ var defaultReason = hasMissing
4915
+ ? "This feature requires the following skill(s) to be installed."
4916
+ : "Newer versions of the following skill(s) are available.";
4917
+ if (hasMissing && hasOutdated) {
4918
+ defaultTitle = "Skill Installation / Update Required";
4919
+ defaultReason = "Some skills need to be installed or updated.";
4920
+ }
4921
+ skillInstallTitle.textContent = opts.title || defaultTitle;
4922
+ skillInstallReason.textContent = opts.reason || defaultReason;
4834
4923
  skillInstallList.innerHTML = "";
4835
4924
  for (var i = 0; i < missing.length; i++) {
4836
4925
  var s = missing[i];
4926
+ var badge = s.status === "outdated"
4927
+ ? '<span class="skill-badge skill-badge-update">Update ' + escapeHtml(s.installedVersion || "") + ' → ' + escapeHtml(s.remoteVersion || "") + '</span>'
4928
+ : '<span class="skill-badge skill-badge-new">New</span>';
4837
4929
  var item = document.createElement("div");
4838
4930
  item.className = "skill-install-item";
4839
4931
  item.setAttribute("data-skill", s.name);
4840
4932
  item.innerHTML = '<span class="skill-icon">&#x1f9e9;</span>' +
4841
4933
  '<div class="skill-info">' +
4842
4934
  '<span class="skill-name">' + escapeHtml(s.name) + '</span>' +
4843
- '<span class="skill-scope">' + escapeHtml(s.scope || "global") + '</span>' +
4935
+ badge +
4844
4936
  '</div>' +
4845
4937
  '<span class="skill-status"></span>';
4846
4938
  skillInstallList.appendChild(item);
@@ -4848,7 +4940,9 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4848
4940
  skillInstallStatus.classList.add("hidden");
4849
4941
  skillInstallStatus.innerHTML = "";
4850
4942
  skillInstallOk.disabled = false;
4851
- skillInstallOk.textContent = "Install";
4943
+ var btnLabel = hasMissing ? "Install" : "Update";
4944
+ if (hasMissing && hasOutdated) btnLabel = "Install / Update";
4945
+ skillInstallOk.textContent = btnLabel;
4852
4946
  skillInstallOk.className = "confirm-btn confirm-delete";
4853
4947
  skillInstallModal.classList.remove("hidden");
4854
4948
  }
@@ -4899,7 +4993,12 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4899
4993
  });
4900
4994
 
4901
4995
  function updateSkillInstallProgress(done, total) {
4902
- skillInstallStatus.innerHTML = '<div class="skills-spinner small"></div> Installing skills... (' + done + '/' + total + ')';
4996
+ var hasUpdates = false;
4997
+ for (var u = 0; u < pendingSkillInstalls.length; u++) {
4998
+ if (pendingSkillInstalls[u].status === "outdated") { hasUpdates = true; break; }
4999
+ }
5000
+ var label = hasUpdates ? "Updating" : "Installing";
5001
+ skillInstallStatus.innerHTML = '<div class="skills-spinner small"></div> ' + label + ' skills... (' + done + '/' + total + ')';
4903
5002
  }
4904
5003
 
4905
5004
  function updateSkillListItems() {
@@ -4949,7 +5048,12 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4949
5048
 
4950
5049
  if (doneCount === totalCount) {
4951
5050
  skillInstallDone = true;
4952
- skillInstallStatus.innerHTML = '<span class="skill-status-ok">' + iconHtml("circle-check") + '</span> All skills installed successfully.';
5051
+ var hasUpdates = false;
5052
+ for (var u = 0; u < pendingSkillInstalls.length; u++) {
5053
+ if (pendingSkillInstalls[u].status === "outdated") { hasUpdates = true; break; }
5054
+ }
5055
+ var doneMsg = hasUpdates ? "All skills updated successfully." : "All skills installed successfully.";
5056
+ skillInstallStatus.innerHTML = '<span class="skill-status-ok">' + iconHtml("circle-check") + '</span> ' + doneMsg;
4953
5057
  refreshIcons();
4954
5058
  skillInstallOk.disabled = false;
4955
5059
  skillInstallOk.textContent = "Proceed";
@@ -4958,21 +5062,39 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4958
5062
  }
4959
5063
 
4960
5064
  function requireSkills(opts, cb) {
4961
- fetch(basePath + "api/installed-skills")
5065
+ fetch(basePath + "api/check-skill-updates", {
5066
+ method: "POST",
5067
+ headers: { "Content-Type": "application/json" },
5068
+ body: JSON.stringify({ skills: opts.skills }),
5069
+ })
4962
5070
  .then(function (res) { return res.json(); })
4963
5071
  .then(function (data) {
4964
- var installed = data.installed || {};
4965
- var missing = [];
4966
- for (var i = 0; i < opts.skills.length; i++) {
4967
- var sName = opts.skills[i].name;
4968
- if (!installed[sName] && !knownInstalledSkills[sName]) {
4969
- missing.push({ name: sName, url: opts.skills[i].url, scope: opts.skills[i].scope || "global", installed: false });
5072
+ var results = data.results || [];
5073
+ var actionable = [];
5074
+ for (var i = 0; i < results.length; i++) {
5075
+ var r = results[i];
5076
+ if (r.status === "missing" || r.status === "outdated") {
5077
+ // Find the original skill entry for url/scope
5078
+ var orig = null;
5079
+ for (var j = 0; j < opts.skills.length; j++) {
5080
+ if (opts.skills[j].name === r.name) { orig = opts.skills[j]; break; }
5081
+ }
5082
+ if (!orig) continue;
5083
+ actionable.push({
5084
+ name: r.name,
5085
+ url: orig.url,
5086
+ scope: orig.scope || "global",
5087
+ installed: false,
5088
+ status: r.status,
5089
+ installedVersion: r.installedVersion,
5090
+ remoteVersion: r.remoteVersion,
5091
+ });
4970
5092
  }
4971
5093
  }
4972
- if (missing.length === 0) { cb(); return; }
4973
- pendingSkillInstalls = missing;
5094
+ if (actionable.length === 0) { cb(); return; }
5095
+ pendingSkillInstalls = actionable;
4974
5096
  skillInstallCallback = cb;
4975
- renderSkillInstallDialog(opts, missing);
5097
+ renderSkillInstallDialog(opts, actionable);
4976
5098
  })
4977
5099
  .catch(function () { cb(); });
4978
5100
  }
@@ -4995,18 +5117,56 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
4995
5117
 
4996
5118
  // --- Ralph Wizard ---
4997
5119
 
4998
- function openRalphWizard() {
5120
+ var wizardMode = "draft"; // "draft" or "own"
5121
+
5122
+ function openRalphWizard(source) {
4999
5123
  requireClayRalph(function () {
5124
+ wizardSource = source || "ralph";
5000
5125
  wizardData = { name: "", task: "", maxIterations: 3 };
5001
5126
  var el = document.getElementById("ralph-wizard");
5002
5127
  if (!el) return;
5003
5128
 
5004
5129
  var taskEl = document.getElementById("ralph-task");
5005
5130
  if (taskEl) taskEl.value = "";
5131
+ var promptInput = document.getElementById("ralph-prompt-input");
5132
+ if (promptInput) promptInput.value = "";
5133
+ var judgeInput = document.getElementById("ralph-judge-input");
5134
+ if (judgeInput) judgeInput.value = "";
5006
5135
  var iterEl = document.getElementById("ralph-max-iterations");
5007
5136
  if (iterEl) iterEl.value = "25";
5008
5137
 
5009
- wizardStep = 1;
5138
+ // Update text based on source
5139
+ var isTask = wizardSource === "task";
5140
+ var headerSpan = el.querySelector(".ralph-wizard-header > span");
5141
+ if (headerSpan) headerSpan.textContent = isTask ? "New Task" : "New Ralph Loop";
5142
+
5143
+ var step2heading = el.querySelector('.ralph-step[data-step="2"] h3');
5144
+ if (step2heading) step2heading.textContent = isTask ? "Describe your task" : "What do you want to build?";
5145
+
5146
+ var draftHint = el.querySelector('.ralph-mode-panel[data-mode="draft"] .ralph-hint');
5147
+ if (draftHint) draftHint.textContent = isTask
5148
+ ? "Describe what you want done. Clay will craft a precise prompt and you can review it before scheduling."
5149
+ : "Write a rough idea, Clay will refine it into detailed instructions. You can review and edit everything before the loop starts.";
5150
+
5151
+ var ownHint = el.querySelector('.ralph-mode-panel[data-mode="own"] .ralph-hint');
5152
+ if (ownHint) ownHint.textContent = isTask
5153
+ ? "Paste the prompt to run. It will execute as-is when triggered."
5154
+ : "Paste your PROMPT.md content. JUDGE.md is optional; if omitted, Clay will generate it for you.";
5155
+
5156
+ // Update task description placeholder
5157
+ if (taskEl) taskEl.placeholder = isTask
5158
+ ? "e.g. Check for dependency updates and create a summary"
5159
+ : "e.g. Add dark mode toggle to the settings page";
5160
+
5161
+ wizardMode = "draft";
5162
+ updateWizardModeTabs();
5163
+
5164
+ if (wizardSource === "task") {
5165
+ // Tasks skip step 1 (Ralph intro), go directly to step 2
5166
+ wizardStep = 2;
5167
+ } else {
5168
+ wizardStep = 1;
5169
+ }
5010
5170
  el.classList.remove("hidden");
5011
5171
  var statusEl = document.getElementById("ralph-install-status");
5012
5172
  if (statusEl) { statusEl.classList.add("hidden"); statusEl.innerHTML = ""; }
@@ -5014,6 +5174,25 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
5014
5174
  });
5015
5175
  }
5016
5176
 
5177
+ function updateWizardModeTabs() {
5178
+ var tabs = document.querySelectorAll(".ralph-mode-tab");
5179
+ var panels = document.querySelectorAll(".ralph-mode-panel");
5180
+ for (var i = 0; i < tabs.length; i++) {
5181
+ if (tabs[i].getAttribute("data-mode") === wizardMode) {
5182
+ tabs[i].classList.add("active");
5183
+ } else {
5184
+ tabs[i].classList.remove("active");
5185
+ }
5186
+ }
5187
+ for (var j = 0; j < panels.length; j++) {
5188
+ if (panels[j].getAttribute("data-mode") === wizardMode) {
5189
+ panels[j].classList.add("active");
5190
+ } else {
5191
+ panels[j].classList.remove("active");
5192
+ }
5193
+ }
5194
+ }
5195
+
5017
5196
  function closeRalphWizard() {
5018
5197
  var el = document.getElementById("ralph-wizard");
5019
5198
  if (el) el.classList.add("hidden");
@@ -5040,18 +5219,33 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
5040
5219
  var backBtn = document.getElementById("ralph-wizard-back");
5041
5220
  var skipBtn = document.getElementById("ralph-wizard-skip");
5042
5221
  var nextBtn = document.getElementById("ralph-wizard-next");
5043
- if (backBtn) backBtn.style.visibility = wizardStep === 1 ? "hidden" : "visible";
5222
+ if (backBtn) {
5223
+ backBtn.style.visibility = (wizardStep === 1 && wizardSource !== "task") ? "hidden" : "visible";
5224
+ backBtn.textContent = (wizardSource === "task" && wizardStep <= 2) ? "Cancel" : "Back";
5225
+ }
5044
5226
  if (skipBtn) skipBtn.style.display = "none";
5045
5227
  if (nextBtn) nextBtn.textContent = wizardStep === 2 ? "Launch" : "Get Started";
5046
5228
  }
5047
5229
 
5048
5230
  function collectWizardData() {
5049
- var taskEl = document.getElementById("ralph-task");
5050
5231
  var iterEl = document.getElementById("ralph-max-iterations");
5051
5232
  wizardData.name = "";
5052
- wizardData.task = taskEl ? taskEl.value.trim() : "";
5053
5233
  wizardData.maxIterations = iterEl ? parseInt(iterEl.value, 10) || 3 : 3;
5054
5234
  wizardData.cron = null;
5235
+ wizardData.mode = wizardMode;
5236
+
5237
+ if (wizardMode === "draft") {
5238
+ var taskEl = document.getElementById("ralph-task");
5239
+ wizardData.task = taskEl ? taskEl.value.trim() : "";
5240
+ wizardData.promptText = null;
5241
+ wizardData.judgeText = null;
5242
+ } else {
5243
+ var promptInput = document.getElementById("ralph-prompt-input");
5244
+ var judgeInput = document.getElementById("ralph-judge-input");
5245
+ wizardData.task = "";
5246
+ wizardData.promptText = promptInput ? promptInput.value.trim() : "";
5247
+ wizardData.judgeText = judgeInput ? judgeInput.value.trim() : "";
5248
+ }
5055
5249
  }
5056
5250
 
5057
5251
  function buildWizardCron() {
@@ -5116,10 +5310,18 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
5116
5310
  }
5117
5311
 
5118
5312
  if (wizardStep === 2) {
5119
- var taskEl = document.getElementById("ralph-task");
5120
- if (!wizardData.task) {
5121
- if (taskEl) { taskEl.focus(); taskEl.style.borderColor = "#e74c3c"; setTimeout(function() { taskEl.style.borderColor = ""; }, 2000); }
5122
- return;
5313
+ if (wizardMode === "draft") {
5314
+ var taskEl = document.getElementById("ralph-task");
5315
+ if (!wizardData.task) {
5316
+ if (taskEl) { taskEl.focus(); taskEl.style.borderColor = "#e74c3c"; setTimeout(function() { taskEl.style.borderColor = ""; }, 2000); }
5317
+ return;
5318
+ }
5319
+ } else {
5320
+ var promptInput = document.getElementById("ralph-prompt-input");
5321
+ if (!wizardData.promptText) {
5322
+ if (promptInput) { promptInput.focus(); promptInput.style.borderColor = "#e74c3c"; setTimeout(function() { promptInput.style.borderColor = ""; }, 2000); }
5323
+ return;
5324
+ }
5123
5325
  }
5124
5326
  wizardSubmit();
5125
5327
  return;
@@ -5129,6 +5331,10 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
5129
5331
  }
5130
5332
 
5131
5333
  function wizardBack() {
5334
+ if (wizardSource === "task" && wizardStep <= 2) {
5335
+ closeRalphWizard();
5336
+ return;
5337
+ }
5132
5338
  if (wizardStep > 1) {
5133
5339
  collectWizardData();
5134
5340
  wizardStep--;
@@ -5145,6 +5351,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
5145
5351
 
5146
5352
  function wizardSubmit() {
5147
5353
  collectWizardData();
5354
+ wizardData.source = wizardSource === "task" ? "task" : undefined;
5148
5355
  closeRalphWizard();
5149
5356
  if (ws && ws.readyState === 1) {
5150
5357
  ws.send(JSON.stringify({ type: "ralph_wizard_complete", data: wizardData }));
@@ -5164,6 +5371,15 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
5164
5371
  if (wizardSkipBtn) wizardSkipBtn.addEventListener("click", wizardSkip);
5165
5372
  if (wizardNextBtn) wizardNextBtn.addEventListener("click", wizardNext);
5166
5373
 
5374
+ // Mode tab switching
5375
+ var modeTabs = document.querySelectorAll(".ralph-mode-tab");
5376
+ for (var mt = 0; mt < modeTabs.length; mt++) {
5377
+ modeTabs[mt].addEventListener("click", function () {
5378
+ wizardMode = this.getAttribute("data-mode");
5379
+ updateWizardModeTabs();
5380
+ });
5381
+ }
5382
+
5167
5383
  // --- Repeat picker handlers ---
5168
5384
  var repeatSelect = document.getElementById("ralph-repeat");
5169
5385
  var repeatTimeRow = document.getElementById("ralph-time-row");
@@ -5450,7 +5666,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
5450
5666
  get activeSessionId() { return activeSessionId; },
5451
5667
  basePath: basePath,
5452
5668
  currentSlug: currentSlug,
5453
- openRalphWizard: function () { openRalphWizard(); },
5669
+ openRalphWizard: function (source) { openRalphWizard(source); },
5454
5670
  requireClayRalph: function (cb) { requireClayRalph(cb); },
5455
5671
  getProjects: function () { return cachedProjects; },
5456
5672
  });
@@ -663,3 +663,74 @@
663
663
  width: 12px;
664
664
  height: 12px;
665
665
  }
666
+
667
+ /* --- Permissions modal --- */
668
+ .admin-perms-list {
669
+ display: flex;
670
+ flex-direction: column;
671
+ gap: 2px;
672
+ }
673
+
674
+ .admin-perm-row {
675
+ display: flex;
676
+ align-items: center;
677
+ justify-content: space-between;
678
+ padding: 10px 12px;
679
+ border-radius: 8px;
680
+ cursor: pointer;
681
+ transition: background 0.15s;
682
+ }
683
+
684
+ .admin-perm-row:hover {
685
+ background: rgba(var(--overlay-rgb), 0.06);
686
+ }
687
+
688
+ .admin-perm-info {
689
+ display: flex;
690
+ flex-direction: column;
691
+ gap: 2px;
692
+ }
693
+
694
+ .admin-perm-label {
695
+ font-size: 14px;
696
+ font-weight: 600;
697
+ color: var(--text);
698
+ }
699
+
700
+ .admin-perm-desc {
701
+ font-size: 12px;
702
+ color: var(--text-muted);
703
+ }
704
+
705
+ .admin-perm-toggle {
706
+ width: 36px;
707
+ height: 20px;
708
+ appearance: none;
709
+ -webkit-appearance: none;
710
+ background: var(--border);
711
+ border-radius: 10px;
712
+ position: relative;
713
+ cursor: pointer;
714
+ transition: background 0.2s;
715
+ flex-shrink: 0;
716
+ }
717
+
718
+ .admin-perm-toggle::before {
719
+ content: "";
720
+ position: absolute;
721
+ top: 2px;
722
+ left: 2px;
723
+ width: 16px;
724
+ height: 16px;
725
+ border-radius: 50%;
726
+ background: #fff;
727
+ transition: transform 0.2s;
728
+ }
729
+
730
+ .admin-perm-toggle:checked {
731
+ background: var(--accent);
732
+ }
733
+
734
+ .admin-perm-toggle:checked::before {
735
+ transform: translateX(16px);
736
+ }