clay-server 2.9.0 → 2.9.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/pages.js CHANGED
@@ -22,11 +22,10 @@ function pinPageHtml() {
22
22
  'if(d.ok){location.reload();return}' +
23
23
  'if(d.locked){for(var i=0;i<boxes.length;i++)boxes[i].disabled=true;' +
24
24
  'err.textContent="Too many attempts. Try again in "+Math.ceil(d.retryAfter/60)+" min";' +
25
- 'setTimeout(function(){for(var i=0;i<boxes.length;i++){boxes[i].disabled=false;boxes[i].value="";boxes[i].classList.remove("filled")}' +
26
- 'document.getElementById("pin").value="";err.textContent="";boxes[0].focus()},d.retryAfter*1000);return}' +
25
+ 'setTimeout(function(){for(var i=0;i<boxes.length;i++)boxes[i].disabled=false;' +
26
+ 'err.textContent="";resetPinBoxes()},d.retryAfter*1000);return}' +
27
27
  'var msg="Wrong PIN";if(typeof d.attemptsLeft==="number"&&d.attemptsLeft<=3)msg+=" ("+d.attemptsLeft+" left)";' +
28
- 'err.textContent=msg;for(var i=0;i<boxes.length;i++){boxes[i].value="";boxes[i].classList.remove("filled")}' +
29
- 'document.getElementById("pin").value="";boxes[0].focus()})' +
28
+ 'err.textContent=msg;resetPinBoxes()})' +
30
29
  '.catch(function(){err.textContent="Connection error"})}' +
31
30
  'initPinBoxes("pin-boxes","pin",submitPin);' +
32
31
  '</script></div></body></html>';
@@ -727,9 +726,9 @@ var authPageStyles =
727
726
  // PIN digit boxes
728
727
  '.pin-wrap{display:flex;gap:8px;justify-content:center}' +
729
728
  '.pin-digit{width:44px;height:56px;background:var(--input-bg);border:1.5px solid var(--border);border-radius:8px;' +
730
- 'color:var(--accent);font-family:"Courier New",Courier,"Roboto Mono",monospace;font-size:28px;font-weight:700;' +
729
+ 'color:var(--text);font-family:"Courier New",Courier,"Roboto Mono",monospace;font-size:28px;font-weight:700;' +
731
730
  'text-align:center;line-height:56px;outline:none;caret-color:transparent;' +
732
- '-webkit-text-security:disc;transition:border-color 0.15s,box-shadow 0.15s}' +
731
+ 'transition:border-color 0.15s,box-shadow 0.15s}' +
733
732
  '.pin-digit:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-20)}' +
734
733
  '.pin-digit.filled{color:var(--text)}' +
735
734
  // Legacy single-input fallback
@@ -754,28 +753,30 @@ var pinBoxScript =
754
753
  'function initPinBoxes(cId,hId,onComplete){' +
755
754
  'var wrap=document.getElementById(cId),hidden=document.getElementById(hId);' +
756
755
  'var boxes=wrap.querySelectorAll(".pin-digit");' +
756
+ 'var digits=["","","","","",""];' +
757
757
  'boxes[0].focus();' +
758
+ 'function setDigit(idx,v){digits[idx]=v;boxes[idx].value=v?"\\u2022":"";boxes[idx].classList.toggle("filled",v.length>0);syncHidden()}' +
758
759
  'for(var i=0;i<boxes.length;i++){(function(idx){' +
759
760
  'boxes[idx].addEventListener("input",function(e){' +
760
- 'var v=this.value.replace(/[^0-9]/g,"");' +
761
- 'if(v.length>1)v=v.charAt(v.length-1);' +
762
- 'this.value=v;' +
763
- 'this.classList.toggle("filled",v.length>0);' +
764
- 'syncHidden();' +
761
+ 'var raw=this.value.replace(/[^0-9]/g,"");' +
762
+ 'if(!raw){setDigit(idx,"");return}' +
763
+ 'var v=raw.charAt(raw.length-1);' +
764
+ 'setDigit(idx,v);' +
765
765
  'if(v&&idx<5)boxes[idx+1].focus();' +
766
766
  'if(hidden.value.length===6&&onComplete)onComplete()});' +
767
767
  'boxes[idx].addEventListener("keydown",function(e){' +
768
- 'if(e.key==="Backspace"&&!this.value&&idx>0){boxes[idx-1].focus();boxes[idx-1].value="";boxes[idx-1].classList.remove("filled");syncHidden()}' +
768
+ 'if(e.key==="Backspace"){if(!digits[idx]&&idx>0){setDigit(idx-1,"");boxes[idx-1].focus()}else{setDigit(idx,"")}syncHidden();return}' +
769
769
  'if(e.key==="ArrowLeft"&&idx>0)boxes[idx-1].focus();' +
770
770
  'if(e.key==="ArrowRight"&&idx<5)boxes[idx+1].focus();' +
771
771
  'if(e.key==="Enter"&&hidden.value.length===6&&onComplete){e.preventDefault();onComplete()}});' +
772
772
  'boxes[idx].addEventListener("paste",function(e){' +
773
773
  'e.preventDefault();var d=(e.clipboardData||window.clipboardData).getData("text").replace(/[^0-9]/g,"").slice(0,6);' +
774
- 'for(var j=0;j<d.length&&j<6;j++){boxes[j].value=d.charAt(j);boxes[j].classList.add("filled")}' +
775
- 'syncHidden();if(d.length>=6){boxes[5].focus();if(onComplete)onComplete()}else if(d.length>0)boxes[d.length].focus()});' +
774
+ 'for(var j=0;j<d.length&&j<6;j++){setDigit(j,d.charAt(j))}' +
775
+ 'if(d.length>=6){boxes[5].focus();if(onComplete)onComplete()}else if(d.length>0)boxes[d.length].focus()});' +
776
776
  'boxes[idx].addEventListener("focus",function(){this.select()});' +
777
777
  '})(i)}' +
778
- 'function syncHidden(){var v="";for(var j=0;j<boxes.length;j++)v+=boxes[j].value;hidden.value=v}' +
778
+ 'function syncHidden(){var v="";for(var j=0;j<digits.length;j++)v+=digits[j];hidden.value=v}' +
779
+ 'window.resetPinBoxes=function(){for(var j=0;j<6;j++){digits[j]="";boxes[j].value="";boxes[j].classList.remove("filled")}hidden.value="";boxes[0].focus()}' +
779
780
  '}';
780
781
 
781
782
  // HTML fragment for 6 PIN digit boxes + hidden input
@@ -938,8 +939,8 @@ function multiUserLoginPageHtml() {
938
939
 
939
940
  'function resetPin(){' +
940
941
  'var boxes=document.querySelectorAll(".pin-digit");' +
941
- 'for(var i=0;i<boxes.length;i++){boxes[i].value="";boxes[i].classList.remove("filled");boxes[i].disabled=false}' +
942
- 'pinEl.value="";btns[1].disabled=true;if(boxes[0])boxes[0].focus()}' +
942
+ 'for(var i=0;i<boxes.length;i++)boxes[i].disabled=false;' +
943
+ 'if(window.resetPinBoxes)resetPinBoxes();btns[1].disabled=true}' +
943
944
 
944
945
  'function goBackToUsername(){' +
945
946
  'steps[1].classList.remove("active");dots[1].classList.remove("current");dots[1].classList.remove("done");' +
package/lib/project.js CHANGED
@@ -156,6 +156,15 @@ function createProjectContext(opts) {
156
156
  }
157
157
  }
158
158
 
159
+ function sendToSessionOthers(sender, sessionId, obj) {
160
+ var data = JSON.stringify(obj);
161
+ for (var ws of clients) {
162
+ if (ws !== sender && ws.readyState === 1 && ws._clayActiveSession === sessionId) {
163
+ ws.send(data);
164
+ }
165
+ }
166
+ }
167
+
159
168
  // --- File watcher ---
160
169
  var fileWatcher = null;
161
170
  var watchedPath = null;
@@ -1530,7 +1539,7 @@ function createProjectContext(opts) {
1530
1539
  }
1531
1540
 
1532
1541
  if (msg.type === "input_sync") {
1533
- sendToOthers(ws, msg);
1542
+ sendToSessionOthers(ws, ws._clayActiveSession, msg);
1534
1543
  return;
1535
1544
  }
1536
1545
 
@@ -2612,7 +2621,7 @@ function createProjectContext(opts) {
2612
2621
  }
2613
2622
  session.history.push(userMsg);
2614
2623
  sm.appendToSessionFile(session, userMsg);
2615
- sendToOthers(ws, userMsg);
2624
+ sendToSessionOthers(ws, session.localId, userMsg);
2616
2625
 
2617
2626
  if (!session.title) {
2618
2627
  session.title = (msg.text || "Image").substring(0, 50);
@@ -2845,7 +2854,8 @@ function createProjectContext(opts) {
2845
2854
  return;
2846
2855
  }
2847
2856
  var spawnCwd = scope === "global" ? os.homedir() : cwd;
2848
- var child = spawn("npx", ["skills", "add", url, "--skill", skill], {
2857
+ var scopeFlag = scope === "global" ? "--global" : "--project";
2858
+ var child = spawn("npx", ["skills", "add", url, "--skill", skill, "--yes", scopeFlag], {
2849
2859
  cwd: spawnCwd,
2850
2860
  stdio: "ignore",
2851
2861
  detached: false,
@@ -3116,29 +3126,6 @@ function createProjectContext(opts) {
3116
3126
  },
3117
3127
  warmup: function () {
3118
3128
  sdk.warmup();
3119
- // Auto-install clay-ralph skill globally if not present
3120
- var clayRalphDir = path.join(os.homedir(), ".claude", "skills", "clay-ralph", "SKILL.md");
3121
- try {
3122
- fs.accessSync(clayRalphDir, fs.constants.R_OK);
3123
- } catch (e) {
3124
- console.log("[project] Auto-installing clay-ralph skill...");
3125
- var child = spawn("npx", ["skills", "add", "https://github.com/chadbyte/clay-ralph", "--skill", "clay-ralph"], {
3126
- cwd: os.homedir(),
3127
- stdio: "ignore",
3128
- detached: false,
3129
- });
3130
- child.on("close", function (code) {
3131
- if (code === 0) {
3132
- console.log("[project] clay-ralph skill installed successfully");
3133
- send({ type: "skill_installed", skill: "clay-ralph", scope: "global", success: true, error: null });
3134
- } else {
3135
- console.log("[project] clay-ralph skill install failed (code " + code + ")");
3136
- }
3137
- });
3138
- child.on("error", function (err) {
3139
- console.log("[project] clay-ralph skill install error: " + err.message);
3140
- });
3141
- }
3142
3129
  },
3143
3130
  destroy: destroy,
3144
3131
  };
package/lib/public/app.js CHANGED
@@ -542,7 +542,11 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
542
542
  }
543
543
  renderHomeHub(cachedProjects);
544
544
  startTipRotation();
545
- history.pushState(null, "", "/");
545
+ if (document.documentElement.classList.contains("pwa-standalone")) {
546
+ history.replaceState(null, "", "/");
547
+ } else {
548
+ history.pushState(null, "", "/");
549
+ }
546
550
  // Update icon strip active state
547
551
  var homeIcon = document.querySelector(".icon-strip-home");
548
552
  if (homeIcon) homeIcon.classList.add("active");
@@ -557,7 +561,11 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
557
561
  hubCloseBtn.addEventListener("click", function () {
558
562
  hideHomeHub();
559
563
  if (currentSlug) {
560
- history.pushState(null, "", "/p/" + currentSlug + "/");
564
+ if (document.documentElement.classList.contains("pwa-standalone")) {
565
+ history.replaceState(null, "", "/p/" + currentSlug + "/");
566
+ } else {
567
+ history.pushState(null, "", "/p/" + currentSlug + "/");
568
+ }
561
569
  // Restore icon strip active state
562
570
  var homeIcon = document.querySelector(".icon-strip-home");
563
571
  if (homeIcon) homeIcon.classList.remove("active");
@@ -1830,6 +1838,7 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
1830
1838
 
1831
1839
  // --- DOM: Messages ---
1832
1840
  function addUserMessage(text, images, pastes) {
1841
+ if (!text && (!images || images.length === 0) && (!pastes || pastes.length === 0)) return;
1833
1842
  var div = document.createElement("div");
1834
1843
  div.className = "msg-user";
1835
1844
  div.dataset.turn = ++turnCounter;
@@ -2360,7 +2369,11 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
2360
2369
  currentSlug = slug;
2361
2370
  basePath = "/p/" + slug + "/";
2362
2371
  wsPath = "/p/" + slug + "/ws";
2363
- history.pushState(null, "", basePath);
2372
+ if (document.documentElement.classList.contains("pwa-standalone")) {
2373
+ history.replaceState(null, "", basePath);
2374
+ } else {
2375
+ history.pushState(null, "", basePath);
2376
+ }
2364
2377
  resetClientState();
2365
2378
  connect();
2366
2379
  }
@@ -2555,10 +2568,6 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
2555
2568
  var vEl = $("footer-version");
2556
2569
  if (vEl) vEl.textContent = "v" + msg.version;
2557
2570
  }
2558
- if (msg.debug) {
2559
- var debugWrap = $("debug-menu-wrap");
2560
- if (debugWrap) debugWrap.classList.remove("hidden");
2561
- }
2562
2571
  if (msg.lanHost) window.__lanHost = msg.lanHost;
2563
2572
  if (msg.dangerouslySkipPermissions) {
2564
2573
  skipPermsEnabled = true;
@@ -2667,24 +2676,13 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
2667
2676
 
2668
2677
  case "skill_installed":
2669
2678
  handleSkillInstalled(msg);
2670
- // Advance ralph wizard if we were installing clay-ralph
2671
- if (msg.skill === "clay-ralph" && ralphSkillInstalling) {
2672
- ralphSkillInstalling = false;
2673
- ralphSkillInstalled = true;
2674
- if (msg.success) {
2675
- wizardStep = 2;
2676
- updateWizardStep();
2677
- } else {
2678
- var rNextBtn = document.getElementById("ralph-wizard-next");
2679
- if (rNextBtn) { rNextBtn.disabled = false; rNextBtn.textContent = "Get Started"; }
2680
- var rStatusEl = document.getElementById("ralph-install-status");
2681
- if (rStatusEl) { rStatusEl.innerHTML = "Failed to install skill. Try again."; }
2682
- }
2683
- }
2679
+ if (msg.success) knownInstalledSkills[msg.skill] = true;
2680
+ handleSkillInstallWs(msg);
2684
2681
  break;
2685
2682
 
2686
2683
  case "skill_uninstalled":
2687
2684
  handleSkillUninstalled(msg);
2685
+ if (msg.success) delete knownInstalledSkills[msg.skill];
2688
2686
  break;
2689
2687
 
2690
2688
  case "loop_registry_updated":
@@ -3797,35 +3795,191 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
3797
3795
  }
3798
3796
  }
3799
3797
 
3800
- // --- Ralph Wizard ---
3801
- var ralphSkillInstalled = false;
3802
- var ralphSkillInstalling = false;
3798
+ // --- Skill install dialog (generic) ---
3799
+ var skillInstallModal = document.getElementById("skill-install-modal");
3800
+ var skillInstallTitle = document.getElementById("skill-install-title");
3801
+ var skillInstallReason = document.getElementById("skill-install-reason");
3802
+ var skillInstallList = document.getElementById("skill-install-list");
3803
+ var skillInstallOk = document.getElementById("skill-install-ok");
3804
+ var skillInstallCancel = document.getElementById("skill-install-cancel");
3805
+ var skillInstallStatus = document.getElementById("skill-install-status");
3806
+
3807
+ var pendingSkillInstalls = []; // [{ name, url, scope, installed }]
3808
+ var skillInstallCallback = null;
3809
+ var skillInstalling = false;
3810
+ var knownInstalledSkills = {}; // client-side cache of installed skills
3811
+
3812
+ function renderSkillInstallDialog(opts, missing) {
3813
+ skillInstallTitle.textContent = opts.title || "Skill Installation Required";
3814
+ skillInstallReason.textContent = opts.reason || "";
3815
+ skillInstallList.innerHTML = "";
3816
+ for (var i = 0; i < missing.length; i++) {
3817
+ var s = missing[i];
3818
+ var item = document.createElement("div");
3819
+ item.className = "skill-install-item";
3820
+ item.setAttribute("data-skill", s.name);
3821
+ item.innerHTML = '<span class="skill-icon">&#x1f9e9;</span>' +
3822
+ '<div class="skill-info">' +
3823
+ '<span class="skill-name">' + escapeHtml(s.name) + '</span>' +
3824
+ '<span class="skill-scope">' + escapeHtml(s.scope || "global") + '</span>' +
3825
+ '</div>' +
3826
+ '<span class="skill-status"></span>';
3827
+ skillInstallList.appendChild(item);
3828
+ }
3829
+ skillInstallStatus.classList.add("hidden");
3830
+ skillInstallStatus.innerHTML = "";
3831
+ skillInstallOk.disabled = false;
3832
+ skillInstallOk.textContent = "Install";
3833
+ skillInstallOk.className = "confirm-btn confirm-delete";
3834
+ skillInstallModal.classList.remove("hidden");
3835
+ }
3836
+
3837
+ function hideSkillInstallModal() {
3838
+ skillInstallModal.classList.add("hidden");
3839
+ skillInstallCallback = null;
3840
+ pendingSkillInstalls = [];
3841
+ skillInstalling = false;
3842
+ skillInstallDone = false;
3843
+ }
3844
+
3845
+ skillInstallCancel.addEventListener("click", hideSkillInstallModal);
3846
+ skillInstallModal.querySelector(".confirm-backdrop").addEventListener("click", hideSkillInstallModal);
3847
+
3848
+ var skillInstallDone = false;
3849
+
3850
+ skillInstallOk.addEventListener("click", function () {
3851
+ // "Proceed" state — all done, close and invoke callback
3852
+ if (skillInstallDone) {
3853
+ var proceedCb = skillInstallCallback;
3854
+ skillInstallCallback = null;
3855
+ hideSkillInstallModal();
3856
+ if (proceedCb) proceedCb();
3857
+ return;
3858
+ }
3859
+ if (skillInstalling) return;
3860
+ skillInstalling = true;
3861
+ skillInstallOk.disabled = true;
3862
+ skillInstallOk.textContent = "Installing...";
3863
+
3864
+ var total = 0;
3865
+ for (var i = 0; i < pendingSkillInstalls.length; i++) {
3866
+ if (!pendingSkillInstalls[i].installed) total++;
3867
+ }
3868
+ skillInstallStatus.classList.remove("hidden");
3869
+ updateSkillInstallProgress(0, total);
3870
+
3871
+ for (var j = 0; j < pendingSkillInstalls.length; j++) {
3872
+ var s = pendingSkillInstalls[j];
3873
+ if (s.installed) continue;
3874
+ fetch(basePath + "api/install-skill", {
3875
+ method: "POST",
3876
+ headers: { "Content-Type": "application/json" },
3877
+ body: JSON.stringify({ url: s.url, skill: s.name, scope: s.scope || "global" }),
3878
+ }).catch(function () {});
3879
+ }
3880
+ });
3881
+
3882
+ function updateSkillInstallProgress(done, total) {
3883
+ skillInstallStatus.innerHTML = '<div class="skills-spinner small"></div> Installing skills... (' + done + '/' + total + ')';
3884
+ }
3885
+
3886
+ function updateSkillListItems() {
3887
+ var items = skillInstallList.querySelectorAll(".skill-install-item");
3888
+ for (var i = 0; i < items.length; i++) {
3889
+ var name = items[i].getAttribute("data-skill");
3890
+ for (var j = 0; j < pendingSkillInstalls.length; j++) {
3891
+ if (pendingSkillInstalls[j].name === name) {
3892
+ var statusEl = items[i].querySelector(".skill-status");
3893
+ if (pendingSkillInstalls[j].installed) {
3894
+ if (statusEl) {
3895
+ statusEl.innerHTML = '<span class="skill-status-ok">' + iconHtml("circle-check") + '</span>';
3896
+ refreshIcons();
3897
+ }
3898
+ }
3899
+ break;
3900
+ }
3901
+ }
3902
+ }
3903
+ }
3904
+
3905
+ function handleSkillInstallWs(msg) {
3906
+ if (!skillInstalling || pendingSkillInstalls.length === 0) return;
3907
+ for (var i = 0; i < pendingSkillInstalls.length; i++) {
3908
+ if (pendingSkillInstalls[i].name === msg.skill) {
3909
+ if (msg.success) {
3910
+ pendingSkillInstalls[i].installed = true;
3911
+ knownInstalledSkills[msg.skill] = true;
3912
+ } else {
3913
+ skillInstalling = false;
3914
+ skillInstallOk.disabled = false;
3915
+ skillInstallOk.textContent = "Install";
3916
+ skillInstallStatus.innerHTML = "Failed to install " + escapeHtml(msg.skill) + ". Try again.";
3917
+ updateSkillListItems();
3918
+ return;
3919
+ }
3920
+ }
3921
+ }
3922
+
3923
+ var doneCount = 0;
3924
+ var totalCount = pendingSkillInstalls.length;
3925
+ for (var k = 0; k < pendingSkillInstalls.length; k++) {
3926
+ if (pendingSkillInstalls[k].installed) doneCount++;
3927
+ }
3928
+ updateSkillListItems();
3929
+ updateSkillInstallProgress(doneCount, totalCount);
3930
+
3931
+ if (doneCount === totalCount) {
3932
+ skillInstallDone = true;
3933
+ skillInstallStatus.innerHTML = '<span class="skill-status-ok">' + iconHtml("circle-check") + '</span> All skills installed successfully.';
3934
+ refreshIcons();
3935
+ skillInstallOk.disabled = false;
3936
+ skillInstallOk.textContent = "Proceed";
3937
+ skillInstallOk.className = "confirm-btn confirm-proceed";
3938
+ }
3939
+ }
3803
3940
 
3804
- function checkRalphSkillInstalled(cb) {
3941
+ function requireSkills(opts, cb) {
3805
3942
  fetch(basePath + "api/installed-skills")
3806
3943
  .then(function (res) { return res.json(); })
3807
3944
  .then(function (data) {
3808
3945
  var installed = data.installed || {};
3809
- ralphSkillInstalled = !!installed["clay-ralph"];
3810
- cb(ralphSkillInstalled);
3946
+ var missing = [];
3947
+ for (var i = 0; i < opts.skills.length; i++) {
3948
+ var sName = opts.skills[i].name;
3949
+ if (!installed[sName] && !knownInstalledSkills[sName]) {
3950
+ missing.push({ name: sName, url: opts.skills[i].url, scope: opts.skills[i].scope || "global", installed: false });
3951
+ }
3952
+ }
3953
+ if (missing.length === 0) { cb(); return; }
3954
+ pendingSkillInstalls = missing;
3955
+ skillInstallCallback = cb;
3956
+ renderSkillInstallDialog(opts, missing);
3811
3957
  })
3812
- .catch(function () { cb(false); });
3958
+ .catch(function () { cb(); });
3813
3959
  }
3814
3960
 
3961
+ function requireClayRalph(cb) {
3962
+ requireSkills({
3963
+ title: "Skill Installation Required",
3964
+ reason: "This feature requires the following skill to be installed.",
3965
+ skills: [{ name: "clay-ralph", url: "https://github.com/chadbyte/clay-ralph", scope: "global" }]
3966
+ }, cb);
3967
+ }
3968
+
3969
+ // --- Ralph Wizard ---
3970
+
3815
3971
  function openRalphWizard() {
3816
- wizardData = { name: "", task: "", maxIterations: 3 };
3817
- ralphSkillInstalling = false;
3818
- var el = document.getElementById("ralph-wizard");
3819
- if (!el) return;
3972
+ requireClayRalph(function () {
3973
+ wizardData = { name: "", task: "", maxIterations: 3 };
3974
+ var el = document.getElementById("ralph-wizard");
3975
+ if (!el) return;
3820
3976
 
3821
- var taskEl = document.getElementById("ralph-task");
3822
- if (taskEl) taskEl.value = "";
3823
- var iterEl = document.getElementById("ralph-max-iterations");
3824
- if (iterEl) iterEl.value = "25";
3977
+ var taskEl = document.getElementById("ralph-task");
3978
+ if (taskEl) taskEl.value = "";
3979
+ var iterEl = document.getElementById("ralph-max-iterations");
3980
+ if (iterEl) iterEl.value = "25";
3825
3981
 
3826
- // Check if clay-ralph skill is installed — skip onboarding if so
3827
- checkRalphSkillInstalled(function (installed) {
3828
- wizardStep = installed ? 2 : 1;
3982
+ wizardStep = 1;
3829
3983
  el.classList.remove("hidden");
3830
3984
  var statusEl = document.getElementById("ralph-install-status");
3831
3985
  if (statusEl) { statusEl.classList.add("hidden"); statusEl.innerHTML = ""; }
@@ -3928,38 +4082,9 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
3928
4082
  function wizardNext() {
3929
4083
  collectWizardData();
3930
4084
 
3931
- // Step 1: install clay-ralph skill if needed, otherwise just advance
3932
4085
  if (wizardStep === 1) {
3933
- if (ralphSkillInstalled) {
3934
- wizardStep++;
3935
- updateWizardStep();
3936
- return;
3937
- }
3938
- if (ralphSkillInstalling) return;
3939
- ralphSkillInstalling = true;
3940
- var nextBtn = document.getElementById("ralph-wizard-next");
3941
- if (nextBtn) {
3942
- nextBtn.disabled = true;
3943
- nextBtn.textContent = "Installing...";
3944
- }
3945
- var statusEl = document.getElementById("ralph-install-status");
3946
- if (statusEl) {
3947
- statusEl.classList.remove("hidden");
3948
- statusEl.innerHTML = '<div class="skills-spinner small"></div> Installing clay-ralph skill...';
3949
- }
3950
- fetch(basePath + "api/install-skill", {
3951
- method: "POST",
3952
- headers: { "Content-Type": "application/json" },
3953
- body: JSON.stringify({ url: "https://github.com/chadbyte/clay-ralph", skill: "clay-ralph", scope: "global" }),
3954
- })
3955
- .then(function () {
3956
- // Wait for skill_installed websocket message to advance
3957
- })
3958
- .catch(function () {
3959
- ralphSkillInstalling = false;
3960
- if (nextBtn) { nextBtn.disabled = false; nextBtn.textContent = "Get Started"; }
3961
- if (statusEl) { statusEl.innerHTML = "Failed to install skill. Try again."; }
3962
- });
4086
+ wizardStep++;
4087
+ updateWizardStep();
3963
4088
  return;
3964
4089
  }
3965
4090
 
@@ -4299,6 +4424,7 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
4299
4424
  basePath: basePath,
4300
4425
  currentSlug: currentSlug,
4301
4426
  openRalphWizard: function () { openRalphWizard(); },
4427
+ requireClayRalph: function (cb) { requireClayRalph(cb); },
4302
4428
  getProjects: function () { return cachedProjects; },
4303
4429
  });
4304
4430
 
@@ -4607,6 +4733,57 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
4607
4733
  closeAddProjectModal();
4608
4734
  });
4609
4735
 
4736
+ // --- PWA install prompt ---
4737
+ (function () {
4738
+ var installPill = document.getElementById("pwa-install-pill");
4739
+ var modal = document.getElementById("pwa-install-modal");
4740
+ var confirmBtn = document.getElementById("pwa-modal-confirm");
4741
+ var cancelBtn = document.getElementById("pwa-modal-cancel");
4742
+ if (!installPill || !modal) return;
4743
+
4744
+ // Already standalone — never show
4745
+ if (document.documentElement.classList.contains("pwa-standalone")) return;
4746
+
4747
+ // Show pill on mobile browsers (the primary target for PWA install)
4748
+ var isMobile = /Mobi|Android|iPad|iPhone|iPod/.test(navigator.userAgent) ||
4749
+ (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
4750
+ if (isMobile) {
4751
+ installPill.classList.remove("hidden");
4752
+ }
4753
+
4754
+ // Also show on desktop if beforeinstallprompt fires
4755
+ window.addEventListener("beforeinstallprompt", function (e) {
4756
+ e.preventDefault();
4757
+ installPill.classList.remove("hidden");
4758
+ });
4759
+
4760
+ function openModal() {
4761
+ modal.classList.remove("hidden");
4762
+ lucide.createIcons({ nodes: [modal] });
4763
+ }
4764
+
4765
+ function closeModal() {
4766
+ modal.classList.add("hidden");
4767
+ }
4768
+
4769
+ installPill.addEventListener("click", openModal);
4770
+ cancelBtn.addEventListener("click", closeModal);
4771
+ modal.querySelector(".pwa-modal-backdrop").addEventListener("click", closeModal);
4772
+
4773
+ confirmBtn.addEventListener("click", function () {
4774
+ // Redirect to HTTP setup page (port + 1)
4775
+ var port = parseInt(location.port, 10) || (location.protocol === "https:" ? 443 : 80);
4776
+ var setupUrl = "http://" + location.hostname + ":" + (port + 1) + "/setup";
4777
+ location.href = setupUrl;
4778
+ });
4779
+
4780
+ // Hide after install
4781
+ window.addEventListener("appinstalled", function () {
4782
+ installPill.classList.add("hidden");
4783
+ closeModal();
4784
+ });
4785
+ })();
4786
+
4610
4787
  // --- Init ---
4611
4788
  lucide.createIcons();
4612
4789
  connect();
@@ -158,6 +158,8 @@ img.emoji {
158
158
  --content-width: 760px;
159
159
  }
160
160
 
161
+ /* PWA standalone: home indicator safe area still applies on notched devices */
162
+
161
163
  ::selection {
162
164
  background: rgba(80, 250, 123, 0.25);
163
165
  }
@@ -170,6 +172,7 @@ html, body {
170
172
  font-size: 15px;
171
173
  line-height: 1.6;
172
174
  overflow: hidden;
175
+ overscroll-behavior: none;
173
176
  -webkit-font-smoothing: antialiased;
174
177
  -moz-osx-font-smoothing: grayscale;
175
178
  }
@@ -677,6 +677,7 @@
677
677
  color: var(--text);
678
678
  white-space: pre-wrap;
679
679
  word-break: break-word;
680
+ tab-size: 2;
680
681
  }
681
682
 
682
683
  .file-viewer-body pre code {
@@ -700,6 +701,7 @@
700
701
  line-height: 1.55;
701
702
  white-space: pre;
702
703
  word-break: normal;
704
+ tab-size: 2;
703
705
  }
704
706
 
705
707
  .file-viewer-gutter {
@@ -890,7 +892,12 @@
890
892
  @media (max-width: 1023px) {
891
893
  #terminal-container {
892
894
  position: fixed;
893
- inset: 0;
895
+ top: 0;
896
+ left: 0;
897
+ right: 0;
898
+ bottom: 0;
899
+ padding-top: var(--safe-top);
900
+ padding-bottom: var(--safe-bottom);
894
901
  background: var(--bg);
895
902
  z-index: 300;
896
903
  display: flex;
@@ -431,7 +431,7 @@
431
431
  /* --- Mobile --- */
432
432
  @media (max-width: 768px) {
433
433
  #home-hub {
434
- padding: 32px 16px 32px;
434
+ padding: 32px 16px calc(80px + var(--safe-bottom, 0px));
435
435
  }
436
436
  .hub-greeting h1 {
437
437
  font-size: 22px;
@@ -559,6 +559,10 @@
559
559
  ========================================================================== */
560
560
 
561
561
  @media (max-width: 768px) {
562
+ #layout-body {
563
+ background: var(--bg);
564
+ }
565
+
562
566
  #main-area {
563
567
  overflow: visible;
564
568
  border-radius: 0;
@@ -142,46 +142,6 @@
142
142
  flex-shrink: 0;
143
143
  }
144
144
 
145
- /* --- Debug menu --- */
146
- #debug-menu-wrap {
147
- position: relative;
148
- }
149
-
150
- #debug-menu-wrap.hidden { display: none; }
151
-
152
- #debug-btn {
153
- display: flex;
154
- align-items: center;
155
- justify-content: center;
156
- background: none;
157
- border: 1px solid transparent;
158
- border-radius: 8px;
159
- color: var(--error);
160
- cursor: pointer;
161
- padding: 4px;
162
- opacity: 0.6;
163
- transition: opacity 0.15s, background 0.15s, border-color 0.15s;
164
- }
165
-
166
- #debug-btn .lucide { width: 15px; height: 15px; }
167
- #debug-btn:hover { opacity: 1; background: var(--error-8); border-color: var(--error-25); }
168
- #debug-btn.active { opacity: 1; background: var(--error-12); border-color: var(--error-25); }
169
-
170
- #debug-menu {
171
- position: absolute;
172
- top: calc(100% + 6px);
173
- right: 0;
174
- background: var(--bg-alt);
175
- border: 1px solid var(--border);
176
- border-radius: 10px;
177
- padding: 8px 0;
178
- min-width: 200px;
179
- box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.4);
180
- z-index: 200;
181
- }
182
-
183
- #debug-menu.hidden { display: none; }
184
-
185
145
  /* --- Terminal toggle button (mobile only) --- */
186
146
  #footer-status,
187
147
  #terminal-toggle-btn {