tmux-weblink 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
var Ft="https://esm.sh/ghostty-web@0.4.0",Vt=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function ue(e){return e.replace(Vt,"")}var pe;function Yt(){return pe||(document.querySelector("link[data-tmux-web-xterm-css]")?pe=Promise.resolve():(pe=new Promise(e=>{let t=document.createElement("link");t.rel="stylesheet",t.href="/assets/xterm.css",t.setAttribute("data-tmux-web-xterm-css",""),t.addEventListener("load",()=>e(),{once:!0}),t.addEventListener("error",()=>e(),{once:!0}),document.head.appendChild(t)}),pe))}var Ze=class Lt{terminal;fitAddon;container;constructor(t,n,s){this.container=t,this.terminal=n,this.fitAddon=s,this.terminal.loadAddon(this.fitAddon),this.terminal.open(t)}static async create(t,n,s){let[{Terminal:l},{FitAddon:o}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),Yt()]),c=new l({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,theme:s});return new Lt(t,c,new o)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(t){this.terminal.scrollToLine(Math.max(0,t))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){let t=Ge(this.container);return t.width<=0||t.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(t){this.terminal.paste(t)}input(t){this.terminal.input(t,!1)}onData(t){this.terminal.onData(t),this.terminal.onBinary(t)}onResize(t){this.terminal.onResize(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){let t=document.activeElement;return!!t&&(t===this.container||this.container.contains(t))}},
|
|
1
|
+
var Ft="https://esm.sh/ghostty-web@0.4.0",Vt=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function ue(e){return e.replace(Vt,"")}var pe;function Yt(){return pe||(document.querySelector("link[data-tmux-web-xterm-css]")?pe=Promise.resolve():(pe=new Promise(e=>{let t=document.createElement("link");t.rel="stylesheet",t.href="/assets/xterm.css",t.setAttribute("data-tmux-web-xterm-css",""),t.addEventListener("load",()=>e(),{once:!0}),t.addEventListener("error",()=>e(),{once:!0}),document.head.appendChild(t)}),pe))}var Ze=class Lt{terminal;fitAddon;container;constructor(t,n,s){this.container=t,this.terminal=n,this.fitAddon=s,this.terminal.loadAddon(this.fitAddon),this.terminal.open(t)}static async create(t,n,s){let[{Terminal:l},{FitAddon:o}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),Yt()]),c=new l({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,theme:s});return new Lt(t,c,new o)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(t){this.terminal.scrollToLine(Math.max(0,t))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){let t=Ge(this.container);return t.width<=0||t.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(t){this.terminal.paste(t)}input(t){this.terminal.input(t,!1)}onData(t){this.terminal.onData(t),this.terminal.onBinary(t)}onResize(t){this.terminal.onResize(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){let t=document.activeElement;return!!t&&(t===this.container||this.container.contains(t))}},Ot=class Et{terminal;container;colsValue=80;rowsValue=24;charW=0;charH=0;resizeCallbacks=[];constructor(t,n){this.container=t,this.terminal=n,this.terminal.open(t)}static async create(t,n,s){let l=await import(Ft);await l.init();let o=new l.Terminal({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,theme:s});return new Et(t,o)}get cols(){return this.colsValue}get rows(){return this.rowsValue}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom?.()}scrollToLine(t){this.terminal.scrollToLine?.(Math.max(0,t))}isNearScrollbackTop(){let t=this.terminal.buffer?.active;return t?t.viewportY>=t.baseY:!1}viewportY(){return this.terminal.buffer?.active?.viewportY??0}baseY(){return this.terminal.buffer?.active?.baseY??0}fit(){let t=Ge(this.container);if(t.width<=0||t.height<=0)return!1;this.updateCellMetrics(!this.charW||!this.charH);let n=Math.floor(t.width/this.charW),s=Math.floor(t.height/this.charH);if(n<10||s<5)return!1;if(n!==this.colsValue||s!==this.rowsValue){this.colsValue=n,this.rowsValue=s,this.terminal.resize(n,s);for(let l of this.resizeCallbacks)l({cols:n,rows:s})}return!0}focus(){(this.terminal.textarea||this.container.querySelector("textarea")||this.container)?.focus?.()}pasteText(t){let n=this.terminal.getMode?.(2004)??!1;this.input(n?"\x1B[200~"+t+"\x1B[201~":t)}input(t){this.terminal.input?.(t,!1)}onData(t){this.terminal.onData(n=>t(n))}onResize(t){this.resizeCallbacks.push(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){let t=document.activeElement;return!!t&&(t===this.container||t===this.terminal.textarea||this.container.contains(t))}updateCellMetrics(t=!1){let n=this.container.querySelector("canvas");if(n instanceof HTMLElement&&n.offsetWidth>0&&n.offsetHeight>0&&this.colsValue>0&&this.rowsValue>0){let s=n.offsetWidth/this.colsValue,l=n.offsetHeight/this.rowsValue;(t||!this.charW||!this.charH)&&(this.charW=s,this.charH=l)}(!this.charW||!this.charH)&&(this.charW=9,this.charH=18)}};function Ge(e){let t=e.getBoundingClientRect(),n=window.visualViewport;return n?{width:Math.min(t.width,n.width),height:Math.max(0,Math.min(t.height,n.height-Math.max(0,t.top)))}:t}function Kt(e,t,n){return e.innerHTML="",e.classList.add("terminal-pending"),(async()=>{let s;if(n.renderer==="ghostty")try{s=await Ot.create(e,n.scrollback,n.theme)}catch{console.error("[tmux-web] ghostty-web failed; falling back to xterm.js"),s=await Ze.create(e,n.scrollback,n.theme)}else s=await Ze.create(e,n.scrollback,n.theme);let l,o=0,c,r=null,f=0,g="connecting",B=0,oe=!1,re=[],W="",Be=!1,O=1e3,kt=(location.protocol==="https:"?"wss:":"ws:")+"//"+location.host+"/ws/"+encodeURIComponent(t),xt="/api/session/"+encodeURIComponent(t)+"/upload",Tt=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function It(){return re.join("")+W}async function Mt(i,a=0){let d=i?s.viewportY():0,E=i?s.baseY():0,T=ue(It());if(s.reset(),!T){i||s.scrollToBottom();return}await s.write(T),i?s.scrollToLine(E+d+a):s.scrollToBottom()}function K(i){l?.readyState===WebSocket.OPEN&&l.send(JSON.stringify(i))}function Bt(i){let a;try{a=JSON.parse(i)}catch{g==="live"&&(W+=i,s.write(ue(i)));return}if(a.type==="auth.required"){let d=localStorage.getItem("tmux-web-token");d?K({type:"auth.token",token:d}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(a.type==="auth.ok"){a.token&&localStorage.setItem("tmux-web-token",a.token),O=1e3,_(),K({type:"resize",cols:s.cols,rows:s.rows}),L();return}if(a.type==="auth.failed"){if(a.permanentLock||a.retryAfterMs&&a.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(a.message);return}return}if(a.type==="snapshot"&&typeof a.data=="string"){let d=ue(a.data);re=[d],W="",B=typeof a.lines=="number"?a.lines:n.terminal.initialLines,g="live",s.reset(),s.write(d).then(()=>s.scrollToBottom());return}if(a.type==="window_changed"&&typeof a.activeIndex=="number"){let d=new URL(location.href);d.searchParams.get("window")!==String(a.activeIndex)&&(d.searchParams.set("window",String(a.activeIndex)),history.replaceState(history.state,"",d)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:a.windows}));return}if(a.type==="history"&&typeof a.data=="string"){oe=!1,a.lines>0&&a.data&&(re.unshift(a.data),B+=a.lines,Mt(!0,a.lines));return}if(a.type==="data"&&typeof a.data=="string"){let d=ue(a.data);if(g==="connecting"){g="live",W=d,s.write(d);return}g==="live"&&(W+=d,s.write(d))}}function je(){let i=kt,a=localStorage.getItem("tmux-web-token");a&&(i+=(i.includes("?")?"&":"?")+"token="+encodeURIComponent(a)),l=new WebSocket(i),l.onopen=()=>{g="connecting",B=0,oe=!1,re=[],W="",s.reset(),O=1e3},l.onmessage=d=>{typeof d.data=="string"&&Bt(d.data)},l.onclose=()=>{Be||(g="connecting",setTimeout(()=>{O=Math.min(O*2,1e4),Be||je()},O))},l.onerror=()=>l?.close()}function ce(i){K({type:"input",data:i})}function _(){return s.fit()}function Ce(){e.classList.remove("terminal-pending")}function L(){o&&cancelAnimationFrame(o),c&&clearTimeout(c),_(),o=requestAnimationFrame(()=>{o=0,_()&&Ce()}),c=setTimeout(()=>{_()&&Ce()},120)}function Ct(){L(),setTimeout(L,50),setTimeout(L,150),setTimeout(L,300)}function Ue(i){return typeof i=="string"&&i.startsWith("image/")}function Fe(i){return i?.files?.length?i.files[0]??null:null}function St(i){if(!i?.items)return null;for(let a=0;a<i.items.length;a++){let d=i.items[a];if(d?.kind==="file"&&Ue(d.type))return d.getAsFile()}return null}async function Ht(i){let a=new FormData,d=i instanceof File&&i.name?i.name:"upload";a.append("file",i,d);let E=localStorage.getItem("tmux-web-token"),T={};E&&(T.Authorization="Bearer "+E);let He=await fetch(xt,{method:"POST",body:a,headers:T});if(!He.ok){let Xe="upload failed";try{let Je=await He.json();Je.error&&(Xe=Je.error)}catch{}throw new Error(Xe)}let _e=await He.json();if(!_e.path)throw new Error("no path in response");return _e.path}async function Se(i){try{let a=await Ht(i);ce(a)}catch{}}s.onData(i=>ce(i)),s.onResize(({cols:i,rows:a})=>K({type:"resize",cols:i,rows:a})),s.onScroll(()=>{g!=="live"||oe||!s.isNearScrollbackTop()||(oe=!0,K({type:"load_history",before:B}))}),s.attachCustomKeyEventHandler(i=>i.type!=="keydown"?!0:(i.ctrlKey||i.metaKey)&&i.code==="KeyV"?(i.preventDefault(),(async()=>{try{if(navigator.clipboard?.read){let a=await navigator.clipboard.read();for(let d of a)for(let E of d.types)if(Ue(E)){let T=await d.getType(E);await Se(T);return}}}catch{}try{let a=await navigator.clipboard?.readText();a&&s.pasteText(Ve(a))}catch{}})(),!1):!0);let de=0;function Ve(i){return i.split(`\r
|
|
2
2
|
`).join(`
|
|
3
3
|
`).split(`
|
|
4
|
-
`).join("\r")}async function Dt(i){let a=St(i.clipboardData);if(a){i.preventDefault(),i.stopPropagation();let T=Date.now();if(T-de<50)return;de=T,await Se(a);return}let d=i.clipboardData?.getData("text/plain");if(!d)return;i.preventDefault(),i.stopPropagation();let E=Date.now();E-de<50||(de=E,s.pasteText(Ve(d)))}let X=0;function $t(i){i.preventDefault(),X++,Fe(i.dataTransfer)&&e.classList.add("terminal-drag-over")}function Rt(i){i.preventDefault(),i.dataTransfer&&(i.dataTransfer.dropEffect="copy")}function At(i){i.preventDefault(),X--,X<=0&&(X=0,e.classList.remove("terminal-drag-over"))}async function Pt(i){i.preventDefault(),X=0,e.classList.remove("terminal-drag-over");let a=Fe(i.dataTransfer);a&&await Se(a)}function Ye(i){!Tt||i.key!=="Escape"||i.defaultPrevented||i.repeat||i.metaKey||i.ctrlKey||i.altKey||s.isFocused()&&(i.preventDefault(),i.stopPropagation(),ce("\x1B"))}e.addEventListener("paste",Dt),e.addEventListener("dragenter",$t),e.addEventListener("dragover",Rt),e.addEventListener("dragleave",At),e.addEventListener("drop",Pt),document.addEventListener("keydown",Ye,!0);function zt(i){if(i.touches.length!==1){r=null;return}let a=i.touches[0];a&&(r={startX:a.clientX,startY:a.clientY,lastX:a.clientX,lastY:a.clientY,scrolling:!1},i.stopPropagation())}function Nt(i){if(!r||i.touches.length!==1)return;let a=i.touches[0];if(!a)return;let d=a.clientY-r.startY,E=a.clientX-r.startX;if(!r.scrolling){if(Math.abs(d)<8||Math.abs(d)<Math.abs(E))return;r.scrolling=!0,f=Date.now()+500}i.preventDefault(),i.stopPropagation(),Ut(-(a.clientY-r.lastY),a.clientX,a.clientY),r.lastX=a.clientX,r.lastY=a.clientY}function Wt(i){if(!r)return;let a=r.scrolling;r=null,i.stopPropagation(),a?(f=Date.now()+500,i.preventDefault()):s.focus()}function qt(i){r=null,i.stopPropagation()}function jt(i){i.pointerType==="touch"&&Date.now()<f&&(i.preventDefault(),i.stopPropagation())}function Ut(i,a,d){(e.querySelector(".xterm-screen")??e).dispatchEvent(new WheelEvent("wheel",{deltaY:i,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:a,clientY:d,bubbles:!0,cancelable:!0}))}e.addEventListener("touchstart",zt,{passive:!0,capture:!0}),e.addEventListener("touchmove",Nt,{passive:!1,capture:!0}),e.addEventListener("touchend",Wt,{passive:!1,capture:!0}),e.addEventListener("touchcancel",qt,{passive:!0,capture:!0}),e.addEventListener("pointerup",jt,!0);function
|
|
4
|
+
`).join("\r")}async function Dt(i){let a=St(i.clipboardData);if(a){i.preventDefault(),i.stopPropagation();let T=Date.now();if(T-de<50)return;de=T,await Se(a);return}let d=i.clipboardData?.getData("text/plain");if(!d)return;i.preventDefault(),i.stopPropagation();let E=Date.now();E-de<50||(de=E,s.pasteText(Ve(d)))}let X=0;function $t(i){i.preventDefault(),X++,Fe(i.dataTransfer)&&e.classList.add("terminal-drag-over")}function Rt(i){i.preventDefault(),i.dataTransfer&&(i.dataTransfer.dropEffect="copy")}function At(i){i.preventDefault(),X--,X<=0&&(X=0,e.classList.remove("terminal-drag-over"))}async function Pt(i){i.preventDefault(),X=0,e.classList.remove("terminal-drag-over");let a=Fe(i.dataTransfer);a&&await Se(a)}function Ye(i){!Tt||i.key!=="Escape"||i.defaultPrevented||i.repeat||i.metaKey||i.ctrlKey||i.altKey||s.isFocused()&&(i.preventDefault(),i.stopPropagation(),ce("\x1B"))}e.addEventListener("paste",Dt),e.addEventListener("dragenter",$t),e.addEventListener("dragover",Rt),e.addEventListener("dragleave",At),e.addEventListener("drop",Pt),document.addEventListener("keydown",Ye,!0);function zt(i){if(i.touches.length!==1){r=null;return}let a=i.touches[0];a&&(r={startX:a.clientX,startY:a.clientY,lastX:a.clientX,lastY:a.clientY,scrolling:!1},i.stopPropagation())}function Nt(i){if(!r||i.touches.length!==1)return;let a=i.touches[0];if(!a)return;let d=a.clientY-r.startY,E=a.clientX-r.startX;if(!r.scrolling){if(Math.abs(d)<8||Math.abs(d)<Math.abs(E))return;r.scrolling=!0,f=Date.now()+500}i.preventDefault(),i.stopPropagation(),Ut(-(a.clientY-r.lastY),a.clientX,a.clientY),r.lastX=a.clientX,r.lastY=a.clientY}function Wt(i){if(!r)return;let a=r.scrolling;r=null,i.stopPropagation(),a?(f=Date.now()+500,i.preventDefault()):s.focus()}function qt(i){r=null,i.stopPropagation()}function jt(i){i.pointerType==="touch"&&Date.now()<f&&(i.preventDefault(),i.stopPropagation())}function Ut(i,a,d){(e.querySelector(".xterm-screen")??e).dispatchEvent(new WheelEvent("wheel",{deltaY:i,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:a,clientY:d,bubbles:!0,cancelable:!0}))}e.addEventListener("touchstart",zt,{passive:!0,capture:!0}),e.addEventListener("touchmove",Nt,{passive:!1,capture:!0}),e.addEventListener("touchend",Wt,{passive:!1,capture:!0}),e.addEventListener("touchcancel",qt,{passive:!0,capture:!0}),e.addEventListener("pointerup",jt,!0);function Oe(){L()}window.addEventListener("resize",Oe),window.visualViewport?.addEventListener("resize",L),window.visualViewport?.addEventListener("scroll",L);let Ke=new ResizeObserver(()=>L());return Ke.observe(e),document.fonts?.ready.then(()=>{L(),setTimeout(L,50),setTimeout(L,200)}),e.addEventListener("focusin",Ct,!0),requestAnimationFrame(()=>{_()&&Ce(),je(),L()}),{destroy(){if(Be=!0,l){try{l.close(1e3,"session switch")}catch{}l=void 0}e.innerHTML="",e.classList.remove("terminal-pending"),window.removeEventListener("resize",Oe),Ke.disconnect(),document.removeEventListener("keydown",Ye,!0)},focus(){s.focus()},sendInput(i){ce(i)}}})()}var J=window.__TMUX_WEB_SHELL__,u=null,v=null,De="sessions",D="",fe="",me="",Qe="",m=document.getElementById("sidebar-content"),C=document.getElementById("main-placeholder"),w=document.getElementById("terminal-container"),$=document.getElementById("file-editor"),_t=document.getElementById("fe-path"),k=document.getElementById("fe-status"),Z=document.getElementById("fe-content"),P=document.getElementById("fe-save"),Xt=document.getElementById("fe-delete"),Jt=document.getElementById("fe-back"),et=document.getElementById("fe-new-name"),Zt=document.getElementById("fe-new-btn"),$e=document.getElementById("settings-backdrop"),tt=document.getElementById("settings-popover"),nt=document.querySelector(".sidebar");function Gt(){return window.matchMedia("(max-width: 640px)").matches}function Re(){Gt()&&nt.classList.add("collapsed")}function he(){nt.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{he(),Ae("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{he(),Ae("files")}),document.getElementById("mode-browser").addEventListener("click",()=>{he(),Ae("browser")});function Ae(e){De=e,document.querySelectorAll(".mode-btn").forEach(n=>n.classList.remove("active"));let t=document.getElementById(e==="sessions"?"mode-sessions":e==="files"?"mode-files":"mode-browser");t&&t.classList.add("active"),e==="sessions"?(se(),G()):e==="files"?(se(),rn()):(Ln(),I())}async function G(){m.innerHTML='<div class="sidebar-section-label">Sessions</div><button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';try{let t=(await(await fetch("/api/sidebar/sessions")).json()).recent||[];for(let n of t){let s=document.createElement("div");s.className="session-item"+(n.name===v?" active":""),s.setAttribute("data-session",n.name),s.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${n.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(n.name)}</span>
|
|
6
6
|
<button class="session-edit-btn" data-session="${p(n.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
|
-
`,s.addEventListener("click",o=>{o.target.closest(".session-edit-btn")||Q(n.name)}),m.appendChild(s);let l=s.querySelector(".session-edit-btn");l&&l.addEventListener("click",o=>{o.stopPropagation(),kn(n.name,l)})}}catch{m.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function p(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function Q(e){if(!(v===e&&u)){se(),$.style.display="none",C.style.display="none",w.style.display="",w.classList.add("terminal-pending"),Re(),u&&(U(),u.destroy(),u=null),v=e;for(let t of m.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e);try{u=await
|
|
7
|
+
`,s.addEventListener("click",o=>{o.target.closest(".session-edit-btn")||Q(n.name)}),m.appendChild(s);let l=s.querySelector(".session-edit-btn");l&&l.addEventListener("click",o=>{o.stopPropagation(),kn(n.name,l)})}}catch{m.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function p(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function Q(e){if(!(v===e&&u)){se(),$.style.display="none",C.style.display="none",w.style.display="",w.classList.add("terminal-pending"),Re(),u&&(U(),u.destroy(),u=null),v=e;for(let t of m.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e);try{u=await Kt(w,e,{terminal:J.terminal,scrollback:J.scrollback,theme:J.theme,renderer:J.renderer}),Ne()}catch(t){console.error("[shell] terminal init failed:",t);let n=t&&typeof t=="object"&&"message"in t?t.message:String(t);w.textContent="Failed to open terminal for "+e+": "+n,w.classList.remove("terminal-pending")}}}var ve=[],y=null,st=!1;function Qt(e){if(!y||!y.files.length)return null;let t=y.repoRoot;if(!t||!e.startsWith(t))return null;let n=e.slice(t.length+1);for(let s of y.files)if(s.path===n)return s.status;return null}async function en(e){st=!0;try{let t=await fetch("/api/git/status?path="+encodeURIComponent(e));t.ok?y=await t.json():y=null}catch{y=null}finally{st=!1}}function tn(){if(!y||!y.branch)return"";let{branch:e,linesAdded:t,linesRemoved:n}=y,s=`<div class="sidebar-git"><div class="git-branch-badge">
|
|
8
8
|
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M11.63 1.22a.75.75 0 00-1.06 0L5.99 5.8 4.12 3.93a.75.75 0 10-1.06 1.06L4.93 6.87A3.5 3.5 0 003 10.09v3.16a.75.75 0 001 0v-3.16a2.5 2.5 0 012.5-2.5h.27l-1.06 1.06a.75.75 0 001.06 1.06l2.37-2.38a.75.75 0 000-1.06L7.59 5.27l4.04-4.05a.75.75 0 000-1.06L11.63 1.22zM3.25 13.94a.75.75 0 01-.75-.75v-.69a.75.75 0 011.5 0v.69c0 .414-.336.75-.75.75z"/></svg>
|
|
9
9
|
${p(e)}</div>`;return(t>0||n>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${t}</span> <span class="del">-${n}</span></div>`),s+"</div>"}m.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&q&&y&&(e.stopPropagation(),ot())});var ee=document.getElementById("header-git"),Pe=document.getElementById("git-diff-view"),it=document.getElementById("gd-path"),ye=document.getElementById("gd-status"),ge=document.getElementById("gd-content"),nn=document.getElementById("gd-back"),q=null;function j(e){if(!e||!e.branch){ee.style.display="none",q=null;return}q=e.repoRoot;let{branch:t,linesAdded:n,linesRemoved:s}=e,l=`<span class="branch">${p(t)}</span>`;(n>0||s>0)&&(l+=`<span class="sep">\xB7</span>+<span class="diff-add">${n}</span> <span class="diff-del">-${s}</span>`),ee.innerHTML=l,ee.style.display="block"}ee.addEventListener("click",e=>{q&&y&&(e.stopPropagation(),ot())});var b=document.getElementById("git-popover"),we=document.getElementById("git-popover-backdrop");function ze(){let e=ee.getBoundingClientRect();b.style.top=e.bottom+4+"px",b.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function at(){let e=q,t=document.getElementById("git-popover-browse");t&&t.addEventListener("click",()=>{if(Le(),e){De="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));let n=document.getElementById("mode-files");n&&n.classList.add("active"),R(e)}})}var lt="";function ot(){if(!y)return;let e=y.files||[];if(!e.length)b.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
|
|
10
10
|
<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>`;else{let t=`<div class="git-popover-header">${e.length} file${e.length>1?"s":""} changed</div>`;for(let n=0;n<e.length;n++){let s=e[n],l=s.additions||0,o=s.deletions||0,c="";switch(s.status){case"M":c='<span class="git-file-status M">M</span>';break;case"A":c='<span class="git-file-status A">A</span>';break;case"D":c='<span class="git-file-status D">D</span>';break;case"?":c='<span class="git-file-status ?">?</span>';break;case"R":c='<span class="git-file-status R">R</span>';break}t+=`<div class="git-popover-item" data-idx="${n}">
|
|
@@ -27,7 +27,7 @@ var Ft="https://esm.sh/ghostty-web@0.4.0",Vt=/\x1b\](?:10|11|110|111|104)(?:;[^\
|
|
|
27
27
|
<button class="diff-back">\u2190 Back</button>
|
|
28
28
|
<span class="diff-filename">${p(e)}</span>
|
|
29
29
|
<span class="diff-filesize">${l} lines</span>
|
|
30
|
-
</div><div class="diff-content">${s}</div></div>`,b.querySelector(".diff-back")?.addEventListener("click",()=>{b.innerHTML=lt,ze(),y&&b.querySelectorAll(".git-popover-item").forEach(o=>{let c=parseInt(o.dataset.idx||"0",10),r=y.files[c];!r||r.status==="D"||(o.style.cursor="pointer",o.addEventListener("click",()=>ct(r.path)))}),at()})}function ln(e,t,n){if(!t&&!n){ye.textContent="No diff",ge.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}let{html:s,lineCount:l}=rt(t,n);it.textContent=e,ye.textContent=l+" lines",ge.innerHTML=s}function on(){Pe.style.display="none",be?(w.style.display="",w.classList.add("terminal-pending"),Q(be),be=null):v?(w.style.display="",w.classList.add("terminal-pending"),Ne()):C.style.display="flex"}nn.addEventListener("click",on);function Le(){b.classList.remove("open"),we.classList.remove("open")}we.addEventListener("click",Le),document.addEventListener("keydown",e=>{e.key==="Escape"&&(Le(),Me())});async function dt(e){try{let t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){let n=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(n.ok){let s=await n.json();y=s,j(s);return}}}catch{}y=null,j(null)}var Ee=null;function Ne(){U(),v&&(dt(v),Ee=setInterval(()=>{v&&dt(v)},3e4))}function U(){Ee&&(clearInterval(Ee),Ee=null)}function rn(){if(ve=J.fsRoots||[],ve.length===0){if(v){cn(v);return}m.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}m.innerHTML="";for(let e of ve){let 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> ${p(e)}`,t.addEventListener("click",()=>R(e)),m.appendChild(t)}}async function cn(e){m.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();t.path?R(t.path):m.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{m.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function R(e){D=e,m.innerHTML='<div class="file-tree-empty">Loading...</div>';let t=en(e);try{let n=await(await fetch("/api/fs/list?path="+encodeURIComponent(e))).json();await t,m.innerHTML="",j(y),m.innerHTML+=tn();let s=dn(e);if(s&&!ve.some(o=>e===o)){let o=document.createElement("div");o.className="file-tree-item",o.innerHTML="..",o.addEventListener("click",()=>R(s)),m.appendChild(o)}if(n.dirs)for(let o of n.dirs){let c=o.split("/").pop()||o.split("\\").pop(),r=document.createElement("div");r.className="file-tree-item",r.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)}`,r.addEventListener("click",()=>R(o)),m.appendChild(r)}else m.innerHTML="";let l=n.files||[];for(let o of l){let c=o.split("/").pop()||o.split("\\").pop(),r=Qt(o),f="",g="";if(r)switch(r){case"M":f="git-mod",g='<span class="git-file-status M">M</span>';break;case"A":f="git-add",g='<span class="git-file-status A">A</span>';break;case"D":f="git-del",g='<span class="git-file-status D">D</span>';break;case"?":f="git-untracked",g='<span class="git-file-status ?">?</span>';break;case"R":f="git-mod",g='<span class="git-file-status R">R</span>';break}let B=document.createElement("div");B.className="file-tree-item file"+(f?" "+f:""),B.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${g}${p(c)}`,B.addEventListener("click",()=>un(o)),m.appendChild(B)}}catch{m.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function dn(e){let t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function un(e){fe=e,se(),w.style.display="none",u&&(U(),u.destroy(),u=null,v=null),j(null),Re(),C.style.display="none",$.style.display="flex",
|
|
30
|
+
</div><div class="diff-content">${s}</div></div>`,b.querySelector(".diff-back")?.addEventListener("click",()=>{b.innerHTML=lt,ze(),y&&b.querySelectorAll(".git-popover-item").forEach(o=>{let c=parseInt(o.dataset.idx||"0",10),r=y.files[c];!r||r.status==="D"||(o.style.cursor="pointer",o.addEventListener("click",()=>ct(r.path)))}),at()})}function ln(e,t,n){if(!t&&!n){ye.textContent="No diff",ge.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}let{html:s,lineCount:l}=rt(t,n);it.textContent=e,ye.textContent=l+" lines",ge.innerHTML=s}function on(){Pe.style.display="none",be?(w.style.display="",w.classList.add("terminal-pending"),Q(be),be=null):v?(w.style.display="",w.classList.add("terminal-pending"),Ne()):C.style.display="flex"}nn.addEventListener("click",on);function Le(){b.classList.remove("open"),we.classList.remove("open")}we.addEventListener("click",Le),document.addEventListener("keydown",e=>{e.key==="Escape"&&(Le(),Me())});async function dt(e){try{let t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){let n=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(n.ok){let s=await n.json();y=s,j(s);return}}}catch{}y=null,j(null)}var Ee=null;function Ne(){U(),v&&(dt(v),Ee=setInterval(()=>{v&&dt(v)},3e4))}function U(){Ee&&(clearInterval(Ee),Ee=null)}function rn(){if(ve=J.fsRoots||[],ve.length===0){if(v){cn(v);return}m.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}m.innerHTML="";for(let e of ve){let 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> ${p(e)}`,t.addEventListener("click",()=>R(e)),m.appendChild(t)}}async function cn(e){m.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();t.path?R(t.path):m.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{m.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function R(e){D=e,m.innerHTML='<div class="file-tree-empty">Loading...</div>';let t=en(e);try{let n=await(await fetch("/api/fs/list?path="+encodeURIComponent(e))).json();await t,m.innerHTML="",j(y),m.innerHTML+=tn();let s=dn(e);if(s&&!ve.some(o=>e===o)){let o=document.createElement("div");o.className="file-tree-item",o.innerHTML="..",o.addEventListener("click",()=>R(s)),m.appendChild(o)}if(n.dirs)for(let o of n.dirs){let c=o.split("/").pop()||o.split("\\").pop(),r=document.createElement("div");r.className="file-tree-item",r.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)}`,r.addEventListener("click",()=>R(o)),m.appendChild(r)}else m.innerHTML="";let l=n.files||[];for(let o of l){let c=o.split("/").pop()||o.split("\\").pop(),r=Qt(o),f="",g="";if(r)switch(r){case"M":f="git-mod",g='<span class="git-file-status M">M</span>';break;case"A":f="git-add",g='<span class="git-file-status A">A</span>';break;case"D":f="git-del",g='<span class="git-file-status D">D</span>';break;case"?":f="git-untracked",g='<span class="git-file-status ?">?</span>';break;case"R":f="git-mod",g='<span class="git-file-status R">R</span>';break}let B=document.createElement("div");B.className="file-tree-item file"+(f?" "+f:""),B.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${g}${p(c)}`,B.addEventListener("click",()=>un(o)),m.appendChild(B)}}catch{m.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function dn(e){let t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function un(e){fe=e,se(),w.style.display="none",u&&(U(),u.destroy(),u=null,v=null),j(null),Re(),C.style.display="none",$.style.display="flex",_t.textContent=e,k.textContent="Loading...",P.style.display="none";try{let t=await fetch("/api/file?path="+encodeURIComponent(e));if(!t.ok){let s=await t.json();k.textContent=s.error||"Failed to load",Z.value="";return}let n=await t.json();Z.value=n.content,me=n.content,Qe=n.content,k.textContent=n.size+" bytes",P.style.display=""}catch{k.textContent="Failed to load",Z.value=""}}Z.addEventListener("input",()=>{me=Z.value}),P.addEventListener("click",async()=>{P.textContent="Saving...",P.disabled=!0;try{let e=await fetch("/api/file",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:fe,content:me})});if(!e.ok){let t=await e.json();k.textContent=t.error||"Save failed";return}Qe=me,k.textContent="Saved"}catch{k.textContent="Save failed"}finally{P.disabled=!1,P.textContent="Save"}}),Xt.addEventListener("click",async()=>{if(confirm("Delete "+fe+"?"))try{let e=await fetch("/api/file/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:fe})});if(!e.ok){let t=await e.json();k.textContent=t.error||"Delete failed";return}$.style.display="none",C.style.display="flex",D&&R(D)}catch{k.textContent="Delete failed"}}),Jt.addEventListener("click",()=>{$.style.display="none",C.style.display="flex",D&&R(D)}),Zt.addEventListener("click",async()=>{let e=et.value.trim();if(!e)return;let t=D+"/"+e;try{let n=await fetch("/api/file/touch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok){let s=await n.json();k.textContent=s.error||"Create failed";return}et.value="",D&&R(D)}catch{k.textContent="Create failed"}});var ut=document.getElementById("browser-view"),pt=document.getElementById("br-back"),ft=document.getElementById("br-fwd"),pn=document.getElementById("br-reload"),x=document.getElementById("br-address"),fn=document.getElementById("br-new"),z=document.getElementById("browser-content"),mt=document.getElementById("mobile-keys"),S=[],h=null,ht=0,ke=!1,F="none",xe=null,mn="https://html.duckduckgo.com/html/?q=";function V(){return h?S.find(e=>e.id===h)??null:null}function Te(e){return`<svg viewBox="0 0 24 24" fill="currentColor" width="${e}" height="${e}"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>`}function hn(e){let t=e.trim();return/^https?:\/\//i.test(t)||/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(t)?t:!/\s/.test(t)&&/\./.test(t)?"https://"+t:/^(localhost|127\.0\.0\.1)(:\d+)?$/i.test(t)?"http://"+t:mn+encodeURIComponent(t)}function te(){ht++;let e="t"+ht;return S.push({id:e,title:"New Tab",url:null,history:[],historyIdx:-1,error:null,seq:0}),h=e,I(),Ie(),e}function I(){if(De!=="browser")return;let e='<div class="sidebar-section-label">Tabs</div>';for(let n of S){let s=n.id===h?" active":"";e+=`<div class="tab-item${s}" data-tab="${n.id}">
|
|
31
31
|
<span class="tab-fav">${Te(16)}</span>
|
|
32
32
|
<span class="tab-title">${p(n.title)}</span>
|
|
33
33
|
<button class="tab-close" data-close="${n.id}" title="Close tab" aria-label="Close tab">×</button>
|
|
@@ -42,7 +42,7 @@ var Ft="https://esm.sh/ghostty-web@0.4.0",Vt=/\x1b\](?:10|11|110|111|104)(?:;[^\
|
|
|
42
42
|
</div>`}function yt(){z.innerHTML=`<div class="browser-state">
|
|
43
43
|
<div class="big">${Te(44)}</div>
|
|
44
44
|
<div>Loading\u2026</div>
|
|
45
|
-
</div>`}async function A(e,t,n){let s=S.find(c=>c.id===e);if(!s)return;let l=hn(t);if(!l)return;let o=++s.seq;n&&(s.history=s.history.slice(0,s.historyIdx+1),s.history.push(l),s.historyIdx=s.history.length-1),s.url=l,s.error=null,h===e&&(x.value=l,Y(),yt());try{let c=await fetch("/api/browse?url="+encodeURIComponent(l)+"&tab="+encodeURIComponent(e)),r=await c.json();if(s.seq!==o)return;if(!c.ok||r.error){let g=r.error||"HTTP "+c.status;s.error=g,h===e&&ne(g),I();return}let f=r.finalUrl||l;s.url=f,s.title=r.title||s.title,s.history[s.historyIdx]=f,h===e&&(x.value=f,Y(),gt(r)),I()}catch(c){if(s.seq!==o)return;s.error="Network error: "+(c instanceof Error?c.message:String(c)),h===e&&ne(s.error),I()}}function yn(){let e=V();!e||e.historyIdx<=0||(e.historyIdx--,A(e.id,e.history[e.historyIdx],!1))}function gn(){let e=V();!e||e.historyIdx>=e.history.length-1||(e.historyIdx++,A(e.id,e.history[e.historyIdx],!1))}function gt(e){let t="";e.truncated&&(t+='<div class="browser-bar"><span class="err">Response truncated \u2014 page may be incomplete.</span></div>'),t+=e.html||"",z.innerHTML=t,wn(),z.scrollTop=0}function wn(){z.querySelectorAll("a[href]").forEach(e=>{e.addEventListener("click",t=>{let n=e.getAttribute("href")||"";if(n.startsWith("/api/browse?")){t.preventDefault();let s=t,l=null;try{l=new URL(n,location.origin).searchParams.get("url")}catch{return}if(!l)return;if(s.ctrlKey||s.metaKey||s.shiftKey){let o=te();A(o,l,!0)}else A(h,l,!0)}else/^(about|blob|file):/i.test(n)&&t.preventDefault()})}),z.querySelectorAll("form").forEach(e=>{e.addEventListener("submit",t=>{t.preventDefault();let n=e.getAttribute("action")||"";if(!n.startsWith("/api/browse/submit?"))return;let s=null;try{s=new URL(n,location.origin).searchParams.get("tab")||h}catch{s=h}if(!s)return;let l=new FormData(e),o=new URLSearchParams;l.forEach((c,r)=>o.append(r,String(c))),bn(s,o)})})}async function bn(e,t){let n=S.find(o=>o.id===e);if(!n)return;let s=++n.seq,l=t.get("__browse_action")||"";
|
|
45
|
+
</div>`}async function A(e,t,n){let s=S.find(c=>c.id===e);if(!s)return;let l=hn(t);if(!l)return;let o=++s.seq;n&&(s.history=s.history.slice(0,s.historyIdx+1),s.history.push(l),s.historyIdx=s.history.length-1),s.url=l,s.error=null,h===e&&(x.value=l,Y(),yt());try{let c=await fetch("/api/browse?url="+encodeURIComponent(l)+"&tab="+encodeURIComponent(e)),r=await c.json();if(s.seq!==o)return;if(!c.ok||r.error){let g=r.error||"HTTP "+c.status;s.error=g,h===e&&ne(g),I();return}let f=r.finalUrl||l;s.url=f,s.title=r.title||s.title,s.history[s.historyIdx]=f,h===e&&(x.value=f,Y(),gt(r)),I()}catch(c){if(s.seq!==o)return;s.error="Network error: "+(c instanceof Error?c.message:String(c)),h===e&&ne(s.error),I()}}function yn(){let e=V();!e||e.historyIdx<=0||(e.historyIdx--,A(e.id,e.history[e.historyIdx],!1))}function gn(){let e=V();!e||e.historyIdx>=e.history.length-1||(e.historyIdx++,A(e.id,e.history[e.historyIdx],!1))}function gt(e){let t="";e.truncated&&(t+='<div class="browser-bar"><span class="err">Response truncated \u2014 page may be incomplete.</span></div>'),t+=e.html||"",z.innerHTML=t,wn(),z.scrollTop=0}function wn(){z.querySelectorAll("a[href]").forEach(e=>{e.addEventListener("click",t=>{let n=e.getAttribute("href")||"";if(n.startsWith("/api/browse?")){t.preventDefault();let s=t,l=null;try{l=new URL(n,location.origin).searchParams.get("url")}catch{return}if(!l)return;if(s.ctrlKey||s.metaKey||s.shiftKey){let o=te();A(o,l,!0)}else A(h,l,!0)}else/^(about|blob|file):/i.test(n)&&t.preventDefault()})}),z.querySelectorAll("form").forEach(e=>{e.addEventListener("submit",t=>{t.preventDefault();let n=e.getAttribute("action")||"";if(!n.startsWith("/api/browse/submit?"))return;let s=null;try{s=new URL(n,location.origin).searchParams.get("tab")||h}catch{s=h}if(!s)return;let l=new FormData(e),o=new URLSearchParams;l.forEach((c,r)=>o.append(r,String(c))),bn(s,o)})})}async function bn(e,t){let n=S.find(o=>o.id===e);if(!n)return;let s=++n.seq,l=t.get("__browse_action")||"";h===e&&yt();try{let o=await fetch("/api/browse/submit?tab="+encodeURIComponent(e),{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:t.toString()}),c=await o.json();if(n.seq!==s)return;if(!o.ok||c.error){let f=c.error||"HTTP "+o.status;n.error=f,h===e&&ne(f);return}let r=c.finalUrl||l||n.url||"";n.url=r,n.title=c.title||n.title,n.history=n.history.slice(0,n.historyIdx+1),n.history.push(r),n.historyIdx=n.history.length-1,n.error=null,h===e&&(x.value=r,Y(),gt(c)),I()}catch(o){if(n.seq!==s)return;n.error="Network error: "+(o instanceof Error?o.message:String(o)),h===e&&ne(n.error)}}function Ln(){if(!ke){if(ke=!0,$.style.display!=="none"?(F="files",u&&(U(),u.destroy(),u=null)):u||w.style.display!=="none"?(F="terminal",xe=v,u&&(U(),u.destroy(),u=null),v=null):F="none",w.style.display="none",$.style.display="none",Pe.style.display="none",C.style.display="none",j(null),mt.style.display="none",ut.style.display="flex",!h)te();else{I();let e=V();e&&(x.value=e.url||"",Y(),e.url?A(e.id,e.url,!1):Ie())}he()}}function se(){if(ke){if(ke=!1,ut.style.display="none",mt.style.display="",F==="files")$.style.display="flex";else if(F==="terminal"&&xe){let e=xe;xe=null,Q(e)}else v?(w.style.display="",w.classList.add("terminal-pending"),Ne()):C.style.display="flex";F="none"}}pt.addEventListener("click",yn),ft.addEventListener("click",gn),pn.addEventListener("click",()=>{let e=V();e&&e.url&&A(e.id,e.url,!1)}),fn.addEventListener("click",()=>{te(),x.focus(),x.select()}),x.addEventListener("keydown",e=>{if(e.key==="Enter"){e.preventDefault();let t=h||te();A(t,x.value,!0),x.blur()}}),document.getElementById("mode-settings").addEventListener("click",()=>{let e=tt.classList.toggle("open");$e.classList.toggle("open",e)}),$e.addEventListener("click",()=>{tt.classList.remove("open"),$e.classList.remove("open")});var N="",ie=document.createElement("div");ie.className="session-popover-backdrop",document.body.appendChild(ie);var H=document.createElement("div");H.id="session-popover",H.className="session-popover",H.style.display="none",H.innerHTML=`<div class="sp-field">
|
|
46
46
|
<label>Rename session</label>
|
|
47
47
|
<input type="text" id="sp-name" />
|
|
48
48
|
</div>
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import{initTerminal as
|
|
1
|
+
import{initTerminal as We}from"./terminal-core.js";const q=window.__TMUX_WEB_SHELL__;let c=null,u=null,pe="sessions",B="",X="",Y="",Le="";const f=document.getElementById("sidebar-content"),E=document.getElementById("main-placeholder"),v=document.getElementById("terminal-container"),I=document.getElementById("file-editor"),Ge=document.getElementById("fe-path"),g=document.getElementById("fe-status"),U=document.getElementById("fe-content"),M=document.getElementById("fe-save"),Oe=document.getElementById("fe-delete"),Ve=document.getElementById("fe-back"),Ee=document.getElementById("fe-new-name"),_e=document.getElementById("fe-new-btn"),ue=document.getElementById("settings-backdrop"),ke=document.getElementById("settings-popover"),xe=document.querySelector(".sidebar");function Ke(){return window.matchMedia("(max-width: 640px)").matches}function me(){Ke()&&xe.classList.add("collapsed")}function Q(){xe.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{Q(),ve("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{Q(),ve("files")}),document.getElementById("mode-browser").addEventListener("click",()=>{Q(),ve("browser")});function ve(e){pe=e,document.querySelectorAll(".mode-btn").forEach(n=>n.classList.remove("active"));const t=document.getElementById(e==="sessions"?"mode-sessions":e==="files"?"mode-files":"mode-browser");t&&t.classList.add("active"),e==="sessions"?(V(),j()):e==="files"?(V(),st()):(yt(),w())}async function j(){f.innerHTML='<div class="sidebar-section-label">Sessions</div><button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';try{const s=(await(await fetch("/api/sidebar/sessions")).json()).recent||[];for(const i of s){const a=document.createElement("div");a.className="session-item"+(i.name===u?" active":""),a.setAttribute("data-session",i.name),a.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${i.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
2
|
<span>${d(i.name)}</span>
|
|
3
3
|
<button class="session-edit-btn" data-session="${d(i.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
|
-
`,a.addEventListener("click",l=>{l.target.closest(".session-edit-btn")||z(i.name)}),f.appendChild(a);const o=a.querySelector(".session-edit-btn");o&&o.addEventListener("click",l=>{l.stopPropagation(),gt(i.name,o)})}}catch{f.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 d(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function z(e){if(!(u===e&&c)){
|
|
4
|
+
`,a.addEventListener("click",l=>{l.target.closest(".session-edit-btn")||z(i.name)}),f.appendChild(a);const o=a.querySelector(".session-edit-btn");o&&o.addEventListener("click",l=>{l.stopPropagation(),gt(i.name,o)})}}catch{f.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 d(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function z(e){if(!(u===e&&c)){V(),I.style.display="none",E.style.display="none",v.style.display="",v.classList.add("terminal-pending"),me(),c&&(N(),c.destroy(),c=null),u=e;for(const t of f.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e);try{c=await We(v,e,{terminal:q.terminal,scrollback:q.scrollback,theme:q.theme,renderer:q.renderer}),ge()}catch(t){console.error("[shell] terminal init failed:",t);const n=t&&typeof t=="object"&&"message"in t?t.message:String(t);v.textContent="Failed to open terminal for "+e+": "+n,v.classList.remove("terminal-pending")}}}let ee=[],m=null,Be=!1;function Je(e){if(!m||!m.files.length)return null;const t=m.repoRoot;if(!t||!e.startsWith(t))return null;const n=e.slice(t.length+1);for(const s of m.files)if(s.path===n)return s.status;return null}async function Ze(e){Be=!0;try{const t=await fetch("/api/git/status?path="+encodeURIComponent(e));t.ok?m=await t.json():m=null}catch{m=null}finally{Be=!1}}function Xe(){if(!m||!m.branch)return"";const{branch:e,linesAdded:t,linesRemoved:n}=m;let s=`<div class="sidebar-git"><div class="git-branch-badge">
|
|
5
5
|
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M11.63 1.22a.75.75 0 00-1.06 0L5.99 5.8 4.12 3.93a.75.75 0 10-1.06 1.06L4.93 6.87A3.5 3.5 0 003 10.09v3.16a.75.75 0 001 0v-3.16a2.5 2.5 0 012.5-2.5h.27l-1.06 1.06a.75.75 0 001.06 1.06l2.37-2.38a.75.75 0 000-1.06L7.59 5.27l4.04-4.05a.75.75 0 000-1.06L11.63 1.22zM3.25 13.94a.75.75 0 01-.75-.75v-.69a.75.75 0 011.5 0v.69c0 .414-.336.75-.75.75z"/></svg>
|
|
6
|
-
${d(e)}</div>`;return(t>0||n>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${t}</span> <span class="del">-${n}</span></div>`),s+"</div>"}f.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&R&&m&&(e.stopPropagation(),Me())});const
|
|
6
|
+
${d(e)}</div>`;return(t>0||n>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${t}</span> <span class="del">-${n}</span></div>`),s+"</div>"}f.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&R&&m&&(e.stopPropagation(),Me())});const W=document.getElementById("header-git"),ye=document.getElementById("git-diff-view"),Ie=document.getElementById("gd-path"),te=document.getElementById("gd-status"),ne=document.getElementById("gd-content"),Ye=document.getElementById("gd-back");let R=null;function D(e){if(!e||!e.branch){W.style.display="none",R=null;return}R=e.repoRoot;const{branch:t,linesAdded:n,linesRemoved:s}=e;let i=`<span class="branch">${d(t)}</span>`;(n>0||s>0)&&(i+=`<span class="sep">\xB7</span>+<span class="diff-add">${n}</span> <span class="diff-del">-${s}</span>`),W.innerHTML=i,W.style.display="block"}W.addEventListener("click",e=>{R&&m&&(e.stopPropagation(),Me())});const y=document.getElementById("git-popover"),se=document.getElementById("git-popover-backdrop");function he(){const e=W.getBoundingClientRect();y.style.top=e.bottom+4+"px",y.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function Ce(){const e=R,t=document.getElementById("git-popover-browse");t&&t.addEventListener("click",()=>{if(oe(),e){pe="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));const n=document.getElementById("mode-files");n&&n.classList.add("active"),C(e)}})}let Te="";function Me(){if(!m)return;const e=m.files||[];if(!e.length)y.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
|
|
7
7
|
<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>`;else{let t=`<div class="git-popover-header">${e.length} file${e.length>1?"s":""} changed</div>`;for(let n=0;n<e.length;n++){const s=e[n],i=s.additions||0,a=s.deletions||0;let o="";switch(s.status){case"M":o='<span class="git-file-status M">M</span>';break;case"A":o='<span class="git-file-status A">A</span>';break;case"D":o='<span class="git-file-status D">D</span>';break;case"?":o='<span class="git-file-status ?">?</span>';break;case"R":o='<span class="git-file-status R">R</span>';break}t+=`<div class="git-popover-item" data-idx="${n}">
|
|
8
8
|
${o}<span class="file-path">${d(s.path)}</span>
|
|
9
9
|
<span class="file-add">${i>0?"+"+i:""}</span>
|
|
10
10
|
<span class="file-del">${a>0?"-"+a:""}</span>
|
|
11
11
|
</div>`}t+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',Te=t,y.innerHTML=t,y.querySelectorAll(".git-popover-item").forEach(n=>{const s=parseInt(n.dataset.idx||"0",10),i=e[s];!i||i.status==="D"||(n.classList.add("clickable"),n.style.cursor="pointer",n.addEventListener("click",()=>He(i.path)))})}he(),y.classList.add("open"),se.classList.add("open"),Ce()}function Qe(){return window.innerWidth>=1024}function Se(e,t){const s=(e+(t&&e?`
|
|
12
12
|
`:"")+t).split(`
|
|
13
|
-
`);let i="",a=0,o=0;for(const l of s){const r=l;if(r.startsWith("@@")){const h=r.match(/@@\s+-(\d+)[^+]*\+(\d+)/);h&&(a=parseInt(h[1],10)-1,o=parseInt(h[2],10)-1),i+=`<div class="diff-line hunk"><span class="ln-body">${d(r)}</span></div>`;continue}if(r.startsWith("diff --git")||r.startsWith("index ")||r.startsWith("--- ")||r.startsWith("+++ ")||r.startsWith("new file")||r.startsWith("deleted file")){r.startsWith("diff --git")&&(i+=`<div class="diff-line header"><span class="ln-body">${d(r)}</span></div>`);continue}if(r.startsWith("+"))o++,i+=`<div class="diff-line add"><span class="ln-no">${o}</span><span class="ln-body">${d(r)}</span></div>`;else if(r.startsWith("-"))a++,i+=`<div class="diff-line del"><span class="ln-no">${a}</span><span class="ln-body">${d(r)}</span></div>`;else if(r.startsWith("\\"))i+=`<div class="diff-line"><span class="ln-body">${d(r)}</span></div>`;else{if(!r&&!a&&!o)continue;a++,o++,i+=`<div class="diff-line"><span class="ln-no">${a}</span><span class="ln-no" style="margin-right:12px">${o}</span><span class="ln-body">${d(r)}</span></div>`}}return{html:i,lineCount:s.length}}let ie=null;async function He(e){const t=R;if(t)if(oe(),
|
|
13
|
+
`);let i="",a=0,o=0;for(const l of s){const r=l;if(r.startsWith("@@")){const h=r.match(/@@\s+-(\d+)[^+]*\+(\d+)/);h&&(a=parseInt(h[1],10)-1,o=parseInt(h[2],10)-1),i+=`<div class="diff-line hunk"><span class="ln-body">${d(r)}</span></div>`;continue}if(r.startsWith("diff --git")||r.startsWith("index ")||r.startsWith("--- ")||r.startsWith("+++ ")||r.startsWith("new file")||r.startsWith("deleted file")){r.startsWith("diff --git")&&(i+=`<div class="diff-line header"><span class="ln-body">${d(r)}</span></div>`);continue}if(r.startsWith("+"))o++,i+=`<div class="diff-line add"><span class="ln-no">${o}</span><span class="ln-body">${d(r)}</span></div>`;else if(r.startsWith("-"))a++,i+=`<div class="diff-line del"><span class="ln-no">${a}</span><span class="ln-body">${d(r)}</span></div>`;else if(r.startsWith("\\"))i+=`<div class="diff-line"><span class="ln-body">${d(r)}</span></div>`;else{if(!r&&!a&&!o)continue;a++,o++,i+=`<div class="diff-line"><span class="ln-no">${a}</span><span class="ln-no" style="margin-right:12px">${o}</span><span class="ln-body">${d(r)}</span></div>`}}return{html:i,lineCount:s.length}}let ie=null;async function He(e){const t=R;if(t)if(oe(),V(),Qe()){v.style.display="none",c&&(N(),c.destroy(),c=null,ie=u,u=null),D(null),I.style.display="none",E.style.display="none",ye.style.display="flex",Ie.textContent=e,te.textContent="Loading\u2026",ne.innerHTML="";try{const n=await fetch("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e));if(!n.ok)throw new Error(await n.text());const s=await n.json();tt(e,s.diff,s.stagedDiff)}catch(n){const s=n instanceof Error?n.message:String(n);te.textContent="Error",ne.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${d(s)}</div>`}}else{y.innerHTML=`<div class="git-diff-view"><div class="diff-header">
|
|
14
14
|
<button class="diff-back">\u2190 Back</button>
|
|
15
15
|
<span class="diff-filename">${d(e)}</span>
|
|
16
16
|
<span class="diff-loading">Loading\u2026</span>
|
|
@@ -24,22 +24,22 @@ import{initTerminal as _e}from"./terminal-core.js";const q=window.__TMUX_WEB_SHE
|
|
|
24
24
|
<button class="diff-back">\u2190 Back</button>
|
|
25
25
|
<span class="diff-filename">${d(e)}</span>
|
|
26
26
|
<span class="diff-filesize">${i} lines</span>
|
|
27
|
-
</div><div class="diff-content">${s}</div></div>`,y.querySelector(".diff-back")?.addEventListener("click",()=>{y.innerHTML=Te,he(),m&&y.querySelectorAll(".git-popover-item").forEach(a=>{const o=parseInt(a.dataset.idx||"0",10),l=m.files[o];!l||l.status==="D"||(a.style.cursor="pointer",a.addEventListener("click",()=>He(l.path)))}),Ce()})}function tt(e,t,n){if(!t&&!n){te.textContent="No diff",ne.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}const{html:s,lineCount:i}=Se(t,n);Ie.textContent=e,te.textContent=i+" lines",ne.innerHTML=s}function nt(){ye.style.display="none",ie?(v.style.display="",v.classList.add("terminal-pending"),z(ie),ie=null):u?(v.style.display="",v.classList.add("terminal-pending"),ge()):E.style.display="flex"}Ye.addEventListener("click",nt);function oe(){y.classList.remove("open"),se.classList.remove("open")}se.addEventListener("click",oe),document.addEventListener("keydown",e=>{e.key==="Escape"&&(oe(),fe())});async function $e(e){try{const n=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(n.path){const s=await fetch("/api/git/status?path="+encodeURIComponent(n.path));if(s.ok){const i=await s.json();m=i,D(i);return}}}catch{}m=null,D(null)}let ae=null;function ge(){N(),u&&($e(u),ae=setInterval(()=>{u&&$e(u)},3e4))}function N(){ae&&(clearInterval(ae),ae=null)}function st(){if(ee=q.fsRoots||[],ee.length===0){if(u){it(u);return}f.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}f.innerHTML="";for(const e of ee){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> ${d(e)}`,t.addEventListener("click",()=>C(e)),f.appendChild(t)}}async function it(e){f.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const n=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();n.path?C(n.path):f.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{f.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function C(e){B=e,f.innerHTML='<div class="file-tree-empty">Loading...</div>';const t=Ze(e);try{const s=await(await fetch("/api/fs/list?path="+encodeURIComponent(e))).json();await t,f.innerHTML="",D(m),f.innerHTML+=Xe();const i=ot(e);if(i&&!ee.some(l=>e===l)){const l=document.createElement("div");l.className="file-tree-item",l.innerHTML="..",l.addEventListener("click",()=>C(i)),f.appendChild(l)}if(s.dirs)for(const o of s.dirs){const l=o.split("/").pop()||o.split("\\").pop(),r=document.createElement("div");r.className="file-tree-item",r.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> ${d(l)}`,r.addEventListener("click",()=>C(o)),f.appendChild(r)}else f.innerHTML="";const a=s.files||[];for(const o of a){const l=o.split("/").pop()||o.split("\\").pop(),r=Je(o);let h="",$="";if(r)switch(r){case"M":h="git-mod",$='<span class="git-file-status M">M</span>';break;case"A":h="git-add",$='<span class="git-file-status A">A</span>';break;case"D":h="git-del",$='<span class="git-file-status D">D</span>';break;case"?":h="git-untracked",$='<span class="git-file-status ?">?</span>';break;case"R":h="git-mod",$='<span class="git-file-status R">R</span>';break}const Z=document.createElement("div");Z.className="file-tree-item file"+(h?" "+h:""),Z.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${$}${d(l)}`,Z.addEventListener("click",()=>at(o)),f.appendChild(Z)}}catch{f.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function ot(e){const t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function at(e){X=e,
|
|
27
|
+
</div><div class="diff-content">${s}</div></div>`,y.querySelector(".diff-back")?.addEventListener("click",()=>{y.innerHTML=Te,he(),m&&y.querySelectorAll(".git-popover-item").forEach(a=>{const o=parseInt(a.dataset.idx||"0",10),l=m.files[o];!l||l.status==="D"||(a.style.cursor="pointer",a.addEventListener("click",()=>He(l.path)))}),Ce()})}function tt(e,t,n){if(!t&&!n){te.textContent="No diff",ne.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}const{html:s,lineCount:i}=Se(t,n);Ie.textContent=e,te.textContent=i+" lines",ne.innerHTML=s}function nt(){ye.style.display="none",ie?(v.style.display="",v.classList.add("terminal-pending"),z(ie),ie=null):u?(v.style.display="",v.classList.add("terminal-pending"),ge()):E.style.display="flex"}Ye.addEventListener("click",nt);function oe(){y.classList.remove("open"),se.classList.remove("open")}se.addEventListener("click",oe),document.addEventListener("keydown",e=>{e.key==="Escape"&&(oe(),fe())});async function $e(e){try{const n=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(n.path){const s=await fetch("/api/git/status?path="+encodeURIComponent(n.path));if(s.ok){const i=await s.json();m=i,D(i);return}}}catch{}m=null,D(null)}let ae=null;function ge(){N(),u&&($e(u),ae=setInterval(()=>{u&&$e(u)},3e4))}function N(){ae&&(clearInterval(ae),ae=null)}function st(){if(ee=q.fsRoots||[],ee.length===0){if(u){it(u);return}f.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}f.innerHTML="";for(const e of ee){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> ${d(e)}`,t.addEventListener("click",()=>C(e)),f.appendChild(t)}}async function it(e){f.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const n=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();n.path?C(n.path):f.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{f.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function C(e){B=e,f.innerHTML='<div class="file-tree-empty">Loading...</div>';const t=Ze(e);try{const s=await(await fetch("/api/fs/list?path="+encodeURIComponent(e))).json();await t,f.innerHTML="",D(m),f.innerHTML+=Xe();const i=ot(e);if(i&&!ee.some(l=>e===l)){const l=document.createElement("div");l.className="file-tree-item",l.innerHTML="..",l.addEventListener("click",()=>C(i)),f.appendChild(l)}if(s.dirs)for(const o of s.dirs){const l=o.split("/").pop()||o.split("\\").pop(),r=document.createElement("div");r.className="file-tree-item",r.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> ${d(l)}`,r.addEventListener("click",()=>C(o)),f.appendChild(r)}else f.innerHTML="";const a=s.files||[];for(const o of a){const l=o.split("/").pop()||o.split("\\").pop(),r=Je(o);let h="",$="";if(r)switch(r){case"M":h="git-mod",$='<span class="git-file-status M">M</span>';break;case"A":h="git-add",$='<span class="git-file-status A">A</span>';break;case"D":h="git-del",$='<span class="git-file-status D">D</span>';break;case"?":h="git-untracked",$='<span class="git-file-status ?">?</span>';break;case"R":h="git-mod",$='<span class="git-file-status R">R</span>';break}const Z=document.createElement("div");Z.className="file-tree-item file"+(h?" "+h:""),Z.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${$}${d(l)}`,Z.addEventListener("click",()=>at(o)),f.appendChild(Z)}}catch{f.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function ot(e){const t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function at(e){X=e,V(),v.style.display="none",c&&(N(),c.destroy(),c=null,u=null),D(null),me(),E.style.display="none",I.style.display="flex",Ge.textContent=e,g.textContent="Loading...",M.style.display="none";try{const t=await fetch("/api/file?path="+encodeURIComponent(e));if(!t.ok){const s=await t.json();g.textContent=s.error||"Failed to load",U.value="";return}const n=await t.json();U.value=n.content,Y=n.content,Le=n.content,g.textContent=n.size+" bytes",M.style.display=""}catch{g.textContent="Failed to load",U.value=""}}U.addEventListener("input",()=>{Y=U.value}),M.addEventListener("click",async()=>{M.textContent="Saving...",M.disabled=!0;try{const e=await fetch("/api/file",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:X,content:Y})});if(!e.ok){const t=await e.json();g.textContent=t.error||"Save failed";return}Le=Y,g.textContent="Saved"}catch{g.textContent="Save failed"}finally{M.disabled=!1,M.textContent="Save"}}),Oe.addEventListener("click",async()=>{if(confirm("Delete "+X+"?"))try{const e=await fetch("/api/file/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:X})});if(!e.ok){const t=await e.json();g.textContent=t.error||"Delete failed";return}I.style.display="none",E.style.display="flex",B&&C(B)}catch{g.textContent="Delete failed"}}),Ve.addEventListener("click",()=>{I.style.display="none",E.style.display="flex",B&&C(B)}),_e.addEventListener("click",async()=>{const e=Ee.value.trim();if(!e)return;const t=B+"/"+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 s=await n.json();g.textContent=s.error||"Create failed";return}Ee.value="",B&&C(B)}catch{g.textContent="Create failed"}});const Re=document.getElementById("browser-view"),De=document.getElementById("br-back"),Ne=document.getElementById("br-fwd"),rt=document.getElementById("br-reload"),b=document.getElementById("br-address"),lt=document.getElementById("br-new"),S=document.getElementById("browser-content"),Pe=document.getElementById("mobile-keys");let k=[],p=null,Ae=0,re=!1,P="none",le=null;const ct="https://html.duckduckgo.com/html/?q=";function A(){return p?k.find(e=>e.id===p)??null:null}function ce(e){return`<svg viewBox="0 0 24 24" fill="currentColor" width="${e}" height="${e}"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>`}function dt(e){let t=e.trim();return/^https?:\/\//i.test(t)||/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(t)?t:!/\s/.test(t)&&/\./.test(t)?"https://"+t:/^(localhost|127\.0\.0\.1)(:\d+)?$/i.test(t)?"http://"+t:ct+encodeURIComponent(t)}function G(){Ae++;const e="t"+Ae;return k.push({id:e,title:"New Tab",url:null,history:[],historyIdx:-1,error:null,seq:0}),p=e,w(),de(),e}function w(){if(pe!=="browser")return;let e='<div class="sidebar-section-label">Tabs</div>';for(const n of k){const s=n.id===p?" active":"";e+=`<div class="tab-item${s}" data-tab="${n.id}">
|
|
28
28
|
<span class="tab-fav">${ce(16)}</span>
|
|
29
29
|
<span class="tab-title">${d(n.title)}</span>
|
|
30
30
|
<button class="tab-close" data-close="${n.id}" title="Close tab" aria-label="Close tab">×</button>
|
|
31
|
-
</div>`}e+='<button class="tab-new" id="tab-new-btn"><svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg> New Tab</button>',f.innerHTML=e,f.querySelectorAll(".tab-item").forEach(n=>{n.addEventListener("click",s=>{s.target.closest(".tab-close")||Fe(n.dataset.tab)})}),f.querySelectorAll(".tab-close").forEach(n=>{n.addEventListener("click",s=>{s.stopPropagation(),ft(n.dataset.close)})});const t=document.getElementById("tab-new-btn");t&&t.addEventListener("click",()=>{
|
|
31
|
+
</div>`}e+='<button class="tab-new" id="tab-new-btn"><svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg> New Tab</button>',f.innerHTML=e,f.querySelectorAll(".tab-item").forEach(n=>{n.addEventListener("click",s=>{s.target.closest(".tab-close")||Fe(n.dataset.tab)})}),f.querySelectorAll(".tab-close").forEach(n=>{n.addEventListener("click",s=>{s.stopPropagation(),ft(n.dataset.close)})});const t=document.getElementById("tab-new-btn");t&&t.addEventListener("click",()=>{G(),b.focus()})}function Fe(e){p=e,w();const t=A();t&&(b.value=t.url||"",F(),t.error?O(t.error):t.url?T(t.id,t.url,!1):de())}function ft(e){const t=k.findIndex(n=>n.id===e);if(t!==-1){if(k.splice(t,1),k.length===0){p=null,w(),de();return}p===e?(p=k[Math.max(0,t-1)].id,Fe(p)):w()}}function F(){const e=A();De.disabled=!e||e.historyIdx<=0,Ne.disabled=!e||e.historyIdx>=e.history.length-1}function de(){S.innerHTML=`<div class="browser-state">
|
|
32
32
|
<div class="big">${ce(44)}</div>
|
|
33
33
|
<div>Type a URL or search terms in the address bar above.</div>
|
|
34
34
|
<div class="hint">Ctrl/Cmd-click any link to open it in a new tab.</div>
|
|
35
|
-
</div>`,F()}function
|
|
35
|
+
</div>`,F()}function O(e){S.innerHTML=`<div class="browser-state">
|
|
36
36
|
<div class="big" style="color:#ef4444">${ce(44)}</div>
|
|
37
37
|
<div>Could not load page</div>
|
|
38
38
|
<div class="hint" style="color:#ef4444">${d(e)}</div>
|
|
39
39
|
</div>`}function qe(){S.innerHTML=`<div class="browser-state">
|
|
40
40
|
<div class="big">${ce(44)}</div>
|
|
41
41
|
<div>Loading\u2026</div>
|
|
42
|
-
</div>`}async function T(e,t,n){const s=k.find(o=>o.id===e);if(!s)return;const i=dt(t);if(!i)return;const a=++s.seq;n&&(s.history=s.history.slice(0,s.historyIdx+1),s.history.push(i),s.historyIdx=s.history.length-1),s.url=i,s.error=null,p===e&&(b.value=i,F(),qe());try{const o=await fetch("/api/browse?url="+encodeURIComponent(i)+"&tab="+encodeURIComponent(e)),l=await o.json();if(s.seq!==a)return;if(!o.ok||l.error){const h=l.error||"HTTP "+o.status;s.error=h,p===e&&
|
|
42
|
+
</div>`}async function T(e,t,n){const s=k.find(o=>o.id===e);if(!s)return;const i=dt(t);if(!i)return;const a=++s.seq;n&&(s.history=s.history.slice(0,s.historyIdx+1),s.history.push(i),s.historyIdx=s.history.length-1),s.url=i,s.error=null,p===e&&(b.value=i,F(),qe());try{const o=await fetch("/api/browse?url="+encodeURIComponent(i)+"&tab="+encodeURIComponent(e)),l=await o.json();if(s.seq!==a)return;if(!o.ok||l.error){const h=l.error||"HTTP "+o.status;s.error=h,p===e&&O(h),w();return}const r=l.finalUrl||i;s.url=r,s.title=l.title||s.title,s.history[s.historyIdx]=r,p===e&&(b.value=r,F(),Ue(l)),w()}catch(o){if(s.seq!==a)return;s.error="Network error: "+(o instanceof Error?o.message:String(o)),p===e&&O(s.error),w()}}function pt(){const e=A();!e||e.historyIdx<=0||(e.historyIdx--,T(e.id,e.history[e.historyIdx],!1))}function ut(){const e=A();!e||e.historyIdx>=e.history.length-1||(e.historyIdx++,T(e.id,e.history[e.historyIdx],!1))}function Ue(e){let t="";e.truncated&&(t+='<div class="browser-bar"><span class="err">Response truncated \u2014 page may be incomplete.</span></div>'),t+=e.html||"",S.innerHTML=t,mt(),S.scrollTop=0}function mt(){S.querySelectorAll("a[href]").forEach(e=>{e.addEventListener("click",t=>{const n=e.getAttribute("href")||"";if(n.startsWith("/api/browse?")){t.preventDefault();const s=t;let i=null;try{i=new URL(n,location.origin).searchParams.get("url")}catch{return}if(!i)return;if(s.ctrlKey||s.metaKey||s.shiftKey){const a=G();T(a,i,!0)}else T(p,i,!0)}else/^(about|blob|file):/i.test(n)&&t.preventDefault()})}),S.querySelectorAll("form").forEach(e=>{e.addEventListener("submit",t=>{t.preventDefault();const n=e.getAttribute("action")||"";if(!n.startsWith("/api/browse/submit?"))return;let s=null;try{s=new URL(n,location.origin).searchParams.get("tab")||p}catch{s=p}if(!s)return;const i=new FormData(e),a=new URLSearchParams;i.forEach((o,l)=>a.append(l,String(o))),vt(s,a)})})}async function vt(e,t){const n=k.find(a=>a.id===e);if(!n)return;const s=++n.seq,i=t.get("__browse_action")||"";p===e&&qe();try{const a=await fetch("/api/browse/submit?tab="+encodeURIComponent(e),{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:t.toString()}),o=await a.json();if(n.seq!==s)return;if(!a.ok||o.error){const r=o.error||"HTTP "+a.status;n.error=r,p===e&&O(r);return}const l=o.finalUrl||i||n.url||"";n.url=l,n.title=o.title||n.title,n.history=n.history.slice(0,n.historyIdx+1),n.history.push(l),n.historyIdx=n.history.length-1,n.error=null,p===e&&(b.value=l,F(),Ue(o)),w()}catch(a){if(n.seq!==s)return;n.error="Network error: "+(a instanceof Error?a.message:String(a)),p===e&&O(n.error)}}function yt(){if(!re){if(re=!0,I.style.display!=="none"?(P="files",c&&(N(),c.destroy(),c=null)):c||v.style.display!=="none"?(P="terminal",le=u,c&&(N(),c.destroy(),c=null),u=null):P="none",v.style.display="none",I.style.display="none",ye.style.display="none",E.style.display="none",D(null),Pe.style.display="none",Re.style.display="flex",!p)G();else{w();const e=A();e&&(b.value=e.url||"",F(),e.url?T(e.id,e.url,!1):de())}Q()}}function V(){if(re){if(re=!1,Re.style.display="none",Pe.style.display="",P==="files")I.style.display="flex";else if(P==="terminal"&&le){const e=le;le=null,z(e)}else u?(v.style.display="",v.classList.add("terminal-pending"),ge()):E.style.display="flex";P="none"}}De.addEventListener("click",pt),Ne.addEventListener("click",ut),rt.addEventListener("click",()=>{const e=A();e&&e.url&&T(e.id,e.url,!1)}),lt.addEventListener("click",()=>{G(),b.focus(),b.select()}),b.addEventListener("keydown",e=>{if(e.key==="Enter"){e.preventDefault();const t=p||G();T(t,b.value,!0),b.blur()}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=ke.classList.toggle("open");ue.classList.toggle("open",e)}),ue.addEventListener("click",()=>{ke.classList.remove("open"),ue.classList.remove("open")});let H="";const _=document.createElement("div");_.className="session-popover-backdrop",document.body.appendChild(_);const x=document.createElement("div");x.id="session-popover",x.className="session-popover",x.style.display="none",x.innerHTML=`<div class="sp-field">
|
|
43
43
|
<label>Rename session</label>
|
|
44
44
|
<input type="text" id="sp-name" />
|
|
45
45
|
</div>
|
|
@@ -48,5 +48,5 @@ import{initTerminal as _e}from"./terminal-core.js";const q=window.__TMUX_WEB_SHE
|
|
|
48
48
|
<button class="btn" id="sp-cancel">Cancel</button>
|
|
49
49
|
</div>
|
|
50
50
|
<hr class="sp-divider" />
|
|
51
|
-
<button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(x);function ht(e){const t=e.getBoundingClientRect();x.style.top=t.bottom+4+"px",x.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function gt(e,t){H=e;const n=document.getElementById("sp-name");n.value=e,
|
|
51
|
+
<button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(x);function ht(e){const t=e.getBoundingClientRect();x.style.top=t.bottom+4+"px",x.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function gt(e,t){H=e;const n=document.getElementById("sp-name");n.value=e,_.classList.add("open"),x.style.display="block",ht(t),setTimeout(()=>n.focus(),50)}function K(){_.classList.remove("open"),x.style.display="none"}_.addEventListener("click",K),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===H){K();return}try{if(!(await fetch("/api/sessions/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:H,newName:e})})).ok)return;K(),j(),u===H&&z(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+H+'"?'))try{if(!(await fetch("/api/sessions/kill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:H})})).ok)return;K(),u===H&&(c&&(c.destroy(),c=null),u=null,v.style.display="none",E.style.display="flex"),j()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",K);const bt={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||!c)return;const n=bt[t.dataset.key];n&&(c.sendInput(n),c.focus())});const L=document.getElementById("mk-input"),je=document.getElementById("mk-send");function ze(){if(!L||!c)return;const e=L.value;e&&(c.sendInput(e+"\r"),L.value="",L.rows=1,c.focus())}if(L&&(L.addEventListener("input",()=>{L.rows=1;const e=L.value.split(`
|
|
52
52
|
`).length;L.rows=Math.min(e,4)}),L.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),ze())})),je&&je.addEventListener("click",ze),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})}const be=document.getElementById("process-panel"),J=document.getElementById("proc-list"),wt=document.getElementById("proc-close"),we=document.getElementById("proc-backdrop"),Lt=document.getElementById("header-ram");function Et(){J.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch("/api/system/processes").then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){J.innerHTML='<div class="proc-empty">No processes</div>';return}let t="";for(const n of e){const s=n.rss,i=s<1048576?(s/1024).toFixed(0)+"K":(s/1048576).toFixed(1)+"M";t+='<div class="proc-row" data-pid="'+n.pid+'"><span class="proc-mem">'+n.mem+'%</span><span class="proc-rss">'+i+'</span><span class="proc-cmd" title="'+d(n.command)+'">'+d(n.command)+'</span><button class="proc-kill" title="Kill PID '+n.pid+'">×</button></div>'}J.innerHTML=t;for(const n of J.querySelectorAll(".proc-row")){const s=n.querySelector(".proc-kill");s&&s.addEventListener("click",i=>{const a=i.target.closest(".proc-row");!a||!confirm("Kill PID "+a.dataset.pid+"?")||fetch("/api/system/kill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(a.dataset.pid,10)})}).then(o=>o.json()).then(o=>{o.ok?a.style.opacity="0.3":alert("Failed: "+(o.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{J.innerHTML='<div class="proc-empty">Failed to load</div>'})}function kt(){be.classList.add("open"),we.classList.add("open"),Et()}function fe(){be.classList.remove("open"),we.classList.remove("open")}Lt.addEventListener("click",e=>{e.stopPropagation(),be.classList.contains("open")?fe():kt()}),wt.addEventListener("click",fe),we.addEventListener("click",fe),window.__openSession=z,window.__refreshSidebar=j,j(),me();
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{chmodSync as Ge,existsSync as B,mkdirSync as ze,readdirSync as
|
|
2
|
+
import{chmodSync as Ge,existsSync as B,mkdirSync as ze,readdirSync as me,readFileSync as fe,statSync as D,unlinkSync as Je,writeFileSync as Ke}from"node:fs";import{readFile as Qe}from"node:fs/promises";import{execSync as V,execFileSync as pe}from"node:child_process";import{createRequire as Xe}from"node:module";import j from"node:path";import{fileURLToPath as Ye}from"node:url";import{Hono as Ze}from"hono";import{serve as et}from"@hono/node-server";import{WebSocketServer as tt,WebSocket as he}from"ws";import*as rt from"node-pty";try{const e=j.dirname(Xe(import.meta.url).resolve("node-pty/package.json")),t=j.join(e,"prebuilds");if(B(t))for(const r of me(t)){const n=j.join(t,r,"spawn-helper");B(n)&&!(D(n).mode&73)&&Ge(n,493)}}catch{}import{listSessions as A}from"./sessions.js";import{renderLoginPage as nt,renderNotesIndex as ot,renderNotesPage as st,renderSettings as it,renderThemeSettings as at,renderScheduleIndex as dt,renderHistoryIndex as ct,renderQuickCommandsPage as ut,renderFilesIndex as lt,renderShell as mt}from"./frontend.js";import{loadSecurityConfig as ft,saveSecurityConfig as ye}from"./lib/security-config.js";import{hashPassword as ge,verifyPassword as we,validatePassword as je,TokenStore as pt}from"./lib/auth.js";import{RateLimiter as ht}from"./lib/rateLimiter.js";import{atomicWriteFileSync as yt}from"./lib/atomicWrite.js";import{resolveFsPath as H,resolveFsRoots as ke,MAX_FILE_BYTES as be,walkRecursive as gt}from"./lib/fs-access.js";import{captureSessionWindowsWithPath as wt}from"./lib/tmux-windows.js";import{audit as l}from"./lib/auditLog.js";import{db as S}from"./lib/db.js";import{getSessionAccessMap as E}from"./lib/session-access.js";import{listWindowHistory as jt,clearWindowHistory as kt}from"./lib/window-history.js";import{loadExtensions as bt,spawnExtensionBackend as vt,registerExtensionRoutes as St}from"./lib/ext-loader.js";import{SchedulerService as xt,isValidScheduleInput as Tt,isValidRescheduleInput as _t}from"./lib/scheduler.js";import{getScheduleDelayError as ve}from"./lib/schedule-delay.js";import{handleClientMessage as qt}from"./lib/ws-message.js";import{loadDotEnv as At}from"./lib/load-env.js";import{cmdAdd as Ct,cmdRemove as It,cmdList as Et,cmdSetup as Rt,cmdTheme as Ot,printUsage as Se,printVersion as Mt}from"./lib/cli.js";import{readSettings as te,writeSettings as Lt}from"./lib/settings.js";import{readActiveTheme as Pt,setActiveThemeTemplate as xe}from"./lib/theme-store.js";import{isThemeTemplateId as Te,THEME_TEMPLATE_IDS as Nt}from"./lib/themes/index.js";import{installPlugin as Dt,uninstallPlugin as Ht}from"./lib/plugins.js";import{buildCommandbarSessions as O}from"./lib/commandbar.js";import{pinView as Ft,unpinView as $t,listPinnedViews as Ut}from"./lib/pinned-views.js";import{listWindowLabels as Wt,setWindowLabel as Bt}from"./lib/window-labels.js";import{captureAndStoreWindows as _e,getStoredWindows as qe}from"./lib/session-windows.js";import{acquireControlClient as Vt,killAllControlClients as Gt}from"./lib/tmux-control.js";import{buildSidebarSessions as zt}from"./lib/sessions-sidebar.js";import{createQuickCommand as Jt,deleteQuickCommand as Kt,listQuickCommands as Ae,updateQuickCommand as Qt}from"./lib/quick-commands.js";import{getSessionPaneTarget as Xt,capturePaneTail as Yt,capturePaneHistoryChunk as Zt,toCrlf as Ce}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as er}from"./lib/terminal-config.js";import{ImageUploadError as tr,saveUploadedImage as rr}from"./lib/image-upload.js";import{browsePage as nr,browseAsset as or,browseSubmit as sr,BrowseError as re}from"./lib/web-browse.js";import{getSystemStatus as ir,getTopProcesses as ar,killProcess as dr}from"./lib/system-monitor.js";import{listSessionWindows as cr,selectSessionWindow as ur,newSessionWindow as lr,renameSessionWindow as mr,newTmuxSession as fr,renameSession as pr,killSession as hr,TmuxWindowsError as K}from"./lib/tmux-windows.js";At();const Q=er(),m=ft(),g=new pt,M=new ht;let F=!1;const X="tmux-web-token",yr=365;function Ie(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function Y(e){if(m.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 gr(e){if(m.security.trustProxy){const t=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(t)return t}return e.socket.remoteAddress||"unknown"}function ne(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*)${X}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function Ee(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*)${X}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function Sn(e){const t=ne(e);return t?g.validateToken(t):null}function xn(e){const t=Ee(e);return t?g.validateToken(t):null}function Re(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${X}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${yr*86400}${r?"; Secure":""}`)}function wr(e){e.header("Set-Cookie",`${X}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}function d(){return async(e,t)=>{const r=ne(e);return!r||!g.validateToken(r)?(l("http_unauthorized",{ip:Y(e)}),e.json({error:"unauthorized"},401)):t()}}function jr(e){const t=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${t}`,302)}function R(){return async(e,t)=>{const r=ne(e);return!r||!g.validateToken(r)?jr(e):t()}}const $=new Map;function kr(e){let t=0;for(const r of $.values())r.ip===e&&t++;return t}function oe(e,t,r){try{e.close(t,r)}catch{}}function Tn(e,t){e.readyState===he.OPEN&&e.send(JSON.stringify(t))}function br(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 s of e)s==="--ghostty"&&(r="ghostty"),s==="--xterm"&&(r="xterm");return r}const vr=7;function Oe(e){return typeof e!="number"||!Number.isFinite(e)?vr:Math.min(365,Math.max(1,Math.round(e)))}const Me=process.argv.slice(2);{const e=Me.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 Ct(r),process.exit(0);case"remove":case"rm":r||(console.error("usage: tmux-web remove <package>"),process.exit(1)),await It(r),process.exit(0);case"list":case"ls":await Et(),process.exit(0);case"setup":await Rt(e),process.exit(0);case"theme":await Ot(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":Se(),process.exit(0);case"-V":case"--version":case"-v":Mt(),process.exit(0);default:console.error(`unknown argument: ${t}`),Se(),process.exit(1)}}}function k(e,t){e.readyState===he.OPEN&&e.send(JSON.stringify(t))}const U=new Set,Le=[];await S.read(),S.data.sessionAccess??=[],S.data.pinnedViews??=[],S.data.watchedPanes??=[],S.data.triggeredTasks??=[],S.data.quickCommands??=[];const se=await te();let x=await Pt();const b=se.commandbar===!0,ie=br(Me,se.terminalRenderer),Pe=Oe(se.scheduleHistoryDays),Ne=j.join(process.cwd(),"extensions"),De=await bt(Ne);for(const e of De)e.start&&Le.push(vt(e.dir,e));const L=new xt({db:S,historyRetentionMs:Pe*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await L.restoreFromDb();const i=new Ze;i.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")}),i.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()}),St(i,Ne,De);const Sr=j.dirname(Ye(import.meta.url)),xr=[j.join(Sr,"assets"),j.join(process.cwd(),"dist","assets")];i.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 xr){const n=j.join(r,t);if(!B(n))continue;const s=await Qe(n),o=j.extname(t),a={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(s,200,{"Content-Type":a[o]??"application/octet-stream","Cache-Control":o===".js"||o===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const Tr=`<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>`,He=e=>e.body(Tr,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});
|
|
6
|
+
</svg>`,He=e=>e.body(Tr,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});i.get("/favicon.svg",He),i.get("/favicon.ico",He);const _r={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"}]};i.get("/manifest.json",e=>e.json(_r,200,{"Cache-Control":"public, max-age=3600"}));const qr=`// tmux-weblink Service Worker
|
|
7
7
|
const CACHE = "tmux-weblink-v1";
|
|
8
8
|
const ASSETS = ["/", "/favicon.svg"];
|
|
9
9
|
self.addEventListener("install", (e) => {
|
|
@@ -14,14 +14,14 @@ self.addEventListener("activate", (e) => { e.waitUntil(clients.claim()); });
|
|
|
14
14
|
self.addEventListener("fetch", (e) => {
|
|
15
15
|
e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
|
|
16
16
|
});
|
|
17
|
-
`;
|
|
18
|
-
`)){if(!
|
|
19
|
-
`)){if(!y.trim())continue;const p=y.split(" ");if(p.length>=3){const
|
|
20
|
-
`).length;u.additions=q,g+=q}catch{}return{repoRoot:r,branch:n,files:o,linesAdded:g,linesRemoved:m}}catch{return t}}s.get("/api/git/status",d(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=H(t),n=Cr(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",d(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=H(t);if(console.error("[git/diff] path=%s resolved=%s file=%s",t,n,r),!B(n)||!D(n).isDirectory())return console.error("[git/diff] resolved path does not exist or is not a directory: %s",n),e.json({error:"invalid repository path: "+n},400);try{const i=pe("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),o=pe("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:i,stagedDiff:o})}catch(i){throw console.error("[git/diff] execFileSync failed:",i?.code,i.message),i}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",d(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=H(t);if(!D(r).isFile())return e.json({error:"not a file"},400);const n=D(r).size;if(n>ve)return e.json({error:"file too large",size:n,maxBytes:ve},413);const i=me(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",d(),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=H(t.path);return yt(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",d(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=H(t.path);return D(r).isFile()?(Je(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",d(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=H(t.path);return B(r)?e.json({error:"file already exists"},409):(ze(j.dirname(r),{recursive:!0}),Ke(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 ae(e){return typeof e=="string"&&e.length<=64?e:"default"}s.get("/api/browse",d(),async e=>{const t=e.req.query("url");if(!t)return e.json({error:"url is required"},400);const r=ae(e.req.query("tab"));try{const n=await nr(t,r);return e.json(n)}catch(n){return n instanceof re?e.json({error:n.message},n.status):(console.error("[browse]",n),e.json({error:"browse failed"},500))}}),s.get("/api/browse/asset",d(),async e=>{const t=e.req.query("url");if(!t)return e.json({error:"url is required"},400);const r=ae(e.req.query("tab"));try{const{buf:n,contentType:i}=await or(t,r);return e.body(n,200,{"Content-Type":i,"Cache-Control":"no-cache"})}catch(n){return n instanceof re?e.json({error:n.message},n.status):(console.error("[browse/asset]",n),e.json({error:"asset fetch failed"},500))}}),s.post("/api/browse/submit",d(),async e=>{const t=ae(e.req.query("tab"));let r;try{r=await e.req.parseBody()}catch{return e.json({error:"invalid form body"},400)}const n=typeof r.__browse_action=="string"?r.__browse_action:"",i=typeof r.__browse_method=="string"&&r.__browse_method.toLowerCase()==="get"?"get":"post";if(!n)return e.json({error:"missing form action"},400);const o=[];for(const[a,g]of Object.entries(r))a==="__browse_action"||a==="__browse_method"||o.push([a,String(g)]);try{const a=await sr(n,o,i,t);return e.json(a)}catch(a){return a instanceof re?e.json({error:a.message},a.status):(console.error("[browse/submit]",a),e.json({error:"form submit failed"},500))}});function de(e){return{...zt(A(),E(),Wt()),currentSession:e??null}}function Fe(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",d(),e=>{const t=e.req.query("currentSession");return e.json(de(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",d(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(_e(t))}),s.post("/api/pinned-views",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Fe(t);return"error"in r?e.json({error:r.error},400):(await Ft(r.sessionName,r.windowIndex),e.json(de()))}),s.delete("/api/pinned-views",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Fe(t);return"error"in r?e.json({error:r.error},400):(await Ut(r.sessionName,r.windowIndex),e.json(de()))}),s.get("/api/notes",d(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",d(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=S.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",d(),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=S.data.notes.findIndex(o=>o.scope===t);return i>=0?S.data.notes[i]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!A().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(),a=Buffer.from(o),{path:g}=await rr(a,i.type||void 0,i.name||void 0);return e.json({path:g})}catch(o){return o instanceof tr?e.json({error:o.message},o.status):(console.error("[upload]",o),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",d(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map($t(t).map(o=>[o.windowIndex,o.label])),n=new Map(_e(t).map(o=>[o.index,o])),i=ur(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",d(),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 Bt(t,n,i);return e.json(o)}),s.post("/api/session/:session/select-window",d(),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 cr(t,n),e.json({ok:!0})}catch(i){return i instanceof K?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",d(),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 fr(t,n,r.name),qe(t),e.json({ok:!0})}catch(i){return i instanceof K?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",d(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return lr(t),qe(t),e.json({ok:!0})}catch(r){return r instanceof K?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",d(),e=>e.json(L.list(e.req.query("session")))),s.post("/api/schedule",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=be(t);if(r)return e.json({error:r},400);if(!Tt(t))return e.json({error:"invalid body"},400);const n=await L.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",d(),async e=>await L.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=be(t);if(r)return e.json({error:r},400);if(!qt(t))return e.json({error:"invalid body"},400);const n=await L.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const Ir=parseInt(process.env.PORT||"21000",10),Er=et({fetch:s.fetch,port:Ir,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),ue=new tt({noServer:!0});function Ue(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
|
|
17
|
+
`;i.get("/sw.js",e=>e.body(qr,200,{"Content-Type":"application/javascript; charset=utf-8"})),i.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!m.passwordHash,n=e.req.query("error");return e.html(nt({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:x}))}),i.get("/",R(),e=>{const t=b?O(A(),E()):[],r=ke();return e.html(mt({theme:x,commandbarEnabled:b,commandbarSessions:t,fsRoots:r,terminalCfg:Q,renderer:ie,scrollback:Q.initialLines+2*Q.historyChunk}))}),i.get("/notes",R(),e=>{const t=b?O(A(),E()):[];return e.html(ot(S.data.notes,x,b,t))}),i.get("/notes/:session",R(),e=>{const t=decodeURIComponent(e.req.param("session")),r=b?O(A(),E()):[];return e.html(st(t,x,b,r))}),i.get("/schedule",R(),e=>{const t=b?O(A(),E()):[];return e.html(dt(L.list(),L.listTriggered(),x,Pe,b,t))}),i.get("/history",R(),e=>{const t=A(),r=b?O(t,E()):[],n=new Set(t.map(s=>s.name));return e.html(ct(jt(),x,b,r,n))}),i.get("/quick-commands",R(),e=>{const t=b?O(A(),E()):[];return e.html(ut(Ae(),x,b,t))}),i.get("/files",R(),e=>{const t=b?O(A(),E()):[],r=ke();return e.html(lt(x,b,t,r))}),i.post("/api/history/clear",d(),async e=>(await kt(),e.json({ok:!0}))),i.get("/api/quick-commands",d(),e=>e.json(Ae())),i.post("/api/quick-commands",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Jt(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),i.patch("/api/quick-commands/:id",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Qt(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),i.delete("/api/quick-commands/:id",d(),async e=>await Kt(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),i.post("/api/auth/password",async e=>{const t=Y(e),r=M.check(t);if(!r.allowed)return l("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 s=typeof n.password=="string"?n.password:"";if(!m.passwordHash){if(!m.security.allowRemoteSetup&&!Ie(t))return l("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(F)return e.json({error:"Password setup in progress"},409);const f=je(s);if(f)return e.json({error:f},400);F=!0;try{m.passwordHash=await ge(s),ye(m),l("password_set",{ip:t})}finally{F=!1}const{plaintext:c}=g.createAccessToken("setup",m.security.tokenTtlDays);return Re(e,c),e.json({ok:!0,token:c,setupMode:!0})}if(!await we(s,m.passwordHash)){const f=M.recordFailure(t);return l("auth_failed",{ip:t,method:"password",failures:f.failures,permanentLock:f.permanentLock}),f.permanentLock?(g.revokeAll(),l("permanent_lock",{ip:t,failures:f.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):f.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:f.retryAfterMs},429)}M.recordSuccess(t);const a=`browser-${t}`,{plaintext:w}=g.createAccessToken(a,m.security.tokenTtlDays);return Re(e,w),l("auth_success",{ip:t,method:"password",tokenName:a}),e.json({ok:!0,token:w})}),i.post("/api/auth/token",d(),async e=>{const t=Y(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}`,s=typeof r.ttlDays=="number"?r.ttlDays:m.security.tokenTtlDays,{stored:o,plaintext:a}=g.createAccessToken(n,s);return l("token_created",{ip:t,name:o.name,tokenId:o.id}),e.json({id:o.id,name:o.name,token:a,expiresAt:o.expiresAt})}),i.get("/api/auth/tokens",d(),e=>e.json(g.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),i.delete("/api/auth/tokens/:id",d(),e=>g.revoke(e.req.param("id"))?(l("token_revoked",{ip:Y(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),i.post("/api/auth/logout",d(),e=>(wr(e),e.json({ok:!0}))),i.get("/settings",R(),async e=>{const t=await te(),r=t.terminalRenderer??"xterm";return e.html(it({settings:t,renderer:ie,rendererOverridden:ie!==r,theme:x,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),i.post("/settings",d(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await te(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",s=t.defaultView==="recent"?"recent":"default",o=Oe(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Lt({...r,commandbar:t.commandbar!==void 0,terminalRenderer:n,defaultView:s,scheduleHistoryDays:o}),e.redirect("/settings?saved=1",303)}),i.post("/settings/plugins",d(),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 s=r==="remove"?await Ht(n):r==="add"?await Dt(n):{ok:!1,output:"unknown action"};return s.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(s.output.slice(0,800)),303)}),i.get("/settings/theme",R(),e=>e.html(at({theme:x,saved:e.req.query("saved")==="1"}))),i.post("/settings/theme",d(),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"||!Te(r)?e.redirect("/settings/theme",303):(x=await xe(r),e.redirect("/settings/theme?saved=1",303))});const Ar={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};i.get("/api/theme",d(),e=>e.json({active:x.template,templates:Nt.map(t=>({id:t,name:Ar[t]}))})),i.post("/api/theme",d(),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"||!Te(r)?e.json({error:"invalid theme template"},400):(x=await xe(r),e.json({ok:!0,active:x.template}))}),i.get("/api/system/status",d(),e=>e.json(ir())),i.get("/api/system/processes",d(),e=>e.json(ar())),i.post("/api/system/kill",d(),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=dr(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),i.get("/api/sessions",d(),e=>b?e.json(O(A(),E())):e.json({error:"commandbar disabled"},404)),i.post("/api/sessions/new",d(),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(o=>o.name===r))return e.json({error:"session already exists"},409);try{return fr(r,n),e.json({ok:!0})}catch(o){const a=o instanceof K?o.message:"failed to create session";return e.json({error:a},500)}}),i.post("/api/sessions/rename",d(),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)?(pr(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),i.post("/api/sessions/kill",d(),async e=>{try{const{name:t}=await e.req.json();return t?(hr(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),i.get("/api/fs/session-path",d(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=wt(t),s=r.find(o=>o.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:s})}catch{return e.json({path:process.env.HOME??"/"})}}),i.get("/api/fs/list",d(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=j.join(t,r));const n=e.req.query("recursive")==="true";let s=r,o="",a=r.endsWith("/");if(!a)try{a=D(r).isDirectory()}catch{}a||(s=j.dirname(r),o=j.basename(r).toLowerCase());try{const w=me(s),f=[],c=[];for(const y of w)if(!y.startsWith(".")&&!(o&&!y.toLowerCase().startsWith(o))){try{const p=j.join(s,y);D(p).isDirectory()?(f.push(p),n&>(p,f,c,0)):c.push(p)}catch{}if(f.length+c.length>=5e3)break}return e.json({dirs:f,files:c})}catch{return e.json({dirs:[],files:[]})}});function Cr(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=V("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=V("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=V("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let s="";try{s=V("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const o=[];for(const c of s.split(`
|
|
18
|
+
`)){if(!c.trim())continue;const y=c[0],p=c[1],_=c.substring(3).trim(),C=p!==" "?p:y;o.push({path:_,status:C,staged:y,unstaged:p,additions:0,deletions:0})}const a=new Map;try{const c=V("git diff --numstat HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3});for(const y of c.split(`
|
|
19
|
+
`)){if(!y.trim())continue;const p=y.split(" ");if(p.length>=3){const _=parseInt(p[0],10)||0,C=parseInt(p[1],10)||0;a.set(p[2],{added:_,deleted:C})}}}catch{}for(const c of o){const y=a.get(c.path);y&&(c.additions=y.added,c.deletions=y.deleted)}let w=0,f=0;for(const c of o)w+=c.additions,f+=c.deletions;for(const c of o)if(c.status==="?"&&c.additions===0)try{const y=j.join(r,c.path),_=fe(y,"utf-8").split(`
|
|
20
|
+
`).length;c.additions=_,w+=_}catch{}return{repoRoot:r,branch:n,files:o,linesAdded:w,linesRemoved:f}}catch{return t}}i.get("/api/git/status",d(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=H(t),n=Cr(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),i.get("/api/git/diff",d(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=H(t);if(console.error("[git/diff] path=%s resolved=%s file=%s",t,n,r),!B(n)||!D(n).isDirectory())return console.error("[git/diff] resolved path does not exist or is not a directory: %s",n),e.json({error:"invalid repository path: "+n},400);try{const s=pe("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),o=pe("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:s,stagedDiff:o})}catch(s){throw console.error("[git/diff] execFileSync failed:",s?.code,s.message),s}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),i.get("/api/file",d(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=H(t);if(!D(r).isFile())return e.json({error:"not a file"},400);const n=D(r).size;if(n>be)return e.json({error:"file too large",size:n,maxBytes:be},413);const s=fe(r,"utf-8");return e.json({path:r,content:s,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)}}),i.put("/api/file",d(),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=H(t.path);return yt(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)}}),i.post("/api/file/delete",d(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=H(t.path);return D(r).isFile()?(Je(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)}}),i.post("/api/file/touch",d(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=H(t.path);return B(r)?e.json({error:"file already exists"},409):(ze(j.dirname(r),{recursive:!0}),Ke(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 ae(e){return typeof e=="string"&&e.length<=64?e:"default"}i.get("/api/browse",d(),async e=>{const t=e.req.query("url");if(!t)return e.json({error:"url is required"},400);const r=ae(e.req.query("tab"));try{const n=await nr(t,r),s=/<form[\s>]/i.test(n.html),o=n.html.includes("__browse_action");return console.error(`[browse] ok tab=${r} url=${t} title=${n.title} html=${n.html.length} form=${s} injected=${o}`),e.json(n)}catch(n){return n instanceof re?(console.error(`[browse] ${n.status} url=${t} -> ${n.message}`),e.json({error:n.message},n.status)):(console.error("[browse]",n),e.json({error:"browse failed"},500))}}),i.get("/api/browse/asset",d(),async e=>{const t=e.req.query("url");if(!t)return e.json({error:"url is required"},400);const r=ae(e.req.query("tab"));try{const{buf:n,contentType:s}=await or(t,r);return e.body(n,200,{"Content-Type":s,"Cache-Control":"no-cache"})}catch(n){return n instanceof re?(console.error(`[browse/asset] ${n.status} url=${t} -> ${n.message}`),e.json({error:n.message},n.status)):(console.error("[browse/asset]",n),e.json({error:"asset fetch failed"},500))}}),i.post("/api/browse/submit",d(),async e=>{const t=ae(e.req.query("tab"));let r;try{r=await e.req.parseBody()}catch{return e.json({error:"invalid form body"},400)}const n=typeof r.__browse_action=="string"?r.__browse_action:"",s=typeof r.__browse_method=="string"&&r.__browse_method.toLowerCase()==="get"?"get":"post";if(!n){const a=typeof r.__diag=="string"?r.__diag:"";return console.error(`[browse/submit] missing form action; tab=${t} ct=${e.req.header("content-type")} body=${JSON.stringify(r).slice(0,400)} diag=${a}`),e.json({error:"missing form action"},400)}const o=[];for(const[a,w]of Object.entries(r))a==="__browse_action"||a==="__browse_method"||a.startsWith("__diag")||o.push([a,String(w)]);try{const a=await sr(n,o,s,t);return e.json(a)}catch(a){return a instanceof re?(console.error(`[browse/submit] ${a.status} url=${n} -> ${a.message}`),e.json({error:a.message},a.status)):(console.error("[browse/submit]",a),e.json({error:"form submit failed"},500))}});function de(e){return{...zt(A(),E(),Ut()),currentSession:e??null}}function Fe(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}}i.get("/api/sidebar/sessions",d(),e=>{const t=e.req.query("currentSession");return e.json(de(typeof t=="string"&&t?t:void 0))}),i.get("/api/sidebar/session-windows/:session",d(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(qe(t))}),i.post("/api/pinned-views",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Fe(t);return"error"in r?e.json({error:r.error},400):(await Ft(r.sessionName,r.windowIndex),e.json(de()))}),i.delete("/api/pinned-views",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Fe(t);return"error"in r?e.json({error:r.error},400):(await $t(r.sessionName,r.windowIndex),e.json(de()))}),i.get("/api/notes",d(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),i.get("/api/notes/:scope",d(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=S.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),i.put("/api/notes/:scope",d(),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()},s=S.data.notes.findIndex(o=>o.scope===t);return s>=0?S.data.notes[s]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),i.post("/api/session/:session/upload",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!A().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 s=n.file;if(!(s instanceof File))return e.json({error:"missing file field"},400);try{const o=await s.arrayBuffer(),a=Buffer.from(o),{path:w}=await rr(a,s.type||void 0,s.name||void 0);return e.json({path:w})}catch(o){return o instanceof tr?e.json({error:o.message},o.status):(console.error("[upload]",o),e.json({error:"upload failed"},500))}}),i.get("/api/session/:session/windows",d(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map(Wt(t).map(o=>[o.windowIndex,o.label])),n=new Map(qe(t).map(o=>[o.index,o])),s=cr(t).map(o=>({...o,label:r.get(o.index)??null,worktree:n.get(o.index)?.worktree??!1}));return e.json(s)}),i.post("/api/session/:session/window-label",d(),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 s=typeof r.label=="string"?r.label:"",o=await Bt(t,n,s);return e.json(o)}),i.post("/api/session/:session/select-window",d(),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 ur(t,n),e.json({ok:!0})}catch(s){return s instanceof K?e.json({error:s.message},s.status):(console.error("[select-window]",s),e.json({error:"select-window failed"},500))}}),i.post("/api/session/:session/rename-window",d(),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 mr(t,n,r.name),_e(t),e.json({ok:!0})}catch(s){return s instanceof K?e.json({error:s.message},s.status):(console.error("[rename-window]",s),e.json({error:"rename-window failed"},500))}}),i.post("/api/session/:session/new-window",d(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return lr(t),_e(t),e.json({ok:!0})}catch(r){return r instanceof K?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),i.get("/api/schedule",d(),e=>e.json(L.list(e.req.query("session")))),i.post("/api/schedule",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ve(t);if(r)return e.json({error:r},400);if(!Tt(t))return e.json({error:"invalid body"},400);const n=await L.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),i.delete("/api/schedule/:id",d(),async e=>await L.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),i.patch("/api/schedule/:id",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ve(t);if(r)return e.json({error:r},400);if(!_t(t))return e.json({error:"invalid body"},400);const n=await L.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const Ir=parseInt(process.env.PORT||"21000",10),Er=et({fetch:i.fetch,port:Ir,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),ce=new tt({noServer:!0});function $e(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
|
|
21
21
|
\r
|
|
22
|
-
`),e.destroy()}Er.on("upgrade",(e,t,r)=>{const n=new URL(e.url||"/",`http://${e.headers.host}`),
|
|
23
|
-
`})}
|
|
24
|
-
\x1B[31mFailed to attach to tmux session "${r}": ${
|
|
25
|
-
`}),oe(e,1011,"pty spawn failed");return}
|
|
26
|
-
\x1B[2m--- tmux exited (code ${
|
|
27
|
-
`}),oe(e,1e3,"pty exited")})}function Z(
|
|
22
|
+
`),e.destroy()}Er.on("upgrade",(e,t,r)=>{const n=new URL(e.url||"/",`http://${e.headers.host}`),s=n.pathname.match(/^\/ws\/(.+)$/);if(!s){t.destroy();return}const o=gr(e),a=e.headers.origin,w=m.security.allowedOrigins;if(a&&!(a===`http://${e.headers.host}`||a===`https://${e.headers.host}`)&&(w.length===0||!w.includes(a))){l("ws_rejected_origin",{ip:o,origin:a}),$e(t,403,"Origin not allowed");return}const f=kr(o);if(f>=m.security.maxConnectionsPerIp){l("ws_rejected_per_ip_cap",{ip:o,liveFromIp:f}),$e(t,429,"Too many connections");return}const c=Ee(e)||n.searchParams.get("token")||"",y=c?g.validateToken(c):null;ce.handleUpgrade(e,t,r,p=>{ce.emit("connection",p,e,decodeURIComponent(s[1]),o,y)})}),ce.on("connection",(e,t,r,n,s)=>{const o={ws:e,ip:n,authenticated:!1,authTimeout:null};$.set(e,o),l("ws_connected",{ip:n});let a=null;const{initialLines:w,historyChunk:f,syncIdleMs:c,syncMaxMs:y}=Q;let p=!0,_="",C=null,G=null,z=r,P=null;const J=()=>{C&&(clearTimeout(C),C=null),G&&(clearTimeout(G),G=null)},ue=()=>{if(p){J(),p=!1;try{z=Xt(r)}catch{z=r}try{const v=Yt(z,w);k(e,{type:"snapshot",data:Ce(v),lines:w})}catch{k(e,{type:"data",data:`\r
|
|
23
|
+
`})}_&&(k(e,{type:"data",data:_}),_="")}},We=()=>{p&&(C&&clearTimeout(C),C=setTimeout(ue,c))};function le(){try{a=rt.spawn("tmux",["attach-session","-t",r],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env})}catch(u){k(e,{type:"data",data:`\r
|
|
24
|
+
\x1B[31mFailed to attach to tmux session "${r}": ${u.message}\x1B[0m\r
|
|
25
|
+
`}),oe(e,1011,"pty spawn failed");return}U.add(a);let v=-1,h="";P=Vt(r,({activeIndex:u,windows:T})=>{k(e,{type:"window_changed",activeIndex:u,windows:T});const N=T.map(q=>q.index).join(","),W=u!==v||N!==h;v=u,h=N}),G=setTimeout(ue,y),a.onData(u=>{if(p){_+=u,We();return}k(e,{type:"data",data:u})}),a.onExit(({exitCode:u})=>{J(),a&&U.delete(a),a=null,k(e,{type:"data",data:`\r
|
|
26
|
+
\x1B[2m--- tmux exited (code ${u}) ---\x1B[0m\r
|
|
27
|
+
`}),oe(e,1e3,"pty exited")})}function Z(v,h){o.authenticated=!0,o.authTimeout&&(clearTimeout(o.authTimeout),o.authTimeout=null),l("auth_success",{ip:n,method:v,tokenName:h}),k(e,{type:"auth.ok",setupMode:!m.passwordHash}),le()}function I(v,h={}){k(e,{type:"auth.failed",message:v,...h})}async function Be(v){let h;try{h=JSON.parse(v)}catch{return!1}if(h.type==="auth.token"&&typeof h.token=="string"){const u=g.validateToken(h.token);if(!u){l("token_auth_failed",{ip:n});const T=M.recordFailure(n);return T.permanentLock&&(g.revokeAll(),l("permanent_lock",{ip:n,failures:T.failures})),I("Invalid or expired token",{permanentLock:T.permanentLock,retryAfterMs:T.retryAfterMs}),!0}return g.touch(u.tokenHash),Z("token",u.name),!0}if(h.type==="auth"&&typeof h.password=="string"){const u=M.check(n);if(!u.allowed)return l("rate_limited",{ip:n,retryAfterMs:u.retryAfterMs,permanentLock:u.permanentLock}),I("Too many attempts",{permanentLock:u.permanentLock,retryAfterMs:u.retryAfterMs}),!0;if(!m.passwordHash){if(!m.security.allowRemoteSetup&&!Ie(n))return l("setup_rejected_remote",{ip:n}),I("First-run setup must be performed from localhost"),!0;if(F)return I("Password setup in progress"),!0;const q=je(h.password);if(q)return I(q),!0;F=!0;try{m.passwordHash=await ge(h.password),ye(m),l("password_set",{ip:n})}finally{F=!1}const{stored:ee,plaintext:Ve}=g.createAccessToken("setup",m.security.tokenTtlDays);return k(e,{type:"auth.ok",setupMode:!0,token:Ve}),l("token_created",{ip:n,name:ee.name,tokenId:ee.id}),l("auth_success",{ip:n,method:"password",tokenName:ee.name}),le(),!0}if(!m.passwordHash)return I("Server not configured"),!0;if(!await we(h.password,m.passwordHash)){const q=M.recordFailure(n);return l("auth_failed",{ip:n,method:"password",failures:q.failures,permanentLock:q.permanentLock}),q.permanentLock&&(g.revokeAll(),l("permanent_lock",{ip:n,failures:q.failures})),I("Incorrect password",{permanentLock:q.permanentLock,retryAfterMs:q.retryAfterMs}),!0}M.recordSuccess(n);const N=`ws-${n}`,{plaintext:W}=g.createAccessToken(N,m.security.tokenTtlDays);return Z("password",N),k(e,{type:"auth.ok",setupMode:!1,token:W}),!0}return!1}s?(g.touch(s.tokenHash),Z("token",s.name)):(k(e,{type:"auth.required",setupMode:!m.passwordHash}),o.authTimeout=setTimeout(()=>{o.authenticated||(l("auth_timeout",{ip:n}),I("Authentication timeout"),oe(e,4e3,"Auth timeout"))},m.security.authTimeoutMs)),e.on("message",async v=>{const h=typeof v=="string"?v:v.toString("utf-8");if(!o.authenticated){if(h.length>1e6){k(e,{type:"auth.failed",message:"Message too large"});return}await Be(h)||I("Authentication required");return}if(!a)return;try{if(qt(h,a))return}catch{return}let u;try{u=JSON.parse(h)}catch{return}if(u.type==="load_history"&&typeof u.before=="number"){const T=Math.max(0,Math.floor(u.before));try{const{data:N,lines:W}=Zt(z,T,f);k(e,{type:"history",data:Ce(N),before:T,lines:W})}catch{k(e,{type:"history",data:"",before:T,lines:0})}}}),e.on("close",()=>{o.authTimeout&&clearTimeout(o.authTimeout),$.delete(e),l("ws_disconnected",{ip:n}),J(),P&&(P(),P=null),a&&(U.delete(a),a.kill(),a=null)}),e.on("error",()=>{o.authTimeout&&clearTimeout(o.authTimeout),$.delete(e),J(),P&&(P(),P=null),a&&(U.delete(a),a.kill(),a=null)})});function Ue(){L.cleanup(),Gt();for(const[e,t]of $){try{e.close(1001,"Server shutting down")}catch{}t.authTimeout&&clearTimeout(t.authTimeout)}$.clear();for(const e of U)try{e.kill()}catch{}U.clear();for(const e of Le)try{e.kill("SIGTERM")}catch{}M.dispose(),process.exit(0)}process.on("SIGINT",Ue),process.on("SIGTERM",Ue),setInterval(()=>{g.purgeExpired()},3600*1e3).unref();
|
package/dist/lib/agents-watch.js
CHANGED
|
@@ -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
|
|
2
|
-
`)){const c=a.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);c&&
|
|
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 +1 @@
|
|
|
1
|
-
import{db as a}from"./db.js";const d=50;async function
|
|
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};
|
package/dist/lib/web-browse.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{lookup as
|
|
1
|
+
import{lookup as U}from"node:dns/promises";const A=3*1024*1024,O=8*1024*1024,$=15e3;class v extends Error{status;constructor(n,o=400){super(n),this.status=o,this.name="BrowseError"}}const k=new Map;function J(t){k.delete(t)}function C(t,n,o){let e=k.get(t);e||(e=new Map,k.set(t,e));const s=new URL(n).hostname.toLowerCase(),r=typeof o.getSetCookie=="function"?o.getSetCookie():[];for(const c of r){const i=c.split(";")[0]?.trim();if(!i)continue;const u=/;\s*domain=([^;\s]+)/i.exec(c)?.[1]?.trim().replace(/^\./,"").toLowerCase();e.set(u??s,i)}}function R(t,n){const o=k.get(t);if(!o||o.size===0)return"";const e=new URL(n).hostname.toLowerCase(),s=[];for(const[r,c]of o)(e===r||e.endsWith("."+r))&&s.push(c);return s.join("; ")}function M(t){const n=t.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);if(n){const[e,s]=[Number(n[1]),Number(n[2])];return e===0||e===10||e===127||e===169&&s===254||e===172&&s>=16&&s<=31||e===192&&s===168||e>=224}const o=t.toLowerCase();return o==="::1"||o==="::"||o.startsWith("fe80:")||o.startsWith("fc")||o.startsWith("fd")||o.startsWith("fec0:")}async function T(t){const n=new URL(t);if(n.protocol!=="http:"&&n.protocol!=="https:")throw new v("only http/https URLs are supported");if(process.env.TMUX_WEB_BROWSE_ALLOW_PRIVATE==="1")return;const o=n.hostname;if(M(o))throw new v("private network addresses are blocked");try{const e=await U(o,{all:!0});for(const{address:s}of e)if(M(s))throw new v("private network addresses are blocked")}catch(e){if(e instanceof v)throw e}}async function _(t,n,o){const e=new AbortController,s=setTimeout(()=>e.abort(),$);try{const r=await fetch(t,{...n,redirect:"follow",signal:e.signal});if(!r.ok)throw new v(`the site responded with HTTP ${r.status}`,502);if(!r.body)return{res:r,buf:Buffer.alloc(0),truncated:!1};const c=r.body.getReader(),i=[];let u=0,l=!1;for(;;){const{done:a,value:f}=await c.read();if(a)break;if(u+=f.byteLength,u>o){l=!0,await c.cancel();break}i.push(f)}return{res:r,buf:Buffer.concat(i),truncated:l}}catch(r){if(r instanceof v)throw r;if(r instanceof Error&&r.name==="AbortError")throw new v("timed out after "+$+"ms",504);let c;const i=r.cause;i&&(c=typeof i=="string"?i:i.code||i.message||String(i));const u=r.message||"request failed";throw new v(c&&u!==c?`${u} (${c})`:u,502)}finally{clearTimeout(s)}}function W(t,n){const o=/charset=([\w.-]+)/i.exec(t||"");if(o)return o[1];const e=n.subarray(0,4096).toString("latin1"),s=/<meta[^>]+charset=["']?([\w.-]+)/i.exec(e);return s?s[1]:"utf-8"}const q={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:"\xA0",copy:"\xA9",reg:"\xAE",trade:"\u2122",hellip:"\u2026",mdash:"\u2014",ndash:"\u2013",lsquo:"\u2018",rsquo:"\u2019",ldquo:"\u201C",rdquo:"\u201D"};function E(t){return t.replace(/&#x([0-9a-f]+);/gi,(n,o)=>String.fromCodePoint(parseInt(o,16)||0)).replace(/&#(\d+);/g,(n,o)=>String.fromCodePoint(parseInt(o,10)||0)).replace(/&([a-zA-Z][a-zA-Z0-9]*);/g,(n,o)=>q[o]??n)}function H(t,n,o){return`${o==="nav"?"/api/browse":"/api/browse/asset"}?url=${encodeURIComponent(t)}&tab=${encodeURIComponent(n)}`}function y(t,n,o,e){const s=t.trim();if(!s)return t;if(s.startsWith("#"))return s;if(/^(javascript|data|vbscript):/i.test(s))return e==="nav"?"":t;if(/^(mailto|tel|sms|about|blob|file):/i.test(s))return t;let r;try{r=new URL(s,n).href}catch{return t}if(!/^https?:/i.test(r))return t;try{const c=new URL(r);if(/(^|\.)duckduckgo\.com$/i.test(c.hostname)&&c.pathname==="/l/"&&e==="nav"){const i=c.searchParams.get("uddg");i&&/^https?:/i.test(i)&&(r=i)}}catch{}return H(r,o,e)}function z(t,n,o){return t.split(",").map(e=>{const s=e.trim().match(/^(\S+)(\s+.+)?$/);if(!s)return e;const r=y(s[1],n,o,"asset");return r?r+(s[2]??""):e}).join(", ")}function B(t,n,o){return t.replace(/url\(\s*(['"]?)(.*?)\1\s*\)/g,(e,s,r)=>{const c=E(r.trim()),i=y(c,n,o,"asset");return i?`url(${i})`:e})}function N(t){const n=/([a-zA-Z-]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g,o=[];let e;for(;e=n.exec(t);)o.push({name:e[1],value:e[3]??e[4]??e[5]??""});return o}function b(t){return t.replace(/&/g,"&").replace(/"/g,""")}function j(t,n,o){const e=/<title[^>]*>([\s\S]*?)<\/title>/i.exec(t),s=e?E(e[1]).replace(/\s+/g," ").trim():"",r=n,c=[];let i=0;const u=/<(\/)?([a-zA-Z][a-zA-Z0-9]*)((?:\s+[^<>]*?)?)(\/?)>/gs;let l;for(;l=u.exec(t);){c.push(t.slice(i,l.index));const[a,f,d,h,S]=l,p=d.toLowerCase();if(i=l.index+a.length,f){c.push(a);continue}if(p==="script"||p==="style"){const m=l.index+a.length,x=new RegExp(`</${p}\\s*>`,"gi"),g=t.slice(m),w=x.exec(g),P=w?g.slice(0,w.index):g,L=w?w[0]:"";p==="style"?c.push(a+B(P,r,o)+L):c.push(a+L),i=w?m+w.index+L.length:t.length,u.lastIndex=i;continue}if(p==="iframe"||p==="object"||p==="embed"){if(!S){const m=l.index+a.length,x=new RegExp(`</${p}\\s*>`,"gi"),g=t.slice(m),w=x.exec(g);i=w?m+w.index+w[0].length:t.length}u.lastIndex=i;continue}if(p==="base"){u.lastIndex=i;continue}if(p==="meta"&&/http-equiv=["']?refresh/i.test(h)){u.lastIndex=i;continue}c.push(Z(a,p,h,r,o,l.index,t))}return c.push(t.slice(i)),{html:c.join(""),title:s}}function Z(t,n,o,e,s,r,c){const i=N(o),u=[];for(const a of i){const f=a.name.toLowerCase(),d=a.value;if(f==="href"){if(n==="a"||n==="area")/^javascript:/i.test(d.trim())?a.remove=!0:a.value=y(d,e,s,"nav");else if(n==="link"){const h=(i.find(S=>S.name.toLowerCase()==="rel")?.value||"").toLowerCase();h.includes("stylesheet")||h.includes("icon")?a.value=y(d,e,s,"asset"):a.remove=!0}}else f==="src"&&(n==="img"||n==="source"||n==="video"||n==="audio"||n==="input")?a.value=y(d,e,s,"asset"):f==="srcset"&&n==="img"?a.value=z(d,e,s):f==="poster"&&n==="video"?a.value=y(d,e,s,"asset"):f==="style"?a.value=B(d,e,s):f.startsWith("on")&&(a.remove=!0)}if(n==="form"){let a=i.find(h=>h.name.toLowerCase()==="action"),f=e;if(a&&a.value.trim())try{f=new URL(E(a.value.trim()),e).href}catch{f=e}const d=(i.find(h=>h.name.toLowerCase()==="method")?.value||"get").toLowerCase();a?a.value=`/api/browse/submit?tab=${encodeURIComponent(s)}`:i.push({name:"action",value:`/api/browse/submit?tab=${encodeURIComponent(s)}`}),u.push(`<input type="hidden" name="__browse_action" value="${b(f)}">`),u.push(`<input type="hidden" name="__browse_method" value="${b(d)}">`)}let l="<"+n;for(const a of i)a.remove||a.name==="href"&&n==="a"&&a.value===""||(l+=" "+a.name+'="'+b(a.value)+'"');return l+=">",u.length&&(l+=u.join("")),l}async function X(t,n){await T(t);const o=R(n,t),{res:e,buf:s,truncated:r}=await _(t,{headers:o?{cookie:o}:void 0},A);C(n,t,e.headers);const c=e.headers.get("content-type")||"",i=W(c,s),u=new TextDecoder(i).decode(s),l=e.url||t;if(/html/i.test(c)||!/^(text|application)\/(json|xml|xhtml)/i.test(c)&&/<html[\s>]/i.test(u.slice(0,2e3))){const{html:f,title:d}=j(u,l,n);return{url:t,finalUrl:l,title:d,html:f,contentType:c,truncated:r}}if(/json/i.test(c))try{const f=JSON.stringify(JSON.parse(u),null,2);return{url:t,finalUrl:l,title:new URL(l).hostname,html:`<pre>${b(f)}</pre>`,contentType:c,truncated:r}}catch{}const a=b(u);return{url:t,finalUrl:l,title:new URL(l).hostname,html:`<pre style="white-space:pre-wrap;word-break:break-word;font-family:var(--font-mono);font-size:12px;line-height:1.6;padding:16px">${a}</pre>`,contentType:c,truncated:r}}async function V(t,n){await T(t);const o=R(n,t),{res:e,buf:s,truncated:r}=await _(t,{headers:o?{cookie:o}:void 0},O);return C(n,t,e.headers),{buf:s,contentType:e.headers.get("content-type")||"application/octet-stream"}}async function Y(t,n,o,e){let s=t,r;if(o==="get"){const m=new URL(t);for(const[x,g]of n)m.searchParams.append(x,g);s=m.href,r={}}else{const m=new URLSearchParams;for(const[x,g]of n)m.append(x,g);r={method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:m.toString()}}await T(s);const c=R(e,s),{res:i,buf:u,truncated:l}=await _(s,{...r,headers:{...r.headers,...c?{cookie:c}:{}}},A);C(e,s,i.headers);const a=i.headers.get("content-type")||"",f=W(a,u),d=new TextDecoder(f).decode(u),h=i.url||s,{html:S,title:p}=j(d,h,e);return{url:s,finalUrl:h,title:p,html:S,contentType:a,truncated:l}}export{O as BROWSE_MAX_ASSET_BYTES,A as BROWSE_MAX_HTML_BYTES,$ as BROWSE_TIMEOUT_MS,v as BrowseError,V as browseAsset,X as browsePage,Y as browseSubmit,J as clearTabCookies,j as rewriteHtml};
|