tmux-weblink 1.2.2 → 1.4.0

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.
@@ -1,10 +1,10 @@
1
- var Ke="https://esm.sh/ghostty-web@0.4.0",Xe=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function W(t){return t.replace(Xe,"")}var q;function _e(){return q||(document.querySelector("link[data-tmux-web-xterm-css]")?q=Promise.resolve():(q=new Promise(t=>{let e=document.createElement("link");e.rel="stylesheet",e.href="/assets/xterm.css",e.setAttribute("data-tmux-web-xterm-css",""),e.addEventListener("load",()=>t(),{once:!0}),e.addEventListener("error",()=>t(),{once:!0}),document.head.appendChild(e)}),q))}var pe=class xe{terminal;fitAddon;container;constructor(e,a,o){this.container=e,this.terminal=a,this.fitAddon=o,this.terminal.loadAddon(this.fitAddon),this.terminal.open(e)}static async create(e,a,o){let[{Terminal:s},{FitAddon:c}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),_e()]),u=new s({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:a,convertEol:!1,theme:o});return new xe(e,u,new c)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(e){return new Promise(a=>this.terminal.write(e,a))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(e){this.terminal.scrollToLine(Math.max(0,e))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){let e=he(this.container);return e.width<=0||e.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(e){this.terminal.paste(e)}input(e){this.terminal.input(e,!1)}onData(e){this.terminal.onData(e),this.terminal.onBinary(e)}onResize(e){this.terminal.onResize(e)}onScroll(e){this.terminal.onScroll(e)}attachCustomKeyEventHandler(e){this.terminal.attachCustomKeyEventHandler(e)}isFocused(){let e=document.activeElement;return!!e&&(e===this.container||this.container.contains(e))}},Ue=class ke{terminal;container;colsValue=80;rowsValue=24;charW=0;charH=0;resizeCallbacks=[];constructor(e,a){this.container=e,this.terminal=a,this.terminal.open(e)}static async create(e,a,o){let s=await import(Ke);await s.init();let c=new s.Terminal({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:a,convertEol:!1,theme:o});return new ke(e,c)}get cols(){return this.colsValue}get rows(){return this.rowsValue}write(e){return new Promise(a=>this.terminal.write(e,a))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom?.()}scrollToLine(e){this.terminal.scrollToLine?.(Math.max(0,e))}isNearScrollbackTop(){let e=this.terminal.buffer?.active;return e?e.viewportY>=e.baseY:!1}viewportY(){return this.terminal.buffer?.active?.viewportY??0}baseY(){return this.terminal.buffer?.active?.baseY??0}fit(){let e=he(this.container);if(e.width<=0||e.height<=0)return!1;this.updateCellMetrics(!this.charW||!this.charH);let a=Math.floor(e.width/this.charW),o=Math.floor(e.height/this.charH);if(a<10||o<5)return!1;if(a!==this.colsValue||o!==this.rowsValue){this.colsValue=a,this.rowsValue=o,this.terminal.resize(a,o);for(let s of this.resizeCallbacks)s({cols:a,rows:o})}return!0}focus(){(this.terminal.textarea||this.container.querySelector("textarea")||this.container)?.focus?.()}pasteText(e){let a=this.terminal.getMode?.(2004)??!1;this.input(a?"\x1B[200~"+e+"\x1B[201~":e)}input(e){this.terminal.input?.(e,!1)}onData(e){this.terminal.onData(a=>e(a))}onResize(e){this.resizeCallbacks.push(e)}onScroll(e){this.terminal.onScroll(e)}attachCustomKeyEventHandler(e){this.terminal.attachCustomKeyEventHandler(e)}isFocused(){let e=document.activeElement;return!!e&&(e===this.container||e===this.terminal.textarea||this.container.contains(e))}updateCellMetrics(e=!1){let a=this.container.querySelector("canvas");if(a instanceof HTMLElement&&a.offsetWidth>0&&a.offsetHeight>0&&this.colsValue>0&&this.rowsValue>0){let o=a.offsetWidth/this.colsValue,s=a.offsetHeight/this.rowsValue;(e||!this.charW||!this.charH)&&(this.charW=o,this.charH=s)}(!this.charW||!this.charH)&&(this.charW=9,this.charH=18)}};function he(t){let e=t.getBoundingClientRect(),a=window.visualViewport;return a?{width:Math.min(e.width,a.width),height:Math.max(0,Math.min(e.height,a.height-Math.max(0,e.top)))}:e}function Je(t,e,a){return t.innerHTML="",t.classList.add("terminal-pending"),(async()=>{let o;if(a.renderer==="ghostty")try{o=await Ue.create(t,a.scrollback,a.theme)}catch{console.error("[tmux-web] ghostty-web failed; falling back to xterm.js"),o=await pe.create(t,a.scrollback,a.theme)}else o=await pe.create(t,a.scrollback,a.theme);let s,c=0,u,m=null,J=0,b="connecting",N=0,j=!1,A=[],C="",$=!1,B=1e3,Te=(location.protocol==="https:"?"wss:":"ws:")+"//"+location.host+"/ws/"+encodeURIComponent(e),Ce="/api/session/"+encodeURIComponent(e)+"/upload",Be=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function Se(){return A.join("")+C}async function Me(n,i=0){let r=n?o.viewportY():0,h=n?o.baseY():0,v=W(Se());if(o.reset(),!v){n||o.scrollToBottom();return}await o.write(v),n?o.scrollToLine(h+r+i):o.scrollToBottom()}function S(n){s?.readyState===WebSocket.OPEN&&s.send(JSON.stringify(n))}function Ie(n){let i;try{i=JSON.parse(n)}catch{b==="live"&&(C+=n,o.write(W(n)));return}if(i.type==="auth.required"){let r=localStorage.getItem("tmux-web-token");r?S({type:"auth.token",token:r}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(i.type==="auth.ok"){i.token&&localStorage.setItem("tmux-web-token",i.token),B=1e3,M(),S({type:"resize",cols:o.cols,rows:o.rows}),p();return}if(i.type==="auth.failed"){if(i.permanentLock||i.retryAfterMs&&i.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(i.message);return}return}if(i.type==="snapshot"&&typeof i.data=="string"){let r=W(i.data);A=[r],C="",N=typeof i.lines=="number"?i.lines:a.terminal.initialLines,b="live",o.reset(),o.write(r).then(()=>o.scrollToBottom());return}if(i.type==="window_changed"&&typeof i.activeIndex=="number"){let r=new URL(location.href);r.searchParams.get("window")!==String(i.activeIndex)&&(r.searchParams.set("window",String(i.activeIndex)),history.replaceState(history.state,"",r)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:i.windows}));return}if(i.type==="history"&&typeof i.data=="string"){j=!1,i.lines>0&&i.data&&(A.unshift(i.data),N+=i.lines,Me(!0,i.lines));return}if(i.type==="data"&&typeof i.data=="string"){let r=W(i.data);if(b==="connecting"){b="live",C=r,o.write(r);return}b==="live"&&(C+=r,o.write(r))}}function ie(){let n=Te,i=localStorage.getItem("tmux-web-token");i&&(n+=(n.includes("?")?"&":"?")+"token="+encodeURIComponent(i)),s=new WebSocket(n),s.onopen=()=>{b="connecting",N=0,j=!1,A=[],C="",o.reset(),B=1e3},s.onmessage=r=>{typeof r.data=="string"&&Ie(r.data)},s.onclose=()=>{$||(b="connecting",setTimeout(()=>{B=Math.min(B*2,1e4),$||ie()},B))},s.onerror=()=>s?.close()}function R(n){S({type:"input",data:n})}function M(){return o.fit()}function Z(){t.classList.remove("terminal-pending")}function p(){c&&cancelAnimationFrame(c),u&&clearTimeout(u),M(),c=requestAnimationFrame(()=>{c=0,M()&&Z()}),u=setTimeout(()=>{M()&&Z()},120)}function He(){p(),setTimeout(p,50),setTimeout(p,150),setTimeout(p,300)}function ae(n){return typeof n=="string"&&n.startsWith("image/")}function oe(n){return n?.files?.length?n.files[0]??null:null}function De(n){if(!n?.items)return null;for(let i=0;i<n.items.length;i++){let r=n.items[i];if(r?.kind==="file"&&ae(r.type))return r.getAsFile()}return null}async function ze(n){let i=new FormData,r=n instanceof File&&n.name?n.name:"upload";i.append("file",n,r);let h=localStorage.getItem("tmux-web-token"),v={};h&&(v.Authorization="Bearer "+h);let Q=await fetch(Ce,{method:"POST",body:i,headers:v});if(!Q.ok){let ue="upload failed";try{let me=await Q.json();me.error&&(ue=me.error)}catch{}throw new Error(ue)}let de=await Q.json();if(!de.path)throw new Error("no path in response");return de.path}async function G(n){try{let i=await ze(n);R(i)}catch{}}o.onData(n=>R(n)),o.onResize(({cols:n,rows:i})=>S({type:"resize",cols:n,rows:i})),o.onScroll(()=>{b!=="live"||j||!o.isNearScrollbackTop()||(j=!0,S({type:"load_history",before:N}))}),o.attachCustomKeyEventHandler(n=>n.type!=="keydown"?!0:(n.ctrlKey||n.metaKey)&&n.code==="KeyV"?(n.preventDefault(),(async()=>{try{if(navigator.clipboard?.read){let i=await navigator.clipboard.read();for(let r of i)for(let h of r.types)if(ae(h)){let v=await r.getType(h);await G(v);return}}}catch{}try{let i=await navigator.clipboard?.readText();i&&o.pasteText(se(i))}catch{}})(),!1):!0);let O=0;function se(n){return n.split(`\r
1
+ var Ke="https://esm.sh/ghostty-web@0.4.0",Xe=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function W(t){return t.replace(Xe,"")}var q;function _e(){return q||(document.querySelector("link[data-tmux-web-xterm-css]")?q=Promise.resolve():(q=new Promise(t=>{let e=document.createElement("link");e.rel="stylesheet",e.href="/assets/xterm.css",e.setAttribute("data-tmux-web-xterm-css",""),e.addEventListener("load",()=>t(),{once:!0}),e.addEventListener("error",()=>t(),{once:!0}),document.head.appendChild(e)}),q))}var pe=class xe{terminal;fitAddon;container;constructor(e,a,s){this.container=e,this.terminal=a,this.fitAddon=s,this.terminal.loadAddon(this.fitAddon),this.terminal.open(e)}static async create(e,a,s){let[{Terminal:o},{FitAddon:d}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),_e()]),u=new o({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:a,convertEol:!1,theme:s});return new xe(e,u,new d)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(e){return new Promise(a=>this.terminal.write(e,a))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(e){this.terminal.scrollToLine(Math.max(0,e))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){let e=he(this.container);return e.width<=0||e.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(e){this.terminal.paste(e)}input(e){this.terminal.input(e,!1)}onData(e){this.terminal.onData(e),this.terminal.onBinary(e)}onResize(e){this.terminal.onResize(e)}onScroll(e){this.terminal.onScroll(e)}attachCustomKeyEventHandler(e){this.terminal.attachCustomKeyEventHandler(e)}isFocused(){let e=document.activeElement;return!!e&&(e===this.container||this.container.contains(e))}},Ue=class ke{terminal;container;colsValue=80;rowsValue=24;charW=0;charH=0;resizeCallbacks=[];constructor(e,a){this.container=e,this.terminal=a,this.terminal.open(e)}static async create(e,a,s){let o=await import(Ke);await o.init();let d=new o.Terminal({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:a,convertEol:!1,theme:s});return new ke(e,d)}get cols(){return this.colsValue}get rows(){return this.rowsValue}write(e){return new Promise(a=>this.terminal.write(e,a))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom?.()}scrollToLine(e){this.terminal.scrollToLine?.(Math.max(0,e))}isNearScrollbackTop(){let e=this.terminal.buffer?.active;return e?e.viewportY>=e.baseY:!1}viewportY(){return this.terminal.buffer?.active?.viewportY??0}baseY(){return this.terminal.buffer?.active?.baseY??0}fit(){let e=he(this.container);if(e.width<=0||e.height<=0)return!1;this.updateCellMetrics(!this.charW||!this.charH);let a=Math.floor(e.width/this.charW),s=Math.floor(e.height/this.charH);if(a<10||s<5)return!1;if(a!==this.colsValue||s!==this.rowsValue){this.colsValue=a,this.rowsValue=s,this.terminal.resize(a,s);for(let o of this.resizeCallbacks)o({cols:a,rows:s})}return!0}focus(){(this.terminal.textarea||this.container.querySelector("textarea")||this.container)?.focus?.()}pasteText(e){let a=this.terminal.getMode?.(2004)??!1;this.input(a?"\x1B[200~"+e+"\x1B[201~":e)}input(e){this.terminal.input?.(e,!1)}onData(e){this.terminal.onData(a=>e(a))}onResize(e){this.resizeCallbacks.push(e)}onScroll(e){this.terminal.onScroll(e)}attachCustomKeyEventHandler(e){this.terminal.attachCustomKeyEventHandler(e)}isFocused(){let e=document.activeElement;return!!e&&(e===this.container||e===this.terminal.textarea||this.container.contains(e))}updateCellMetrics(e=!1){let a=this.container.querySelector("canvas");if(a instanceof HTMLElement&&a.offsetWidth>0&&a.offsetHeight>0&&this.colsValue>0&&this.rowsValue>0){let s=a.offsetWidth/this.colsValue,o=a.offsetHeight/this.rowsValue;(e||!this.charW||!this.charH)&&(this.charW=s,this.charH=o)}(!this.charW||!this.charH)&&(this.charW=9,this.charH=18)}};function he(t){let e=t.getBoundingClientRect(),a=window.visualViewport;return a?{width:Math.min(e.width,a.width),height:Math.max(0,Math.min(e.height,a.height-Math.max(0,e.top)))}:e}function Je(t,e,a){return t.innerHTML="",t.classList.add("terminal-pending"),(async()=>{let s;if(a.renderer==="ghostty")try{s=await Ue.create(t,a.scrollback,a.theme)}catch{console.error("[tmux-web] ghostty-web failed; falling back to xterm.js"),s=await pe.create(t,a.scrollback,a.theme)}else s=await pe.create(t,a.scrollback,a.theme);let o,d=0,u,m=null,J=0,b="connecting",A=0,N=!1,j=[],C="",$=!1,B=1e3,Te=(location.protocol==="https:"?"wss:":"ws:")+"//"+location.host+"/ws/"+encodeURIComponent(e),Ce="/api/session/"+encodeURIComponent(e)+"/upload",Be=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function Se(){return j.join("")+C}async function Me(n,i=0){let r=n?s.viewportY():0,h=n?s.baseY():0,v=W(Se());if(s.reset(),!v){n||s.scrollToBottom();return}await s.write(v),n?s.scrollToLine(h+r+i):s.scrollToBottom()}function S(n){o?.readyState===WebSocket.OPEN&&o.send(JSON.stringify(n))}function Ie(n){let i;try{i=JSON.parse(n)}catch{b==="live"&&(C+=n,s.write(W(n)));return}if(i.type==="auth.required"){let r=localStorage.getItem("tmux-web-token");r?S({type:"auth.token",token:r}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(i.type==="auth.ok"){i.token&&localStorage.setItem("tmux-web-token",i.token),B=1e3,M(),S({type:"resize",cols:s.cols,rows:s.rows}),p();return}if(i.type==="auth.failed"){if(i.permanentLock||i.retryAfterMs&&i.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(i.message);return}return}if(i.type==="snapshot"&&typeof i.data=="string"){let r=W(i.data);j=[r],C="",A=typeof i.lines=="number"?i.lines:a.terminal.initialLines,b="live",s.reset(),s.write(r).then(()=>s.scrollToBottom());return}if(i.type==="window_changed"&&typeof i.activeIndex=="number"){let r=new URL(location.href);r.searchParams.get("window")!==String(i.activeIndex)&&(r.searchParams.set("window",String(i.activeIndex)),history.replaceState(history.state,"",r)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:i.windows}));return}if(i.type==="history"&&typeof i.data=="string"){N=!1,i.lines>0&&i.data&&(j.unshift(i.data),A+=i.lines,Me(!0,i.lines));return}if(i.type==="data"&&typeof i.data=="string"){let r=W(i.data);if(b==="connecting"){b="live",C=r,s.write(r);return}b==="live"&&(C+=r,s.write(r))}}function ie(){let n=Te,i=localStorage.getItem("tmux-web-token");i&&(n+=(n.includes("?")?"&":"?")+"token="+encodeURIComponent(i)),o=new WebSocket(n),o.onopen=()=>{b="connecting",A=0,N=!1,j=[],C="",s.reset(),B=1e3},o.onmessage=r=>{typeof r.data=="string"&&Ie(r.data)},o.onclose=()=>{$||(b="connecting",setTimeout(()=>{B=Math.min(B*2,1e4),$||ie()},B))},o.onerror=()=>o?.close()}function R(n){S({type:"input",data:n})}function M(){return s.fit()}function Z(){t.classList.remove("terminal-pending")}function p(){d&&cancelAnimationFrame(d),u&&clearTimeout(u),M(),d=requestAnimationFrame(()=>{d=0,M()&&Z()}),u=setTimeout(()=>{M()&&Z()},120)}function He(){p(),setTimeout(p,50),setTimeout(p,150),setTimeout(p,300)}function ae(n){return typeof n=="string"&&n.startsWith("image/")}function se(n){return n?.files?.length?n.files[0]??null:null}function De(n){if(!n?.items)return null;for(let i=0;i<n.items.length;i++){let r=n.items[i];if(r?.kind==="file"&&ae(r.type))return r.getAsFile()}return null}async function ze(n){let i=new FormData,r=n instanceof File&&n.name?n.name:"upload";i.append("file",n,r);let h=localStorage.getItem("tmux-web-token"),v={};h&&(v.Authorization="Bearer "+h);let Q=await fetch(Ce,{method:"POST",body:i,headers:v});if(!Q.ok){let ue="upload failed";try{let me=await Q.json();me.error&&(ue=me.error)}catch{}throw new Error(ue)}let de=await Q.json();if(!de.path)throw new Error("no path in response");return de.path}async function G(n){try{let i=await ze(n);R(i)}catch{}}s.onData(n=>R(n)),s.onResize(({cols:n,rows:i})=>S({type:"resize",cols:n,rows:i})),s.onScroll(()=>{b!=="live"||N||!s.isNearScrollbackTop()||(N=!0,S({type:"load_history",before:A}))}),s.attachCustomKeyEventHandler(n=>n.type!=="keydown"?!0:(n.ctrlKey||n.metaKey)&&n.code==="KeyV"?(n.preventDefault(),(async()=>{try{if(navigator.clipboard?.read){let i=await navigator.clipboard.read();for(let r of i)for(let h of r.types)if(ae(h)){let v=await r.getType(h);await G(v);return}}}catch{}try{let i=await navigator.clipboard?.readText();i&&s.pasteText(oe(i))}catch{}})(),!1):!0);let O=0;function oe(n){return n.split(`\r
2
2
  `).join(`
3
3
  `).split(`
4
- `).join("\r")}async function Ye(n){let i=De(n.clipboardData);if(i){n.preventDefault(),n.stopPropagation();let v=Date.now();if(v-O<50)return;O=v,await G(i);return}let r=n.clipboardData?.getData("text/plain");if(!r)return;n.preventDefault(),n.stopPropagation();let h=Date.now();h-O<50||(O=h,o.pasteText(se(r)))}let I=0;function Pe(n){n.preventDefault(),I++,oe(n.dataTransfer)&&t.classList.add("terminal-drag-over")}function Ve(n){n.preventDefault(),n.dataTransfer&&(n.dataTransfer.dropEffect="copy")}function Fe(n){n.preventDefault(),I--,I<=0&&(I=0,t.classList.remove("terminal-drag-over"))}async function Ne(n){n.preventDefault(),I=0,t.classList.remove("terminal-drag-over");let i=oe(n.dataTransfer);i&&await G(i)}function re(n){!Be||n.key!=="Escape"||n.defaultPrevented||n.repeat||n.metaKey||n.ctrlKey||n.altKey||o.isFocused()&&(n.preventDefault(),n.stopPropagation(),R("\x1B"))}t.addEventListener("paste",Ye),t.addEventListener("dragenter",Pe),t.addEventListener("dragover",Ve),t.addEventListener("dragleave",Fe),t.addEventListener("drop",Ne),document.addEventListener("keydown",re,!0);function je(n){if(n.touches.length!==1){m=null;return}let i=n.touches[0];i&&(m={startX:i.clientX,startY:i.clientY,lastX:i.clientX,lastY:i.clientY,scrolling:!1},n.stopPropagation())}function Ae(n){if(!m||n.touches.length!==1)return;let i=n.touches[0];if(!i)return;let r=i.clientY-m.startY,h=i.clientX-m.startX;if(!m.scrolling){if(Math.abs(r)<8||Math.abs(r)<Math.abs(h))return;m.scrolling=!0,J=Date.now()+500}n.preventDefault(),n.stopPropagation(),qe(-(i.clientY-m.lastY),i.clientX,i.clientY),m.lastX=i.clientX,m.lastY=i.clientY}function Re(n){if(!m)return;let i=m.scrolling;m=null,n.stopPropagation(),i?(J=Date.now()+500,n.preventDefault()):o.focus()}function Oe(n){m=null,n.stopPropagation()}function We(n){n.pointerType==="touch"&&Date.now()<J&&(n.preventDefault(),n.stopPropagation())}function qe(n,i,r){(t.querySelector(".xterm-screen")??t).dispatchEvent(new WheelEvent("wheel",{deltaY:n,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:i,clientY:r,bubbles:!0,cancelable:!0}))}t.addEventListener("touchstart",je,{passive:!0,capture:!0}),t.addEventListener("touchmove",Ae,{passive:!1,capture:!0}),t.addEventListener("touchend",Re,{passive:!1,capture:!0}),t.addEventListener("touchcancel",Oe,{passive:!0,capture:!0}),t.addEventListener("pointerup",We,!0);function le(){p()}window.addEventListener("resize",le),window.visualViewport?.addEventListener("resize",p),window.visualViewport?.addEventListener("scroll",p);let ce=new ResizeObserver(()=>p());return ce.observe(t),document.fonts?.ready.then(()=>{p(),setTimeout(p,50),setTimeout(p,200)}),t.addEventListener("focusin",He,!0),requestAnimationFrame(()=>{M()&&Z(),ie(),p()}),{destroy(){if($=!0,s){try{s.close(1e3,"session switch")}catch{}s=void 0}t.innerHTML="",t.classList.remove("terminal-pending"),window.removeEventListener("resize",le),ce.disconnect(),document.removeEventListener("keydown",re,!0)},focus(){o.focus()},sendInput(n){R(n)}}})()}var H=window.__TMUX_WEB_SHELL__,l=null,w=null,$e="sessions",E="",K="",X="",fe="",d=document.getElementById("sidebar-content"),D=document.getElementById("main-placeholder"),L=document.getElementById("terminal-container"),_=document.getElementById("file-editor"),Ze=document.getElementById("fe-path"),f=document.getElementById("fe-status"),z=document.getElementById("fe-content"),x=document.getElementById("fe-save"),Ge=document.getElementById("fe-delete"),Qe=document.getElementById("fe-back"),ve=document.getElementById("fe-new-name"),et=document.getElementById("fe-new-btn"),ee=document.getElementById("settings-backdrop"),ye=document.getElementById("settings-popover"),we=document.querySelector(".sidebar");function tt(){return window.matchMedia("(max-width: 640px)").matches}function te(){tt()&&we.classList.add("collapsed")}function ge(){we.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{ge(),be("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{ge(),be("files")});function be(t){$e=t,document.querySelectorAll(".mode-btn").forEach(a=>a.classList.remove("active"));let e=document.getElementById(t==="sessions"?"mode-sessions":"mode-files");e&&e.classList.add("active"),t==="sessions"?Y():nt()}async function Y(){d.innerHTML='<div class="sidebar-section-label">Sessions</div><button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';try{let e=(await(await fetch("/api/sidebar/sessions")).json()).recent||[];for(let a of e){let o=document.createElement("div");o.className="session-item"+(a.name===w?" active":""),o.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${a.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
4
+ `).join("\r")}async function Ye(n){let i=De(n.clipboardData);if(i){n.preventDefault(),n.stopPropagation();let v=Date.now();if(v-O<50)return;O=v,await G(i);return}let r=n.clipboardData?.getData("text/plain");if(!r)return;n.preventDefault(),n.stopPropagation();let h=Date.now();h-O<50||(O=h,s.pasteText(oe(r)))}let I=0;function Pe(n){n.preventDefault(),I++,se(n.dataTransfer)&&t.classList.add("terminal-drag-over")}function Ve(n){n.preventDefault(),n.dataTransfer&&(n.dataTransfer.dropEffect="copy")}function Fe(n){n.preventDefault(),I--,I<=0&&(I=0,t.classList.remove("terminal-drag-over"))}async function Ae(n){n.preventDefault(),I=0,t.classList.remove("terminal-drag-over");let i=se(n.dataTransfer);i&&await G(i)}function re(n){!Be||n.key!=="Escape"||n.defaultPrevented||n.repeat||n.metaKey||n.ctrlKey||n.altKey||s.isFocused()&&(n.preventDefault(),n.stopPropagation(),R("\x1B"))}t.addEventListener("paste",Ye),t.addEventListener("dragenter",Pe),t.addEventListener("dragover",Ve),t.addEventListener("dragleave",Fe),t.addEventListener("drop",Ae),document.addEventListener("keydown",re,!0);function Ne(n){if(n.touches.length!==1){m=null;return}let i=n.touches[0];i&&(m={startX:i.clientX,startY:i.clientY,lastX:i.clientX,lastY:i.clientY,scrolling:!1},n.stopPropagation())}function je(n){if(!m||n.touches.length!==1)return;let i=n.touches[0];if(!i)return;let r=i.clientY-m.startY,h=i.clientX-m.startX;if(!m.scrolling){if(Math.abs(r)<8||Math.abs(r)<Math.abs(h))return;m.scrolling=!0,J=Date.now()+500}n.preventDefault(),n.stopPropagation(),qe(-(i.clientY-m.lastY),i.clientX,i.clientY),m.lastX=i.clientX,m.lastY=i.clientY}function Re(n){if(!m)return;let i=m.scrolling;m=null,n.stopPropagation(),i?(J=Date.now()+500,n.preventDefault()):s.focus()}function Oe(n){m=null,n.stopPropagation()}function We(n){n.pointerType==="touch"&&Date.now()<J&&(n.preventDefault(),n.stopPropagation())}function qe(n,i,r){(t.querySelector(".xterm-screen")??t).dispatchEvent(new WheelEvent("wheel",{deltaY:n,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:i,clientY:r,bubbles:!0,cancelable:!0}))}t.addEventListener("touchstart",Ne,{passive:!0,capture:!0}),t.addEventListener("touchmove",je,{passive:!1,capture:!0}),t.addEventListener("touchend",Re,{passive:!1,capture:!0}),t.addEventListener("touchcancel",Oe,{passive:!0,capture:!0}),t.addEventListener("pointerup",We,!0);function le(){p()}window.addEventListener("resize",le),window.visualViewport?.addEventListener("resize",p),window.visualViewport?.addEventListener("scroll",p);let ce=new ResizeObserver(()=>p());return ce.observe(t),document.fonts?.ready.then(()=>{p(),setTimeout(p,50),setTimeout(p,200)}),t.addEventListener("focusin",He,!0),requestAnimationFrame(()=>{M()&&Z(),ie(),p()}),{destroy(){if($=!0,o){try{o.close(1e3,"session switch")}catch{}o=void 0}t.innerHTML="",t.classList.remove("terminal-pending"),window.removeEventListener("resize",le),ce.disconnect(),document.removeEventListener("keydown",re,!0)},focus(){s.focus()},sendInput(n){R(n)}}})()}var H=window.__TMUX_WEB_SHELL__,l=null,w=null,$e="sessions",E="",K="",X="",fe="",c=document.getElementById("sidebar-content"),D=document.getElementById("main-placeholder"),L=document.getElementById("terminal-container"),_=document.getElementById("file-editor"),Ze=document.getElementById("fe-path"),f=document.getElementById("fe-status"),z=document.getElementById("fe-content"),x=document.getElementById("fe-save"),Ge=document.getElementById("fe-delete"),Qe=document.getElementById("fe-back"),ve=document.getElementById("fe-new-name"),et=document.getElementById("fe-new-btn"),ee=document.getElementById("settings-backdrop"),ye=document.getElementById("settings-popover"),we=document.querySelector(".sidebar");function tt(){return window.matchMedia("(max-width: 640px)").matches}function te(){tt()&&we.classList.add("collapsed")}function ge(){we.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{ge(),be("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{ge(),be("files")});function be(t){$e=t,document.querySelectorAll(".mode-btn").forEach(a=>a.classList.remove("active"));let e=document.getElementById(t==="sessions"?"mode-sessions":"mode-files");e&&e.classList.add("active"),t==="sessions"?Y():nt()}async function Y(){c.innerHTML='<div class="sidebar-section-label">Sessions</div><button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';try{let e=(await(await fetch("/api/sidebar/sessions")).json()).recent||[];for(let a of e){let s=document.createElement("div");s.className="session-item"+(a.name===w?" active":""),s.setAttribute("data-session",a.name),s.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${a.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
5
5
  <span>${P(a.name)}</span>
6
6
  <button class="session-edit-btn" data-session="${P(a.name)}" title="Edit session"><svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
7
- `,o.addEventListener("click",c=>{c.target.closest(".session-edit-btn")||ne(a.name)}),d.appendChild(o);let s=o.querySelector(".session-edit-btn");s&&s.addEventListener("click",c=>{c.stopPropagation(),rt(a.name,s)})}}catch{d.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let t=document.getElementById("ns-btn");t&&t.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function P(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function ne(t){if(!(w===t&&l)){_.style.display="none",D.style.display="none",L.style.display="",L.classList.add("terminal-pending"),te(),l&&(l.destroy(),l=null),w=t;try{l=await Je(L,t,{terminal:H.terminal,scrollback:H.scrollback,theme:H.theme,renderer:H.renderer})}catch(e){console.error("[shell] terminal init failed:",e);let a=e&&typeof e=="object"&&"message"in e?e.message:String(e);L.textContent="Failed to open terminal for "+t+": "+a,L.classList.remove("terminal-pending")}}}var U=[];function nt(){if(U=H.fsRoots||[],U.length===0){if(w){it(w);return}d.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}d.innerHTML="";for(let t of U){let e=document.createElement("div");e.className="file-tree-item",e.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${P(t)}`,e.addEventListener("click",()=>k(t)),d.appendChild(e)}}async function it(t){d.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let e=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(t))).json();e.path?k(e.path):d.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{d.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function k(t){E=t,d.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let e=await(await fetch("/api/fs/list?path="+encodeURIComponent(t))).json();d.innerHTML="";let a=at(t);if(a&&!U.some(s=>t===s)){let s=document.createElement("div");s.className="file-tree-item",s.innerHTML="..",s.addEventListener("click",()=>k(a)),d.appendChild(s)}if(e.dirs)for(let s of e.dirs){let c=s.split("/").pop()||s.split("\\").pop(),u=document.createElement("div");u.className="file-tree-item",u.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${P(c)}`,u.addEventListener("click",()=>k(s)),d.appendChild(u)}else d.innerHTML="";let o=e.files||[];for(let s of o){let c=s.split("/").pop()||s.split("\\").pop(),u=document.createElement("div");u.className="file-tree-item file",u.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${P(c)}`,u.addEventListener("click",()=>ot(s)),d.appendChild(u)}}catch{d.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function at(t){let e=t.lastIndexOf("/");return e<=0?null:t.substring(0,e)}async function ot(t){K=t,L.style.display="none",l&&(l.destroy(),l=null,w=null),te(),D.style.display="none",_.style.display="flex",Ze.textContent=t,f.textContent="Loading...",x.style.display="none";try{let e=await fetch("/api/file?path="+encodeURIComponent(t));if(!e.ok){let o=await e.json();f.textContent=o.error||"Failed to load",z.value="";return}let a=await e.json();z.value=a.content,X=a.content,fe=a.content,f.textContent=a.size+" bytes",x.style.display=""}catch{f.textContent="Failed to load",z.value=""}}z.addEventListener("input",()=>{X=z.value}),x.addEventListener("click",async()=>{x.textContent="Saving...",x.disabled=!0;try{let t=await fetch("/api/file",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:K,content:X})});if(!t.ok){let e=await t.json();f.textContent=e.error||"Save failed";return}fe=X,f.textContent="Saved"}catch{f.textContent="Save failed"}finally{x.disabled=!1,x.textContent="Save"}}),Ge.addEventListener("click",async()=>{if(confirm("Delete "+K+"?"))try{let t=await fetch("/api/file/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:K})});if(!t.ok){let e=await t.json();f.textContent=e.error||"Delete failed";return}_.style.display="none",D.style.display="flex",E&&k(E)}catch{f.textContent="Delete failed"}}),Qe.addEventListener("click",()=>{_.style.display="none",D.style.display="flex",E&&k(E)}),et.addEventListener("click",async()=>{let t=ve.value.trim();if(!t)return;let e=E+"/"+t;try{let a=await fetch("/api/file/touch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:e})});if(!a.ok){let o=await a.json();f.textContent=o.error||"Create failed";return}ve.value="",E&&k(E)}catch{f.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let t=ye.classList.toggle("open");ee.classList.toggle("open",t)}),ee.addEventListener("click",()=>{ye.classList.remove("open"),ee.classList.remove("open")});var T="",V=document.createElement("div");V.className="session-popover-backdrop",document.body.appendChild(V);var g=document.createElement("div");g.id="session-popover",g.className="session-popover",g.style.display="none",g.innerHTML=`<div class="sp-field">
7
+ `,s.addEventListener("click",d=>{d.target.closest(".session-edit-btn")||ne(a.name)}),c.appendChild(s);let o=s.querySelector(".session-edit-btn");o&&o.addEventListener("click",d=>{d.stopPropagation(),rt(a.name,o)})}}catch{c.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let t=document.getElementById("ns-btn");t&&t.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function P(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function ne(t){if(!(w===t&&l)){_.style.display="none",D.style.display="none",L.style.display="",L.classList.add("terminal-pending"),te(),l&&(l.destroy(),l=null),w=t;for(let e of c.querySelectorAll(".session-item"))e.classList.toggle("active",e.getAttribute("data-session")===t);try{l=await Je(L,t,{terminal:H.terminal,scrollback:H.scrollback,theme:H.theme,renderer:H.renderer})}catch(e){console.error("[shell] terminal init failed:",e);let a=e&&typeof e=="object"&&"message"in e?e.message:String(e);L.textContent="Failed to open terminal for "+t+": "+a,L.classList.remove("terminal-pending")}}}var U=[];function nt(){if(U=H.fsRoots||[],U.length===0){if(w){it(w);return}c.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}c.innerHTML="";for(let t of U){let e=document.createElement("div");e.className="file-tree-item",e.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${P(t)}`,e.addEventListener("click",()=>k(t)),c.appendChild(e)}}async function it(t){c.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let e=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(t))).json();e.path?k(e.path):c.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{c.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function k(t){E=t,c.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let e=await(await fetch("/api/fs/list?path="+encodeURIComponent(t))).json();c.innerHTML="";let a=at(t);if(a&&!U.some(o=>t===o)){let o=document.createElement("div");o.className="file-tree-item",o.innerHTML="..",o.addEventListener("click",()=>k(a)),c.appendChild(o)}if(e.dirs)for(let o of e.dirs){let d=o.split("/").pop()||o.split("\\").pop(),u=document.createElement("div");u.className="file-tree-item",u.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${P(d)}`,u.addEventListener("click",()=>k(o)),c.appendChild(u)}else c.innerHTML="";let s=e.files||[];for(let o of s){let d=o.split("/").pop()||o.split("\\").pop(),u=document.createElement("div");u.className="file-tree-item file",u.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${P(d)}`,u.addEventListener("click",()=>st(o)),c.appendChild(u)}}catch{c.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function at(t){let e=t.lastIndexOf("/");return e<=0?null:t.substring(0,e)}async function st(t){K=t,L.style.display="none",l&&(l.destroy(),l=null,w=null),te(),D.style.display="none",_.style.display="flex",Ze.textContent=t,f.textContent="Loading...",x.style.display="none";try{let e=await fetch("/api/file?path="+encodeURIComponent(t));if(!e.ok){let s=await e.json();f.textContent=s.error||"Failed to load",z.value="";return}let a=await e.json();z.value=a.content,X=a.content,fe=a.content,f.textContent=a.size+" bytes",x.style.display=""}catch{f.textContent="Failed to load",z.value=""}}z.addEventListener("input",()=>{X=z.value}),x.addEventListener("click",async()=>{x.textContent="Saving...",x.disabled=!0;try{let t=await fetch("/api/file",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:K,content:X})});if(!t.ok){let e=await t.json();f.textContent=e.error||"Save failed";return}fe=X,f.textContent="Saved"}catch{f.textContent="Save failed"}finally{x.disabled=!1,x.textContent="Save"}}),Ge.addEventListener("click",async()=>{if(confirm("Delete "+K+"?"))try{let t=await fetch("/api/file/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:K})});if(!t.ok){let e=await t.json();f.textContent=e.error||"Delete failed";return}_.style.display="none",D.style.display="flex",E&&k(E)}catch{f.textContent="Delete failed"}}),Qe.addEventListener("click",()=>{_.style.display="none",D.style.display="flex",E&&k(E)}),et.addEventListener("click",async()=>{let t=ve.value.trim();if(!t)return;let e=E+"/"+t;try{let a=await fetch("/api/file/touch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:e})});if(!a.ok){let s=await a.json();f.textContent=s.error||"Create failed";return}ve.value="",E&&k(E)}catch{f.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let t=ye.classList.toggle("open");ee.classList.toggle("open",t)}),ee.addEventListener("click",()=>{ye.classList.remove("open"),ee.classList.remove("open")});var T="",V=document.createElement("div");V.className="session-popover-backdrop",document.body.appendChild(V);var g=document.createElement("div");g.id="session-popover",g.className="session-popover",g.style.display="none",g.innerHTML=`<div class="sp-field">
8
8
  <label>Rename session</label>
9
9
  <input type="text" id="sp-name" />
10
10
  </div>
@@ -13,5 +13,5 @@ var Ke="https://esm.sh/ghostty-web@0.4.0",Xe=/\x1b\](?:10|11|110|111|104)(?:;[^\
13
13
  <button class="btn" id="sp-cancel">Cancel</button>
14
14
  </div>
15
15
  <hr class="sp-divider" />
16
- <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(g);function st(t){let e=t.getBoundingClientRect();g.style.top=e.bottom+4+"px",g.style.left=Math.max(8,Math.min(e.left,window.innerWidth-240))+"px"}function rt(t,e){T=t;let a=document.getElementById("sp-name");a.value=t,V.classList.add("open"),g.style.display="block",st(e),setTimeout(()=>a.focus(),50)}function F(){V.classList.remove("open"),g.style.display="none"}V.addEventListener("click",F),document.getElementById("sp-save").addEventListener("click",async()=>{let t=document.getElementById("sp-name").value.trim();if(!t||t===T){F();return}try{if(!(await fetch("/api/sessions/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:T,newName:t})})).ok)return;F(),Y(),w===T&&ne(t)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+T+'"?'))try{if(!(await fetch("/api/sessions/kill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:T})})).ok)return;F(),w===T&&(l&&(l.destroy(),l=null),w=null,L.style.display="none",D.style.display="flex"),Y()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",F);var lt={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r",exit:"exit\r",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",t=>{let e=t.target.closest("button[data-key]");if(!e||!l)return;let a=lt[e.dataset.key];a&&(l.sendInput(a),l.focus())});var y=document.getElementById("mk-input"),Ee=document.getElementById("mk-send");function Le(){if(!y||!l)return;let t=y.value;t&&(l.sendInput(t+"\r"),y.value="",y.rows=1,l.focus())}if(y&&(y.addEventListener("input",()=>{y.rows=1;let t=y.value.split(`
16
+ <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(g);function ot(t){let e=t.getBoundingClientRect();g.style.top=e.bottom+4+"px",g.style.left=Math.max(8,Math.min(e.left,window.innerWidth-240))+"px"}function rt(t,e){T=t;let a=document.getElementById("sp-name");a.value=t,V.classList.add("open"),g.style.display="block",ot(e),setTimeout(()=>a.focus(),50)}function F(){V.classList.remove("open"),g.style.display="none"}V.addEventListener("click",F),document.getElementById("sp-save").addEventListener("click",async()=>{let t=document.getElementById("sp-name").value.trim();if(!t||t===T){F();return}try{if(!(await fetch("/api/sessions/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:T,newName:t})})).ok)return;F(),Y(),w===T&&ne(t)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+T+'"?'))try{if(!(await fetch("/api/sessions/kill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:T})})).ok)return;F(),w===T&&(l&&(l.destroy(),l=null),w=null,L.style.display="none",D.style.display="flex"),Y()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",F);var lt={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r",exit:"exit\r",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",t=>{let e=t.target.closest("button[data-key]");if(!e||!l)return;let a=lt[e.dataset.key];a&&(l.sendInput(a),l.focus())});var y=document.getElementById("mk-input"),Ee=document.getElementById("mk-send");function Le(){if(!y||!l)return;let t=y.value;t&&(l.sendInput(t+"\r"),y.value="",y.rows=1,l.focus())}if(y&&(y.addEventListener("input",()=>{y.rows=1;let t=y.value.split(`
17
17
  `).length;y.rows=Math.min(t,4)}),y.addEventListener("keydown",t=>{t.key==="Enter"&&!t.shiftKey&&(t.preventDefault(),Le())})),Ee&&Ee.addEventListener("click",Le),window.visualViewport){let t=document.querySelector(".app-layout"),e=()=>{t&&(t.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",e),window.addEventListener("orientationchange",()=>setTimeout(e,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}window.__openSession=ne,window.__refreshSidebar=Y,Y(),te();
@@ -1,7 +1,7 @@
1
- import{initTerminal as V}from"./terminal-core.js";const E=window.__TMUX_WEB_SHELL__;let s=null,r=null,q="sessions",u="",B="",T="",F="";const i=document.getElementById("sidebar-content"),w=document.getElementById("main-placeholder"),y=document.getElementById("terminal-container"),S=document.getElementById("file-editor"),$=document.getElementById("fe-path"),c=document.getElementById("fe-status"),b=document.getElementById("fe-content"),v=document.getElementById("fe-save"),J=document.getElementById("fe-delete"),U=document.getElementById("fe-back"),j=document.getElementById("fe-new-name"),A=document.getElementById("fe-new-btn"),M=document.getElementById("settings-backdrop"),D=document.getElementById("settings-popover"),O=document.querySelector(".sidebar");function W(){return window.matchMedia("(max-width: 640px)").matches}function H(){W()&&O.classList.add("collapsed")}function z(){O.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{z(),P("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{z(),P("files")});function P(e){q=e,document.querySelectorAll(".mode-btn").forEach(n=>n.classList.remove("active"));const t=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");t&&t.classList.add("active"),e==="sessions"?L():K()}async function L(){i.innerHTML='<div class="sidebar-section-label">Sessions</div><button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';try{const m=(await(await fetch("/api/sidebar/sessions")).json()).recent||[];for(const f of m){const o=document.createElement("div");o.className="session-item"+(f.name===r?" active":""),o.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${f.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
2
- <span>${k(f.name)}</span>
3
- <button class="session-edit-btn" data-session="${k(f.name)}" title="Edit session"><svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
4
- `,o.addEventListener("click",l=>{l.target.closest(".session-edit-btn")||N(f.name)}),i.appendChild(o);const a=o.querySelector(".session-edit-btn");a&&a.addEventListener("click",l=>{l.stopPropagation(),Q(f.name,a)})}}catch{i.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}const e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function k(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function N(e){if(!(r===e&&s)){S.style.display="none",w.style.display="none",y.style.display="",y.classList.add("terminal-pending"),H(),s&&(s.destroy(),s=null),r=e;try{s=await V(y,e,{terminal:E.terminal,scrollback:E.scrollback,theme:E.theme,renderer:E.renderer})}catch(t){console.error("[shell] terminal init failed:",t);const n=t&&typeof t=="object"&&"message"in t?t.message:String(t);y.textContent="Failed to open terminal for "+e+": "+n,y.classList.remove("terminal-pending")}}}let I=[];function K(){if(I=E.fsRoots||[],I.length===0){if(r){X(r);return}i.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}i.innerHTML="";for(const e of I){const t=document.createElement("div");t.className="file-tree-item",t.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${k(e)}`,t.addEventListener("click",()=>h(e)),i.appendChild(t)}}async function X(e){i.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const n=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();n.path?h(n.path):i.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{i.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function h(e){u=e,i.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const n=await(await fetch("/api/fs/list?path="+encodeURIComponent(e))).json();i.innerHTML="";const m=Y(e);if(m&&!I.some(a=>e===a)){const a=document.createElement("div");a.className="file-tree-item",a.innerHTML="..",a.addEventListener("click",()=>h(m)),i.appendChild(a)}if(n.dirs)for(const o of n.dirs){const a=o.split("/").pop()||o.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item",l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${k(a)}`,l.addEventListener("click",()=>h(o)),i.appendChild(l)}else i.innerHTML="";const f=n.files||[];for(const o of f){const a=o.split("/").pop()||o.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item file",l.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${k(a)}`,l.addEventListener("click",()=>Z(o)),i.appendChild(l)}}catch{i.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function Y(e){const t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function Z(e){B=e,y.style.display="none",s&&(s.destroy(),s=null,r=null),H(),w.style.display="none",S.style.display="flex",$.textContent=e,c.textContent="Loading...",v.style.display="none";try{const t=await fetch("/api/file?path="+encodeURIComponent(e));if(!t.ok){const m=await t.json();c.textContent=m.error||"Failed to load",b.value="";return}const n=await t.json();b.value=n.content,T=n.content,F=n.content,c.textContent=n.size+" bytes",v.style.display=""}catch{c.textContent="Failed to load",b.value=""}}b.addEventListener("input",()=>{T=b.value}),v.addEventListener("click",async()=>{v.textContent="Saving...",v.disabled=!0;try{const e=await fetch("/api/file",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:B,content:T})});if(!e.ok){const t=await e.json();c.textContent=t.error||"Save failed";return}F=T,c.textContent="Saved"}catch{c.textContent="Save failed"}finally{v.disabled=!1,v.textContent="Save"}}),J.addEventListener("click",async()=>{if(confirm("Delete "+B+"?"))try{const e=await fetch("/api/file/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:B})});if(!e.ok){const t=await e.json();c.textContent=t.error||"Delete failed";return}S.style.display="none",w.style.display="flex",u&&h(u)}catch{c.textContent="Delete failed"}}),U.addEventListener("click",()=>{S.style.display="none",w.style.display="flex",u&&h(u)}),A.addEventListener("click",async()=>{const e=j.value.trim();if(!e)return;const t=u+"/"+e;try{const n=await fetch("/api/file/touch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok){const m=await n.json();c.textContent=m.error||"Create failed";return}j.value="",u&&h(u)}catch{c.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=D.classList.toggle("open");M.classList.toggle("open",e)}),M.addEventListener("click",()=>{D.classList.remove("open"),M.classList.remove("open")});let g="";const C=document.createElement("div");C.className="session-popover-backdrop",document.body.appendChild(C);const p=document.createElement("div");p.id="session-popover",p.className="session-popover",p.style.display="none",p.innerHTML=`<div class="sp-field">
1
+ import{initTerminal as V}from"./terminal-core.js";const E=window.__TMUX_WEB_SHELL__;let s=null,p=null,q="sessions",u="",B="",S="",F="";const i=document.getElementById("sidebar-content"),b=document.getElementById("main-placeholder"),y=document.getElementById("terminal-container"),T=document.getElementById("file-editor"),$=document.getElementById("fe-path"),c=document.getElementById("fe-status"),w=document.getElementById("fe-content"),v=document.getElementById("fe-save"),A=document.getElementById("fe-delete"),J=document.getElementById("fe-back"),j=document.getElementById("fe-new-name"),U=document.getElementById("fe-new-btn"),M=document.getElementById("settings-backdrop"),D=document.getElementById("settings-popover"),O=document.querySelector(".sidebar");function W(){return window.matchMedia("(max-width: 640px)").matches}function H(){W()&&O.classList.add("collapsed")}function z(){O.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{z(),P("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{z(),P("files")});function P(e){q=e,document.querySelectorAll(".mode-btn").forEach(n=>n.classList.remove("active"));const t=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");t&&t.classList.add("active"),e==="sessions"?L():K()}async function L(){i.innerHTML='<div class="sidebar-section-label">Sessions</div><button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';try{const f=(await(await fetch("/api/sidebar/sessions")).json()).recent||[];for(const r of f){const o=document.createElement("div");o.className="session-item"+(r.name===p?" active":""),o.setAttribute("data-session",r.name),o.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${r.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
2
+ <span>${k(r.name)}</span>
3
+ <button class="session-edit-btn" data-session="${k(r.name)}" title="Edit session"><svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
4
+ `,o.addEventListener("click",l=>{l.target.closest(".session-edit-btn")||N(r.name)}),i.appendChild(o);const a=o.querySelector(".session-edit-btn");a&&a.addEventListener("click",l=>{l.stopPropagation(),Q(r.name,a)})}}catch{i.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}const e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function k(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function N(e){if(!(p===e&&s)){T.style.display="none",b.style.display="none",y.style.display="",y.classList.add("terminal-pending"),H(),s&&(s.destroy(),s=null),p=e;for(const t of i.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e);try{s=await V(y,e,{terminal:E.terminal,scrollback:E.scrollback,theme:E.theme,renderer:E.renderer})}catch(t){console.error("[shell] terminal init failed:",t);const n=t&&typeof t=="object"&&"message"in t?t.message:String(t);y.textContent="Failed to open terminal for "+e+": "+n,y.classList.remove("terminal-pending")}}}let I=[];function K(){if(I=E.fsRoots||[],I.length===0){if(p){X(p);return}i.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}i.innerHTML="";for(const e of I){const t=document.createElement("div");t.className="file-tree-item",t.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${k(e)}`,t.addEventListener("click",()=>h(e)),i.appendChild(t)}}async function X(e){i.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const n=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();n.path?h(n.path):i.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{i.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function h(e){u=e,i.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const n=await(await fetch("/api/fs/list?path="+encodeURIComponent(e))).json();i.innerHTML="";const f=Y(e);if(f&&!I.some(a=>e===a)){const a=document.createElement("div");a.className="file-tree-item",a.innerHTML="..",a.addEventListener("click",()=>h(f)),i.appendChild(a)}if(n.dirs)for(const o of n.dirs){const a=o.split("/").pop()||o.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item",l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${k(a)}`,l.addEventListener("click",()=>h(o)),i.appendChild(l)}else i.innerHTML="";const r=n.files||[];for(const o of r){const a=o.split("/").pop()||o.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item file",l.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${k(a)}`,l.addEventListener("click",()=>Z(o)),i.appendChild(l)}}catch{i.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function Y(e){const t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function Z(e){B=e,y.style.display="none",s&&(s.destroy(),s=null,p=null),H(),b.style.display="none",T.style.display="flex",$.textContent=e,c.textContent="Loading...",v.style.display="none";try{const t=await fetch("/api/file?path="+encodeURIComponent(e));if(!t.ok){const f=await t.json();c.textContent=f.error||"Failed to load",w.value="";return}const n=await t.json();w.value=n.content,S=n.content,F=n.content,c.textContent=n.size+" bytes",v.style.display=""}catch{c.textContent="Failed to load",w.value=""}}w.addEventListener("input",()=>{S=w.value}),v.addEventListener("click",async()=>{v.textContent="Saving...",v.disabled=!0;try{const e=await fetch("/api/file",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:B,content:S})});if(!e.ok){const t=await e.json();c.textContent=t.error||"Save failed";return}F=S,c.textContent="Saved"}catch{c.textContent="Save failed"}finally{v.disabled=!1,v.textContent="Save"}}),A.addEventListener("click",async()=>{if(confirm("Delete "+B+"?"))try{const e=await fetch("/api/file/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:B})});if(!e.ok){const t=await e.json();c.textContent=t.error||"Delete failed";return}T.style.display="none",b.style.display="flex",u&&h(u)}catch{c.textContent="Delete failed"}}),J.addEventListener("click",()=>{T.style.display="none",b.style.display="flex",u&&h(u)}),U.addEventListener("click",async()=>{const e=j.value.trim();if(!e)return;const t=u+"/"+e;try{const n=await fetch("/api/file/touch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok){const f=await n.json();c.textContent=f.error||"Create failed";return}j.value="",u&&h(u)}catch{c.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=D.classList.toggle("open");M.classList.toggle("open",e)}),M.addEventListener("click",()=>{D.classList.remove("open"),M.classList.remove("open")});let g="";const C=document.createElement("div");C.className="session-popover-backdrop",document.body.appendChild(C);const m=document.createElement("div");m.id="session-popover",m.className="session-popover",m.style.display="none",m.innerHTML=`<div class="sp-field">
5
5
  <label>Rename session</label>
6
6
  <input type="text" id="sp-name" />
7
7
  </div>
@@ -10,5 +10,5 @@ import{initTerminal as V}from"./terminal-core.js";const E=window.__TMUX_WEB_SHEL
10
10
  <button class="btn" id="sp-cancel">Cancel</button>
11
11
  </div>
12
12
  <hr class="sp-divider" />
13
- <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(p);function G(e){const t=e.getBoundingClientRect();p.style.top=t.bottom+4+"px",p.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function Q(e,t){g=e;const n=document.getElementById("sp-name");n.value=e,C.classList.add("open"),p.style.display="block",G(t),setTimeout(()=>n.focus(),50)}function x(){C.classList.remove("open"),p.style.display="none"}C.addEventListener("click",x),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===g){x();return}try{if(!(await fetch("/api/sessions/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:g,newName:e})})).ok)return;x(),L(),r===g&&N(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+g+'"?'))try{if(!(await fetch("/api/sessions/kill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:g})})).ok)return;x(),r===g&&(s&&(s.destroy(),s=null),r=null,y.style.display="none",w.style.display="flex"),L()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",x);const ee={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r",exit:"exit\r",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{const t=e.target.closest("button[data-key]");if(!t||!s)return;const n=ee[t.dataset.key];n&&(s.sendInput(n),s.focus())});const d=document.getElementById("mk-input"),R=document.getElementById("mk-send");function _(){if(!d||!s)return;const e=d.value;e&&(s.sendInput(e+"\r"),d.value="",d.rows=1,s.focus())}if(d&&(d.addEventListener("input",()=>{d.rows=1;const e=d.value.split(`
13
+ <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(m);function G(e){const t=e.getBoundingClientRect();m.style.top=t.bottom+4+"px",m.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function Q(e,t){g=e;const n=document.getElementById("sp-name");n.value=e,C.classList.add("open"),m.style.display="block",G(t),setTimeout(()=>n.focus(),50)}function x(){C.classList.remove("open"),m.style.display="none"}C.addEventListener("click",x),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===g){x();return}try{if(!(await fetch("/api/sessions/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:g,newName:e})})).ok)return;x(),L(),p===g&&N(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+g+'"?'))try{if(!(await fetch("/api/sessions/kill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:g})})).ok)return;x(),p===g&&(s&&(s.destroy(),s=null),p=null,y.style.display="none",b.style.display="flex"),L()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",x);const ee={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r",exit:"exit\r",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{const t=e.target.closest("button[data-key]");if(!t||!s)return;const n=ee[t.dataset.key];n&&(s.sendInput(n),s.focus())});const d=document.getElementById("mk-input"),R=document.getElementById("mk-send");function _(){if(!d||!s)return;const e=d.value;e&&(s.sendInput(e+"\r"),d.value="",d.rows=1,s.focus())}if(d&&(d.addEventListener("input",()=>{d.rows=1;const e=d.value.split(`
14
14
  `).length;d.rows=Math.min(e,4)}),d.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),_())})),R&&R.addEventListener("click",_),window.visualViewport){const e=document.querySelector(".app-layout"),t=()=>{e&&(e.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",t),window.addEventListener("orientationchange",()=>setTimeout(t,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}window.__openSession=N,window.__refreshSidebar=L,L(),H();
package/dist/index.js CHANGED
@@ -1,12 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import{chmodSync as Fe,existsSync as V,mkdirSync as Ue,readdirSync as de,readFileSync as We,statSync as N,unlinkSync as $e,writeFileSync as Be}from"node:fs";import{readFile as Ve}from"node:fs/promises";import{createRequire as Ge}from"node:module";import w from"node:path";import{fileURLToPath as ze}from"node:url";import{Hono as Ke}from"hono";import{serve as Qe}from"@hono/node-server";import{WebSocketServer as Xe,WebSocket as ue}from"ws";import*as Ye from"node-pty";try{const e=w.dirname(Ge(import.meta.url).resolve("node-pty/package.json")),t=w.join(e,"prebuilds");if(V(t))for(const r of de(t)){const n=w.join(t,r,"spawn-helper");V(n)&&!(N(n).mode&73)&&Fe(n,493)}}catch{}import{listSessions as q}from"./sessions.js";import{renderLoginPage as Je,renderNotesIndex as Ze,renderNotesPage as et,renderSettings as tt,renderThemeSettings as rt,renderScheduleIndex as nt,renderHistoryIndex as ot,renderQuickCommandsPage as st,renderFilesIndex as it,renderShell as at}from"./frontend.js";import{loadSecurityConfig as dt,saveSecurityConfig as ce}from"./lib/security-config.js";import{hashPassword as le,verifyPassword as me,validatePassword as pe,TokenStore as ut}from"./lib/auth.js";import{RateLimiter as ct}from"./lib/rateLimiter.js";import{atomicWriteFileSync as lt}from"./lib/atomicWrite.js";import{resolveFsPath as G,resolveFsRoots as fe,MAX_FILE_BYTES as he,walkRecursive as mt}from"./lib/fs-access.js";import{captureSessionWindowsWithPath as pt}from"./lib/tmux-windows.js";import{audit as c}from"./lib/auditLog.js";import{db as g}from"./lib/db.js";import{getSessionAccessMap as I}from"./lib/session-access.js";import{listWindowHistory as ft,clearWindowHistory as ht}from"./lib/window-history.js";import{loadExtensions as yt,spawnExtensionBackend as wt,registerExtensionRoutes as gt}from"./lib/ext-loader.js";import{SchedulerService as jt,isValidScheduleInput as kt,isValidRescheduleInput as vt}from"./lib/scheduler.js";import{getScheduleDelayError as ye}from"./lib/schedule-delay.js";import{handleClientMessage as xt}from"./lib/ws-message.js";import{loadDotEnv as Tt}from"./lib/load-env.js";import{cmdAdd as St,cmdRemove as bt,cmdList as qt,cmdSetup as At,cmdTheme as It,printUsage as we,printVersion as _t}from"./lib/cli.js";import{readSettings as Z,writeSettings as Ct}from"./lib/settings.js";import{readActiveTheme as Rt,setActiveThemeTemplate as ge}from"./lib/theme-store.js";import{isThemeTemplateId as je,THEME_TEMPLATE_IDS as Et}from"./lib/themes/index.js";import{installPlugin as Mt,uninstallPlugin as Ot}from"./lib/plugins.js";import{buildCommandbarSessions as C}from"./lib/commandbar.js";import{pinView as Lt,unpinView as Pt,listPinnedViews as Nt}from"./lib/pinned-views.js";import{listWindowLabels as Dt,setWindowLabel as Ht}from"./lib/window-labels.js";import{captureAndStoreWindows as ke,getStoredWindows as ve}from"./lib/session-windows.js";import{acquireControlClient as Ft,killAllControlClients as Ut}from"./lib/tmux-control.js";import{buildSidebarSessions as Wt}from"./lib/sessions-sidebar.js";import{createQuickCommand as $t,deleteQuickCommand as Bt,listQuickCommands as xe,updateQuickCommand as Vt}from"./lib/quick-commands.js";import{getSessionPaneTarget as Gt,capturePaneTail as zt,capturePaneHistoryChunk as Kt,isAlternateScreen as Qt,toCrlf as Te}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Xt}from"./lib/terminal-config.js";import{ImageUploadError as Yt,saveUploadedImage as Jt}from"./lib/image-upload.js";import{getSystemStatus as Zt,getTopProcesses as er,killProcess as tr}from"./lib/system-monitor.js";import{listSessionWindows as rr,selectSessionWindow as nr,newSessionWindow as or,renameSessionWindow as sr,newTmuxSession as ir,renameSession as ar,killSession as dr,TmuxWindowsError as z}from"./lib/tmux-windows.js";Tt();const K=Xt(),l=dt(),f=new ut,R=new ct;let D=!1;const Q="tmux-web-token",ur=365;function Se(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function X(e){if(l.security.trustProxy){const t=(e.req.header("x-forwarded-for")||"").split(",")[0].trim();if(t)return t}return e.env?.incoming?.socket?.remoteAddress||"unknown"}function cr(e){if(l.security.trustProxy){const t=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(t)return t}return e.socket.remoteAddress||"unknown"}function ee(e){const t=e.req.header("authorization");if(t?.startsWith("Bearer "))return t.slice(7);const r=e.req.header("cookie");if(!r)return null;const n=r.match(new RegExp(`(?:^|;\\s*)${Q}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function be(e){const t=e.headers.authorization;if(typeof t=="string"&&t.startsWith("Bearer "))return t.slice(7);const r=e.headers.cookie;if(typeof r!="string")return null;const n=r.match(new RegExp(`(?:^|;\\s*)${Q}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function ln(e){const t=ee(e);return t?f.validateToken(t):null}function mn(e){const t=be(e);return t?f.validateToken(t):null}function qe(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${Q}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ur*86400}${r?"; Secure":""}`)}function lr(e){e.header("Set-Cookie",`${Q}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}function a(){return async(e,t)=>{const r=ee(e);return!r||!f.validateToken(r)?(c("http_unauthorized",{ip:X(e)}),e.json({error:"unauthorized"},401)):t()}}function mr(e){const t=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${t}`,302)}function _(){return async(e,t)=>{const r=ee(e);return!r||!f.validateToken(r)?mr(e):t()}}const H=new Map;function pr(e){let t=0;for(const r of H.values())r.ip===e&&t++;return t}function te(e,t,r){try{e.close(t,r)}catch{}}function pn(e,t){e.readyState===ue.OPEN&&e.send(JSON.stringify(t))}function fr(e,t){let r="xterm";(t==="ghostty"||t==="xterm")&&(r=t);const n=process.env.TMUX_WEB_TERMINAL_RENDERER?.trim().toLowerCase();(n==="ghostty"||n==="xterm")&&(r=n);for(const i of e)i==="--ghostty"&&(r="ghostty"),i==="--xterm"&&(r="xterm");return r}const hr=7;function Ae(e){return typeof e!="number"||!Number.isFinite(e)?hr:Math.min(365,Math.max(1,Math.round(e)))}const Ie=process.argv.slice(2);{const e=Ie.filter(t=>t!=="--ghostty"&&t!=="--xterm");if(e.length>0){const[t,r]=e;switch(t){case"add":r||(console.error("usage: tmux-web add <package>"),process.exit(1)),await St(r),process.exit(0);case"remove":case"rm":r||(console.error("usage: tmux-web remove <package>"),process.exit(1)),await bt(r),process.exit(0);case"list":case"ls":await qt(),process.exit(0);case"setup":await At(e),process.exit(0);case"theme":await It(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":we(),process.exit(0);case"-V":case"--version":case"-v":_t(),process.exit(0);default:console.error(`unknown argument: ${t}`),we(),process.exit(1)}}}function v(e,t){e.readyState===ue.OPEN&&e.send(JSON.stringify(t))}const F=new Set,_e=[];await g.read(),g.data.sessionAccess??=[],g.data.pinnedViews??=[],g.data.watchedPanes??=[],g.data.triggeredTasks??=[],g.data.quickCommands??=[];const re=await Z();let j=await Rt();const h=re.commandbar===!0,ne=fr(Ie,re.terminalRenderer),Ce=Ae(re.scheduleHistoryDays),Re=w.join(process.cwd(),"extensions"),Ee=await yt(Re);for(const e of Ee)e.start&&_e.push(wt(e.dir,e));const E=new jt({db:g,historyRetentionMs:Ce*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await E.restoreFromDb();const s=new Ke;s.use("*",async(e,t)=>{await t(),e.header("X-Content-Type-Options","nosniff"),e.header("X-Frame-Options","DENY"),e.header("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline'","style-src 'self' 'unsafe-inline'","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self'","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),s.use("*",async(e,t)=>{const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS")return t();const n=e.req.header("sec-fetch-site");return n&&n!=="same-origin"&&n!=="none"?e.text("cross-site request blocked",403):t()}),gt(s,Re,Ee);const yr=w.dirname(ze(import.meta.url)),wr=[w.join(yr,"assets"),w.join(process.cwd(),"dist","assets")];s.get("/assets/:file",async e=>{const t=e.req.param("file");if(!/^[a-zA-Z0-9._-]+$/.test(t))return e.notFound();for(const r of wr){const n=w.join(r,t);if(!V(n))continue;const i=await Ve(n),o=w.extname(t),d={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(i,200,{"Content-Type":d[o]??"application/octet-stream"})}return e.notFound()});const gr=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ import{chmodSync as Ue,existsSync as V,mkdirSync as Fe,readdirSync as de,readFileSync as We,statSync as N,unlinkSync as $e,writeFileSync as Be}from"node:fs";import{readFile as Ve}from"node:fs/promises";import{createRequire as ze}from"node:module";import w from"node:path";import{fileURLToPath as Ge}from"node:url";import{Hono as Je}from"hono";import{serve as Ke}from"@hono/node-server";import{WebSocketServer as Qe,WebSocket as ue}from"ws";import*as Xe from"node-pty";try{const e=w.dirname(ze(import.meta.url).resolve("node-pty/package.json")),t=w.join(e,"prebuilds");if(V(t))for(const r of de(t)){const n=w.join(t,r,"spawn-helper");V(n)&&!(N(n).mode&73)&&Ue(n,493)}}catch{}import{listSessions as A}from"./sessions.js";import{renderLoginPage as Ye,renderNotesIndex as Ze,renderNotesPage as et,renderSettings as tt,renderThemeSettings as rt,renderScheduleIndex as nt,renderHistoryIndex as ot,renderQuickCommandsPage as st,renderFilesIndex as it,renderShell as at}from"./frontend.js";import{loadSecurityConfig as dt,saveSecurityConfig as ce}from"./lib/security-config.js";import{hashPassword as le,verifyPassword as me,validatePassword as pe,TokenStore as ut}from"./lib/auth.js";import{RateLimiter as ct}from"./lib/rateLimiter.js";import{atomicWriteFileSync as lt}from"./lib/atomicWrite.js";import{resolveFsPath as z,resolveFsRoots as fe,MAX_FILE_BYTES as he,walkRecursive as mt}from"./lib/fs-access.js";import{captureSessionWindowsWithPath as pt}from"./lib/tmux-windows.js";import{audit as c}from"./lib/auditLog.js";import{db as g}from"./lib/db.js";import{getSessionAccessMap as _}from"./lib/session-access.js";import{listWindowHistory as ft,clearWindowHistory as ht}from"./lib/window-history.js";import{loadExtensions as yt,spawnExtensionBackend as wt,registerExtensionRoutes as gt}from"./lib/ext-loader.js";import{SchedulerService as jt,isValidScheduleInput as kt,isValidRescheduleInput as vt}from"./lib/scheduler.js";import{getScheduleDelayError as ye}from"./lib/schedule-delay.js";import{handleClientMessage as xt}from"./lib/ws-message.js";import{loadDotEnv as St}from"./lib/load-env.js";import{cmdAdd as Tt,cmdRemove as bt,cmdList as At,cmdSetup as qt,cmdTheme as _t,printUsage as we,printVersion as It}from"./lib/cli.js";import{readSettings as Z,writeSettings as Ct}from"./lib/settings.js";import{readActiveTheme as Et,setActiveThemeTemplate as ge}from"./lib/theme-store.js";import{isThemeTemplateId as je,THEME_TEMPLATE_IDS as Rt}from"./lib/themes/index.js";import{installPlugin as Mt,uninstallPlugin as Ot}from"./lib/plugins.js";import{buildCommandbarSessions as C}from"./lib/commandbar.js";import{pinView as Lt,unpinView as Pt,listPinnedViews as Nt}from"./lib/pinned-views.js";import{listWindowLabels as Dt,setWindowLabel as Ht}from"./lib/window-labels.js";import{captureAndStoreWindows as ke,getStoredWindows as ve}from"./lib/session-windows.js";import{acquireControlClient as Ut,killAllControlClients as Ft}from"./lib/tmux-control.js";import{buildSidebarSessions as Wt}from"./lib/sessions-sidebar.js";import{createQuickCommand as $t,deleteQuickCommand as Bt,listQuickCommands as xe,updateQuickCommand as Vt}from"./lib/quick-commands.js";import{getSessionPaneTarget as zt,capturePaneTail as Gt,capturePaneHistoryChunk as Jt,isAlternateScreen as Kt,toCrlf as Se}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Qt}from"./lib/terminal-config.js";import{ImageUploadError as Xt,saveUploadedImage as Yt}from"./lib/image-upload.js";import{getSystemStatus as Zt,getTopProcesses as er,killProcess as tr}from"./lib/system-monitor.js";import{listSessionWindows as rr,selectSessionWindow as nr,newSessionWindow as or,renameSessionWindow as sr,newTmuxSession as ir,renameSession as ar,killSession as dr,TmuxWindowsError as G}from"./lib/tmux-windows.js";St();const J=Qt(),l=dt(),f=new ut,E=new ct;let D=!1;const K="tmux-web-token",ur=365;function Te(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function Q(e){if(l.security.trustProxy){const t=(e.req.header("x-forwarded-for")||"").split(",")[0].trim();if(t)return t}return e.env?.incoming?.socket?.remoteAddress||"unknown"}function cr(e){if(l.security.trustProxy){const t=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(t)return t}return e.socket.remoteAddress||"unknown"}function ee(e){const t=e.req.header("authorization");if(t?.startsWith("Bearer "))return t.slice(7);const r=e.req.header("cookie");if(!r)return null;const n=r.match(new RegExp(`(?:^|;\\s*)${K}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function be(e){const t=e.headers.authorization;if(typeof t=="string"&&t.startsWith("Bearer "))return t.slice(7);const r=e.headers.cookie;if(typeof r!="string")return null;const n=r.match(new RegExp(`(?:^|;\\s*)${K}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function pn(e){const t=ee(e);return t?f.validateToken(t):null}function fn(e){const t=be(e);return t?f.validateToken(t):null}function Ae(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${K}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ur*86400}${r?"; Secure":""}`)}function lr(e){e.header("Set-Cookie",`${K}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}function a(){return async(e,t)=>{const r=ee(e);return!r||!f.validateToken(r)?(c("http_unauthorized",{ip:Q(e)}),e.json({error:"unauthorized"},401)):t()}}function mr(e){const t=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${t}`,302)}function I(){return async(e,t)=>{const r=ee(e);return!r||!f.validateToken(r)?mr(e):t()}}const H=new Map;function pr(e){let t=0;for(const r of H.values())r.ip===e&&t++;return t}function te(e,t,r){try{e.close(t,r)}catch{}}function hn(e,t){e.readyState===ue.OPEN&&e.send(JSON.stringify(t))}function fr(e,t){let r="xterm";(t==="ghostty"||t==="xterm")&&(r=t);const n=process.env.TMUX_WEB_TERMINAL_RENDERER?.trim().toLowerCase();(n==="ghostty"||n==="xterm")&&(r=n);for(const i of e)i==="--ghostty"&&(r="ghostty"),i==="--xterm"&&(r="xterm");return r}const hr=7;function qe(e){return typeof e!="number"||!Number.isFinite(e)?hr:Math.min(365,Math.max(1,Math.round(e)))}const _e=process.argv.slice(2);{const e=_e.filter(t=>t!=="--ghostty"&&t!=="--xterm");if(e.length>0){const[t,r]=e;switch(t){case"add":r||(console.error("usage: tmux-web add <package>"),process.exit(1)),await Tt(r),process.exit(0);case"remove":case"rm":r||(console.error("usage: tmux-web remove <package>"),process.exit(1)),await bt(r),process.exit(0);case"list":case"ls":await At(),process.exit(0);case"setup":await qt(e),process.exit(0);case"theme":await _t(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":we(),process.exit(0);case"-V":case"--version":case"-v":It(),process.exit(0);default:console.error(`unknown argument: ${t}`),we(),process.exit(1)}}}function j(e,t){e.readyState===ue.OPEN&&e.send(JSON.stringify(t))}const U=new Set,Ie=[];await g.read(),g.data.sessionAccess??=[],g.data.pinnedViews??=[],g.data.watchedPanes??=[],g.data.triggeredTasks??=[],g.data.quickCommands??=[];const re=await Z();let k=await Et();const h=re.commandbar===!0,ne=fr(_e,re.terminalRenderer),Ce=qe(re.scheduleHistoryDays),Ee=w.join(process.cwd(),"extensions"),Re=await yt(Ee);for(const e of Re)e.start&&Ie.push(wt(e.dir,e));const R=new jt({db:g,historyRetentionMs:Ce*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await R.restoreFromDb();const o=new Je;o.use("*",async(e,t)=>{await t(),e.header("X-Content-Type-Options","nosniff"),e.header("X-Frame-Options","DENY"),e.header("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline'","style-src 'self' 'unsafe-inline'","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self'","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),o.use("*",async(e,t)=>{const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS")return t();const n=e.req.header("sec-fetch-site");return n&&n!=="same-origin"&&n!=="none"?e.text("cross-site request blocked",403):t()}),gt(o,Ee,Re);const yr=w.dirname(Ge(import.meta.url)),wr=[w.join(yr,"assets"),w.join(process.cwd(),"dist","assets")];o.get("/assets/:file",async e=>{const t=e.req.param("file");if(!/^[a-zA-Z0-9._-]+$/.test(t))return e.notFound();for(const r of wr){const n=w.join(r,t);if(!V(n))continue;const i=await Ve(n),s=w.extname(t),d={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(i,200,{"Content-Type":d[s]??"application/octet-stream"})}return e.notFound()});const gr=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
3
3
  <rect x="2.5" y="5.5" width="27" height="21" rx="4" fill="#0d1117" stroke="#7dd3fc" stroke-width="2"/>
4
4
  <path d="M8 13l4.2 3.1L8 19.2" fill="none" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
5
5
  <line x1="15.5" y1="19.6" x2="22.5" y2="19.6" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round"/>
6
- </svg>`,Me=e=>e.body(gr,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",Me),s.get("/favicon.ico",Me),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!l.passwordHash,n=e.req.query("error");return e.html(Je({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:j}))}),s.get("/",_(),e=>{const t=h?C(q(),I()):[],r=fe();return e.html(at({theme:j,commandbarEnabled:h,commandbarSessions:t,fsRoots:r,terminalCfg:K,renderer:ne,scrollback:K.initialLines+2*K.historyChunk}))}),s.get("/notes",_(),e=>{const t=h?C(q(),I()):[];return e.html(Ze(g.data.notes,j,h,t))}),s.get("/notes/:session",_(),e=>{const t=decodeURIComponent(e.req.param("session")),r=h?C(q(),I()):[];return e.html(et(t,j,h,r))}),s.get("/schedule",_(),e=>{const t=h?C(q(),I()):[];return e.html(nt(E.list(),E.listTriggered(),j,Ce,h,t))}),s.get("/history",_(),e=>{const t=q(),r=h?C(t,I()):[],n=new Set(t.map(i=>i.name));return e.html(ot(ft(),j,h,r,n))}),s.get("/quick-commands",_(),e=>{const t=h?C(q(),I()):[];return e.html(st(xe(),j,h,t))}),s.get("/files",_(),e=>{const t=h?C(q(),I()):[],r=fe();return e.html(it(j,h,t,r))}),s.post("/api/history/clear",a(),async e=>(await ht(),e.json({ok:!0}))),s.get("/api/quick-commands",a(),e=>e.json(xe())),s.post("/api/quick-commands",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await $t(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Vt(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",a(),async e=>await Bt(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{const t=X(e),r=R.check(t);if(!r.allowed)return c("rate_limited",{ip:t,retryAfterMs:r.retryAfterMs,permanentLock:r.permanentLock}),r.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:r.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const i=typeof n.password=="string"?n.password:"";if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Se(t))return c("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(D)return e.json({error:"Password setup in progress"},409);const p=pe(i);if(p)return e.json({error:p},400);D=!0;try{l.passwordHash=await le(i),ce(l),c("password_set",{ip:t})}finally{D=!1}const{plaintext:T}=f.createAccessToken("setup",l.security.tokenTtlDays);return qe(e,T),e.json({ok:!0,token:T,setupMode:!0})}if(!await me(i,l.passwordHash)){const p=R.recordFailure(t);return c("auth_failed",{ip:t,method:"password",failures:p.failures,permanentLock:p.permanentLock}),p.permanentLock?(f.revokeAll(),c("permanent_lock",{ip:t,failures:p.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):p.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:p.retryAfterMs},429)}R.recordSuccess(t);const d=`browser-${t}`,{plaintext:x}=f.createAccessToken(d,l.security.tokenTtlDays);return qe(e,x),c("auth_success",{ip:t,method:"password",tokenName:d}),e.json({ok:!0,token:x})}),s.post("/api/auth/token",a(),async e=>{const t=X(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,i=typeof r.ttlDays=="number"?r.ttlDays:l.security.tokenTtlDays,{stored:o,plaintext:d}=f.createAccessToken(n,i);return c("token_created",{ip:t,name:o.name,tokenId:o.id}),e.json({id:o.id,name:o.name,token:d,expiresAt:o.expiresAt})}),s.get("/api/auth/tokens",a(),e=>e.json(f.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",a(),e=>f.revoke(e.req.param("id"))?(c("token_revoked",{ip:X(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",a(),e=>(lr(e),e.json({ok:!0}))),s.get("/settings",_(),async e=>{const t=await Z(),r=t.terminalRenderer??"xterm";return e.html(tt({settings:t,renderer:ne,rendererOverridden:ne!==r,theme:j,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Z(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",i=t.defaultView==="recent"?"recent":"default",o=Ae(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Ct({...r,commandbar:t.commandbar!==void 0,terminalRenderer:n,defaultView:i,scheduleHistoryDays:o}),e.redirect("/settings?saved=1",303)}),s.post("/settings/plugins",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=t.action,n=typeof t.pkg=="string"?t.pkg.trim():"";if(!n)return e.redirect("/settings?error="+encodeURIComponent("missing package name"),303);const i=r==="remove"?await Ot(n):r==="add"?await Mt(n):{ok:!1,output:"unknown action"};return i.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(i.output.slice(0,800)),303)}),s.get("/settings/theme",_(),e=>e.html(rt({theme:j,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!je(r)?e.redirect("/settings/theme",303):(j=await ge(r),e.redirect("/settings/theme?saved=1",303))});const jr={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",a(),e=>e.json({active:j.template,templates:Et.map(t=>({id:t,name:jr[t]}))})),s.post("/api/theme",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!je(r)?e.json({error:"invalid theme template"},400):(j=await ge(r),e.json({ok:!0,active:j.template}))}),s.get("/api/system/status",a(),e=>e.json(Zt())),s.get("/api/system/processes",a(),e=>e.json(er())),s.post("/api/system/kill",a(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=tr(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/sessions",a(),e=>h?e.json(C(q(),I())):e.json({error:"commandbar disabled"},404)),s.post("/api/sessions/new",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(q().some(o=>o.name===r))return e.json({error:"session already exists"},409);try{return ir(r,n),e.json({ok:!0})}catch(o){const d=o instanceof z?o.message:"failed to create session";return e.json({error:d},500)}}),s.post("/api/sessions/rename",a(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(ar(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",a(),async e=>{try{const{name:t}=await e.req.json();return t?(dr(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",a(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=pt(t),i=r.find(o=>o.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:i})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",a(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=w.join(t,r));const n=e.req.query("recursive")==="true";let i=r,o="",d=r.endsWith("/");if(!d)try{d=N(r).isDirectory()}catch{}d||(i=w.dirname(r),o=w.basename(r).toLowerCase());try{const x=de(i),p=[],T=[];for(const M of x)if(!M.startsWith(".")&&!(o&&!M.toLowerCase().startsWith(o))){try{const S=w.join(i,M);N(S).isDirectory()?(p.push(S),n&&mt(S,p,T,0)):T.push(S)}catch{}if(p.length+T.length>=5e3)break}return e.json({dirs:p,files:T})}catch{return e.json({dirs:[],files:[]})}}),s.get("/api/file",a(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=G(t);if(!N(r).isFile())return e.json({error:"not a file"},400);const n=N(r).size;if(n>he)return e.json({error:"file too large",size:n,maxBytes:he},413);const i=We(r,"utf-8");return e.json({path:r,content:i,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",a(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=G(t.path);return lt(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=G(t.path);return N(r).isFile()?($e(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=G(t.path);return V(r)?e.json({error:"file already exists"},409):(Ue(w.dirname(r),{recursive:!0}),Be(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}});function oe(e){return{...Wt(q(),I(),Nt()),currentSession:e??null}}function Oe(e){const t=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!t)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:t};const r=e.windowIndex;return typeof r!="number"||!Number.isInteger(r)||r<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:t,windowIndex:r}}s.get("/api/sidebar/sessions",a(),e=>{const t=e.req.query("currentSession");return e.json(oe(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",a(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(ve(t))}),s.post("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Oe(t);return"error"in r?e.json({error:r.error},400):(await Lt(r.sessionName,r.windowIndex),e.json(oe()))}),s.delete("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Oe(t);return"error"in r?e.json({error:r.error},400):(await Pt(r.sessionName,r.windowIndex),e.json(oe()))}),s.get("/api/notes",a(),e=>{const t=[...g.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",a(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=g.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",a(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},i=g.data.notes.findIndex(o=>o.scope===t);return i>=0?g.data.notes[i]=n:g.data.notes.push(n),await g.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!q().some(o=>o.name===t))return e.json({error:"session not found"},404);let n;try{n=await e.req.parseBody()}catch{return e.json({error:"invalid multipart body"},400)}const i=n.file;if(!(i instanceof File))return e.json({error:"missing file field"},400);try{const o=await i.arrayBuffer(),d=Buffer.from(o),{path:x}=await Jt(d,i.type||void 0,i.name||void 0);return e.json({path:x})}catch(o){return o instanceof Yt?e.json({error:o.message},o.status):(console.error("[upload]",o),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",a(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map(Dt(t).map(o=>[o.windowIndex,o.label])),n=new Map(ve(t).map(o=>[o.index,o])),i=rr(t).map(o=>({...o,label:r.get(o.index)??null,worktree:n.get(o.index)?.worktree??!1}));return e.json(i)}),s.post("/api/session/:session/window-label",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);const i=typeof r.label=="string"?r.label:"",o=await Ht(t,n,i);return e.json(o)}),s.post("/api/session/:session/select-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);try{return nr(t,n),e.json({ok:!0})}catch(i){return i instanceof z?e.json({error:i.message},i.status):(console.error("[select-window]",i),e.json({error:"select-window failed"},500))}}),s.post("/api/session/:session/rename-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);if(typeof r.name!="string"||!r.name.trim())return e.json({error:"name is required"},400);try{return sr(t,n,r.name),ke(t),e.json({ok:!0})}catch(i){return i instanceof z?e.json({error:i.message},i.status):(console.error("[rename-window]",i),e.json({error:"rename-window failed"},500))}}),s.post("/api/session/:session/new-window",a(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return or(t),ke(t),e.json({ok:!0})}catch(r){return r instanceof z?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",a(),e=>e.json(E.list(e.req.query("session")))),s.post("/api/schedule",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ye(t);if(r)return e.json({error:r},400);if(!kt(t))return e.json({error:"invalid body"},400);const n=await E.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",a(),async e=>await E.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ye(t);if(r)return e.json({error:r},400);if(!vt(t))return e.json({error:"invalid body"},400);const n=await E.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const kr=parseInt(process.env.PORT||"21000",10),vr=Qe({fetch:s.fetch,port:kr,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),se=new Xe({noServer:!0});function Le(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
6
+ </svg>`,Me=e=>e.body(gr,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});o.get("/favicon.svg",Me),o.get("/favicon.ico",Me);const jr={name:"tmux-weblink",short_name:"tmux-web",description:"Access your tmux sessions from the browser",start_url:"/",display:"standalone",background_color:"#0d1117",theme_color:"#0d1117",icons:[{src:"/assets/icon-192.png",sizes:"192x192",type:"image/png"},{src:"/assets/icon-512.png",sizes:"512x512",type:"image/png"}]};o.get("/manifest.json",e=>e.json(jr,200,{"Cache-Control":"public, max-age=3600"}));const kr=`// tmux-weblink Service Worker
7
+ const CACHE = "tmux-weblink-v1";
8
+ const ASSETS = ["/", "/favicon.svg"];
9
+ self.addEventListener("install", (e) => {
10
+ e.waitUntil(caches.open(CACHE).then((c) => c.addAll(ASSETS)));
11
+ self.skipWaiting();
12
+ });
13
+ self.addEventListener("activate", (e) => { e.waitUntil(clients.claim()); });
14
+ self.addEventListener("fetch", (e) => {
15
+ e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
16
+ });
17
+ `;o.get("/sw.js",e=>e.body(kr,200,{"Content-Type":"application/javascript; charset=utf-8"})),o.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!l.passwordHash,n=e.req.query("error");return e.html(Ye({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:k}))}),o.get("/",I(),e=>{const t=h?C(A(),_()):[],r=fe();return e.html(at({theme:k,commandbarEnabled:h,commandbarSessions:t,fsRoots:r,terminalCfg:J,renderer:ne,scrollback:J.initialLines+2*J.historyChunk}))}),o.get("/notes",I(),e=>{const t=h?C(A(),_()):[];return e.html(Ze(g.data.notes,k,h,t))}),o.get("/notes/:session",I(),e=>{const t=decodeURIComponent(e.req.param("session")),r=h?C(A(),_()):[];return e.html(et(t,k,h,r))}),o.get("/schedule",I(),e=>{const t=h?C(A(),_()):[];return e.html(nt(R.list(),R.listTriggered(),k,Ce,h,t))}),o.get("/history",I(),e=>{const t=A(),r=h?C(t,_()):[],n=new Set(t.map(i=>i.name));return e.html(ot(ft(),k,h,r,n))}),o.get("/quick-commands",I(),e=>{const t=h?C(A(),_()):[];return e.html(st(xe(),k,h,t))}),o.get("/files",I(),e=>{const t=h?C(A(),_()):[],r=fe();return e.html(it(k,h,t,r))}),o.post("/api/history/clear",a(),async e=>(await ht(),e.json({ok:!0}))),o.get("/api/quick-commands",a(),e=>e.json(xe())),o.post("/api/quick-commands",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await $t(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),o.patch("/api/quick-commands/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Vt(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),o.delete("/api/quick-commands/:id",a(),async e=>await Bt(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),o.post("/api/auth/password",async e=>{const t=Q(e),r=E.check(t);if(!r.allowed)return c("rate_limited",{ip:t,retryAfterMs:r.retryAfterMs,permanentLock:r.permanentLock}),r.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:r.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const i=typeof n.password=="string"?n.password:"";if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Te(t))return c("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(D)return e.json({error:"Password setup in progress"},409);const p=pe(i);if(p)return e.json({error:p},400);D=!0;try{l.passwordHash=await le(i),ce(l),c("password_set",{ip:t})}finally{D=!1}const{plaintext:S}=f.createAccessToken("setup",l.security.tokenTtlDays);return Ae(e,S),e.json({ok:!0,token:S,setupMode:!0})}if(!await me(i,l.passwordHash)){const p=E.recordFailure(t);return c("auth_failed",{ip:t,method:"password",failures:p.failures,permanentLock:p.permanentLock}),p.permanentLock?(f.revokeAll(),c("permanent_lock",{ip:t,failures:p.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):p.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:p.retryAfterMs},429)}E.recordSuccess(t);const d=`browser-${t}`,{plaintext:x}=f.createAccessToken(d,l.security.tokenTtlDays);return Ae(e,x),c("auth_success",{ip:t,method:"password",tokenName:d}),e.json({ok:!0,token:x})}),o.post("/api/auth/token",a(),async e=>{const t=Q(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,i=typeof r.ttlDays=="number"?r.ttlDays:l.security.tokenTtlDays,{stored:s,plaintext:d}=f.createAccessToken(n,i);return c("token_created",{ip:t,name:s.name,tokenId:s.id}),e.json({id:s.id,name:s.name,token:d,expiresAt:s.expiresAt})}),o.get("/api/auth/tokens",a(),e=>e.json(f.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),o.delete("/api/auth/tokens/:id",a(),e=>f.revoke(e.req.param("id"))?(c("token_revoked",{ip:Q(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),o.post("/api/auth/logout",a(),e=>(lr(e),e.json({ok:!0}))),o.get("/settings",I(),async e=>{const t=await Z(),r=t.terminalRenderer??"xterm";return e.html(tt({settings:t,renderer:ne,rendererOverridden:ne!==r,theme:k,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),o.post("/settings",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Z(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",i=t.defaultView==="recent"?"recent":"default",s=qe(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Ct({...r,commandbar:t.commandbar!==void 0,terminalRenderer:n,defaultView:i,scheduleHistoryDays:s}),e.redirect("/settings?saved=1",303)}),o.post("/settings/plugins",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=t.action,n=typeof t.pkg=="string"?t.pkg.trim():"";if(!n)return e.redirect("/settings?error="+encodeURIComponent("missing package name"),303);const i=r==="remove"?await Ot(n):r==="add"?await Mt(n):{ok:!1,output:"unknown action"};return i.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(i.output.slice(0,800)),303)}),o.get("/settings/theme",I(),e=>e.html(rt({theme:k,saved:e.req.query("saved")==="1"}))),o.post("/settings/theme",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!je(r)?e.redirect("/settings/theme",303):(k=await ge(r),e.redirect("/settings/theme?saved=1",303))});const vr={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};o.get("/api/theme",a(),e=>e.json({active:k.template,templates:Rt.map(t=>({id:t,name:vr[t]}))})),o.post("/api/theme",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!je(r)?e.json({error:"invalid theme template"},400):(k=await ge(r),e.json({ok:!0,active:k.template}))}),o.get("/api/system/status",a(),e=>e.json(Zt())),o.get("/api/system/processes",a(),e=>e.json(er())),o.post("/api/system/kill",a(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=tr(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),o.get("/api/sessions",a(),e=>h?e.json(C(A(),_())):e.json({error:"commandbar disabled"},404)),o.post("/api/sessions/new",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(A().some(s=>s.name===r))return e.json({error:"session already exists"},409);try{return ir(r,n),e.json({ok:!0})}catch(s){const d=s instanceof G?s.message:"failed to create session";return e.json({error:d},500)}}),o.post("/api/sessions/rename",a(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(ar(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),o.post("/api/sessions/kill",a(),async e=>{try{const{name:t}=await e.req.json();return t?(dr(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),o.get("/api/fs/session-path",a(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=pt(t),i=r.find(s=>s.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:i})}catch{return e.json({path:process.env.HOME??"/"})}}),o.get("/api/fs/list",a(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=w.join(t,r));const n=e.req.query("recursive")==="true";let i=r,s="",d=r.endsWith("/");if(!d)try{d=N(r).isDirectory()}catch{}d||(i=w.dirname(r),s=w.basename(r).toLowerCase());try{const x=de(i),p=[],S=[];for(const M of x)if(!M.startsWith(".")&&!(s&&!M.toLowerCase().startsWith(s))){try{const T=w.join(i,M);N(T).isDirectory()?(p.push(T),n&&mt(T,p,S,0)):S.push(T)}catch{}if(p.length+S.length>=5e3)break}return e.json({dirs:p,files:S})}catch{return e.json({dirs:[],files:[]})}}),o.get("/api/file",a(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=z(t);if(!N(r).isFile())return e.json({error:"not a file"},400);const n=N(r).size;if(n>he)return e.json({error:"file too large",size:n,maxBytes:he},413);const i=We(r,"utf-8");return e.json({path:r,content:i,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),o.put("/api/file",a(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=z(t.path);return lt(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),o.post("/api/file/delete",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=z(t.path);return N(r).isFile()?($e(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"delete failed"},500)}}),o.post("/api/file/touch",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=z(t.path);return V(r)?e.json({error:"file already exists"},409):(Fe(w.dirname(r),{recursive:!0}),Be(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}});function oe(e){return{...Wt(A(),_(),Nt()),currentSession:e??null}}function Oe(e){const t=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!t)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:t};const r=e.windowIndex;return typeof r!="number"||!Number.isInteger(r)||r<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:t,windowIndex:r}}o.get("/api/sidebar/sessions",a(),e=>{const t=e.req.query("currentSession");return e.json(oe(typeof t=="string"&&t?t:void 0))}),o.get("/api/sidebar/session-windows/:session",a(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(ve(t))}),o.post("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Oe(t);return"error"in r?e.json({error:r.error},400):(await Lt(r.sessionName,r.windowIndex),e.json(oe()))}),o.delete("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Oe(t);return"error"in r?e.json({error:r.error},400):(await Pt(r.sessionName,r.windowIndex),e.json(oe()))}),o.get("/api/notes",a(),e=>{const t=[...g.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),o.get("/api/notes/:scope",a(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=g.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),o.put("/api/notes/:scope",a(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},i=g.data.notes.findIndex(s=>s.scope===t);return i>=0?g.data.notes[i]=n:g.data.notes.push(n),await g.write(),e.json({ok:!0})}),o.post("/api/session/:session/upload",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!A().some(s=>s.name===t))return e.json({error:"session not found"},404);let n;try{n=await e.req.parseBody()}catch{return e.json({error:"invalid multipart body"},400)}const i=n.file;if(!(i instanceof File))return e.json({error:"missing file field"},400);try{const s=await i.arrayBuffer(),d=Buffer.from(s),{path:x}=await Yt(d,i.type||void 0,i.name||void 0);return e.json({path:x})}catch(s){return s instanceof Xt?e.json({error:s.message},s.status):(console.error("[upload]",s),e.json({error:"upload failed"},500))}}),o.get("/api/session/:session/windows",a(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map(Dt(t).map(s=>[s.windowIndex,s.label])),n=new Map(ve(t).map(s=>[s.index,s])),i=rr(t).map(s=>({...s,label:r.get(s.index)??null,worktree:n.get(s.index)?.worktree??!1}));return e.json(i)}),o.post("/api/session/:session/window-label",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);const i=typeof r.label=="string"?r.label:"",s=await Ht(t,n,i);return e.json(s)}),o.post("/api/session/:session/select-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);try{return nr(t,n),e.json({ok:!0})}catch(i){return i instanceof G?e.json({error:i.message},i.status):(console.error("[select-window]",i),e.json({error:"select-window failed"},500))}}),o.post("/api/session/:session/rename-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);if(typeof r.name!="string"||!r.name.trim())return e.json({error:"name is required"},400);try{return sr(t,n,r.name),ke(t),e.json({ok:!0})}catch(i){return i instanceof G?e.json({error:i.message},i.status):(console.error("[rename-window]",i),e.json({error:"rename-window failed"},500))}}),o.post("/api/session/:session/new-window",a(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return or(t),ke(t),e.json({ok:!0})}catch(r){return r instanceof G?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),o.get("/api/schedule",a(),e=>e.json(R.list(e.req.query("session")))),o.post("/api/schedule",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ye(t);if(r)return e.json({error:r},400);if(!kt(t))return e.json({error:"invalid body"},400);const n=await R.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),o.delete("/api/schedule/:id",a(),async e=>await R.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),o.patch("/api/schedule/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ye(t);if(r)return e.json({error:r},400);if(!vt(t))return e.json({error:"invalid body"},400);const n=await R.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const xr=parseInt(process.env.PORT||"21000",10),Sr=Ke({fetch:o.fetch,port:xr,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),se=new Qe({noServer:!0});function Le(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
7
18
  \r
8
- `),e.destroy()}vr.on("upgrade",(e,t,r)=>{const n=new URL(e.url||"/",`http://${e.headers.host}`),i=n.pathname.match(/^\/ws\/(.+)$/);if(!i){t.destroy();return}const o=cr(e),d=e.headers.origin,x=l.security.allowedOrigins;if(d&&!(d===`http://${e.headers.host}`||d===`https://${e.headers.host}`)&&(x.length===0||!x.includes(d))){c("ws_rejected_origin",{ip:o,origin:d}),Le(t,403,"Origin not allowed");return}const p=pr(o);if(p>=l.security.maxConnectionsPerIp){c("ws_rejected_per_ip_cap",{ip:o,liveFromIp:p}),Le(t,429,"Too many connections");return}const T=be(e)||n.searchParams.get("token")||"",M=T?f.validateToken(T):null;se.handleUpgrade(e,t,r,S=>{se.emit("connection",S,e,decodeURIComponent(i[1]),o,M)})}),se.on("connection",(e,t,r,n,i)=>{const o={ws:e,ip:n,authenticated:!1,authTimeout:null};H.set(e,o),c("ws_connected",{ip:n});let d=null;const{initialLines:x,historyChunk:p,syncIdleMs:T,syncMaxMs:M}=K;let S=!0,P=null,$=null,U=r,O=null;const B=()=>{P&&(clearTimeout(P),P=null),$&&(clearTimeout($),$=null)},ie=()=>{if(S){B(),S=!1;try{U=Gt(r)}catch{U=r}if(!Qt(U))try{const y=zt(U,x);v(e,{type:"snapshot",data:Te(y),lines:x})}catch{}}},Ne=()=>{S&&(P&&clearTimeout(P),P=setTimeout(ie,T))};function ae(){try{d=Ye.spawn("tmux",["attach-session","-t",r],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env})}catch(u){v(e,{type:"data",data:`\r
19
+ `),e.destroy()}Sr.on("upgrade",(e,t,r)=>{const n=new URL(e.url||"/",`http://${e.headers.host}`),i=n.pathname.match(/^\/ws\/(.+)$/);if(!i){t.destroy();return}const s=cr(e),d=e.headers.origin,x=l.security.allowedOrigins;if(d&&!(d===`http://${e.headers.host}`||d===`https://${e.headers.host}`)&&(x.length===0||!x.includes(d))){c("ws_rejected_origin",{ip:s,origin:d}),Le(t,403,"Origin not allowed");return}const p=pr(s);if(p>=l.security.maxConnectionsPerIp){c("ws_rejected_per_ip_cap",{ip:s,liveFromIp:p}),Le(t,429,"Too many connections");return}const S=be(e)||n.searchParams.get("token")||"",M=S?f.validateToken(S):null;se.handleUpgrade(e,t,r,T=>{se.emit("connection",T,e,decodeURIComponent(i[1]),s,M)})}),se.on("connection",(e,t,r,n,i)=>{const s={ws:e,ip:n,authenticated:!1,authTimeout:null};H.set(e,s),c("ws_connected",{ip:n});let d=null;const{initialLines:x,historyChunk:p,syncIdleMs:S,syncMaxMs:M}=J;let T=!0,P=null,$=null,F=r,O=null;const B=()=>{P&&(clearTimeout(P),P=null),$&&(clearTimeout($),$=null)},ie=()=>{if(T){B(),T=!1;try{F=zt(r)}catch{F=r}if(!Kt(F))try{const y=Gt(F,x);j(e,{type:"snapshot",data:Se(y),lines:x})}catch{j(e,{type:"data",data:`\r
20
+ `})}}},Ne=()=>{T&&(P&&clearTimeout(P),P=setTimeout(ie,S))};function ae(){try{d=Xe.spawn("tmux",["attach-session","-t",r],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env})}catch(u){j(e,{type:"data",data:`\r
9
21
  \x1B[31mFailed to attach to tmux session "${r}": ${u.message}\x1B[0m\r
10
- `}),te(e,1011,"pty spawn failed");return}F.add(d);let y=-1,m="";O=Ft(r,({activeIndex:u,windows:k})=>{v(e,{type:"window_changed",activeIndex:u,windows:k});const L=k.map(b=>b.index).join(","),W=u!==y||L!==m;y=u,m=L}),$=setTimeout(ie,M),d.onData(u=>{if(S){Ne();return}v(e,{type:"data",data:u})}),d.onExit(({exitCode:u})=>{B(),d&&F.delete(d),d=null,v(e,{type:"data",data:`\r
22
+ `}),te(e,1011,"pty spawn failed");return}U.add(d);let y=-1,m="";O=Ut(r,({activeIndex:u,windows:v})=>{j(e,{type:"window_changed",activeIndex:u,windows:v});const L=v.map(b=>b.index).join(","),W=u!==y||L!==m;y=u,m=L}),$=setTimeout(ie,M),d.onData(u=>{if(T){Ne();return}j(e,{type:"data",data:u})}),d.onExit(({exitCode:u})=>{B(),d&&U.delete(d),d=null,j(e,{type:"data",data:`\r
11
23
  \x1B[2m--- tmux exited (code ${u}) ---\x1B[0m\r
12
- `}),te(e,1e3,"pty exited")})}function Y(y,m){o.authenticated=!0,o.authTimeout&&(clearTimeout(o.authTimeout),o.authTimeout=null),c("auth_success",{ip:n,method:y,tokenName:m}),v(e,{type:"auth.ok",setupMode:!l.passwordHash}),ae()}function A(y,m={}){v(e,{type:"auth.failed",message:y,...m})}async function De(y){let m;try{m=JSON.parse(y)}catch{return!1}if(m.type==="auth.token"&&typeof m.token=="string"){const u=f.validateToken(m.token);if(!u){c("token_auth_failed",{ip:n});const k=R.recordFailure(n);return k.permanentLock&&(f.revokeAll(),c("permanent_lock",{ip:n,failures:k.failures})),A("Invalid or expired token",{permanentLock:k.permanentLock,retryAfterMs:k.retryAfterMs}),!0}return f.touch(u.tokenHash),Y("token",u.name),!0}if(m.type==="auth"&&typeof m.password=="string"){const u=R.check(n);if(!u.allowed)return c("rate_limited",{ip:n,retryAfterMs:u.retryAfterMs,permanentLock:u.permanentLock}),A("Too many attempts",{permanentLock:u.permanentLock,retryAfterMs:u.retryAfterMs}),!0;if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Se(n))return c("setup_rejected_remote",{ip:n}),A("First-run setup must be performed from localhost"),!0;if(D)return A("Password setup in progress"),!0;const b=pe(m.password);if(b)return A(b),!0;D=!0;try{l.passwordHash=await le(m.password),ce(l),c("password_set",{ip:n})}finally{D=!1}const{stored:J,plaintext:He}=f.createAccessToken("setup",l.security.tokenTtlDays);return v(e,{type:"auth.ok",setupMode:!0,token:He}),c("token_created",{ip:n,name:J.name,tokenId:J.id}),c("auth_success",{ip:n,method:"password",tokenName:J.name}),ae(),!0}if(!l.passwordHash)return A("Server not configured"),!0;if(!await me(m.password,l.passwordHash)){const b=R.recordFailure(n);return c("auth_failed",{ip:n,method:"password",failures:b.failures,permanentLock:b.permanentLock}),b.permanentLock&&(f.revokeAll(),c("permanent_lock",{ip:n,failures:b.failures})),A("Incorrect password",{permanentLock:b.permanentLock,retryAfterMs:b.retryAfterMs}),!0}R.recordSuccess(n);const L=`ws-${n}`,{plaintext:W}=f.createAccessToken(L,l.security.tokenTtlDays);return Y("password",L),v(e,{type:"auth.ok",setupMode:!1,token:W}),!0}return!1}i?(f.touch(i.tokenHash),Y("token",i.name)):(v(e,{type:"auth.required",setupMode:!l.passwordHash}),o.authTimeout=setTimeout(()=>{o.authenticated||(c("auth_timeout",{ip:n}),A("Authentication timeout"),te(e,4e3,"Auth timeout"))},l.security.authTimeoutMs)),e.on("message",async y=>{const m=typeof y=="string"?y:y.toString("utf-8");if(!o.authenticated){if(m.length>1e6){v(e,{type:"auth.failed",message:"Message too large"});return}await De(m)||A("Authentication required");return}if(!d)return;try{if(xt(m,d))return}catch{return}let u;try{u=JSON.parse(m)}catch{return}if(u.type==="load_history"&&typeof u.before=="number"){const k=Math.max(0,Math.floor(u.before));try{const{data:L,lines:W}=Kt(U,k,p);v(e,{type:"history",data:Te(L),before:k,lines:W})}catch{v(e,{type:"history",data:"",before:k,lines:0})}}}),e.on("close",()=>{o.authTimeout&&clearTimeout(o.authTimeout),H.delete(e),c("ws_disconnected",{ip:n}),B(),O&&(O(),O=null),d&&(F.delete(d),d.kill(),d=null)}),e.on("error",()=>{o.authTimeout&&clearTimeout(o.authTimeout),H.delete(e),B(),O&&(O(),O=null),d&&(F.delete(d),d.kill(),d=null)})});function Pe(){E.cleanup(),Ut();for(const[e,t]of H){try{e.close(1001,"Server shutting down")}catch{}t.authTimeout&&clearTimeout(t.authTimeout)}H.clear();for(const e of F)try{e.kill()}catch{}F.clear();for(const e of _e)try{e.kill("SIGTERM")}catch{}R.dispose(),process.exit(0)}process.on("SIGINT",Pe),process.on("SIGTERM",Pe),setInterval(()=>{f.purgeExpired()},3600*1e3).unref();
24
+ `}),te(e,1e3,"pty exited")})}function X(y,m){s.authenticated=!0,s.authTimeout&&(clearTimeout(s.authTimeout),s.authTimeout=null),c("auth_success",{ip:n,method:y,tokenName:m}),j(e,{type:"auth.ok",setupMode:!l.passwordHash}),ae()}function q(y,m={}){j(e,{type:"auth.failed",message:y,...m})}async function De(y){let m;try{m=JSON.parse(y)}catch{return!1}if(m.type==="auth.token"&&typeof m.token=="string"){const u=f.validateToken(m.token);if(!u){c("token_auth_failed",{ip:n});const v=E.recordFailure(n);return v.permanentLock&&(f.revokeAll(),c("permanent_lock",{ip:n,failures:v.failures})),q("Invalid or expired token",{permanentLock:v.permanentLock,retryAfterMs:v.retryAfterMs}),!0}return f.touch(u.tokenHash),X("token",u.name),!0}if(m.type==="auth"&&typeof m.password=="string"){const u=E.check(n);if(!u.allowed)return c("rate_limited",{ip:n,retryAfterMs:u.retryAfterMs,permanentLock:u.permanentLock}),q("Too many attempts",{permanentLock:u.permanentLock,retryAfterMs:u.retryAfterMs}),!0;if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Te(n))return c("setup_rejected_remote",{ip:n}),q("First-run setup must be performed from localhost"),!0;if(D)return q("Password setup in progress"),!0;const b=pe(m.password);if(b)return q(b),!0;D=!0;try{l.passwordHash=await le(m.password),ce(l),c("password_set",{ip:n})}finally{D=!1}const{stored:Y,plaintext:He}=f.createAccessToken("setup",l.security.tokenTtlDays);return j(e,{type:"auth.ok",setupMode:!0,token:He}),c("token_created",{ip:n,name:Y.name,tokenId:Y.id}),c("auth_success",{ip:n,method:"password",tokenName:Y.name}),ae(),!0}if(!l.passwordHash)return q("Server not configured"),!0;if(!await me(m.password,l.passwordHash)){const b=E.recordFailure(n);return c("auth_failed",{ip:n,method:"password",failures:b.failures,permanentLock:b.permanentLock}),b.permanentLock&&(f.revokeAll(),c("permanent_lock",{ip:n,failures:b.failures})),q("Incorrect password",{permanentLock:b.permanentLock,retryAfterMs:b.retryAfterMs}),!0}E.recordSuccess(n);const L=`ws-${n}`,{plaintext:W}=f.createAccessToken(L,l.security.tokenTtlDays);return X("password",L),j(e,{type:"auth.ok",setupMode:!1,token:W}),!0}return!1}i?(f.touch(i.tokenHash),X("token",i.name)):(j(e,{type:"auth.required",setupMode:!l.passwordHash}),s.authTimeout=setTimeout(()=>{s.authenticated||(c("auth_timeout",{ip:n}),q("Authentication timeout"),te(e,4e3,"Auth timeout"))},l.security.authTimeoutMs)),e.on("message",async y=>{const m=typeof y=="string"?y:y.toString("utf-8");if(!s.authenticated){if(m.length>1e6){j(e,{type:"auth.failed",message:"Message too large"});return}await De(m)||q("Authentication required");return}if(!d)return;try{if(xt(m,d))return}catch{return}let u;try{u=JSON.parse(m)}catch{return}if(u.type==="load_history"&&typeof u.before=="number"){const v=Math.max(0,Math.floor(u.before));try{const{data:L,lines:W}=Jt(F,v,p);j(e,{type:"history",data:Se(L),before:v,lines:W})}catch{j(e,{type:"history",data:"",before:v,lines:0})}}}),e.on("close",()=>{s.authTimeout&&clearTimeout(s.authTimeout),H.delete(e),c("ws_disconnected",{ip:n}),B(),O&&(O(),O=null),d&&(U.delete(d),d.kill(),d=null)}),e.on("error",()=>{s.authTimeout&&clearTimeout(s.authTimeout),H.delete(e),B(),O&&(O(),O=null),d&&(U.delete(d),d.kill(),d=null)})});function Pe(){R.cleanup(),Ft();for(const[e,t]of H){try{e.close(1001,"Server shutting down")}catch{}t.authTimeout&&clearTimeout(t.authTimeout)}H.clear();for(const e of U)try{e.kill()}catch{}U.clear();for(const e of Ie)try{e.kill("SIGTERM")}catch{}E.dispose(),process.exit(0)}process.on("SIGINT",Pe),process.on("SIGTERM",Pe),setInterval(()=>{f.purgeExpired()},3600*1e3).unref();
@@ -1,2 +1,2 @@
1
- import{execFileSync as k}from"node:child_process";import{capturePaneLines as v}from"./tmux-capture.js";import{listSessionPanes as S}from"./tmux-panes.js";import{getWatchedPanes as B,pruneWatchedPanes as M,recordWatchedPane as D}from"./watched-panes.js";import{identifyAgent as y,detectState as b}from"./agent-detect.js";function C(){const t=new Map;try{const o=k("ps",["-axo","pid=,ppid=,command="],{encoding:"utf-8",timeout:3e3,maxBuffer:8388608});for(const a of o.split(`
2
- `)){const c=a.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);c&&t.set(parseInt(c[1],10),{ppid:parseInt(c[2],10),command:c[3]})}}catch{}return t}function A(t,o){if(o.size===0||t<=0)return[];const a=new Map;for(const[n,e]of o){const s=a.get(e.ppid);s?s.push(n):a.set(e.ppid,[n])}const c=[],f=[...a.get(t)??[]],p=o.get(t);p&&c.push(p.command);let m=0;for(;f.length&&m<200;){m++;const n=f.shift(),e=o.get(n);e&&c.push(e.command);const s=a.get(n);s&&f.push(...s)}return c}const l=new Map;function T(t){let o=5381;for(let a=0;a<t.length;a++)o=(o<<5)+o+t.charCodeAt(a)>>>0;return o}async function N(){const t=B();if(t.length===0)return[];const o=new Map;for(const n of t)o.has(n.sessionName)||o.set(n.sessionName,S(n.sessionName));const a=new Set;for(const n of o.values())for(const e of n)a.add(e.paneId);await M(a);for(const n of l.keys())a.has(n)||l.delete(n);const c=C(),f=new Set(t.map(n=>n.paneId)),p=[...t];for(const[n,e]of o){const s=e.find(d=>d.active&&d.windowActive);if(!s||f.has(s.paneId)||!y(s.command,A(s.pid,c)))continue;const u={paneId:s.paneId,sessionName:n,windowIndex:s.windowIndex,paneIndex:s.paneIndex};await D(u),f.add(s.paneId),p.unshift({...u,watchedAt:Date.now()})}const m=[];for(const n of p){const e=o.get(n.sessionName)?.find(h=>h.paneId===n.paneId);if(!e)continue;const s=A(e.pid,c),u=y(e.command,s);if(!u)continue;let d="unknown";try{const h=v(e.target,0);d=b(u,h);const g=T(h),x=l.get(e.paneId);l.set(e.paneId,g),d==="idle"&&x!==void 0&&x!==g&&(d="working")}catch{}m.push({paneId:e.paneId,sessionName:n.sessionName,windowIndex:e.windowIndex,paneIndex:e.paneIndex,agent:u,state:d,watchedAt:n.watchedAt})}return m}let i=null,I=[],P=0,r=null;const W=1500;function w(){P=Date.now(),N().then(t=>{I=t}).catch(()=>{})}function q(){return I}function z(){return i!==null}function F(){if(!i||r)return;const t=Date.now()-P;if(t>=W){w();return}r=setTimeout(()=>{r=null,w()},W-t),typeof r.unref=="function"&&r.unref()}function L(t=3500){i||(w(),i=setInterval(w,t),typeof i.unref=="function"&&i.unref())}function H(){i&&(clearInterval(i),i=null),r&&(clearTimeout(r),r=null),I=[]}export{q as getCachedAgentStatuses,z as isBackgroundWatchRunning,N as probeWatchedPanes,F as requestProbe,L as startBackgroundWatch,H as stopBackgroundWatch};
1
+ import{execFileSync as k}from"node:child_process";import{capturePaneLines as v}from"./tmux-capture.js";import{listSessionPanes as S}from"./tmux-panes.js";import{getWatchedPanes as B,pruneWatchedPanes as M,recordWatchedPane as D}from"./watched-panes.js";import{identifyAgent as y,detectState as b}from"./agent-detect.js";function C(){const e=new Map;try{const o=k("ps",["-axo","pid=,ppid=,command="],{encoding:"utf-8",timeout:3e3,maxBuffer:8388608});for(const a of o.split(`
2
+ `)){const c=a.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);c&&e.set(parseInt(c[1],10),{ppid:parseInt(c[2],10),command:c[3]})}}catch{}return e}function A(e,o){if(o.size===0||e<=0)return[];const a=new Map;for(const[n,t]of o){const s=a.get(t.ppid);s?s.push(n):a.set(t.ppid,[n])}const c=[],f=[...a.get(e)??[]],p=o.get(e);p&&c.push(p.command);let m=0;for(;f.length&&m<200;){m++;const n=f.shift(),t=o.get(n);t&&c.push(t.command);const s=a.get(n);s&&f.push(...s)}return c}const l=new Map;function T(e){let o=5381;for(let a=0;a<e.length;a++)o=(o<<5)+o+e.charCodeAt(a)>>>0;return o}async function N(){const e=B();if(e.length===0)return[];const o=new Map;for(const n of e)o.has(n.sessionName)||o.set(n.sessionName,S(n.sessionName));const a=new Set;for(const n of o.values())for(const t of n)a.add(t.paneId);await M(a);for(const n of l.keys())a.has(n)||l.delete(n);const c=C(),f=new Set(e.map(n=>n.paneId)),p=[...e];for(const[n,t]of o){const s=t.find(d=>d.active&&d.windowActive);if(!s||f.has(s.paneId)||!y(s.command,A(s.pid,c)))continue;const u={paneId:s.paneId,sessionName:n,windowIndex:s.windowIndex,paneIndex:s.paneIndex};await D(u),f.add(s.paneId),p.unshift({...u,watchedAt:Date.now()})}const m=[];for(const n of p){const t=o.get(n.sessionName)?.find(h=>h.paneId===n.paneId);if(!t)continue;const s=A(t.pid,c),u=y(t.command,s);if(!u)continue;let d="unknown";try{const h=v(t.target,0);d=b(u,h);const g=T(h),x=l.get(t.paneId);l.set(t.paneId,g),d==="idle"&&x!==void 0&&x!==g&&(d="working")}catch{}m.push({paneId:t.paneId,sessionName:n.sessionName,windowIndex:t.windowIndex,paneIndex:t.paneIndex,agent:u,state:d,watchedAt:n.watchedAt})}return m}let i=null,I=[],P=0,r=null;const W=1500;function w(){P=Date.now(),N().then(e=>{I=e}).catch(()=>{})}function q(){return I}function z(){return i!==null}function F(){if(!i||r)return;const e=Date.now()-P;if(e>=W){w();return}r=setTimeout(()=>{r=null,w()},W-e),typeof r.unref=="function"&&r.unref()}function L(e=3500){i||(w(),i=setInterval(w,e),typeof i.unref=="function"&&i.unref())}function H(){i&&(clearInterval(i),i=null),r&&(clearTimeout(r),r=null),I=[]}export{q as getCachedAgentStatuses,z as isBackgroundWatchRunning,N as probeWatchedPanes,F as requestProbe,L as startBackgroundWatch,H as stopBackgroundWatch};
@@ -1,4 +1,4 @@
1
- import{cssVarsStyle as g}from"../theme.js";import{commandbarCSS as x,commandbarHTML as f,commandbarScript as v}from"../commandbar.js";import{newSessionModalCSS as h,newSessionModalHTML as y,newSessionModalScript as k,reducedMotion as w}from"../shared-layout.js";import{icon as n}from"../icons.js";import{getSystemStatus as S}from"../system-monitor.js";function M(o){return o.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function r(o="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${o}; outline: none;`}function P(o){const{theme:e,commandbarEnabled:t,commandbarSessions:l,fsRoots:p,terminalCfg:d,renderer:i,scrollback:c}=o,s=S(),b=s.memory.percent,a=s.cpu.loadAvg[0],m=`
1
+ import{cssVarsStyle as u}from"../theme.js";import{commandbarCSS as x,commandbarHTML as f,commandbarScript as v}from"../commandbar.js";import{newSessionModalCSS as h,newSessionModalHTML as y,newSessionModalScript as k,reducedMotion as w}from"../shared-layout.js";import{icon as n}from"../icons.js";import{getSystemStatus as S}from"../system-monitor.js";function j(o){return o.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function r(o="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${o}; outline: none;`}function M(o){const{theme:e,commandbarEnabled:t,commandbarSessions:l,fsRoots:p,terminalCfg:d,renderer:i,scrollback:c}=o,s=S(),b=s.memory.percent,a=s.cpu.loadAvg[0],m=`
2
2
  *, *::before, *::after { box-sizing: border-box; }
3
3
  html, body {
4
4
  background: var(--page-bg); color: var(--page-fg);
@@ -9,12 +9,14 @@ import{cssVarsStyle as g}from"../theme.js";import{commandbarCSS as x,commandbarH
9
9
  /* \u2500\u2500 Fixed header \u2500\u2500 */
10
10
  .fixed-header {
11
11
  position: fixed; top: 0; left: 0; right: 0; z-index: 200;
12
- height: 48px;
12
+ height: calc(48px + env(safe-area-inset-top, 0px));
13
+ padding-top: env(safe-area-inset-top, 0px);
13
14
  background: color-mix(in srgb, var(--panel-bg) 92%, transparent);
14
15
  backdrop-filter: blur(12px);
15
16
  border-bottom: 1px solid var(--panel-border);
16
17
  display: flex; justify-content: space-between; align-items: center;
17
- padding: 0 12px;
18
+ padding-left: 12px; padding-right: 12px;
19
+ box-sizing: border-box;
18
20
  }
19
21
  .fixed-header .brand {
20
22
  font-size: var(--text-base); font-weight: 600; color: var(--page-fg);
@@ -93,7 +95,7 @@ import{cssVarsStyle as g}from"../theme.js";import{commandbarCSS as x,commandbarH
93
95
  display: flex;
94
96
  height: 100dvh;
95
97
  overflow: hidden;
96
- padding-top: 48px;
98
+ padding-top: calc(48px + env(safe-area-inset-top, 0px));
97
99
  transition: height 0.05s;
98
100
  }
99
101
 
@@ -403,15 +405,20 @@ import{cssVarsStyle as g}from"../theme.js";import{commandbarCSS as x,commandbarH
403
405
  .mobile-keys .mk-buttons { display: none; }
404
406
  .mobile-keys.mk-focused .mk-buttons { display: flex; flex-wrap: wrap; gap: 4px; }
405
407
  }
406
- `,z=JSON.stringify(e.terminal).replace(/</g,"\\u003c"),u=JSON.stringify({terminal:d,scrollback:c,theme:e.terminal,renderer:i,fsRoots:p,commandbarEnabled:t}).replace(/</g,"\\u003c");return`<!DOCTYPE html>
408
+ `,z=JSON.stringify(e.terminal).replace(/</g,"\\u003c"),g=JSON.stringify({terminal:d,scrollback:c,theme:e.terminal,renderer:i,fsRoots:p,commandbarEnabled:t}).replace(/</g,"\\u003c");return`<!DOCTYPE html>
407
409
  <html lang="en">
408
410
  <head>
409
411
  <meta charset="UTF-8" />
410
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
412
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
413
+ <meta name="apple-mobile-web-app-capable" content="yes" />
414
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
415
+ <meta name="apple-mobile-web-app-title" content="tmux-web" />
416
+ <link rel="apple-touch-icon" href="/assets/icon-192.png" />
417
+ <link rel="manifest" href="/manifest.json" />
411
418
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
412
419
  <title>tmux-weblink</title>
413
420
  <style>
414
- ${g(e.shell)}
421
+ ${u(e.shell)}
415
422
  ${m}
416
423
  </style>
417
424
  </head>
@@ -553,7 +560,7 @@ ${y()}
553
560
  ${t?f():""}
554
561
 
555
562
  <script>
556
- window.__TMUX_WEB_SHELL__ = ${u};
563
+ window.__TMUX_WEB_SHELL__ = ${g};
557
564
  </script>
558
565
  <script type="module">
559
566
  await import('/assets/shell-client.js');
@@ -698,5 +705,10 @@ ${k("__onSessionCreated")}
698
705
  }
699
706
  })();
700
707
  </script>
708
+ <script>
709
+ if ('serviceWorker' in navigator) {
710
+ navigator.serviceWorker.register('/sw.js', { scope: '/' });
711
+ }
712
+ </script>
701
713
  </body>
702
- </html>`}export{P as renderShell};
714
+ </html>`}export{M as renderShell};
@@ -1 +1 @@
1
- function n(s,r){if(!s)return r;const e=parseInt(s,10);return Number.isFinite(e)&&e>0?e:r}function i(){return{initialLines:n(process.env.TMUX_WEB_INITIAL_LINES,1e3),historyChunk:n(process.env.TMUX_WEB_HISTORY_CHUNK,500),syncIdleMs:n(process.env.TMUX_WEB_SYNC_IDLE_MS,200),syncMaxMs:n(process.env.TMUX_WEB_SYNC_MAX_MS,3e3)}}export{i as readTerminalBufferConfig};
1
+ function n(s,r){if(!s)return r;const e=parseInt(s,10);return Number.isFinite(e)&&e>0?e:r}function i(){return{initialLines:n(process.env.TMUX_WEB_INITIAL_LINES,1e3),historyChunk:n(process.env.TMUX_WEB_HISTORY_CHUNK,500),syncIdleMs:n(process.env.TMUX_WEB_SYNC_IDLE_MS,20),syncMaxMs:n(process.env.TMUX_WEB_SYNC_MAX_MS,500)}}export{i as readTerminalBufferConfig};
@@ -1 +1 @@
1
- import{db as a}from"./db.js";const d=50;async function c(t){const e={...t,watchedAt:Date.now()},n=a.data.watchedPanes.filter(s=>s.paneId!==t.paneId);a.data.watchedPanes=[e,...n].slice(0,d),await a.write()}function h(){return a.data.watchedPanes}async function r(t){const e=a.data.watchedPanes.length;a.data.watchedPanes=a.data.watchedPanes.filter(n=>t.has(n.paneId)),a.data.watchedPanes.length!==e&&await a.write()}export{d as MAX_WATCHED_PANES,h as getWatchedPanes,r as pruneWatchedPanes,c as recordWatchedPane};
1
+ import{db as a}from"./db.js";const d=50;async function s(e){const t={...e,watchedAt:Date.now()},n=a.data.watchedPanes.filter(c=>c.paneId!==e.paneId);a.data.watchedPanes=[t,...n].slice(0,d),await a.write()}function h(){return a.data.watchedPanes}async function r(e){const t=a.data.watchedPanes.length;a.data.watchedPanes=a.data.watchedPanes.filter(n=>e.has(n.paneId)),a.data.watchedPanes.length!==t&&await a.write()}export{d as MAX_WATCHED_PANES,h as getWatchedPanes,r as pruneWatchedPanes,s as recordWatchedPane};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "1.2.2",
3
+ "version": "1.4.0",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,12 +8,14 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
- "scripts/fix-pty-perms.mjs"
11
+ "scripts/fix-pty-perms.mjs",
12
+ "scripts/generate-icons.mjs"
12
13
  ],
13
14
  "scripts": {
14
15
  "postinstall": "node scripts/fix-pty-perms.mjs",
15
- "prepublishOnly": "tsc && npm run build:client && npm run build:exts && npm run minify:dist",
16
- "build": "tsc && npm run check:drawer-scripts && npm run build:client && npm run build:exts && npm run minify:dist",
16
+ "prepublishOnly": "npm run build",
17
+ "build": "tsc && npm run check:drawer-scripts && npm run generate-icons && npm run build:client && npm run build:exts && npm run minify:dist",
18
+ "generate-icons": "node scripts/generate-icons.mjs",
17
19
  "check:drawer-scripts": "tsx scripts/check-drawer-scripts.ts",
18
20
  "build:client": "node scripts/build-client.mjs",
19
21
  "typecheck": "tsc --noEmit",
@@ -0,0 +1,167 @@
1
+ // Generates PWA icons (PNG) with a terminal-window icon design.
2
+ // Uses pure Node.js (zlib) — no external dependencies.
3
+ import { deflateRawSync } from "node:zlib";
4
+ import { writeFileSync, mkdirSync } from "node:fs";
5
+ import { join, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const ICONS_DIR = join(__dirname, "..", "dist", "assets");
10
+
11
+ function crc32(buf) {
12
+ let crc = 0xffffffff;
13
+ for (let i = 0; i < buf.length; i++) {
14
+ crc ^= buf[i];
15
+ for (let j = 0; j < 8; j++) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
16
+ }
17
+ return (crc ^ 0xffffffff) >>> 0;
18
+ }
19
+
20
+ function pngChunk(type, data) {
21
+ const len = Buffer.alloc(4);
22
+ len.writeUInt32BE(data.length);
23
+ const typeB = Buffer.from(type, "ascii");
24
+ const crc = Buffer.alloc(4);
25
+ crc.writeUInt32BE(crc32(Buffer.concat([typeB, data])));
26
+ return Buffer.concat([len, typeB, data, crc]);
27
+ }
28
+
29
+ function createPNG(width, height, getPixel) {
30
+ const stride = 1 + width * 4;
31
+ const raw = Buffer.alloc(height * stride);
32
+ for (let y = 0; y < height; y++) {
33
+ raw[y * stride] = 0;
34
+ for (let x = 0; x < width; x++) {
35
+ const [r, g, b, a] = getPixel(x, y);
36
+ const p = y * stride + 1 + x * 4;
37
+ raw[p] = Math.max(0, Math.min(255, Math.round(r)));
38
+ raw[p+1] = Math.max(0, Math.min(255, Math.round(g)));
39
+ raw[p+2] = Math.max(0, Math.min(255, Math.round(b)));
40
+ raw[p+3] = Math.max(0, Math.min(255, Math.round(a)));
41
+ }
42
+ }
43
+
44
+ const compressed = deflateRawSync(raw);
45
+ const ihdr = Buffer.alloc(13);
46
+ ihdr.writeUInt32BE(width, 0);
47
+ ihdr.writeUInt32BE(height, 4);
48
+ ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
49
+
50
+ const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
51
+ return Buffer.concat([sig, pngChunk("IHDR", ihdr), pngChunk("IDAT", compressed), pngChunk("IEND", Buffer.alloc(0))]);
52
+ }
53
+
54
+ function sdist(px, py, x1, y1, x2, y2) {
55
+ const dx = x2 - x1, dy = y2 - y1;
56
+ const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy)));
57
+ return Math.sqrt((px - (x1 + t * dx)) ** 2 + (py - (y1 + t * dy)) ** 2);
58
+ }
59
+
60
+ function generateIcon(size) {
61
+ const ss = 4; // supersample
62
+ const s = size * ss;
63
+ const half = s / 2;
64
+
65
+ // Color palette
66
+ const bg = [13, 17, 23]; // #0d1117
67
+ const accent = [125, 211, 252]; // #7dd3fc
68
+ const glow = [30, 60, 90];
69
+
70
+ const accum = new Float64Array(size * size * 4);
71
+
72
+ for (let sy = 0; sy < s; sy++) {
73
+ for (let sx = 0; sx < s; sx++) {
74
+ // Normalize to -1..1 with aspect ratio
75
+ const x = (sx / s) * 2 - 1;
76
+ const y = (sy / s) * 2 - 1;
77
+
78
+ // Terminal window: rounded rect shape
79
+ const rx = 0.85, ry = 0.8, cr = 0.2;
80
+ let dist;
81
+ // Distance to rounded rect
82
+ const ax = Math.abs(x) - (rx - cr), ay = Math.abs(y) - (ry - cr);
83
+ if (ax > 0 && ay > 0) {
84
+ dist = Math.sqrt(ax * ax + ay * ay);
85
+ } else if (ax > 0) {
86
+ dist = ax;
87
+ } else if (ay > 0) {
88
+ dist = ay;
89
+ } else {
90
+ dist = Math.max(ax, ay); // inside
91
+ }
92
+
93
+ // Anti-aliased edge
94
+ if (dist > 0.015) continue; // outside, skip
95
+ const alpha = dist < -0.015 ? 1 : (1 - (dist + 0.015) / 0.03);
96
+
97
+ // Pixel color
98
+ let r = bg[0], g = bg[1], b = bg[2];
99
+
100
+ // Draw a subtle glow inside
101
+ const innerGlow = Math.sqrt(x * x + y * y) / 0.9;
102
+ if (innerGlow < 0.8) {
103
+ const t = 1 - innerGlow / 0.8;
104
+ r += Math.round(glow[0] * t * 0.5);
105
+ g += Math.round(glow[1] * t * 0.5);
106
+ b += Math.round(glow[2] * t * 0.5);
107
+ }
108
+
109
+ // Draw ">_" prompt using simple geometric shapes
110
+ // ">" chevron: two angled strokes
111
+ const px = 0.05, py = 0.0;
112
+
113
+ // Upper stroke of ">"
114
+ const d1 = sdist(x, y, px - 0.15, py - 0.15, px + 0.15, py + 0.15);
115
+ // Lower stroke of ">"
116
+ const d2 = sdist(x, y, px - 0.15, py + 0.15, px + 0.15, py - 0.15);
117
+
118
+ const strokeW = 0.04;
119
+ const strokeGlow = 0.02;
120
+
121
+ if (d1 < strokeW + strokeGlow || d2 < strokeW + strokeGlow) {
122
+ const t = Math.min(1, Math.max(0, 1 - (Math.min(d1, d2) - strokeW) / strokeGlow));
123
+ r = Math.round(r + (accent[0] - r) * t);
124
+ g = Math.round(g + (accent[1] - g) * t);
125
+ b = Math.round(b + (accent[2] - b) * t);
126
+ }
127
+
128
+ // "_" cursor to the right
129
+ const cu = 0.32, cv = 0.18;
130
+ const dc = Math.abs(y - cv);
131
+ const hc = Math.abs(x - cu);
132
+ const cw = 0.12, ch = 0.03;
133
+ if (hc < cw && dc < ch) {
134
+ const t = Math.min(1, 1 - (dc - ch * 0.6) / (ch * 0.4));
135
+ const t2 = Math.min(1, 1 - Math.max(0, x - (cu - cw * 0.6)) / (cw * 0.4));
136
+ const t3 = Math.min(1, 1 - Math.max(0, (cu + cw * 0.6) - x) / (cw * 0.4));
137
+ const final = Math.min(t, Math.max(t2, t3));
138
+ r = Math.round(r + (accent[0] - r) * final);
139
+ g = Math.round(g + (accent[1] - g) * final);
140
+ b = Math.round(b + (accent[2] - b) * final);
141
+ }
142
+
143
+ // Accumulate into output
144
+ const oi = (Math.floor(sy / ss) * size + Math.floor(sx / ss)) * 4;
145
+ accum[oi] += r * alpha;
146
+ accum[oi+1] += g * alpha;
147
+ accum[oi+2] += b * alpha;
148
+ accum[oi+3] += alpha;
149
+ }
150
+ }
151
+
152
+ return createPNG(size, size, (x, y) => {
153
+ const i = (y * size + x) * 4;
154
+ const a = accum[i+3];
155
+ if (a === 0) return [0, 0, 0, 0];
156
+ const inv = 1 / (ss * ss);
157
+ return [accum[i] / a, accum[i+1] / a, accum[i+2] / a, Math.round(a * inv * 255)];
158
+ });
159
+ }
160
+
161
+ mkdirSync(ICONS_DIR, { recursive: true });
162
+ for (const size of [192, 512]) {
163
+ const png = generateIcon(size);
164
+ const outPath = join(ICONS_DIR, `icon-${size}.png`);
165
+ writeFileSync(outPath, png);
166
+ console.log(`[generate-icons] wrote ${outPath} (${png.length} bytes)`);
167
+ }