tmux-weblink 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,38 +1,38 @@
1
- var Zt=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function je(e){return e.replace(/\x1b\[\?1049[hl]/g,"")}function oe(e){return e.replace(Zt,"")}var ke;function Gt(){return ke||(document.querySelector("link[data-tmux-web-xterm-css]")?ke=Promise.resolve():(ke=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)}),ke))}var Qt=class Ct{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:o},{FitAddon:r}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),Gt()]),c=new o({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,smoothScrollDuration:120,theme:s});return new Ct(t,c,new r)}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,n=!1){this.terminal.scrollToLine(Math.max(0,t),n)}scrollLines(t){let n=this.terminal.buffer?.active;this.terminal.scrollLines(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=en(this.container);if(t.width<=0||t.height<=0)return!1;let n=this.terminal._core?._renderService?.dimensions?.css?.cell;if(!n?.width||!n?.height)return this.fitAddon.fit(),!0;let s=n.width,o=n.height,r=this.terminal.element?.querySelector(".xterm-viewport"),c=r?Math.max(0,r.offsetWidth-r.clientWidth):0,l=Math.max(2,Math.floor((t.width-c)/s)),u=Math.max(2,Math.floor(t.height/o));return(l!==this.terminal.cols||u!==this.terminal.rows)&&this.terminal.resize(l,u),!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||t===this.terminal.textarea||this.container.contains(t)||!!t.closest?.(".xterm"):!1}getScreenText(){return this.getRowsText(0,this.terminal.rows-1)}getRowsText(t,n){let s=this.terminal.buffer.active,[o,r]=t<=n?[t,n]:[n,t],c=s.viewportY,l=[];for(let u=o;u<=r;u++){let p=s.getLine(c+u);l.push(p?p.translateToString(!0):"")}return l.join(`
2
- `)}selectRows(t,n){let s=this.terminal.buffer.active,[o,r]=t<=n?[t,n]:[n,t];this.terminal.selectLines(s.viewportY+o,s.viewportY+r)}selectCells(t,n,s,o){let r=this.terminal.buffer.active,c=Math.min(n,o),l=Math.max(n,o);if(c===l){let u=Math.min(t,s),p=Math.max(t,s);this.terminal.select(u,r.viewportY+c,p-u+1)}else this.terminal.selectLines(r.viewportY+c,r.viewportY+l)}getCellsText(t,n,s,o){let r=this.terminal.buffer.active,c=Math.min(n,o),l=Math.max(n,o),u=n<o?t:n>o?s:Math.min(t,s),p=n<o?s:n>o?t:Math.max(t,s),S=r.viewportY,k=[];for(let H=c;H<=l;H++){let ve=r.getLine(S+H);if(!ve){k.push("");continue}let O=H===c?u:0,Ne=H===l?p:this.terminal.cols-1,K="";for(let C=O;C<=Ne;C++){let z=ve.getCell(C);z&&(K+=z.getChars())}k.push(K.replace(/\s+$/,""))}return k.join(`
3
- `)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}};function en(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 tn(e,t,n){return e.innerHTML="",e.classList.add("terminal-pending"),(async()=>{let s=await Qt.create(e,n.scrollback,n.theme),o,r=0,c,l=null,u=0,p=0,S=.2,k=10,H=.95,ve=.04;function O(){u&&(cancelAnimationFrame(u),u=0),p=0}function Ne(i){if(O(),Math.abs(i)<S)return;p=Math.max(-k,Math.min(k,i));let a=()=>{if(N){u=0;return}let d=e.querySelector(".xterm-rows > div"),f=d?d.getBoundingClientRect().height:16,b=f>0?Math.round(p*16/f):p>0?1:-1;b===0&&(b=p>0?1:-1);let I=s.viewportY()-b,W=s.baseY(),ae=Math.max(0,Math.min(I,W));if(s.scrollToLine(ae,!0),p*=H,ae!==I){u=0;return}if(Math.abs(p)<ve){u=0;return}u=requestAnimationFrame(a)};u=requestAnimationFrame(a)}let K=0,C="connecting",z=0,G=!1,ge=[],V="",N=!1,Q=1e3,Mt=location.protocol==="https:"?"wss:":"ws:",Tt=n.wsBase||"/ws",St=Mt+"//"+location.host+Tt+"/"+encodeURIComponent(t),Bt="/api/session/"+encodeURIComponent(t)+"/upload",It=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function Dt(){return ge.join("")+V}async function Ht(i,a=0){let d=i?s.viewportY():0,f=oe(Dt());if(s.reset(),!f){i||s.scrollToBottom();return}await s.write(f),i?s.scrollToLine(a+d):s.scrollToBottom()}function j(i){o?.readyState===WebSocket.OPEN&&o.send(JSON.stringify(i))}function At(i){let a;try{a=JSON.parse(i)}catch{C==="live"&&(V+=i,s.write(oe(i)));return}if(a.type==="auth.required"){let d=localStorage.getItem("tmux-web-token");d?j({type:"auth.token",token:d}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(a.type==="attach_failed"&&typeof a.message=="string"){ye=!0,C="dead",s.reset(),s.write(`\r
1
+ var an=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function Ue(e){return e.replace(/\x1b\[\?1049[hl]/g,"")}function oe(e){return e.replace(an,"")}var Ce;function on(){return Ce||(document.querySelector("link[data-tmux-web-xterm-css]")?Ce=Promise.resolve():(Ce=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)}),Ce))}var ln=class Ht{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:o},{FitAddon:r}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),on()]),c=new o({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,smoothScrollDuration:120,theme:s});return new Ht(t,c,new r)}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,n=!1){this.terminal.scrollToLine(Math.max(0,t),n)}scrollLines(t){let n=this.terminal.buffer?.active;this.terminal.scrollLines(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=rn(this.container);if(t.width<=0||t.height<=0)return!1;let n=this.terminal._core?._renderService?.dimensions?.css?.cell;if(!n?.width||!n?.height)return this.fitAddon.fit(),!0;let s=n.width,o=n.height,r=this.terminal.element?.querySelector(".xterm-viewport"),c=r?Math.max(0,r.offsetWidth-r.clientWidth):0,l=Math.max(2,Math.floor((t.width-c)/s)),u=Math.max(2,Math.floor(t.height/o));return(l!==this.terminal.cols||u!==this.terminal.rows)&&this.terminal.resize(l,u),!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||t===this.terminal.textarea||this.container.contains(t)||!!t.closest?.(".xterm"):!1}getScreenText(){return this.getRowsText(0,this.terminal.rows-1)}getRowsText(t,n){let s=this.terminal.buffer.active,[o,r]=t<=n?[t,n]:[n,t],c=s.viewportY,l=[];for(let u=o;u<=r;u++){let p=s.getLine(c+u);l.push(p?p.translateToString(!0):"")}return l.join(`
2
+ `)}selectRows(t,n){let s=this.terminal.buffer.active,[o,r]=t<=n?[t,n]:[n,t];this.terminal.selectLines(s.viewportY+o,s.viewportY+r)}selectCells(t,n,s,o){let r=this.terminal.buffer.active,c=Math.min(n,o),l=Math.max(n,o);if(c===l){let u=Math.min(t,s),p=Math.max(t,s);this.terminal.select(u,r.viewportY+c,p-u+1)}else this.terminal.selectLines(r.viewportY+c,r.viewportY+l)}getCellsText(t,n,s,o){let r=this.terminal.buffer.active,c=Math.min(n,o),l=Math.max(n,o),u=n<o?t:n>o?s:Math.min(t,s),p=n<o?s:n>o?t:Math.max(t,s),S=r.viewportY,x=[];for(let H=c;H<=l;H++){let ge=r.getLine(S+H);if(!ge){x.push("");continue}let O=H===c?u:0,je=H===l?p:this.terminal.cols-1,K="";for(let C=O;C<=je;C++){let j=ge.getCell(C);j&&(K+=j.getChars())}x.push(K.replace(/\s+$/,""))}return x.join(`
3
+ `)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}};function rn(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 cn(e,t,n){return e.innerHTML="",e.classList.add("terminal-pending"),(async()=>{let s=await ln.create(e,n.scrollback,n.theme),o,r=0,c,l=null,u=0,p=0,S=.2,x=10,H=.95,ge=.04;function O(){u&&(cancelAnimationFrame(u),u=0),p=0}function je(i){if(O(),Math.abs(i)<S)return;p=Math.max(-x,Math.min(x,i));let a=()=>{if(N){u=0;return}let d=e.querySelector(".xterm-rows > div"),f=d?d.getBoundingClientRect().height:16,b=f>0?Math.round(p*16/f):p>0?1:-1;b===0&&(b=p>0?1:-1);let B=s.viewportY()-b,F=s.baseY(),ae=Math.max(0,Math.min(B,F));if(s.scrollToLine(ae,!0),p*=H,ae!==B){u=0;return}if(Math.abs(p)<ge){u=0;return}u=requestAnimationFrame(a)};u=requestAnimationFrame(a)}let K=0,C="connecting",j=0,G=!1,ye=[],V="",N=!1,Q=1e3,At=location.protocol==="https:"?"wss:":"ws:",Rt=n.wsBase||"/ws",$t=At+"//"+location.host+Rt+"/"+encodeURIComponent(t),Nt="/api/session/"+encodeURIComponent(t)+"/upload",Pt=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function _t(){return ye.join("")+V}async function jt(i,a=0){let d=i?s.viewportY():0,f=oe(_t());if(s.reset(),!f){i||s.scrollToBottom();return}await s.write(f),i?s.scrollToLine(a+d):s.scrollToBottom()}function z(i){o?.readyState===WebSocket.OPEN&&o.send(JSON.stringify(i))}function zt(i){let a;try{a=JSON.parse(i)}catch{C==="live"&&(V+=i,s.write(oe(i)));return}if(a.type==="auth.required"){let d=localStorage.getItem("tmux-web-token");d?z({type:"auth.token",token:d}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(a.type==="attach_failed"&&typeof a.message=="string"){we=!0,C="dead",s.reset(),s.write(`\r
4
4
  \x1B[31m`+oe(a.message)+`\x1B[0m\r
5
- `).then(()=>{be();try{o?.close()}catch{}});return}if(a.type==="auth.ok"){a.token&&localStorage.setItem("tmux-web-token",a.token),Q=1e3,F(),j({type:"resize",cols:s.cols,rows:s.rows}),M();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=je(oe(a.data));ge=[d],V="",z=typeof a.lines=="number"?a.lines:n.terminal.initialLines,C="live",s.reset(),F(),j({type:"resize",cols:s.cols,rows:s.rows}),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"){a.lines>0&&a.data?(ge.unshift(je(a.data)),z+=a.lines,Ht(!0,a.lines).finally(()=>{G=!1})):G=!1;return}if(a.type==="data"&&typeof a.data=="string"){let d=je(oe(a.data));if(C==="connecting"){C="live",V=d,s.write(d);return}C==="live"&&(V+=d,s.write(d))}}let ye=!1;function Ve(){if(ye)return;let i=St,a=localStorage.getItem("tmux-web-token");a&&(i+=(i.includes("?")?"&":"?")+"token="+encodeURIComponent(a)),i+=(i.includes("?")?"&":"?")+"cols="+s.cols+"&rows="+s.rows,o=new WebSocket(i),o.onopen=()=>{C="connecting",z=0,G=!1,ge=[],V="",s.reset(),Q=1e3},o.onmessage=d=>{typeof d.data=="string"&&At(d.data)},o.onclose=()=>{N||(C="connecting",!ye&&setTimeout(()=>{Q=Math.min(Q*2,1e4),!N&&!ye&&Ve()},Q))},o.onerror=()=>o?.close()}function we(i){j({type:"input",data:i})}function F(){return s.fit()}function be(){e.classList.remove("terminal-pending")}function M(){r&&cancelAnimationFrame(r),c&&clearTimeout(c),F(),r=requestAnimationFrame(()=>{r=0,F()&&be()}),c=setTimeout(()=>{F()&&be()},120)}function Rt(){M(),setTimeout(M,50),setTimeout(M,150),setTimeout(M,300)}function Xe(i){return typeof i=="string"&&i.startsWith("image/")}function Je(i){return i?.files?.length?i.files[0]??null:null}function $t(i){if(!i?.items)return null;for(let a=0;a<i.items.length;a++){let d=i.items[a];if(d?.kind==="file"&&Xe(d.type))return d.getAsFile()}return null}async function Nt(i){let a=new FormData,d=i instanceof File&&i.name?i.name:"upload";a.append("file",i,d);let f=localStorage.getItem("tmux-web-token"),b={};f&&(b.Authorization="Bearer "+f);let I=await fetch(Bt,{method:"POST",body:a,headers:b});if(!I.ok){let ae="upload failed";try{let st=await I.json();st.error&&(ae=st.error)}catch{}throw new Error(ae)}let W=await I.json();if(!W.path)throw new Error("no path in response");return W.path}async function Pe(i){try{let a=await Nt(i);we(a)}catch{}}s.onData(i=>we(i)),s.onResize(({cols:i,rows:a})=>j({type:"resize",cols:i,rows:a})),s.onScroll(()=>{C!=="live"||G||!s.isNearScrollbackTop()||(G=!0,j({type:"load_history",before:z}))});let ee=!1,te=!1,w=null,L=null,ne=document.createElement("div");ne.className="terminal-copy-banner",ne.innerHTML='<strong>Copy mode</strong><span class="hint">drag to copy \xB7 Esc exits</span>',ne.hidden=!0,e.appendChild(ne);let R=document.createElement("div");R.className="terminal-copy-banner terminal-copy-toast",R.textContent="Copied",R.hidden=!0,e.appendChild(R);function Ze(i,a){let d=e.querySelector(".xterm-screen, canvas"),f=(d&&d.getBoundingClientRect().width>0?d:e).getBoundingClientRect(),b=f.height>0&&s.rows>0?f.height/s.rows:18,I=f.width>0&&s.cols>0?f.width/s.cols:9;return{row:Math.max(0,Math.min(s.rows-1,Math.floor((a-f.top)/b))),col:Math.max(0,Math.min(s.cols-1,Math.floor((i-f.left)/I)))}}async function Le(i){if(!i)return;try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(i);return}}catch{}let a=document.createElement("textarea");a.value=i,a.style.position="fixed",a.style.opacity="0",a.style.pointerEvents="none",document.body.appendChild(a),a.focus(),a.select();try{document.execCommand("copy")}catch{}a.remove()}let se;function Ee(){R.hidden=!1,se&&clearTimeout(se),se=setTimeout(()=>{R.hidden=!0},1e3)}function X(i){ee=i,ne.hidden=!i,e.classList.toggle("terminal-copy-mode-active",i),i&&s.focus()}function Ge(){X(!ee)}function Pt(i){N||i.button!==0||te||(ee||i.shiftKey)&&(i.preventDefault(),i.stopPropagation(),te=!0,w=Ze(i.clientX,i.clientY),L=w,s.selectCells(w.col,w.row,L.col,L.row),document.addEventListener("mousemove",_e,!0),document.addEventListener("mouseup",ze,!0))}function _e(i){N||!te||!w||(i.preventDefault(),i.stopPropagation(),L=Ze(i.clientX,i.clientY),s.selectCells(w.col,w.row,L.col,L.row))}function ze(i){if(N||!te||!w||!L)return;i.preventDefault(),i.stopPropagation(),document.removeEventListener("mousemove",_e,!0),document.removeEventListener("mouseup",ze,!0),te=!1;let a=s.getCellsText(w.col,w.row,L.col,L.row);w=null,L=null,s.clearSelection(),X(!1),a&&(R.textContent="\u5DF2\u590D\u5236",Le(a).then(Ee)),s.focus()}e.addEventListener("mousedown",Pt,!0),document.addEventListener("keydown",_t,!0);function _t(i){if(!(N||!s.isFocused()||i.defaultPrevented||i.isComposing)){if(ee){if(i.key==="Escape"){i.preventDefault(),i.stopPropagation(),w=null,L=null,s.clearSelection(),X(!1);return}if(i.key==="Enter"||(i.ctrlKey||i.metaKey)&&i.code==="KeyC"){i.preventDefault(),i.stopPropagation(),Le(s.getScreenText()).then(Ee),X(!1);return}}if(i.altKey&&i.code==="KeyC"&&!i.ctrlKey&&!i.metaKey){if(i.preventDefault(),i.stopPropagation(),w&&L){let a=s.getCellsText(w.col,w.row,L.col,L.row);w=null,L=null,s.clearSelection(),X(!1),a&&(R.textContent="Copied",Le(a).then(Ee))}else{let a=s.getSelection();a?(s.clearSelection(),R.textContent="Copied",Le(a).then(Ee)):Ge()}return}if((i.ctrlKey||i.metaKey)&&i.code==="KeyV"){i.preventDefault(),i.stopPropagation(),(async()=>{try{if(navigator.clipboard?.read){let a=await navigator.clipboard.read();for(let d of a)for(let f of d.types)if(Xe(f)){let b=await d.getType(f);await Pe(b);return}}}catch{}try{let a=await navigator.clipboard?.readText();a&&s.pasteText(Qe(a))}catch{}})();return}}}let xe=0;function Qe(i){return i.split(`\r
5
+ `).then(()=>{Le();try{o?.close()}catch{}});return}if(a.type==="auth.ok"){a.token&&localStorage.setItem("tmux-web-token",a.token),Q=1e3,W(),z({type:"resize",cols:s.cols,rows:s.rows}),M();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(oe(a.data));ye=[d],V="",j=typeof a.lines=="number"?a.lines:n.terminal.initialLines,C="live",s.reset(),W(),z({type:"resize",cols:s.cols,rows:s.rows}),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"){a.lines>0&&a.data?(ye.unshift(Ue(a.data)),j+=a.lines,jt(!0,a.lines).finally(()=>{G=!1})):G=!1;return}if(a.type==="data"&&typeof a.data=="string"){let d=Ue(oe(a.data));if(C==="connecting"){C="live",V=d,s.write(d);return}C==="live"&&(V+=d,s.write(d))}}let we=!1;function Ge(){if(we)return;let i=$t,a=localStorage.getItem("tmux-web-token");a&&(i+=(i.includes("?")?"&":"?")+"token="+encodeURIComponent(a)),i+=(i.includes("?")?"&":"?")+"cols="+s.cols+"&rows="+s.rows,o=new WebSocket(i),o.onopen=()=>{C="connecting",j=0,G=!1,ye=[],V="",s.reset(),Q=1e3},o.onmessage=d=>{typeof d.data=="string"&&zt(d.data)},o.onclose=()=>{N||(C="connecting",!we&&setTimeout(()=>{Q=Math.min(Q*2,1e4),!N&&!we&&Ge()},Q))},o.onerror=()=>o?.close()}function be(i){z({type:"input",data:i})}function W(){return s.fit()}function Le(){e.classList.remove("terminal-pending")}function M(){r&&cancelAnimationFrame(r),c&&clearTimeout(c),W(),r=requestAnimationFrame(()=>{r=0,W()&&Le()}),c=setTimeout(()=>{W()&&Le()},120)}function Wt(){M(),setTimeout(M,50),setTimeout(M,150),setTimeout(M,300)}function Qe(i){return typeof i=="string"&&i.startsWith("image/")}function et(i){return i?.files?.length?i.files[0]??null:null}function Ft(i){if(!i?.items)return null;for(let a=0;a<i.items.length;a++){let d=i.items[a];if(d?.kind==="file"&&Qe(d.type))return d.getAsFile()}return null}async function Ut(i){let a=new FormData,d=i instanceof File&&i.name?i.name:"upload";a.append("file",i,d);let f=localStorage.getItem("tmux-web-token"),b={};f&&(b.Authorization="Bearer "+f);let B=await fetch(Nt,{method:"POST",body:a,headers:b});if(!B.ok){let ae="upload failed";try{let lt=await B.json();lt.error&&(ae=lt.error)}catch{}throw new Error(ae)}let F=await B.json();if(!F.path)throw new Error("no path in response");return F.path}async function ze(i){try{let a=await Ut(i);be(a)}catch{}}s.onData(i=>be(i)),s.onResize(({cols:i,rows:a})=>z({type:"resize",cols:i,rows:a})),s.onScroll(()=>{C!=="live"||G||!s.isNearScrollbackTop()||(G=!0,z({type:"load_history",before:j}))});let ee=!1,te=!1,w=null,L=null,ne=document.createElement("div");ne.className="terminal-copy-banner",ne.innerHTML='<strong>Copy mode</strong><span class="hint">drag to copy \xB7 Esc exits</span>',ne.hidden=!0,e.appendChild(ne);let R=document.createElement("div");R.className="terminal-copy-banner terminal-copy-toast",R.textContent="Copied",R.hidden=!0,e.appendChild(R);function tt(i,a){let d=e.querySelector(".xterm-screen, canvas"),f=(d&&d.getBoundingClientRect().width>0?d:e).getBoundingClientRect(),b=f.height>0&&s.rows>0?f.height/s.rows:18,B=f.width>0&&s.cols>0?f.width/s.cols:9;return{row:Math.max(0,Math.min(s.rows-1,Math.floor((a-f.top)/b))),col:Math.max(0,Math.min(s.cols-1,Math.floor((i-f.left)/B)))}}async function Ee(i){if(!i)return;try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(i);return}}catch{}let a=document.createElement("textarea");a.value=i,a.style.position="fixed",a.style.opacity="0",a.style.pointerEvents="none",document.body.appendChild(a),a.focus(),a.select();try{document.execCommand("copy")}catch{}a.remove()}let se;function ke(){R.hidden=!1,se&&clearTimeout(se),se=setTimeout(()=>{R.hidden=!0},1e3)}function J(i){ee=i,ne.hidden=!i,e.classList.toggle("terminal-copy-mode-active",i),i&&s.focus()}function nt(){J(!ee)}function Yt(i){N||i.button!==0||te||(ee||i.shiftKey)&&(i.preventDefault(),i.stopPropagation(),te=!0,w=tt(i.clientX,i.clientY),L=w,s.selectCells(w.col,w.row,L.col,L.row),document.addEventListener("mousemove",We,!0),document.addEventListener("mouseup",Fe,!0))}function We(i){N||!te||!w||(i.preventDefault(),i.stopPropagation(),L=tt(i.clientX,i.clientY),s.selectCells(w.col,w.row,L.col,L.row))}function Fe(i){if(N||!te||!w||!L)return;i.preventDefault(),i.stopPropagation(),document.removeEventListener("mousemove",We,!0),document.removeEventListener("mouseup",Fe,!0),te=!1;let a=s.getCellsText(w.col,w.row,L.col,L.row);w=null,L=null,s.clearSelection(),J(!1),a&&(R.textContent="\u5DF2\u590D\u5236",Ee(a).then(ke)),s.focus()}e.addEventListener("mousedown",Yt,!0),document.addEventListener("keydown",qt,!0);function qt(i){if(!(N||!s.isFocused()||i.defaultPrevented||i.isComposing)){if(ee){if(i.key==="Escape"){i.preventDefault(),i.stopPropagation(),w=null,L=null,s.clearSelection(),J(!1);return}if(i.key==="Enter"||(i.ctrlKey||i.metaKey)&&i.code==="KeyC"){i.preventDefault(),i.stopPropagation(),Ee(s.getScreenText()).then(ke),J(!1);return}}if(i.altKey&&i.code==="KeyC"&&!i.ctrlKey&&!i.metaKey){if(i.preventDefault(),i.stopPropagation(),w&&L){let a=s.getCellsText(w.col,w.row,L.col,L.row);w=null,L=null,s.clearSelection(),J(!1),a&&(R.textContent="Copied",Ee(a).then(ke))}else{let a=s.getSelection();a?(s.clearSelection(),R.textContent="Copied",Ee(a).then(ke)):nt()}return}if((i.ctrlKey||i.metaKey)&&i.code==="KeyV"){i.preventDefault(),i.stopPropagation(),(async()=>{try{if(navigator.clipboard?.read){let a=await navigator.clipboard.read();for(let d of a)for(let f of d.types)if(Qe(f)){let b=await d.getType(f);await ze(b);return}}}catch{}try{let a=await navigator.clipboard?.readText();a&&s.pasteText(st(a))}catch{}})();return}}}let xe=0;function st(i){return i.split(`\r
6
6
  `).join(`
7
7
  `).split(`
8
- `).join("\r")}async function zt(i){let a=$t(i.clipboardData);if(a){i.preventDefault(),i.stopPropagation();let b=Date.now();if(b-xe<50)return;xe=b,await Pe(a);return}let d=i.clipboardData?.getData("text/plain");if(!d)return;i.preventDefault(),i.stopPropagation();let f=Date.now();f-xe<50||(xe=f,s.pasteText(Qe(d)))}let ie=0;function jt(i){i.preventDefault(),ie++,Je(i.dataTransfer)&&e.classList.add("terminal-drag-over")}function Ft(i){i.preventDefault(),i.dataTransfer&&(i.dataTransfer.dropEffect="copy")}function Wt(i){i.preventDefault(),ie--,ie<=0&&(ie=0,e.classList.remove("terminal-drag-over"))}async function Yt(i){i.preventDefault(),ie=0,e.classList.remove("terminal-drag-over");let a=Je(i.dataTransfer);a&&await Pe(a)}function et(i){!It||i.key!=="Escape"||ee||i.defaultPrevented||i.repeat||i.metaKey||i.ctrlKey||i.altKey||s.isFocused()&&(i.preventDefault(),i.stopPropagation(),we("\x1B"))}e.addEventListener("paste",zt),e.addEventListener("dragenter",jt),e.addEventListener("dragover",Ft),e.addEventListener("dragleave",Wt),e.addEventListener("drop",Yt),document.addEventListener("keydown",et,!0);function Ut(i){if(i.touches.length!==1){l=null;return}let a=i.touches[0];a&&(O(),l={startX:a.clientX,startY:a.clientY,lastX:a.clientX,lastY:a.clientY,lastT:performance.now(),velocity:0,scrolling:!1},i.stopPropagation())}function qt(i){if(!l||i.touches.length!==1)return;let a=i.touches[0];if(!a)return;let d=a.clientY-l.startY,f=a.clientX-l.startX;if(!l.scrolling){if(Math.abs(d)<8||Math.abs(d)<Math.abs(f))return;l.scrolling=!0,K=Date.now()+500}i.preventDefault(),i.stopPropagation();let b=performance.now(),I=a.clientY-l.lastY,W=b-l.lastT;W>0&&(l.velocity=I/W),Xt(-I),l.lastX=a.clientX,l.lastY=a.clientY,l.lastT=b}function Ot(i){if(!l)return;let a=l.scrolling,d=l.velocity;if(l=null,i.stopPropagation(),!a){s.focus();return}K=Date.now()+500,i.preventDefault(),d&&Ne(d)}function Kt(i){l=null,O(),i.stopPropagation()}function Vt(i){i.pointerType==="touch"&&Date.now()<K&&(i.preventDefault(),i.stopPropagation())}function Xt(i){let a=e.querySelector(".xterm-rows > div"),d=a?a.getBoundingClientRect().height:16,f=d>0?Math.round(i/d):i>0?1:-1;f===0&&(f=i>0?1:-1),s.scrollLines(f)}e.addEventListener("touchstart",Ut,{passive:!0,capture:!0}),e.addEventListener("touchmove",qt,{passive:!1,capture:!0}),e.addEventListener("touchend",Ot,{passive:!1,capture:!0}),e.addEventListener("touchcancel",Kt,{passive:!0,capture:!0}),e.addEventListener("pointerup",Vt,!0);function tt(){M()}window.addEventListener("resize",tt),window.visualViewport?.addEventListener("resize",M),window.visualViewport?.addEventListener("scroll",M);let nt=new ResizeObserver(()=>M());nt.observe(e),document.fonts?.ready.then(()=>{M(),setTimeout(M,50),setTimeout(M,200)}),e.addEventListener("focusin",Rt,!0);async function Jt(){if(document.fonts?.ready)try{await Promise.race([document.fonts.ready,new Promise(i=>setTimeout(i,1e3))])}catch{}}return requestAnimationFrame(()=>{F()&&be(),(async()=>(await Jt(),F()&&j({type:"resize",cols:s.cols,rows:s.rows}),Ve(),M()))()}),{destroy(){if(N=!0,O(),X(!1),se&&clearTimeout(se),document.removeEventListener("mousemove",_e,!0),document.removeEventListener("mouseup",ze,!0),o){try{o.close(1e3,"session switch")}catch{}o=void 0}e.innerHTML="",e.classList.remove("terminal-pending"),window.removeEventListener("resize",tt),nt.disconnect(),document.removeEventListener("keydown",et,!0)},focus(){s.focus()},sendInput(i){we(i)},toggleCopyMode(){Ge()}}})()}var le=window.__TMUX_WEB_SHELL__,v=null,y=null,it=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null,x=it;window.__tmuxWebScopeAgent=it;var Fe="sessions",P="",Ce="",Me="",at="";function T(e){return x?"/a/"+encodeURIComponent(x)+e:e}var h=document.getElementById("sidebar-content"),Y=document.getElementById("main-placeholder"),B=document.getElementById("terminal-container"),re=document.getElementById("file-editor"),nn=document.getElementById("fe-path"),D=document.getElementById("fe-status"),ce=document.getElementById("fe-content"),U=document.getElementById("fe-save"),sn=document.getElementById("fe-delete"),an=document.getElementById("fe-back"),ot=document.getElementById("fe-new-name"),on=document.getElementById("fe-new-btn"),We=document.getElementById("settings-backdrop"),lt=document.getElementById("settings-popover"),rt=document.querySelector(".sidebar");function ln(){return window.matchMedia("(max-width: 640px)").matches}function Ye(){ln()&&rt.classList.add("collapsed")}function ct(){rt.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{ct(),dt("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{ct(),dt("files")});function dt(e){Fe=e,document.querySelectorAll(".mode-btn").forEach(n=>n.classList.remove("active"));let t=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");t&&t.classList.add("active"),e==="sessions"?(Ue="",J()):Ln()}var Ue="";async function J(){if(Fe!=="sessions")return;try{let t=await(await fetch("/api/sidebar/sessions")).json(),n=JSON.stringify({recent:t.recent,agents:t.agents});if(n===Ue)return;Ue=n,pn(t)}catch{h.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>{let t=window.__openNewSessionModal;typeof t=="function"?t():document.getElementById("new-session-modal")?.classList.add("open")})}var ut="tmux-web-sidebar-group-collapsed";function pt(){try{return new Set(JSON.parse(localStorage.getItem(ut)||"[]"))}catch{return new Set}}function rn(e){try{localStorage.setItem(ut,JSON.stringify([...e]))}catch{}}function cn(e,t,n){if(!e)return"\u2026";let s=n&&(e===n||e.startsWith(n+"/"))?"~"+e.slice(n.length):e,o=s.split("/").filter(Boolean);if(o.length===0)return e;for(let r=1;r<=o.length;r++){let c=o.slice(-r).join("/");if(!t.some(l=>!l||l===e?!1:(n&&(l===n||l.startsWith(n+"/"))?"~"+l.slice(n.length):l).split("/").filter(Boolean).slice(-r).join("/")===c))return c}return s}function dn(e,t){let n=new Map;for(let r of e){let c=r.path||"";n.has(c)||n.set(c,[]),n.get(c).push(r)}let s=[...n.keys()],o=pt();for(let[r,c]of n){let l=cn(r,s,t),u=o.has(r),p=document.createElement("div");p.className="session-group-header"+(u?" collapsed":""),p.title=r||"No working directory",p.innerHTML=`<span class="sg-folder">&#128193;</span><span class="sg-label">${m(l)}</span>`,p.addEventListener("click",()=>{let k=pt(),H=p.nextElementSibling;k.has(r)?(k.delete(r),p.classList.remove("collapsed"),H?.classList.remove("collapsed")):(k.add(r),p.classList.add("collapsed"),H?.classList.add("collapsed")),rn(k)});let S=document.createElement("div");S.className="session-group-body"+(u?" collapsed":"");for(let k of c)S.appendChild(un(k));h.appendChild(p),h.appendChild(S)}}function un(e){let t=document.createElement("div");t.className="session-item"+(e.name===y&&!x?" active":""),t.setAttribute("data-session",e.name),t.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${e.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>
9
- <span>${m(e.name)}</span>
10
- <button class="session-edit-btn" data-session="${m(e.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>
11
- `,t.addEventListener("click",s=>{s.target.closest(".session-edit-btn")||de(e.name)});let n=t.querySelector(".session-edit-btn");return n&&n.addEventListener("click",s=>{s.stopPropagation(),Mn(e.name,n)}),t}function pn(e){h.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';let t=e.recent||[],n=document.createElement("div");n.className="sidebar-section-label"+(x?" clickable":""),n.textContent=x?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",x&&(n.title="Back to hub",n.addEventListener("click",()=>{window.location.href="/"})),h.appendChild(n),dn(t,e.home);let s=e.agents||[];for(let o of s){if(!o.sessions||o.sessions.length===0)continue;let r=o.agentId===x,c=document.createElement("div");c.className="sidebar-section-label machine-label"+(o.online?"":" agent-offline-label")+(!r&&o.online?" clickable":""),c.textContent=o.agentName+(o.online?"":" (offline)"),!r&&o.online&&(c.title="\u6253\u5F00 "+o.agentName,c.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(o.agentId)+"/"})),h.appendChild(c);for(let l of o.sessions){let u=document.createElement("div");u.className="session-item"+(l.name===y&&x===o.agentId?" active":"")+(o.online?"":" session-offline"),u.setAttribute("data-session",l.name),u.setAttribute("data-agent",o.agentId),u.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${l.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>
12
- <span>${m(l.name)}</span><span class="meta">${o.online?"":"offline"}</span>`,o.online&&u.addEventListener("click",p=>{p.target.closest(".session-edit-btn")||de(l.name,o.agentId)}),h.appendChild(u)}}}function m(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function de(e,t){if(!(y===e&&v&&x===(t||null))){re.style.display="none",Y.style.display="none",B.style.display="",B.classList.add("terminal-pending"),Ye(),v&&(Re(),v.destroy(),v=null),y=e,x=t||null,window.__tmuxWebScopeAgent=x,window.__tmuxWebCurrentSession=y;for(let n of h.querySelectorAll(".session-item"))n.classList.toggle("active",n.getAttribute("data-session")===e&&(n.getAttribute("data-agent")||null)===x);try{v=await tn(B,e,{terminal:le.terminal,scrollback:le.scrollback,theme:le.theme,wsBase:x?"/ws/a/"+encodeURIComponent(x):le.wsBase||"/ws"}),Et()}catch(n){console.error("[shell] terminal init failed:",n);let s=n&&typeof n=="object"&&"message"in n?n.message:String(n);B.textContent="Failed to open terminal for "+e+": "+s,B.classList.remove("terminal-pending")}}}var Te=[],g=null,ft=!1;function fn(e){if(!g||!g.files.length)return null;let t=g.repoRoot;if(!t||!e.startsWith(t))return null;let n=e.slice(t.length+1);for(let s of g.files)if(s.path===n)return s.status;return null}async function mn(e){ft=!0;try{let t=await fetch(T("/api/git/status?path="+encodeURIComponent(e)));t.ok?g=await t.json():g=null}catch{g=null}finally{ft=!1}}function hn(){if(!g||!g.branch)return"";let{branch:e,linesAdded:t,linesRemoved:n}=g,s=`<div class="sidebar-git"><div class="git-branch-badge">
8
+ `).join("\r")}async function Ot(i){let a=Ft(i.clipboardData);if(a){i.preventDefault(),i.stopPropagation();let b=Date.now();if(b-xe<50)return;xe=b,await ze(a);return}let d=i.clipboardData?.getData("text/plain");if(!d)return;i.preventDefault(),i.stopPropagation();let f=Date.now();f-xe<50||(xe=f,s.pasteText(st(d)))}let ie=0;function Kt(i){i.preventDefault(),ie++,et(i.dataTransfer)&&e.classList.add("terminal-drag-over")}function Vt(i){i.preventDefault(),i.dataTransfer&&(i.dataTransfer.dropEffect="copy")}function Jt(i){i.preventDefault(),ie--,ie<=0&&(ie=0,e.classList.remove("terminal-drag-over"))}async function Xt(i){i.preventDefault(),ie=0,e.classList.remove("terminal-drag-over");let a=et(i.dataTransfer);a&&await ze(a)}function it(i){!Pt||i.key!=="Escape"||ee||i.defaultPrevented||i.repeat||i.metaKey||i.ctrlKey||i.altKey||s.isFocused()&&(i.preventDefault(),i.stopPropagation(),be("\x1B"))}e.addEventListener("paste",Ot),e.addEventListener("dragenter",Kt),e.addEventListener("dragover",Vt),e.addEventListener("dragleave",Jt),e.addEventListener("drop",Xt),document.addEventListener("keydown",it,!0);function Zt(i){if(i.touches.length!==1){l=null;return}let a=i.touches[0];a&&(O(),l={startX:a.clientX,startY:a.clientY,lastX:a.clientX,lastY:a.clientY,lastT:performance.now(),velocity:0,scrolling:!1},i.stopPropagation())}function Gt(i){if(!l||i.touches.length!==1)return;let a=i.touches[0];if(!a)return;let d=a.clientY-l.startY,f=a.clientX-l.startX;if(!l.scrolling){if(Math.abs(d)<8||Math.abs(d)<Math.abs(f))return;l.scrolling=!0,K=Date.now()+500}i.preventDefault(),i.stopPropagation();let b=performance.now(),B=a.clientY-l.lastY,F=b-l.lastT;F>0&&(l.velocity=B/F),nn(-B),l.lastX=a.clientX,l.lastY=a.clientY,l.lastT=b}function Qt(i){if(!l)return;let a=l.scrolling,d=l.velocity;if(l=null,i.stopPropagation(),!a){s.focus();return}K=Date.now()+500,i.preventDefault(),d&&je(d)}function en(i){l=null,O(),i.stopPropagation()}function tn(i){i.pointerType==="touch"&&Date.now()<K&&(i.preventDefault(),i.stopPropagation())}function nn(i){let a=e.querySelector(".xterm-rows > div"),d=a?a.getBoundingClientRect().height:16,f=d>0?Math.round(i/d):i>0?1:-1;f===0&&(f=i>0?1:-1),s.scrollLines(f)}e.addEventListener("touchstart",Zt,{passive:!0,capture:!0}),e.addEventListener("touchmove",Gt,{passive:!1,capture:!0}),e.addEventListener("touchend",Qt,{passive:!1,capture:!0}),e.addEventListener("touchcancel",en,{passive:!0,capture:!0}),e.addEventListener("pointerup",tn,!0);function at(){M()}window.addEventListener("resize",at),window.visualViewport?.addEventListener("resize",M),window.visualViewport?.addEventListener("scroll",M);let ot=new ResizeObserver(()=>M());ot.observe(e),document.fonts?.ready.then(()=>{M(),setTimeout(M,50),setTimeout(M,200)}),e.addEventListener("focusin",Wt,!0);async function sn(){if(document.fonts?.ready)try{await Promise.race([document.fonts.ready,new Promise(i=>setTimeout(i,1e3))])}catch{}}return requestAnimationFrame(()=>{W()&&Le(),(async()=>(await sn(),W()&&z({type:"resize",cols:s.cols,rows:s.rows}),Ge(),M()))()}),{destroy(){if(N=!0,O(),J(!1),se&&clearTimeout(se),document.removeEventListener("mousemove",We,!0),document.removeEventListener("mouseup",Fe,!0),o){try{o.close(1e3,"session switch")}catch{}o=void 0}e.innerHTML="",e.classList.remove("terminal-pending"),window.removeEventListener("resize",at),ot.disconnect(),document.removeEventListener("keydown",it,!0)},focus(){s.focus()},sendInput(i){be(i)},toggleCopyMode(){nt()}}})()}var le=window.__TMUX_WEB_SHELL__,v=null,y=null,rt=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null,k=rt;window.__tmuxWebScopeAgent=rt;var Ye="sessions",P="",Me="",Te="",ct="";function T(e){return k?"/a/"+encodeURIComponent(k)+e:e}var m=document.getElementById("sidebar-content"),U=document.getElementById("main-placeholder"),I=document.getElementById("terminal-container"),re=document.getElementById("file-editor"),dn=document.getElementById("fe-path"),D=document.getElementById("fe-status"),ce=document.getElementById("fe-content"),Y=document.getElementById("fe-save"),un=document.getElementById("fe-delete"),pn=document.getElementById("fe-back"),dt=document.getElementById("fe-new-name"),fn=document.getElementById("fe-new-btn"),qe=document.getElementById("settings-backdrop"),ut=document.getElementById("settings-popover"),pt=document.querySelector(".sidebar");function mn(){return window.matchMedia("(max-width: 640px)").matches}function Oe(){mn()&&pt.classList.add("collapsed")}function ft(){pt.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{ft(),mt("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{ft(),mt("files")});function mt(e){Ye=e,document.querySelectorAll(".mode-btn").forEach(n=>n.classList.remove("active"));let t=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");t&&t.classList.add("active"),e==="sessions"?(Ke="",X()):Sn()}var Ke="";async function X(){if(Ye!=="sessions")return;try{let t=await(await fetch("/api/sidebar/sessions")).json(),n=JSON.stringify({recent:t.recent,agents:t.agents});if(n===Ke)return;Ke=n,wn(t)}catch{m.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>{let t=window.__openNewSessionModal;typeof t=="function"?t():document.getElementById("new-session-modal")?.classList.add("open")}),Bt()}var ht="tmux-web-sidebar-group-collapsed";function vt(){try{return new Set(JSON.parse(localStorage.getItem(ht)||"[]"))}catch{return new Set}}function hn(e){try{localStorage.setItem(ht,JSON.stringify([...e]))}catch{}}function vn(e,t,n){if(!e)return"\u2026";let s=n&&(e===n||e.startsWith(n+"/"))?"~"+e.slice(n.length):e,o=s.split("/").filter(Boolean);if(o.length===0)return e;for(let r=1;r<=o.length;r++){let c=o.slice(-r).join("/");if(!t.some(l=>!l||l===e?!1:(n&&(l===n||l.startsWith(n+"/"))?"~"+l.slice(n.length):l).split("/").filter(Boolean).slice(-r).join("/")===c))return c}return s}function gn(e,t){let n=new Map;for(let r of e){let c=r.path||"";n.has(c)||n.set(c,[]),n.get(c).push(r)}let s=[...n.keys()],o=vt();for(let[r,c]of n){let l=vn(r,s,t),u=o.has(r),p=document.createElement("div");p.className="session-group-header"+(u?" collapsed":""),p.title=r||"No working directory",p.innerHTML=`<span class="sg-folder">&#128193;</span><span class="sg-label">${h(l)}</span>`,p.addEventListener("click",()=>{let x=vt(),H=p.nextElementSibling;x.has(r)?(x.delete(r),p.classList.remove("collapsed"),H?.classList.remove("collapsed")):(x.add(r),p.classList.add("collapsed"),H?.classList.add("collapsed")),hn(x)});let S=document.createElement("div");S.className="session-group-body"+(u?" collapsed":"");for(let x of c)S.appendChild(yn(x));m.appendChild(p),m.appendChild(S)}}function yn(e){let t=document.createElement("div");t.className="session-item"+(e.name===y&&!k?" active":""),t.setAttribute("data-session",e.name),t.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${e.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>
9
+ <span>${h(e.name)}</span>
10
+ <button class="session-edit-btn" data-session="${h(e.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>
11
+ `,t.addEventListener("click",s=>{s.target.closest(".session-edit-btn")||de(e.name)});let n=t.querySelector(".session-edit-btn");return n&&n.addEventListener("click",s=>{s.stopPropagation(),An(e.name,n)}),t}function wn(e){m.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';let t=e.recent||[],n=document.createElement("div");n.className="sidebar-section-label"+(k?" clickable":""),n.textContent=k?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",k&&(n.title="Back to hub",n.addEventListener("click",()=>{window.location.href="/"})),m.appendChild(n),gn(t,e.home);let s=e.agents||[];for(let o of s){if(!o.sessions||o.sessions.length===0)continue;let r=o.agentId===k,c=document.createElement("div");c.className="sidebar-section-label machine-label"+(o.online?"":" agent-offline-label")+(!r&&o.online?" clickable":""),c.textContent=o.agentName+(o.online?"":" (offline)"),!r&&o.online&&(c.title="\u6253\u5F00 "+o.agentName,c.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(o.agentId)+"/"})),m.appendChild(c);for(let l of o.sessions){let u=document.createElement("div");u.className="session-item"+(l.name===y&&k===o.agentId?" active":"")+(o.online?"":" session-offline"),u.setAttribute("data-session",l.name),u.setAttribute("data-agent",o.agentId),u.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${l.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>
12
+ <span>${h(l.name)}</span><span class="meta">${o.online?"":"offline"}</span>`,o.online&&u.addEventListener("click",p=>{p.target.closest(".session-edit-btn")||de(l.name,o.agentId)}),m.appendChild(u)}}}function h(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function de(e,t){if(!(y===e&&v&&k===(t||null))){re.style.display="none",U.style.display="none",I.style.display="",I.classList.add("terminal-pending"),Oe(),v&&($e(),v.destroy(),v=null),y=e,k=t||null,window.__tmuxWebScopeAgent=k,window.__tmuxWebCurrentSession=y;for(let n of m.querySelectorAll(".session-item"))n.classList.toggle("active",n.getAttribute("data-session")===e&&(n.getAttribute("data-agent")||null)===k);try{v=await cn(I,e,{terminal:le.terminal,scrollback:le.scrollback,theme:le.theme,wsBase:k?"/ws/a/"+encodeURIComponent(k):le.wsBase||"/ws"}),Mt()}catch(n){console.error("[shell] terminal init failed:",n);let s=n&&typeof n=="object"&&"message"in n?n.message:String(n);I.textContent="Failed to open terminal for "+e+": "+s,I.classList.remove("terminal-pending")}}}var Se=[],g=null,gt=!1;function bn(e){if(!g||!g.files.length)return null;let t=g.repoRoot;if(!t||!e.startsWith(t))return null;let n=e.slice(t.length+1);for(let s of g.files)if(s.path===n)return s.status;return null}async function Ln(e){gt=!0;try{let t=await fetch(T("/api/git/status?path="+encodeURIComponent(e)));t.ok?g=await t.json():g=null}catch{g=null}finally{gt=!1}}function En(){if(!g||!g.branch)return"";let{branch:e,linesAdded:t,linesRemoved:n}=g,s=`<div class="sidebar-git"><div class="git-branch-badge">
13
13
  <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>
14
- ${m(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>"}h.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&Z&&g&&(e.stopPropagation(),yt())});var ue=document.getElementById("header-git"),mt=document.getElementById("git-diff-view"),ht=document.getElementById("gd-path"),Se=document.getElementById("gd-status"),Be=document.getElementById("gd-content"),vn=document.getElementById("gd-back"),Z=null;function pe(e){if(!e||!e.branch){ue.style.display="none",Z=null;return}Z=e.repoRoot;let{branch:t,linesAdded:n,linesRemoved:s}=e,o=`<span class="branch">${m(t)}</span>`;(n>0||s>0)&&(o+=`<span class="sep">\xB7</span>+<span class="diff-add">${n}</span> <span class="diff-del">-${s}</span>`),ue.innerHTML=o,ue.style.display="block"}ue.addEventListener("click",e=>{Z&&g&&(e.stopPropagation(),yt())});var E=document.getElementById("git-popover"),Ie=document.getElementById("git-popover-backdrop");function qe(){let e=ue.getBoundingClientRect();E.style.top=e.bottom+4+"px",E.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function vt(){let e=Z,t=document.getElementById("git-popover-browse");t&&t.addEventListener("click",()=>{if(He(),e){Fe="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));let n=document.getElementById("mode-files");n&&n.classList.add("active"),_(e)}})}var gt="";function yt(){if(!g)return;let e=g.files||[];if(!e.length)E.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
14
+ ${h(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")&&Z&&g&&(e.stopPropagation(),Et())});var ue=document.getElementById("header-git"),yt=document.getElementById("git-diff-view"),wt=document.getElementById("gd-path"),Ie=document.getElementById("gd-status"),Be=document.getElementById("gd-content"),kn=document.getElementById("gd-back"),Z=null;function pe(e){if(!e||!e.branch){ue.style.display="none",Z=null;return}Z=e.repoRoot;let{branch:t,linesAdded:n,linesRemoved:s}=e,o=`<span class="branch">${h(t)}</span>`;(n>0||s>0)&&(o+=`<span class="sep">\xB7</span>+<span class="diff-add">${n}</span> <span class="diff-del">-${s}</span>`),ue.innerHTML=o,ue.style.display="block"}ue.addEventListener("click",e=>{Z&&g&&(e.stopPropagation(),Et())});var E=document.getElementById("git-popover"),De=document.getElementById("git-popover-backdrop");function Ve(){let e=ue.getBoundingClientRect();E.style.top=e.bottom+4+"px",E.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function bt(){let e=Z,t=document.getElementById("git-popover-browse");t&&t.addEventListener("click",()=>{if(Ae(),e){Ye="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));let n=document.getElementById("mode-files");n&&n.classList.add("active"),_(e)}})}var Lt="";function Et(){if(!g)return;let e=g.files||[];if(!e.length)E.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
15
15
  <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],o=s.additions||0,r=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}">
16
- ${c}<span class="file-path">${m(s.path)}</span>
16
+ ${c}<span class="file-path">${h(s.path)}</span>
17
17
  <span class="file-add">${o>0?"+"+o:""}</span>
18
18
  <span class="file-del">${r>0?"-"+r:""}</span>
19
- </div>`}t+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',gt=t,E.innerHTML=t,E.querySelectorAll(".git-popover-item").forEach(n=>{let s=parseInt(n.dataset.idx||"0",10),o=e[s];!o||o.status==="D"||(n.classList.add("clickable"),n.style.cursor="pointer",n.addEventListener("click",()=>bt(o.path)))})}qe(),E.classList.add("open"),Ie.classList.add("open"),vt()}function gn(){return window.innerWidth>=1024}function wt(e,t){let n=(e+(t&&e?`
19
+ </div>`}t+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',Lt=t,E.innerHTML=t,E.querySelectorAll(".git-popover-item").forEach(n=>{let s=parseInt(n.dataset.idx||"0",10),o=e[s];!o||o.status==="D"||(n.classList.add("clickable"),n.style.cursor="pointer",n.addEventListener("click",()=>xt(o.path)))})}Ve(),E.classList.add("open"),De.classList.add("open"),bt()}function xn(){return window.innerWidth>=1024}function kt(e,t){let n=(e+(t&&e?`
20
20
  `:"")+t).split(`
21
- `),s="",o=0,r=0;for(let c of n){let l=c;if(l.startsWith("@@")){let u=l.match(/@@\s+-(\d+)[^+]*\+(\d+)/);u&&(o=parseInt(u[1],10)-1,r=parseInt(u[2],10)-1),s+=`<div class="diff-line hunk"><span class="ln-body">${m(l)}</span></div>`;continue}if(l.startsWith("diff --git")||l.startsWith("index ")||l.startsWith("--- ")||l.startsWith("+++ ")||l.startsWith("new file")||l.startsWith("deleted file")){l.startsWith("diff --git")&&(s+=`<div class="diff-line header"><span class="ln-body">${m(l)}</span></div>`);continue}if(l.startsWith("+"))r++,s+=`<div class="diff-line add"><span class="ln-no">${r}</span><span class="ln-body">${m(l)}</span></div>`;else if(l.startsWith("-"))o++,s+=`<div class="diff-line del"><span class="ln-no">${o}</span><span class="ln-body">${m(l)}</span></div>`;else if(l.startsWith("\\"))s+=`<div class="diff-line"><span class="ln-body">${m(l)}</span></div>`;else{if(!l&&!o&&!r)continue;o++,r++,s+=`<div class="diff-line"><span class="ln-no">${o}</span><span class="ln-no" style="margin-right:12px">${r}</span><span class="ln-body">${m(l)}</span></div>`}}return{html:s,lineCount:n.length}}var De=null;async function bt(e){let t=Z;if(t)if(He(),gn()){B.style.display="none",v&&(Re(),v.destroy(),v=null,De=y,y=null),pe(null),re.style.display="none",Y.style.display="none",mt.style.display="flex",ht.textContent=e,Se.textContent="Loading\u2026",Be.innerHTML="";try{let n=await fetch(T("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!n.ok)throw new Error(await n.text());let s=await n.json();wn(e,s.diff,s.stagedDiff)}catch(n){let s=n instanceof Error?n.message:String(n);Se.textContent="Error",Be.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${m(s)}</div>`}}else{E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
21
+ `),s="",o=0,r=0;for(let c of n){let l=c;if(l.startsWith("@@")){let u=l.match(/@@\s+-(\d+)[^+]*\+(\d+)/);u&&(o=parseInt(u[1],10)-1,r=parseInt(u[2],10)-1),s+=`<div class="diff-line hunk"><span class="ln-body">${h(l)}</span></div>`;continue}if(l.startsWith("diff --git")||l.startsWith("index ")||l.startsWith("--- ")||l.startsWith("+++ ")||l.startsWith("new file")||l.startsWith("deleted file")){l.startsWith("diff --git")&&(s+=`<div class="diff-line header"><span class="ln-body">${h(l)}</span></div>`);continue}if(l.startsWith("+"))r++,s+=`<div class="diff-line add"><span class="ln-no">${r}</span><span class="ln-body">${h(l)}</span></div>`;else if(l.startsWith("-"))o++,s+=`<div class="diff-line del"><span class="ln-no">${o}</span><span class="ln-body">${h(l)}</span></div>`;else if(l.startsWith("\\"))s+=`<div class="diff-line"><span class="ln-body">${h(l)}</span></div>`;else{if(!l&&!o&&!r)continue;o++,r++,s+=`<div class="diff-line"><span class="ln-no">${o}</span><span class="ln-no" style="margin-right:12px">${r}</span><span class="ln-body">${h(l)}</span></div>`}}return{html:s,lineCount:n.length}}var He=null;async function xt(e){let t=Z;if(t)if(Ae(),xn()){I.style.display="none",v&&($e(),v.destroy(),v=null,He=y,y=null),pe(null),re.style.display="none",U.style.display="none",yt.style.display="flex",wt.textContent=e,Ie.textContent="Loading\u2026",Be.innerHTML="";try{let n=await fetch(T("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!n.ok)throw new Error(await n.text());let s=await n.json();Mn(e,s.diff,s.stagedDiff)}catch(n){let s=n instanceof Error?n.message:String(n);Ie.textContent="Error",Be.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${h(s)}</div>`}}else{E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
22
22
  <button class="diff-back">\u2190 Back</button>
23
- <span class="diff-filename">${m(e)}</span>
23
+ <span class="diff-filename">${h(e)}</span>
24
24
  <span class="diff-loading">Loading\u2026</span>
25
- </div></div>`,qe(),E.classList.add("open"),Ie.classList.add("open");try{let n=await fetch(T("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!n.ok)throw new Error(await n.text());let s=await n.json();yn(e,s.diff,s.stagedDiff)}catch(n){let s=n instanceof Error?n.message:String(n);E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
25
+ </div></div>`,Ve(),E.classList.add("open"),De.classList.add("open");try{let n=await fetch(T("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!n.ok)throw new Error(await n.text());let s=await n.json();Cn(e,s.diff,s.stagedDiff)}catch(n){let s=n instanceof Error?n.message:String(n);E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
26
26
  <button class="diff-back">\u2190 Back</button>
27
- <span class="diff-filename">${m(e)}</span>
28
- </div><div class="diff-empty">Error loading diff: ${m(s)}</div></div>`}}}function yn(e,t,n){if(!t&&!n){E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
27
+ <span class="diff-filename">${h(e)}</span>
28
+ </div><div class="diff-empty">Error loading diff: ${h(s)}</div></div>`}}}function Cn(e,t,n){if(!t&&!n){E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
29
29
  <button class="diff-back">\u2190 Back</button>
30
- <span class="diff-filename">${m(e)}</span>
31
- </div><div class="diff-empty">No diff content available</div></div>`;return}let{html:s,lineCount:o}=wt(t,n);E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
30
+ <span class="diff-filename">${h(e)}</span>
31
+ </div><div class="diff-empty">No diff content available</div></div>`;return}let{html:s,lineCount:o}=kt(t,n);E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
32
32
  <button class="diff-back">\u2190 Back</button>
33
- <span class="diff-filename">${m(e)}</span>
33
+ <span class="diff-filename">${h(e)}</span>
34
34
  <span class="diff-filesize">${o} lines</span>
35
- </div><div class="diff-content">${s}</div></div>`,E.querySelector(".diff-back")?.addEventListener("click",()=>{E.innerHTML=gt,qe(),g&&E.querySelectorAll(".git-popover-item").forEach(r=>{let c=parseInt(r.dataset.idx||"0",10),l=g.files[c];!l||l.status==="D"||(r.style.cursor="pointer",r.addEventListener("click",()=>bt(l.path)))}),vt()})}function wn(e,t,n){if(!t&&!n){Se.textContent="No diff",Be.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:o}=wt(t,n);ht.textContent=e,Se.textContent=o+" lines",Be.innerHTML=s}function bn(){mt.style.display="none",De?(B.style.display="",B.classList.add("terminal-pending"),de(De),De=null):y?(B.style.display="",B.classList.add("terminal-pending"),Et()):Y.style.display="flex"}vn.addEventListener("click",bn);function He(){E.classList.remove("open"),Ie.classList.remove("open")}Ie.addEventListener("click",He),document.addEventListener("keydown",e=>{e.key==="Escape"&&(He(),$e())});async function Lt(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();g=s,pe(s);return}}}catch{}g=null,pe(null)}var Ae=null;function Et(){Re(),y&&(Lt(y),Ae=setInterval(()=>{y&&Lt(y)},3e4))}function Re(){Ae&&(clearInterval(Ae),Ae=null)}function Ln(){if(Te=le.fsRoots||[],Te.length===0){if(y){En(y);return}h.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}h.innerHTML="";for(let e of Te){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> ${m(e)}`,t.addEventListener("click",()=>_(e)),h.appendChild(t)}}async function En(e){h.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let t=await(await fetch(T("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?_(t.path):h.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{h.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function _(e){P=e,h.innerHTML='<div class="file-tree-empty">Loading...</div>';let t=mn(e);try{let n=await(await fetch(T("/api/fs/list?path="+encodeURIComponent(e)))).json();await t,h.innerHTML="",pe(g),h.innerHTML+=hn();let s=xn(e);if(s&&!Te.some(r=>e===r)){let r=document.createElement("div");r.className="file-tree-item",r.innerHTML="..",r.addEventListener("click",()=>_(s)),h.appendChild(r)}if(n.dirs)for(let r of n.dirs){let c=r.split("/").pop()||r.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item",l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${m(c)}`,l.addEventListener("click",()=>_(r)),h.appendChild(l)}else h.innerHTML="";let o=n.files||[];for(let r of o){let c=r.split("/").pop()||r.split("\\").pop(),l=fn(r),u="",p="";if(l)switch(l){case"M":u="git-mod",p='<span class="git-file-status M">M</span>';break;case"A":u="git-add",p='<span class="git-file-status A">A</span>';break;case"D":u="git-del",p='<span class="git-file-status D">D</span>';break;case"?":u="git-untracked",p='<span class="git-file-status ?">?</span>';break;case"R":u="git-mod",p='<span class="git-file-status R">R</span>';break}let S=document.createElement("div");S.className="file-tree-item file"+(u?" "+u:""),S.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${p}${m(c)}`,S.addEventListener("click",()=>kn(r)),h.appendChild(S)}}catch{h.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function xn(e){let t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function kn(e){Ce=e,B.style.display="none",v&&(Re(),v.destroy(),v=null,y=null),pe(null),Ye(),Y.style.display="none",re.style.display="flex",nn.textContent=e,D.textContent="Loading...",U.style.display="none";try{let t=await fetch(T("/api/file?path="+encodeURIComponent(e)));if(!t.ok){let s=await t.json();D.textContent=s.error||"Failed to load",ce.value="";return}let n=await t.json();ce.value=n.content,Me=n.content,at=n.content,D.textContent=n.size+" bytes",U.style.display=""}catch{D.textContent="Failed to load",ce.value=""}}ce.addEventListener("input",()=>{Me=ce.value}),U.addEventListener("click",async()=>{U.textContent="Saving...",U.disabled=!0;try{let e=await fetch(T("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:Ce,content:Me})});if(!e.ok){let t=await e.json();D.textContent=t.error||"Save failed";return}at=Me,D.textContent="Saved"}catch{D.textContent="Save failed"}finally{U.disabled=!1,U.textContent="Save"}}),sn.addEventListener("click",async()=>{if(confirm("Delete "+Ce+"?"))try{let e=await fetch(T("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:Ce})});if(!e.ok){let t=await e.json();D.textContent=t.error||"Delete failed";return}re.style.display="none",Y.style.display="flex",P&&_(P)}catch{D.textContent="Delete failed"}}),an.addEventListener("click",()=>{re.style.display="none",Y.style.display="flex",P&&_(P)}),on.addEventListener("click",async()=>{let e=ot.value.trim();if(!e)return;let t=P+"/"+e;try{let n=await fetch(T("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok){let s=await n.json();D.textContent=s.error||"Create failed";return}ot.value="",P&&_(P)}catch{D.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let e=lt.classList.toggle("open");We.classList.toggle("open",e)}),We.addEventListener("click",()=>{lt.classList.remove("open"),We.classList.remove("open")});var q="",fe=document.createElement("div");fe.className="session-popover-backdrop",document.body.appendChild(fe);var $=document.createElement("div");$.id="session-popover",$.className="session-popover",$.style.display="none",$.innerHTML=`<div class="sp-field">
35
+ </div><div class="diff-content">${s}</div></div>`,E.querySelector(".diff-back")?.addEventListener("click",()=>{E.innerHTML=Lt,Ve(),g&&E.querySelectorAll(".git-popover-item").forEach(r=>{let c=parseInt(r.dataset.idx||"0",10),l=g.files[c];!l||l.status==="D"||(r.style.cursor="pointer",r.addEventListener("click",()=>xt(l.path)))}),bt()})}function Mn(e,t,n){if(!t&&!n){Ie.textContent="No diff",Be.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:o}=kt(t,n);wt.textContent=e,Ie.textContent=o+" lines",Be.innerHTML=s}function Tn(){yt.style.display="none",He?(I.style.display="",I.classList.add("terminal-pending"),de(He),He=null):y?(I.style.display="",I.classList.add("terminal-pending"),Mt()):U.style.display="flex"}kn.addEventListener("click",Tn);function Ae(){E.classList.remove("open"),De.classList.remove("open")}De.addEventListener("click",Ae),document.addEventListener("keydown",e=>{e.key==="Escape"&&(Ae(),Ne())});async function Ct(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();g=s,pe(s);return}}}catch{}g=null,pe(null)}var Re=null;function Mt(){$e(),y&&(Ct(y),Re=setInterval(()=>{y&&Ct(y)},3e4))}function $e(){Re&&(clearInterval(Re),Re=null)}function Sn(){if(Se=le.fsRoots||[],Se.length===0){if(y){In(y);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 Se){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> ${h(e)}`,t.addEventListener("click",()=>_(e)),m.appendChild(t)}}async function In(e){m.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let t=await(await fetch(T("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?_(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 _(e){P=e,m.innerHTML='<div class="file-tree-empty">Loading...</div>';let t=Ln(e);try{let n=await(await fetch(T("/api/fs/list?path="+encodeURIComponent(e)))).json();await t,m.innerHTML="",pe(g),m.innerHTML+=En();let s=Bn(e);if(s&&!Se.some(r=>e===r)){let r=document.createElement("div");r.className="file-tree-item",r.innerHTML="..",r.addEventListener("click",()=>_(s)),m.appendChild(r)}if(n.dirs)for(let r of n.dirs){let c=r.split("/").pop()||r.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item",l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${h(c)}`,l.addEventListener("click",()=>_(r)),m.appendChild(l)}else m.innerHTML="";let o=n.files||[];for(let r of o){let c=r.split("/").pop()||r.split("\\").pop(),l=bn(r),u="",p="";if(l)switch(l){case"M":u="git-mod",p='<span class="git-file-status M">M</span>';break;case"A":u="git-add",p='<span class="git-file-status A">A</span>';break;case"D":u="git-del",p='<span class="git-file-status D">D</span>';break;case"?":u="git-untracked",p='<span class="git-file-status ?">?</span>';break;case"R":u="git-mod",p='<span class="git-file-status R">R</span>';break}let S=document.createElement("div");S.className="file-tree-item file"+(u?" "+u:""),S.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${p}${h(c)}`,S.addEventListener("click",()=>Dn(r)),m.appendChild(S)}}catch{m.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function Bn(e){let t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function Dn(e){Me=e,I.style.display="none",v&&($e(),v.destroy(),v=null,y=null),pe(null),Oe(),U.style.display="none",re.style.display="flex",dn.textContent=e,D.textContent="Loading...",Y.style.display="none";try{let t=await fetch(T("/api/file?path="+encodeURIComponent(e)));if(!t.ok){let s=await t.json();D.textContent=s.error||"Failed to load",ce.value="";return}let n=await t.json();ce.value=n.content,Te=n.content,ct=n.content,D.textContent=n.size+" bytes",Y.style.display=""}catch{D.textContent="Failed to load",ce.value=""}}ce.addEventListener("input",()=>{Te=ce.value}),Y.addEventListener("click",async()=>{Y.textContent="Saving...",Y.disabled=!0;try{let e=await fetch(T("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:Me,content:Te})});if(!e.ok){let t=await e.json();D.textContent=t.error||"Save failed";return}ct=Te,D.textContent="Saved"}catch{D.textContent="Save failed"}finally{Y.disabled=!1,Y.textContent="Save"}}),un.addEventListener("click",async()=>{if(confirm("Delete "+Me+"?"))try{let e=await fetch(T("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:Me})});if(!e.ok){let t=await e.json();D.textContent=t.error||"Delete failed";return}re.style.display="none",U.style.display="flex",P&&_(P)}catch{D.textContent="Delete failed"}}),pn.addEventListener("click",()=>{re.style.display="none",U.style.display="flex",P&&_(P)}),fn.addEventListener("click",async()=>{let e=dt.value.trim();if(!e)return;let t=P+"/"+e;try{let n=await fetch(T("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok){let s=await n.json();D.textContent=s.error||"Create failed";return}dt.value="",P&&_(P)}catch{D.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let e=ut.classList.toggle("open");qe.classList.toggle("open",e)}),qe.addEventListener("click",()=>{ut.classList.remove("open"),qe.classList.remove("open")});var q="",fe=document.createElement("div");fe.className="session-popover-backdrop",document.body.appendChild(fe);var $=document.createElement("div");$.id="session-popover",$.className="session-popover",$.style.display="none",$.innerHTML=`<div class="sp-field">
36
36
  <label>Rename session</label>
37
37
  <input type="text" id="sp-name" />
38
38
  </div>
@@ -41,5 +41,5 @@ var Zt=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function
41
41
  <button class="btn" id="sp-cancel">Cancel</button>
42
42
  </div>
43
43
  <hr class="sp-divider" />
44
- <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild($);function Cn(e){let t=e.getBoundingClientRect();$.style.top=t.bottom+4+"px",$.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function Mn(e,t){q=e;let n=document.getElementById("sp-name");n.value=e,fe.classList.add("open"),$.style.display="block",Cn(t),setTimeout(()=>n.focus(),50)}function me(){fe.classList.remove("open"),$.style.display="none"}fe.addEventListener("click",me),document.getElementById("sp-save").addEventListener("click",async()=>{let e=document.getElementById("sp-name").value.trim();if(!e||e===q){me();return}try{if(!(await fetch(T("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:q,newName:e})})).ok)return;me(),J(),y===q&&de(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+q+'"?'))try{if(!(await fetch(T("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:q})})).ok)return;me(),y===q&&(v&&(v.destroy(),v=null),y=null,B.style.display="none",Y.style.display="flex"),J()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",me);var Tn={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r","ctrl-r":"","ctrl-c":"",btw:"/btw ",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{let t=e.target.closest("button[data-key]");if(!t||!v)return;let n=Tn[t.dataset.key];n&&(v.sendInput(n),v.focus())});var A=document.getElementById("mk-input"),xt=document.getElementById("mk-send");function kt(){if(!A||!v)return;let e=A.value;e&&(v.sendInput(e+"\r"),A.value="",A.rows=1,v.focus())}if(A&&(A.addEventListener("input",()=>{A.rows=1;let e=A.value.split(`
45
- `).length;A.rows=Math.min(e,4)}),A.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),kt())})),xt&&xt.addEventListener("click",kt),window.visualViewport){let 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})}var Oe=document.getElementById("process-panel"),he=document.getElementById("proc-list"),Sn=document.getElementById("proc-close"),Ke=document.getElementById("proc-backdrop"),Bn=document.getElementById("header-ram");function In(){he.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(T("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){he.innerHTML='<div class="proc-empty">No processes</div>';return}let t="";for(let n of e){let s=n.rss,o=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">'+o+'</span><span class="proc-cmd" title="'+m(n.command)+'">'+m(n.command)+'</span><button class="proc-kill" title="Kill PID '+n.pid+'">&times;</button></div>'}he.innerHTML=t;for(let n of he.querySelectorAll(".proc-row")){let s=n.querySelector(".proc-kill");s&&s.addEventListener("click",o=>{let r=o.target.closest(".proc-row");!r||!confirm("Kill PID "+r.dataset.pid+"?")||fetch(T("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(r.dataset.pid,10)})}).then(c=>c.json()).then(c=>{c.ok?r.style.opacity="0.3":alert("Failed: "+(c.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{he.innerHTML='<div class="proc-empty">Failed to load</div>'})}function Dn(){Oe.classList.add("open"),Ke.classList.add("open"),In()}function $e(){Oe.classList.remove("open"),Ke.classList.remove("open")}Bn.addEventListener("click",e=>{e.stopPropagation(),Oe.classList.contains("open")?$e():Dn()}),Sn.addEventListener("click",$e),Ke.addEventListener("click",$e),window.__openSession=de,window.__refreshSidebar=J,J(),Ye(),setInterval(()=>{document.visibilityState==="visible"&&J()},1e4);
44
+ <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild($);function Hn(e){let t=e.getBoundingClientRect();$.style.top=t.bottom+4+"px",$.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function An(e,t){q=e;let n=document.getElementById("sp-name");n.value=e,fe.classList.add("open"),$.style.display="block",Hn(t),setTimeout(()=>n.focus(),50)}function me(){fe.classList.remove("open"),$.style.display="none"}fe.addEventListener("click",me),document.getElementById("sp-save").addEventListener("click",async()=>{let e=document.getElementById("sp-name").value.trim();if(!e||e===q){me();return}try{if(!(await fetch(T("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:q,newName:e})})).ok)return;me(),X(),y===q&&de(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+q+'"?'))try{if(!(await fetch(T("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:q})})).ok)return;me(),y===q&&(v&&(v.destroy(),v=null),y=null,I.style.display="none",U.style.display="flex"),X()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",me);var Rn={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r","ctrl-r":"","ctrl-c":"",btw:"/btw ",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{let t=e.target.closest("button[data-key]");if(!t||!v)return;let n=Rn[t.dataset.key];n&&(v.sendInput(n),v.focus())});var A=document.getElementById("mk-input"),Tt=document.getElementById("mk-send");function St(){if(!A||!v)return;let e=A.value;e&&(v.sendInput(e+"\r"),A.value="",A.rows=1,v.focus())}if(A&&(A.addEventListener("input",()=>{A.rows=1;let e=A.value.split(`
45
+ `).length;A.rows=Math.min(e,4)}),A.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),St())})),Tt&&Tt.addEventListener("click",St),window.visualViewport){let 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})}var Je=document.getElementById("process-panel"),he=document.getElementById("proc-list"),$n=document.getElementById("proc-close"),Xe=document.getElementById("proc-backdrop"),Nn=document.getElementById("header-ram");function Pn(){he.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(T("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){he.innerHTML='<div class="proc-empty">No processes</div>';return}let t="";for(let n of e){let s=n.rss,o=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">'+o+'</span><span class="proc-cmd" title="'+h(n.command)+'">'+h(n.command)+'</span><button class="proc-kill" title="Kill PID '+n.pid+'">&times;</button></div>'}he.innerHTML=t;for(let n of he.querySelectorAll(".proc-row")){let s=n.querySelector(".proc-kill");s&&s.addEventListener("click",o=>{let r=o.target.closest(".proc-row");!r||!confirm("Kill PID "+r.dataset.pid+"?")||fetch(T("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(r.dataset.pid,10)})}).then(c=>c.json()).then(c=>{c.ok?r.style.opacity="0.3":alert("Failed: "+(c.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{he.innerHTML='<div class="proc-empty">Failed to load</div>'})}function _n(){Je.classList.add("open"),Xe.classList.add("open"),Pn()}function Ne(){Je.classList.remove("open"),Xe.classList.remove("open")}Nn.addEventListener("click",e=>{e.stopPropagation(),Je.classList.contains("open")?Ne():_n()}),$n.addEventListener("click",Ne),Xe.addEventListener("click",Ne);var jn=2e3,ve=null,Ze=null,Pe=new Map,_e=new Map;function zn(e,t){return(t?"a:"+t+":":"l:")+e}function Wn(e,t,n){let s=zn(e,t),o=_e.get(s);_e.set(s,{session:e,agentId:t,state:n,doneUntil:n==="idle"&&o?.state==="working"?Date.now()+jn:void 0});let r=t?'.session-item[data-agent="'+CSS.escape(t)+'"][data-session="'+CSS.escape(e)+'"]':'.session-item[data-session="'+CSS.escape(e)+'"]:not([data-agent])',c=m.querySelector(r);c&&It(c,s,_e.get(s))}function It(e,t,n){let s=Pe.get(t);s&&(clearTimeout(s),Pe.delete(t)),e.classList.remove("working","just-done"),n.state==="working"?e.classList.add("working"):n.doneUntil&&Date.now()<n.doneUntil&&(e.classList.add("just-done"),Pe.set(t,setTimeout(()=>{e.classList.remove("just-done"),Pe.delete(t)},Math.max(0,n.doneUntil-Date.now()))))}function Bt(){for(let[e,t]of _e){let n=t.agentId?'.session-item[data-agent="'+CSS.escape(t.agentId)+'"][data-session="'+CSS.escape(t.session)+'"]':'.session-item[data-session="'+CSS.escape(t.session)+'"]:not([data-agent])',s=m.querySelector(n);s&&It(s,e,t)}}function Dt(){if(ve||Ze)return;let e=localStorage.getItem("tmux-web-token"),t=(location.protocol==="https:"?"wss:":"ws:")+"//"+location.host+"/ws/activity";e&&(t+="?token="+encodeURIComponent(e));try{let n=new WebSocket(t);ve=n,n.onmessage=s=>{try{let o=JSON.parse(String(s.data));o&&o.type==="session.activity"&&typeof o.session=="string"&&Wn(o.session,o.agentId||void 0,o.state==="working"?"working":"idle")}catch{}},n.onclose=()=>{ve===n&&(ve=null,Ze=setTimeout(()=>{Ze=null,Dt()},5e3))},n.onerror=()=>{try{n.close()}catch{}}}catch{ve=null}}window.__openSession=de,window.__refreshSidebar=X,X(),Oe(),Bt(),Dt(),setInterval(()=>{document.visibilityState==="visible"&&X()},1e4);
@@ -1,31 +1,31 @@
1
- import{initTerminal as Be}from"./terminal-core.js";const H=window.__TMUX_WEB_SHELL__;let f=null,m=null;const oe=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null;let g=oe;window.__tmuxWebScopeAgent=oe;let Y="sessions",C="",F="",W="",ae="";function y(e){return g?"/a/"+encodeURIComponent(g)+e:e}const p=document.getElementById("sidebar-content"),B=document.getElementById("main-placeholder"),h=document.getElementById("terminal-container"),$=document.getElementById("file-editor"),xe=document.getElementById("fe-path"),w=document.getElementById("fe-status"),R=document.getElementById("fe-content"),x=document.getElementById("fe-save"),Se=document.getElementById("fe-delete"),Me=document.getElementById("fe-back"),le=document.getElementById("fe-new-name"),Te=document.getElementById("fe-new-btn"),Z=document.getElementById("settings-backdrop"),ce=document.getElementById("settings-popover"),de=document.querySelector(".sidebar");function He(){return window.matchMedia("(max-width: 640px)").matches}function Q(){He()&&de.classList.add("collapsed")}function re(){de.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{re(),pe("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{re(),pe("files")});function pe(e){Y=e,document.querySelectorAll(".mode-btn").forEach(t=>t.classList.remove("active"));const n=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");n&&n.classList.add("active"),e==="sessions"?(ee="",M()):Ue()}let ee="";async function M(){if(Y!=="sessions")return;try{const t=await(await fetch("/api/sidebar/sessions")).json(),s=JSON.stringify({recent:t.recent,agents:t.agents});if(s===ee)return;ee=s,Pe(t)}catch{p.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}const e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>{const n=window.__openNewSessionModal;typeof n=="function"?n():document.getElementById("new-session-modal")?.classList.add("open")})}const fe="tmux-web-sidebar-group-collapsed";function ue(){try{return new Set(JSON.parse(localStorage.getItem(fe)||"[]"))}catch{return new Set}}function $e(e){try{localStorage.setItem(fe,JSON.stringify([...e]))}catch{}}function Re(e,n,t){if(!e)return"\u2026";const s=t&&(e===t||e.startsWith(t+"/"))?"~"+e.slice(t.length):e,i=s.split("/").filter(Boolean);if(i.length===0)return e;for(let l=1;l<=i.length;l++){const o=i.slice(-l).join("/");if(!n.some(a=>!a||a===e?!1:(t&&(a===t||a.startsWith(t+"/"))?"~"+a.slice(t.length):a).split("/").filter(Boolean).slice(-l).join("/")===o))return o}return s}function Ne(e,n){const t=new Map;for(const l of e){const o=l.path||"";t.has(o)||t.set(o,[]),t.get(o).push(l)}const s=[...t.keys()],i=ue();for(const[l,o]of t){const c=Re(l,s,n),a=i.has(l),r=document.createElement("div");r.className="session-group-header"+(a?" collapsed":""),r.title=l||"No working directory",r.innerHTML=`<span class="sg-folder">&#128193;</span><span class="sg-label">${d(c)}</span>`,r.addEventListener("click",()=>{const b=ue(),ie=r.nextElementSibling;b.has(l)?(b.delete(l),r.classList.remove("collapsed"),ie?.classList.remove("collapsed")):(b.add(l),r.classList.add("collapsed"),ie?.classList.add("collapsed")),$e(b)});const L=document.createElement("div");L.className="session-group-body"+(a?" collapsed":"");for(const b of o)L.appendChild(_e(b));p.appendChild(r),p.appendChild(L)}}function _e(e){const n=document.createElement("div");n.className="session-item"+(e.name===m&&!g?" active":""),n.setAttribute("data-session",e.name),n.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${e.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>
1
+ import{initTerminal as $e}from"./terminal-core.js";const H=window.__TMUX_WEB_SHELL__;let f=null,m=null;const re=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null;let g=re;window.__tmuxWebScopeAgent=re;let ee="sessions",C="",F="",O="",de="";function y(e){return g?"/a/"+encodeURIComponent(g)+e:e}const r=document.getElementById("sidebar-content"),I=document.getElementById("main-placeholder"),h=document.getElementById("terminal-container"),R=document.getElementById("file-editor"),Ne=document.getElementById("fe-path"),b=document.getElementById("fe-status"),$=document.getElementById("fe-content"),x=document.getElementById("fe-save"),_e=document.getElementById("fe-delete"),Ae=document.getElementById("fe-back"),pe=document.getElementById("fe-new-name"),De=document.getElementById("fe-new-btn"),te=document.getElementById("settings-backdrop"),fe=document.getElementById("settings-popover"),ue=document.querySelector(".sidebar");function Pe(){return window.matchMedia("(max-width: 640px)").matches}function ne(){Pe()&&ue.classList.add("collapsed")}function me(){ue.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{me(),ve("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{me(),ve("files")});function ve(e){ee=e,document.querySelectorAll(".mode-btn").forEach(t=>t.classList.remove("active"));const n=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");n&&n.classList.add("active"),e==="sessions"?(se="",M()):Ze()}let se="";async function M(){if(ee!=="sessions")return;try{const t=await(await fetch("/api/sidebar/sessions")).json(),s=JSON.stringify({recent:t.recent,agents:t.agents});if(s===se)return;se=s,Ue(t)}catch{r.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}const e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>{const n=window.__openNewSessionModal;typeof n=="function"?n():document.getElementById("new-session-modal")?.classList.add("open")}),He()}const ge="tmux-web-sidebar-group-collapsed";function ye(){try{return new Set(JSON.parse(localStorage.getItem(ge)||"[]"))}catch{return new Set}}function je(e){try{localStorage.setItem(ge,JSON.stringify([...e]))}catch{}}function We(e,n,t){if(!e)return"\u2026";const s=t&&(e===t||e.startsWith(t+"/"))?"~"+e.slice(t.length):e,i=s.split("/").filter(Boolean);if(i.length===0)return e;for(let a=1;a<=i.length;a++){const o=i.slice(-a).join("/");if(!n.some(l=>!l||l===e?!1:(t&&(l===t||l.startsWith(t+"/"))?"~"+l.slice(t.length):l).split("/").filter(Boolean).slice(-a).join("/")===o))return o}return s}function Fe(e,n){const t=new Map;for(const a of e){const o=a.path||"";t.has(o)||t.set(o,[]),t.get(o).push(a)}const s=[...t.keys()],i=ye();for(const[a,o]of t){const c=We(a,s,n),l=i.has(a),p=document.createElement("div");p.className="session-group-header"+(l?" collapsed":""),p.title=a||"No working directory",p.innerHTML=`<span class="sg-folder">&#128193;</span><span class="sg-label">${d(c)}</span>`,p.addEventListener("click",()=>{const w=ye(),ce=p.nextElementSibling;w.has(a)?(w.delete(a),p.classList.remove("collapsed"),ce?.classList.remove("collapsed")):(w.add(a),p.classList.add("collapsed"),ce?.classList.add("collapsed")),je(w)});const L=document.createElement("div");L.className="session-group-body"+(l?" collapsed":"");for(const w of o)L.appendChild(Oe(w));r.appendChild(p),r.appendChild(L)}}function Oe(e){const n=document.createElement("div");n.className="session-item"+(e.name===m&&!g?" active":""),n.setAttribute("data-session",e.name),n.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${e.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(e.name)}</span>
3
3
  <button class="session-edit-btn" data-session="${d(e.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
- `,n.addEventListener("click",s=>{s.target.closest(".session-edit-btn")||N(e.name)});const t=n.querySelector(".session-edit-btn");return t&&t.addEventListener("click",s=>{s.stopPropagation(),Xe(e.name,t)}),n}function Pe(e){p.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';const n=e.recent||[],t=document.createElement("div");t.className="sidebar-section-label"+(g?" clickable":""),t.textContent=g?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",g&&(t.title="Back to hub",t.addEventListener("click",()=>{window.location.href="/"})),p.appendChild(t),Ne(n,e.home);const s=e.agents||[];for(const i of s){if(!i.sessions||i.sessions.length===0)continue;const l=i.agentId===g,o=document.createElement("div");o.className="sidebar-section-label machine-label"+(i.online?"":" agent-offline-label")+(!l&&i.online?" clickable":""),o.textContent=i.agentName+(i.online?"":" (offline)"),!l&&i.online&&(o.title="\u6253\u5F00 "+i.agentName,o.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(i.agentId)+"/"})),p.appendChild(o);for(const c of i.sessions){const a=document.createElement("div");a.className="session-item"+(c.name===m&&g===i.agentId?" active":"")+(i.online?"":" session-offline"),a.setAttribute("data-session",c.name),a.setAttribute("data-agent",i.agentId),a.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${c.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
- <span>${d(c.name)}</span><span class="meta">${i.online?"":"offline"}</span>`,i.online&&a.addEventListener("click",r=>{r.target.closest(".session-edit-btn")||N(c.name,i.agentId)}),p.appendChild(a)}}}function d(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function N(e,n){if(!(m===e&&f&&g===(n||null))){$.style.display="none",B.style.display="none",h.style.display="",h.classList.add("terminal-pending"),Q(),f&&(K(),f.destroy(),f=null),m=e,g=n||null,window.__tmuxWebScopeAgent=g,window.__tmuxWebCurrentSession=m;for(const t of p.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e&&(t.getAttribute("data-agent")||null)===g);try{f=await Be(h,e,{terminal:H.terminal,scrollback:H.scrollback,theme:H.theme,wsBase:g?"/ws/a/"+encodeURIComponent(g):H.wsBase||"/ws"}),ke()}catch(t){console.error("[shell] terminal init failed:",t);const s=t&&typeof t=="object"&&"message"in t?t.message:String(t);h.textContent="Failed to open terminal for "+e+": "+s,h.classList.remove("terminal-pending")}}}let G=[],u=null,me=!1;function Ae(e){if(!u||!u.files.length)return null;const n=u.repoRoot;if(!n||!e.startsWith(n))return null;const t=e.slice(n.length+1);for(const s of u.files)if(s.path===t)return s.status;return null}async function De(e){me=!0;try{const n=await fetch(y("/api/git/status?path="+encodeURIComponent(e)));n.ok?u=await n.json():u=null}catch{u=null}finally{me=!1}}function je(){if(!u||!u.branch)return"";const{branch:e,linesAdded:n,linesRemoved:t}=u;let s=`<div class="sidebar-git"><div class="git-branch-badge">
4
+ `,n.addEventListener("click",s=>{s.target.closest(".session-edit-btn")||N(e.name)});const t=n.querySelector(".session-edit-btn");return t&&t.addEventListener("click",s=>{s.stopPropagation(),st(e.name,t)}),n}function Ue(e){r.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';const n=e.recent||[],t=document.createElement("div");t.className="sidebar-section-label"+(g?" clickable":""),t.textContent=g?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",g&&(t.title="Back to hub",t.addEventListener("click",()=>{window.location.href="/"})),r.appendChild(t),Fe(n,e.home);const s=e.agents||[];for(const i of s){if(!i.sessions||i.sessions.length===0)continue;const a=i.agentId===g,o=document.createElement("div");o.className="sidebar-section-label machine-label"+(i.online?"":" agent-offline-label")+(!a&&i.online?" clickable":""),o.textContent=i.agentName+(i.online?"":" (offline)"),!a&&i.online&&(o.title="\u6253\u5F00 "+i.agentName,o.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(i.agentId)+"/"})),r.appendChild(o);for(const c of i.sessions){const l=document.createElement("div");l.className="session-item"+(c.name===m&&g===i.agentId?" active":"")+(i.online?"":" session-offline"),l.setAttribute("data-session",c.name),l.setAttribute("data-agent",i.agentId),l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${c.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
+ <span>${d(c.name)}</span><span class="meta">${i.online?"":"offline"}</span>`,i.online&&l.addEventListener("click",p=>{p.target.closest(".session-edit-btn")||N(c.name,i.agentId)}),r.appendChild(l)}}}function d(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function N(e,n){if(!(m===e&&f&&g===(n||null))){R.style.display="none",I.style.display="none",h.style.display="",h.classList.add("terminal-pending"),ne(),f&&(X(),f.destroy(),f=null),m=e,g=n||null,window.__tmuxWebScopeAgent=g,window.__tmuxWebCurrentSession=m;for(const t of r.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e&&(t.getAttribute("data-agent")||null)===g);try{f=await $e(h,e,{terminal:H.terminal,scrollback:H.scrollback,theme:H.theme,wsBase:g?"/ws/a/"+encodeURIComponent(g):H.wsBase||"/ws"}),xe()}catch(t){console.error("[shell] terminal init failed:",t);const s=t&&typeof t=="object"&&"message"in t?t.message:String(t);h.textContent="Failed to open terminal for "+e+": "+s,h.classList.remove("terminal-pending")}}}let U=[],u=null,he=!1;function Ge(e){if(!u||!u.files.length)return null;const n=u.repoRoot;if(!n||!e.startsWith(n))return null;const t=e.slice(n.length+1);for(const s of u.files)if(s.path===t)return s.status;return null}async function ze(e){he=!0;try{const n=await fetch(y("/api/git/status?path="+encodeURIComponent(e)));n.ok?u=await n.json():u=null}catch{u=null}finally{he=!1}}function qe(){if(!u||!u.branch)return"";const{branch:e,linesAdded:n,linesRemoved:t}=u;let s=`<div class="sidebar-git"><div class="git-branch-badge">
6
6
  <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>
7
- ${d(e)}</div>`;return(n>0||t>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${n}</span> <span class="del">-${t}</span></div>`),s+"</div>"}p.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&T&&u&&(e.stopPropagation(),be())});const _=document.getElementById("header-git"),ve=document.getElementById("git-diff-view"),ge=document.getElementById("gd-path"),O=document.getElementById("gd-status"),z=document.getElementById("gd-content"),Fe=document.getElementById("gd-back");let T=null;function P(e){if(!e||!e.branch){_.style.display="none",T=null;return}T=e.repoRoot;const{branch:n,linesAdded:t,linesRemoved:s}=e;let i=`<span class="branch">${d(n)}</span>`;(t>0||s>0)&&(i+=`<span class="sep">\xB7</span>+<span class="diff-add">${t}</span> <span class="diff-del">-${s}</span>`),_.innerHTML=i,_.style.display="block"}_.addEventListener("click",e=>{T&&u&&(e.stopPropagation(),be())});const v=document.getElementById("git-popover"),U=document.getElementById("git-popover-backdrop");function te(){const e=_.getBoundingClientRect();v.style.top=e.bottom+4+"px",v.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function ye(){const e=T,n=document.getElementById("git-popover-browse");n&&n.addEventListener("click",()=>{if(q(),e){Y="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));const t=document.getElementById("mode-files");t&&t.classList.add("active"),I(e)}})}let he="";function be(){if(!u)return;const e=u.files||[];if(!e.length)v.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
8
- <div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>`;else{let n=`<div class="git-popover-header">${e.length} file${e.length>1?"s":""} changed</div>`;for(let t=0;t<e.length;t++){const s=e[t],i=s.additions||0,l=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}n+=`<div class="git-popover-item" data-idx="${t}">
7
+ ${d(e)}</div>`;return(n>0||t>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${n}</span> <span class="del">-${t}</span></div>`),s+"</div>"}r.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&T&&u&&(e.stopPropagation(),ke())});const _=document.getElementById("header-git"),we=document.getElementById("git-diff-view"),be=document.getElementById("gd-path"),G=document.getElementById("gd-status"),z=document.getElementById("gd-content"),Ve=document.getElementById("gd-back");let T=null;function A(e){if(!e||!e.branch){_.style.display="none",T=null;return}T=e.repoRoot;const{branch:n,linesAdded:t,linesRemoved:s}=e;let i=`<span class="branch">${d(n)}</span>`;(t>0||s>0)&&(i+=`<span class="sep">\xB7</span>+<span class="diff-add">${t}</span> <span class="diff-del">-${s}</span>`),_.innerHTML=i,_.style.display="block"}_.addEventListener("click",e=>{T&&u&&(e.stopPropagation(),ke())});const v=document.getElementById("git-popover"),q=document.getElementById("git-popover-backdrop");function ie(){const e=_.getBoundingClientRect();v.style.top=e.bottom+4+"px",v.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function Le(){const e=T,n=document.getElementById("git-popover-browse");n&&n.addEventListener("click",()=>{if(J(),e){ee="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));const t=document.getElementById("mode-files");t&&t.classList.add("active"),S(e)}})}let Ee="";function ke(){if(!u)return;const e=u.files||[];if(!e.length)v.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
8
+ <div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>`;else{let n=`<div class="git-popover-header">${e.length} file${e.length>1?"s":""} changed</div>`;for(let t=0;t<e.length;t++){const s=e[t],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}n+=`<div class="git-popover-item" data-idx="${t}">
9
9
  ${o}<span class="file-path">${d(s.path)}</span>
10
10
  <span class="file-add">${i>0?"+"+i:""}</span>
11
- <span class="file-del">${l>0?"-"+l:""}</span>
12
- </div>`}n+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',he=n,v.innerHTML=n,v.querySelectorAll(".git-popover-item").forEach(t=>{const s=parseInt(t.dataset.idx||"0",10),i=e[s];!i||i.status==="D"||(t.classList.add("clickable"),t.style.cursor="pointer",t.addEventListener("click",()=>Le(i.path)))})}te(),v.classList.add("open"),U.classList.add("open"),ye()}function We(){return window.innerWidth>=1024}function we(e,n){const s=(e+(n&&e?`
11
+ <span class="file-del">${a>0?"-"+a:""}</span>
12
+ </div>`}n+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',Ee=n,v.innerHTML=n,v.querySelectorAll(".git-popover-item").forEach(t=>{const s=parseInt(t.dataset.idx||"0",10),i=e[s];!i||i.status==="D"||(t.classList.add("clickable"),t.style.cursor="pointer",t.addEventListener("click",()=>Se(i.path)))})}ie(),v.classList.add("open"),q.classList.add("open"),Le()}function Je(){return window.innerWidth>=1024}function Ce(e,n){const s=(e+(n&&e?`
13
13
  `:"")+n).split(`
14
- `);let i="",l=0,o=0;for(const c of s){const a=c;if(a.startsWith("@@")){const r=a.match(/@@\s+-(\d+)[^+]*\+(\d+)/);r&&(l=parseInt(r[1],10)-1,o=parseInt(r[2],10)-1),i+=`<div class="diff-line hunk"><span class="ln-body">${d(a)}</span></div>`;continue}if(a.startsWith("diff --git")||a.startsWith("index ")||a.startsWith("--- ")||a.startsWith("+++ ")||a.startsWith("new file")||a.startsWith("deleted file")){a.startsWith("diff --git")&&(i+=`<div class="diff-line header"><span class="ln-body">${d(a)}</span></div>`);continue}if(a.startsWith("+"))o++,i+=`<div class="diff-line add"><span class="ln-no">${o}</span><span class="ln-body">${d(a)}</span></div>`;else if(a.startsWith("-"))l++,i+=`<div class="diff-line del"><span class="ln-no">${l}</span><span class="ln-body">${d(a)}</span></div>`;else if(a.startsWith("\\"))i+=`<div class="diff-line"><span class="ln-body">${d(a)}</span></div>`;else{if(!a&&!l&&!o)continue;l++,o++,i+=`<div class="diff-line"><span class="ln-no">${l}</span><span class="ln-no" style="margin-right:12px">${o}</span><span class="ln-body">${d(a)}</span></div>`}}return{html:i,lineCount:s.length}}let V=null;async function Le(e){const n=T;if(n)if(q(),We()){h.style.display="none",f&&(K(),f.destroy(),f=null,V=m,m=null),P(null),$.style.display="none",B.style.display="none",ve.style.display="flex",ge.textContent=e,O.textContent="Loading\u2026",z.innerHTML="";try{const t=await fetch(y("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Oe(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);O.textContent="Error",z.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${d(s)}</div>`}}else{v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
14
+ `);let i="",a=0,o=0;for(const c of s){const l=c;if(l.startsWith("@@")){const p=l.match(/@@\s+-(\d+)[^+]*\+(\d+)/);p&&(a=parseInt(p[1],10)-1,o=parseInt(p[2],10)-1),i+=`<div class="diff-line hunk"><span class="ln-body">${d(l)}</span></div>`;continue}if(l.startsWith("diff --git")||l.startsWith("index ")||l.startsWith("--- ")||l.startsWith("+++ ")||l.startsWith("new file")||l.startsWith("deleted file")){l.startsWith("diff --git")&&(i+=`<div class="diff-line header"><span class="ln-body">${d(l)}</span></div>`);continue}if(l.startsWith("+"))o++,i+=`<div class="diff-line add"><span class="ln-no">${o}</span><span class="ln-body">${d(l)}</span></div>`;else if(l.startsWith("-"))a++,i+=`<div class="diff-line del"><span class="ln-no">${a}</span><span class="ln-body">${d(l)}</span></div>`;else if(l.startsWith("\\"))i+=`<div class="diff-line"><span class="ln-body">${d(l)}</span></div>`;else{if(!l&&!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(l)}</span></div>`}}return{html:i,lineCount:s.length}}let V=null;async function Se(e){const n=T;if(n)if(J(),Je()){h.style.display="none",f&&(X(),f.destroy(),f=null,V=m,m=null),A(null),R.style.display="none",I.style.display="none",we.style.display="flex",be.textContent=e,G.textContent="Loading\u2026",z.innerHTML="";try{const t=await fetch(y("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Xe(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);G.textContent="Error",z.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${d(s)}</div>`}}else{v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
15
15
  <button class="diff-back">\u2190 Back</button>
16
16
  <span class="diff-filename">${d(e)}</span>
17
17
  <span class="diff-loading">Loading\u2026</span>
18
- </div></div>`,te(),v.classList.add("open"),U.classList.add("open");try{const t=await fetch(y("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Ge(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
18
+ </div></div>`,ie(),v.classList.add("open"),q.classList.add("open");try{const t=await fetch(y("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Ke(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
19
19
  <button class="diff-back">\u2190 Back</button>
20
20
  <span class="diff-filename">${d(e)}</span>
21
- </div><div class="diff-empty">Error loading diff: ${d(s)}</div></div>`}}}function Ge(e,n,t){if(!n&&!t){v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
21
+ </div><div class="diff-empty">Error loading diff: ${d(s)}</div></div>`}}}function Ke(e,n,t){if(!n&&!t){v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
22
22
  <button class="diff-back">\u2190 Back</button>
23
23
  <span class="diff-filename">${d(e)}</span>
24
- </div><div class="diff-empty">No diff content available</div></div>`;return}const{html:s,lineCount:i}=we(n,t);v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
24
+ </div><div class="diff-empty">No diff content available</div></div>`;return}const{html:s,lineCount:i}=Ce(n,t);v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
25
25
  <button class="diff-back">\u2190 Back</button>
26
26
  <span class="diff-filename">${d(e)}</span>
27
27
  <span class="diff-filesize">${i} lines</span>
28
- </div><div class="diff-content">${s}</div></div>`,v.querySelector(".diff-back")?.addEventListener("click",()=>{v.innerHTML=he,te(),u&&v.querySelectorAll(".git-popover-item").forEach(l=>{const o=parseInt(l.dataset.idx||"0",10),c=u.files[o];!c||c.status==="D"||(l.style.cursor="pointer",l.addEventListener("click",()=>Le(c.path)))}),ye()})}function Oe(e,n,t){if(!n&&!t){O.textContent="No diff",z.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}=we(n,t);ge.textContent=e,O.textContent=i+" lines",z.innerHTML=s}function ze(){ve.style.display="none",V?(h.style.display="",h.classList.add("terminal-pending"),N(V),V=null):m?(h.style.display="",h.classList.add("terminal-pending"),ke()):B.style.display="flex"}Fe.addEventListener("click",ze);function q(){v.classList.remove("open"),U.classList.remove("open")}U.addEventListener("click",q),document.addEventListener("keydown",e=>{e.key==="Escape"&&(q(),X())});async function Ee(e){try{const t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){const s=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(s.ok){const i=await s.json();u=i,P(i);return}}}catch{}u=null,P(null)}let J=null;function ke(){K(),m&&(Ee(m),J=setInterval(()=>{m&&Ee(m)},3e4))}function K(){J&&(clearInterval(J),J=null)}function Ue(){if(G=H.fsRoots||[],G.length===0){if(m){Ve(m);return}p.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}p.innerHTML="";for(const e of G){const n=document.createElement("div");n.className="file-tree-item",n.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)}`,n.addEventListener("click",()=>I(e)),p.appendChild(n)}}async function Ve(e){p.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const t=await(await fetch(y("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?I(t.path):p.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{p.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function I(e){C=e,p.innerHTML='<div class="file-tree-empty">Loading...</div>';const n=De(e);try{const s=await(await fetch(y("/api/fs/list?path="+encodeURIComponent(e)))).json();await n,p.innerHTML="",P(u),p.innerHTML+=je();const i=qe(e);if(i&&!G.some(c=>e===c)){const c=document.createElement("div");c.className="file-tree-item",c.innerHTML="..",c.addEventListener("click",()=>I(i)),p.appendChild(c)}if(s.dirs)for(const o of s.dirs){const c=o.split("/").pop()||o.split("\\").pop(),a=document.createElement("div");a.className="file-tree-item",a.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(c)}`,a.addEventListener("click",()=>I(o)),p.appendChild(a)}else p.innerHTML="";const l=s.files||[];for(const o of l){const c=o.split("/").pop()||o.split("\\").pop(),a=Ae(o);let r="",L="";if(a)switch(a){case"M":r="git-mod",L='<span class="git-file-status M">M</span>';break;case"A":r="git-add",L='<span class="git-file-status A">A</span>';break;case"D":r="git-del",L='<span class="git-file-status D">D</span>';break;case"?":r="git-untracked",L='<span class="git-file-status ?">?</span>';break;case"R":r="git-mod",L='<span class="git-file-status R">R</span>';break}const b=document.createElement("div");b.className="file-tree-item file"+(r?" "+r:""),b.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${L}${d(c)}`,b.addEventListener("click",()=>Je(o)),p.appendChild(b)}}catch{p.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function qe(e){const n=e.lastIndexOf("/");return n<=0?null:e.substring(0,n)}async function Je(e){F=e,h.style.display="none",f&&(K(),f.destroy(),f=null,m=null),P(null),Q(),B.style.display="none",$.style.display="flex",xe.textContent=e,w.textContent="Loading...",x.style.display="none";try{const n=await fetch(y("/api/file?path="+encodeURIComponent(e)));if(!n.ok){const s=await n.json();w.textContent=s.error||"Failed to load",R.value="";return}const t=await n.json();R.value=t.content,W=t.content,ae=t.content,w.textContent=t.size+" bytes",x.style.display=""}catch{w.textContent="Failed to load",R.value=""}}R.addEventListener("input",()=>{W=R.value}),x.addEventListener("click",async()=>{x.textContent="Saving...",x.disabled=!0;try{const e=await fetch(y("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:F,content:W})});if(!e.ok){const n=await e.json();w.textContent=n.error||"Save failed";return}ae=W,w.textContent="Saved"}catch{w.textContent="Save failed"}finally{x.disabled=!1,x.textContent="Save"}}),Se.addEventListener("click",async()=>{if(confirm("Delete "+F+"?"))try{const e=await fetch(y("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:F})});if(!e.ok){const n=await e.json();w.textContent=n.error||"Delete failed";return}$.style.display="none",B.style.display="flex",C&&I(C)}catch{w.textContent="Delete failed"}}),Me.addEventListener("click",()=>{$.style.display="none",B.style.display="flex",C&&I(C)}),Te.addEventListener("click",async()=>{const e=le.value.trim();if(!e)return;const n=C+"/"+e;try{const t=await fetch(y("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:n})});if(!t.ok){const s=await t.json();w.textContent=s.error||"Create failed";return}le.value="",C&&I(C)}catch{w.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=ce.classList.toggle("open");Z.classList.toggle("open",e)}),Z.addEventListener("click",()=>{ce.classList.remove("open"),Z.classList.remove("open")});let S="";const A=document.createElement("div");A.className="session-popover-backdrop",document.body.appendChild(A);const k=document.createElement("div");k.id="session-popover",k.className="session-popover",k.style.display="none",k.innerHTML=`<div class="sp-field">
28
+ </div><div class="diff-content">${s}</div></div>`,v.querySelector(".diff-back")?.addEventListener("click",()=>{v.innerHTML=Ee,ie(),u&&v.querySelectorAll(".git-popover-item").forEach(a=>{const o=parseInt(a.dataset.idx||"0",10),c=u.files[o];!c||c.status==="D"||(a.style.cursor="pointer",a.addEventListener("click",()=>Se(c.path)))}),Le()})}function Xe(e,n,t){if(!n&&!t){G.textContent="No diff",z.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}=Ce(n,t);be.textContent=e,G.textContent=i+" lines",z.innerHTML=s}function Ye(){we.style.display="none",V?(h.style.display="",h.classList.add("terminal-pending"),N(V),V=null):m?(h.style.display="",h.classList.add("terminal-pending"),xe()):I.style.display="flex"}Ve.addEventListener("click",Ye);function J(){v.classList.remove("open"),q.classList.remove("open")}q.addEventListener("click",J),document.addEventListener("keydown",e=>{e.key==="Escape"&&(J(),Y())});async function Ie(e){try{const t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){const s=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(s.ok){const i=await s.json();u=i,A(i);return}}}catch{}u=null,A(null)}let K=null;function xe(){X(),m&&(Ie(m),K=setInterval(()=>{m&&Ie(m)},3e4))}function X(){K&&(clearInterval(K),K=null)}function Ze(){if(U=H.fsRoots||[],U.length===0){if(m){Qe(m);return}r.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}r.innerHTML="";for(const e of U){const n=document.createElement("div");n.className="file-tree-item",n.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)}`,n.addEventListener("click",()=>S(e)),r.appendChild(n)}}async function Qe(e){r.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const t=await(await fetch(y("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?S(t.path):r.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{r.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function S(e){C=e,r.innerHTML='<div class="file-tree-empty">Loading...</div>';const n=ze(e);try{const s=await(await fetch(y("/api/fs/list?path="+encodeURIComponent(e)))).json();await n,r.innerHTML="",A(u),r.innerHTML+=qe();const i=et(e);if(i&&!U.some(c=>e===c)){const c=document.createElement("div");c.className="file-tree-item",c.innerHTML="..",c.addEventListener("click",()=>S(i)),r.appendChild(c)}if(s.dirs)for(const o of s.dirs){const c=o.split("/").pop()||o.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item",l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${d(c)}`,l.addEventListener("click",()=>S(o)),r.appendChild(l)}else r.innerHTML="";const a=s.files||[];for(const o of a){const c=o.split("/").pop()||o.split("\\").pop(),l=Ge(o);let p="",L="";if(l)switch(l){case"M":p="git-mod",L='<span class="git-file-status M">M</span>';break;case"A":p="git-add",L='<span class="git-file-status A">A</span>';break;case"D":p="git-del",L='<span class="git-file-status D">D</span>';break;case"?":p="git-untracked",L='<span class="git-file-status ?">?</span>';break;case"R":p="git-mod",L='<span class="git-file-status R">R</span>';break}const w=document.createElement("div");w.className="file-tree-item file"+(p?" "+p:""),w.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${L}${d(c)}`,w.addEventListener("click",()=>tt(o)),r.appendChild(w)}}catch{r.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function et(e){const n=e.lastIndexOf("/");return n<=0?null:e.substring(0,n)}async function tt(e){F=e,h.style.display="none",f&&(X(),f.destroy(),f=null,m=null),A(null),ne(),I.style.display="none",R.style.display="flex",Ne.textContent=e,b.textContent="Loading...",x.style.display="none";try{const n=await fetch(y("/api/file?path="+encodeURIComponent(e)));if(!n.ok){const s=await n.json();b.textContent=s.error||"Failed to load",$.value="";return}const t=await n.json();$.value=t.content,O=t.content,de=t.content,b.textContent=t.size+" bytes",x.style.display=""}catch{b.textContent="Failed to load",$.value=""}}$.addEventListener("input",()=>{O=$.value}),x.addEventListener("click",async()=>{x.textContent="Saving...",x.disabled=!0;try{const e=await fetch(y("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:F,content:O})});if(!e.ok){const n=await e.json();b.textContent=n.error||"Save failed";return}de=O,b.textContent="Saved"}catch{b.textContent="Save failed"}finally{x.disabled=!1,x.textContent="Save"}}),_e.addEventListener("click",async()=>{if(confirm("Delete "+F+"?"))try{const e=await fetch(y("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:F})});if(!e.ok){const n=await e.json();b.textContent=n.error||"Delete failed";return}R.style.display="none",I.style.display="flex",C&&S(C)}catch{b.textContent="Delete failed"}}),Ae.addEventListener("click",()=>{R.style.display="none",I.style.display="flex",C&&S(C)}),De.addEventListener("click",async()=>{const e=pe.value.trim();if(!e)return;const n=C+"/"+e;try{const t=await fetch(y("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:n})});if(!t.ok){const s=await t.json();b.textContent=s.error||"Create failed";return}pe.value="",C&&S(C)}catch{b.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=fe.classList.toggle("open");te.classList.toggle("open",e)}),te.addEventListener("click",()=>{fe.classList.remove("open"),te.classList.remove("open")});let B="";const D=document.createElement("div");D.className="session-popover-backdrop",document.body.appendChild(D);const k=document.createElement("div");k.id="session-popover",k.className="session-popover",k.style.display="none",k.innerHTML=`<div class="sp-field">
29
29
  <label>Rename session</label>
30
30
  <input type="text" id="sp-name" />
31
31
  </div>
@@ -34,5 +34,5 @@ import{initTerminal as Be}from"./terminal-core.js";const H=window.__TMUX_WEB_SHE
34
34
  <button class="btn" id="sp-cancel">Cancel</button>
35
35
  </div>
36
36
  <hr class="sp-divider" />
37
- <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(k);function Ke(e){const n=e.getBoundingClientRect();k.style.top=n.bottom+4+"px",k.style.left=Math.max(8,Math.min(n.left,window.innerWidth-240))+"px"}function Xe(e,n){S=e;const t=document.getElementById("sp-name");t.value=e,A.classList.add("open"),k.style.display="block",Ke(n),setTimeout(()=>t.focus(),50)}function D(){A.classList.remove("open"),k.style.display="none"}A.addEventListener("click",D),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===S){D();return}try{if(!(await fetch(y("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:S,newName:e})})).ok)return;D(),M(),m===S&&N(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+S+'"?'))try{if(!(await fetch(y("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:S})})).ok)return;D(),m===S&&(f&&(f.destroy(),f=null),m=null,h.style.display="none",B.style.display="flex"),M()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",D);const Ye={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r","ctrl-r":"","ctrl-c":"",btw:"/btw ",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{const n=e.target.closest("button[data-key]");if(!n||!f)return;const t=Ye[n.dataset.key];t&&(f.sendInput(t),f.focus())});const E=document.getElementById("mk-input"),Ce=document.getElementById("mk-send");function Ie(){if(!E||!f)return;const e=E.value;e&&(f.sendInput(e+"\r"),E.value="",E.rows=1,f.focus())}if(E&&(E.addEventListener("input",()=>{E.rows=1;const e=E.value.split(`
38
- `).length;E.rows=Math.min(e,4)}),E.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),Ie())})),Ce&&Ce.addEventListener("click",Ie),window.visualViewport){const e=document.querySelector(".app-layout"),n=()=>{e&&(e.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",n),window.addEventListener("orientationchange",()=>setTimeout(n,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}const ne=document.getElementById("process-panel"),j=document.getElementById("proc-list"),Ze=document.getElementById("proc-close"),se=document.getElementById("proc-backdrop"),Qe=document.getElementById("header-ram");function et(){j.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(y("/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 n="";for(const t of e){const s=t.rss,i=s<1048576?(s/1024).toFixed(0)+"K":(s/1048576).toFixed(1)+"M";n+='<div class="proc-row" data-pid="'+t.pid+'"><span class="proc-mem">'+t.mem+'%</span><span class="proc-rss">'+i+'</span><span class="proc-cmd" title="'+d(t.command)+'">'+d(t.command)+'</span><button class="proc-kill" title="Kill PID '+t.pid+'">&times;</button></div>'}j.innerHTML=n;for(const t of j.querySelectorAll(".proc-row")){const s=t.querySelector(".proc-kill");s&&s.addEventListener("click",i=>{const l=i.target.closest(".proc-row");!l||!confirm("Kill PID "+l.dataset.pid+"?")||fetch(y("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(l.dataset.pid,10)})}).then(o=>o.json()).then(o=>{o.ok?l.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 tt(){ne.classList.add("open"),se.classList.add("open"),et()}function X(){ne.classList.remove("open"),se.classList.remove("open")}Qe.addEventListener("click",e=>{e.stopPropagation(),ne.classList.contains("open")?X():tt()}),Ze.addEventListener("click",X),se.addEventListener("click",X),window.__openSession=N,window.__refreshSidebar=M,M(),Q(),setInterval(()=>{document.visibilityState==="visible"&&M()},1e4);
37
+ <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(k);function nt(e){const n=e.getBoundingClientRect();k.style.top=n.bottom+4+"px",k.style.left=Math.max(8,Math.min(n.left,window.innerWidth-240))+"px"}function st(e,n){B=e;const t=document.getElementById("sp-name");t.value=e,D.classList.add("open"),k.style.display="block",nt(n),setTimeout(()=>t.focus(),50)}function P(){D.classList.remove("open"),k.style.display="none"}D.addEventListener("click",P),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===B){P();return}try{if(!(await fetch(y("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:B,newName:e})})).ok)return;P(),M(),m===B&&N(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+B+'"?'))try{if(!(await fetch(y("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:B})})).ok)return;P(),m===B&&(f&&(f.destroy(),f=null),m=null,h.style.display="none",I.style.display="flex"),M()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",P);const it={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r","ctrl-r":"","ctrl-c":"",btw:"/btw ",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{const n=e.target.closest("button[data-key]");if(!n||!f)return;const t=it[n.dataset.key];t&&(f.sendInput(t),f.focus())});const E=document.getElementById("mk-input"),Be=document.getElementById("mk-send");function Me(){if(!E||!f)return;const e=E.value;e&&(f.sendInput(e+"\r"),E.value="",E.rows=1,f.focus())}if(E&&(E.addEventListener("input",()=>{E.rows=1;const e=E.value.split(`
38
+ `).length;E.rows=Math.min(e,4)}),E.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),Me())})),Be&&Be.addEventListener("click",Me),window.visualViewport){const e=document.querySelector(".app-layout"),n=()=>{e&&(e.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",n),window.addEventListener("orientationchange",()=>setTimeout(n,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}const oe=document.getElementById("process-panel"),j=document.getElementById("proc-list"),ot=document.getElementById("proc-close"),ae=document.getElementById("proc-backdrop"),at=document.getElementById("header-ram");function lt(){j.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(y("/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 n="";for(const t of e){const s=t.rss,i=s<1048576?(s/1024).toFixed(0)+"K":(s/1048576).toFixed(1)+"M";n+='<div class="proc-row" data-pid="'+t.pid+'"><span class="proc-mem">'+t.mem+'%</span><span class="proc-rss">'+i+'</span><span class="proc-cmd" title="'+d(t.command)+'">'+d(t.command)+'</span><button class="proc-kill" title="Kill PID '+t.pid+'">&times;</button></div>'}j.innerHTML=n;for(const t of j.querySelectorAll(".proc-row")){const s=t.querySelector(".proc-kill");s&&s.addEventListener("click",i=>{const a=i.target.closest(".proc-row");!a||!confirm("Kill PID "+a.dataset.pid+"?")||fetch(y("/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 ct(){oe.classList.add("open"),ae.classList.add("open"),lt()}function Y(){oe.classList.remove("open"),ae.classList.remove("open")}at.addEventListener("click",e=>{e.stopPropagation(),oe.classList.contains("open")?Y():ct()}),ot.addEventListener("click",Y),ae.addEventListener("click",Y);const rt=2e3;let W=null,le=null;const Z=new Map,Q=new Map;function dt(e,n){return(n?"a:"+n+":":"l:")+e}function pt(e,n,t){const s=dt(e,n),i=Q.get(s);Q.set(s,{session:e,agentId:n,state:t,doneUntil:t==="idle"&&i?.state==="working"?Date.now()+rt:void 0});const a=n?'.session-item[data-agent="'+CSS.escape(n)+'"][data-session="'+CSS.escape(e)+'"]':'.session-item[data-session="'+CSS.escape(e)+'"]:not([data-agent])',o=r.querySelector(a);o&&Te(o,s,Q.get(s))}function Te(e,n,t){const s=Z.get(n);s&&(clearTimeout(s),Z.delete(n)),e.classList.remove("working","just-done"),t.state==="working"?e.classList.add("working"):t.doneUntil&&Date.now()<t.doneUntil&&(e.classList.add("just-done"),Z.set(n,setTimeout(()=>{e.classList.remove("just-done"),Z.delete(n)},Math.max(0,t.doneUntil-Date.now()))))}function He(){for(const[e,n]of Q){const t=n.agentId?'.session-item[data-agent="'+CSS.escape(n.agentId)+'"][data-session="'+CSS.escape(n.session)+'"]':'.session-item[data-session="'+CSS.escape(n.session)+'"]:not([data-agent])',s=r.querySelector(t);s&&Te(s,e,n)}}function Re(){if(W||le)return;const e=localStorage.getItem("tmux-web-token");let t=(location.protocol==="https:"?"wss:":"ws:")+"//"+location.host+"/ws/activity";e&&(t+="?token="+encodeURIComponent(e));try{const s=new WebSocket(t);W=s,s.onmessage=i=>{try{const a=JSON.parse(String(i.data));a&&a.type==="session.activity"&&typeof a.session=="string"&&pt(a.session,a.agentId||void 0,a.state==="working"?"working":"idle")}catch{}},s.onclose=()=>{W===s&&(W=null,le=setTimeout(()=>{le=null,Re()},5e3))},s.onerror=()=>{try{s.close()}catch{}}}catch{W=null}}window.__openSession=N,window.__refreshSidebar=M,M(),ne(),He(),Re(),setInterval(()=>{document.visibilityState==="visible"&&M()},1e4);
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import{chmodSync as ue,existsSync as D,readdirSync as de,statSync as me}from"node:fs";import{createRequire as J}from"node:module";import{hostname as U}from"node:os";const fe=J(import.meta.url),{version:pe}=fe("../package.json");import O from"node:path";import{fileURLToPath as he}from"node:url";import{serve as ge}from"@hono/node-server";import{WebSocketServer as ye,WebSocket as H}from"ws";try{const e=O.dirname(J(import.meta.url).resolve("node-pty/package.json")),n=O.join(e,"prebuilds");if(D(n))for(const s of de(n)){const t=O.join(n,s,"spawn-helper");D(t)&&!(me(t).mode&73)&&ue(t,493)}}catch{}import{loadSecurityConfig as ke}from"./lib/security-config.js";import{TokenStore as _e,hashPassword as Te,verifyPassword as we,validatePassword as Se}from"./lib/auth.js";import{RateLimiter as Ae}from"./lib/rateLimiter.js";import{audit as d}from"./lib/auditLog.js";import{db as N}from"./lib/db.js";import{handleClientMessage as z}from"./lib/ws-message.js";import{loadDotEnv as Ce}from"./lib/load-env.js";import{listSessions as ve,resolveTmuxSocketPath as be}from"./sessions.js";import{cmdSetup as xe,cmdTheme as Ie,printUsage as G,printVersion as Re}from"./lib/cli.js";import{readSettings as Pe}from"./lib/settings.js";import{readActiveTheme as Me}from"./lib/theme-store.js";import{buildApp as V}from"./lib/build-app.js";import{attachTerminal as Oe,killAllAttachedPtys as Ne}from"./lib/attach-terminal.js";import{acquireControlClient as X,killAllControlClients as $e}from"./lib/tmux-control.js";import{getSessionPaneTarget as Y,capturePaneTail as K,capturePaneHistoryChunk as Q,toCrlf as Z}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Le}from"./lib/terminal-config.js";import{AgentRegistry as Ue}from"./lib/agent-registry.js";import{AgentChannel as Ee,MAX_RELAY_CONNS_PER_AGENT as Fe}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as He,listAgentTokens as je}from"./lib/agent-tokens.js";import{saveSecurityConfig as Be}from"./lib/security-config.js";import{readFederationConfig as We,writeFederationConfig as De}from"./lib/federation-config.js";import{startAgentClient as Je}from"./lib/agent-client.js";Ce();const E=Le(),h=ke(),k=new _e,A=new Ae;let j=!1;const ze="tmux-web-token";function B(e){if(h.security.trustProxy){const n=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(n)return n}return e.socket.remoteAddress||"unknown"}function W(e){const n=e.headers.authorization;if(typeof n=="string"&&n.startsWith("Bearer "))return n.slice(7);const s=e.headers.cookie;if(typeof s!="string")return null;const t=s.match(new RegExp(`(?:^|;\\s*)${ze}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function Lt(e){const n=W(e);return n?k.validateToken(n):null}const P=new Map;function Ge(e){let n=0;for(const s of P.values())s.ip===e&&n++;return n}function v(e,n,s){try{e.close(n,s)}catch{}}function q(e,n){e.readyState===H.OPEN&&e.send(JSON.stringify(n))}const ee=process.argv.slice(2),$=ee;if($.length>0){const[e,n]=$;switch(e){case"setup":await xe($),process.exit(0);case"theme":await Ie($.slice(1)),process.exit(0);case"agent":{const s=O.join(O.dirname(he(import.meta.url)),"agent.js"),{spawn:t}=await import("node:child_process"),m=t(process.execPath,[s,...ee],{stdio:"inherit"});m.on("error",r=>{console.error(r),process.exit(1)}),m.on("exit",r=>process.exit(r??0)),await new Promise(()=>{})}case"agent-token":await st($.slice(1)),process.exit(0);case"help":case"--help":case"-h":G(),process.exit(0);case"-V":case"--version":case"-v":Re(),process.exit(0);default:console.error(`unknown argument: ${e}`),G(),process.exit(1)}}function I(e,n){e.readyState===H.OPEN&&e.send(JSON.stringify(n))}const te=new Set;await N.read(),N.data.sessionAccess??=[],N.data.pinnedViews??=[],N.data.watchedPanes??=[],N.data.quickCommands??=[];const ne=await Pe();let Ve=await Me();const y=new Ue({onChange:()=>{}}),T=new Map;function oe(e){return y.get(e)?T.get(e)??null:null}const re={activeTheme:Ve,settingUpPassword:!1},Xe=V({mode:"hub",securityConfig:h,tokenStore:k,rateLimiter:A,settings:ne,terminalBufferConfig:E,state:re,federation:{getConfig:()=>x,setConfig:async e=>{x=e,await De(e),ae(e)},status:Ke,hostname:U()},hub:{agents:y,getChannel:oe}}),Ye=V({mode:"agent",securityConfig:h,tokenStore:k,rateLimiter:A,settings:ne,terminalBufferConfig:E,state:re});let x=await We(),M=null;function se(){M&&(M.stop(),M=null)}function ae(e){se(),!(!e.enabled||!e.hub||!e.token)&&(M=Je({hub:e.hub,token:e.token,name:e.name?.trim()||U(),version:pe,app:Ye,listSessions:ve,terminalBufferConfig:E,getSessionPaneTarget:Y,capturePaneTail:K,capturePaneHistoryChunk:Q,toCrlf:Z,handleClientMessage:z,acquireControlClient:X}))}function Ke(){return x.enabled?M?M.status():{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}:{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}}ae(x);const Qe=parseInt(process.env.PORT||"21000",10),Ze=ge({fetch:Xe.fetch,port:Qe,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),L=new ye({noServer:!0});function R(e,n,s){e.write(`HTTP/1.1 ${n} ${s}\r
2
+ import{chmodSync as pe,existsSync as z,readdirSync as he,statSync as ge}from"node:fs";import{createRequire as G}from"node:module";import{hostname as U}from"node:os";const ye=G(import.meta.url),{version:ke}=ye("../package.json");import N from"node:path";import{fileURLToPath as Te}from"node:url";import{serve as _e}from"@hono/node-server";import{WebSocketServer as ve,WebSocket as E}from"ws";try{const e=N.dirname(G(import.meta.url).resolve("node-pty/package.json")),n=N.join(e,"prebuilds");if(z(n))for(const o of he(n)){const t=N.join(n,o,"spawn-helper");z(t)&&!(ge(t).mode&73)&&pe(t,493)}}catch{}import{loadSecurityConfig as we}from"./lib/security-config.js";import{TokenStore as Se,hashPassword as Ae,verifyPassword as Ce,validatePassword as be}from"./lib/auth.js";import{RateLimiter as xe}from"./lib/rateLimiter.js";import{audit as d}from"./lib/auditLog.js";import{db as $}from"./lib/db.js";import{handleClientMessage as V}from"./lib/ws-message.js";import{loadDotEnv as Ie}from"./lib/load-env.js";import{listSessions as Pe,resolveTmuxSocketPath as Re}from"./sessions.js";import{cmdSetup as Me,cmdTheme as Oe,printUsage as X,printVersion as Ne}from"./lib/cli.js";import{readSettings as $e}from"./lib/settings.js";import{readActiveTheme as Le}from"./lib/theme-store.js";import{buildApp as Y}from"./lib/build-app.js";import{attachTerminal as Ue,killAllAttachedPtys as Ee}from"./lib/attach-terminal.js";import{acquireControlClient as K,killAllControlClients as Fe}from"./lib/tmux-control.js";import{getSessionPaneTarget as Q,capturePaneTail as Z,capturePaneHistoryChunk as q,toCrlf as ee}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as He}from"./lib/terminal-config.js";import{AgentRegistry as je}from"./lib/agent-registry.js";import{AgentChannel as Be,MAX_RELAY_CONNS_PER_AGENT as We}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as Je,listAgentTokens as De}from"./lib/agent-tokens.js";import{saveSecurityConfig as ze}from"./lib/security-config.js";import{readFederationConfig as Ge,writeFederationConfig as Ve}from"./lib/federation-config.js";import{startAgentClient as Xe}from"./lib/agent-client.js";import{ActivityProbe as Ye}from"./lib/activity-probe.js";Ie();const F=He(),h=we(),k=new Se,A=new xe;let J=!1;const Ke="tmux-web-token";function H(e){if(h.security.trustProxy){const n=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(n)return n}return e.socket.remoteAddress||"unknown"}function j(e){const n=e.headers.authorization;if(typeof n=="string"&&n.startsWith("Bearer "))return n.slice(7);const o=e.headers.cookie;if(typeof o!="string")return null;const t=o.match(new RegExp(`(?:^|;\\s*)${Ke}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function Bt(e){const n=j(e);return n?k.validateToken(n):null}const R=new Map;function Qe(e){let n=0;for(const o of R.values())o.ip===e&&n++;return n}function b(e,n,o){try{e.close(n,o)}catch{}}function te(e,n){e.readyState===E.OPEN&&e.send(JSON.stringify(n))}const ne=process.argv.slice(2),L=ne;if(L.length>0){const[e,n]=L;switch(e){case"setup":await Me(L),process.exit(0);case"theme":await Oe(L.slice(1)),process.exit(0);case"agent":{const o=N.join(N.dirname(Te(import.meta.url)),"agent.js"),{spawn:t}=await import("node:child_process"),f=t(process.execPath,[o,...ne],{stdio:"inherit"});f.on("error",s=>{console.error(s),process.exit(1)}),f.on("exit",s=>process.exit(s??0)),await new Promise(()=>{})}case"agent-token":await ut(L.slice(1)),process.exit(0);case"help":case"--help":case"-h":X(),process.exit(0);case"-V":case"--version":case"-v":Ne(),process.exit(0);default:console.error(`unknown argument: ${e}`),X(),process.exit(1)}}function P(e,n){e.readyState===E.OPEN&&e.send(JSON.stringify(n))}const oe=new Set;await $.read(),$.data.sessionAccess??=[],$.data.pinnedViews??=[],$.data.watchedPanes??=[],$.data.quickCommands??=[];const re=await $e();let Ze=await Le();const y=new je({onChange:()=>{}}),v=new Map;function se(e){return y.get(e)?v.get(e)??null:null}const ae={activeTheme:Ze,settingUpPassword:!1},qe=Y({mode:"hub",securityConfig:h,tokenStore:k,rateLimiter:A,settings:re,terminalBufferConfig:F,state:ae,federation:{getConfig:()=>x,setConfig:async e=>{x=e,await Ve(e),ce(e)},status:tt,hostname:U()},hub:{agents:y,getChannel:se}}),et=Y({mode:"agent",securityConfig:h,tokenStore:k,rateLimiter:A,settings:re,terminalBufferConfig:F,state:ae});let x=await Ge(),M=null;function ie(){M&&(M.stop(),M=null)}function ce(e){ie(),!(!e.enabled||!e.hub||!e.token)&&(M=Xe({hub:e.hub,token:e.token,name:e.name?.trim()||U(),version:ke,app:et,listSessions:Pe,terminalBufferConfig:F,getSessionPaneTarget:Q,capturePaneTail:Z,capturePaneHistoryChunk:q,toCrlf:ee,handleClientMessage:V,acquireControlClient:K}))}function tt(){return x.enabled?M?M.status():{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}:{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}}ce(x);const le=new Ye,B=new Map;setInterval(()=>{(async()=>{try{const e=await le.scan(),n=[];for(const[o,t]of e)B.get(o)!==t&&n.push({session:o,state:t});for(const o of B.keys())e.has(o)||n.push({session:o,state:"idle"});B.clear();for(const[o,t]of e)B.set(o,t);for(const o of n)ue({type:"session.activity",session:o.session,state:o.state})}catch{}})()},le.scanMs);const nt=parseInt(process.env.PORT||"21000",10),ot=_e({fetch:qe.fetch,port:nt,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),O=new ve({noServer:!0}),D=new Set;function ue(e){const n=JSON.stringify(e);for(const o of D)o.readyState===E.OPEN&&o.send(n)}function I(e,n,o){e.write(`HTTP/1.1 ${n} ${o}\r
3
3
  \r
4
- `),e.destroy()}Ze.on("upgrade",(e,n,s)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),m=t.pathname;if(m==="/agent/ws"){nt(e,n,s);return}const r=m.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(r){qe(e,n,s,decodeURIComponent(r[1]),decodeURIComponent(r[2]));return}const a=m.match(/^\/ws\/(.+)$/);if(!a){n.destroy();return}const c=B(e),l=e.headers.origin,p=h.security.allowedOrigins;if(l&&!(l===`http://${e.headers.host}`||l===`https://${e.headers.host}`)&&(p.length===0||!p.includes(l))){d("ws_rejected_origin",{ip:c,origin:l}),R(n,403,"Origin not allowed");return}const f=Ge(c);if(f>=h.security.maxConnectionsPerIp){d("ws_rejected_per_ip_cap",{ip:c,liveFromIp:f}),R(n,429,"Too many connections");return}const g=W(e)||t.searchParams.get("token")||"",_=g?k.validateToken(g):null;L.handleUpgrade(e,n,s,S=>{L.emit("connection",S,e,decodeURIComponent(a[1]),c,_)})}),L.on("connection",(e,n,s,t,m)=>{const r={ws:e,ip:t,authenticated:!1,authTimeout:null},a=(()=>{try{const i=new URL(n.url||"/",`http://${n.headers.host}`).searchParams,o=Number(i.get("cols")),u=Number(i.get("rows"));if(Number.isInteger(o)&&Number.isInteger(u)&&o>0&&u>0&&o<=1e3&&u<=1e3)return{cols:o,rows:u}}catch{}})();P.set(e,r),d("ws_connected",{ip:t});let c=null,l=null;function p(i,o){r.authenticated=!0,r.authTimeout&&(clearTimeout(r.authTimeout),r.authTimeout=null),d("auth_success",{ip:t,method:i,tokenName:o}),I(e,{type:"auth.ok",setupMode:!h.passwordHash}),g()}function f(i,o={}){I(e,{type:"auth.failed",message:i,...o})}function g(){c||(c=Oe(s,{send:i=>I(e,i),onMessage:i=>{l=i},onClose:i=>{_=i}},{terminalBufferConfig:E,getSessionPaneTarget:Y,capturePaneTail:K,capturePaneHistoryChunk:Q,toCrlf:Z,handleClientMessage:z,acquireControlClient:X,initialCols:a?.cols,initialRows:a?.rows,tmuxSocketPath:be()??void 0,onPtyExit:(i,o)=>v(e,1e3,"pty exited"),onSpawnError:(i,o)=>v(e,1011,"pty spawn failed")}))}let _=null;async function S(i){let o;try{o=JSON.parse(i)}catch{return!1}if(o.type==="auth.token"&&typeof o.token=="string"){const u=k.validateToken(o.token);if(!u){d("token_auth_failed",{ip:t});const w=A.recordFailure(t);return w.permanentLock&&(k.revokeAll(),d("permanent_lock",{ip:t,failures:w.failures})),f("Invalid or expired token",{permanentLock:w.permanentLock,retryAfterMs:w.retryAfterMs}),!0}return k.touch(u.tokenHash),p("token",u.name),!0}if(o.type==="auth"&&typeof o.password=="string"){const u=A.check(t);if(!u.allowed)return d("rate_limited",{ip:t,retryAfterMs:u.retryAfterMs,permanentLock:u.permanentLock}),f("Too many attempts",{permanentLock:u.permanentLock,retryAfterMs:u.retryAfterMs}),!0;if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!rt(t))return d("setup_rejected_remote",{ip:t}),f("First-run setup must be performed from localhost"),!0;if(j)return f("Password setup in progress"),!0;const C=Se(o.password);if(C)return f(C),!0;j=!0;try{h.passwordHash=await Te(o.password),Be(h),d("password_set",{ip:t})}finally{j=!1}const{stored:F,plaintext:le}=k.createAccessToken("setup",h.security.tokenTtlDays);return I(e,{type:"auth.ok",setupMode:!0,token:le}),d("token_created",{ip:t,name:F.name,tokenId:F.id}),d("auth_success",{ip:t,method:"password",tokenName:F.name}),g(),!0}if(!h.passwordHash)return f("Server not configured"),!0;if(!await we(o.password,h.passwordHash)){const C=A.recordFailure(t);return d("auth_failed",{ip:t,method:"password",failures:C.failures,permanentLock:C.permanentLock}),C.permanentLock&&(k.revokeAll(),d("permanent_lock",{ip:t,failures:C.failures})),f("Incorrect password",{permanentLock:C.permanentLock,retryAfterMs:C.retryAfterMs}),!0}A.recordSuccess(t);const b=`ws-${t}`,{plaintext:ce}=k.createAccessToken(b,h.security.tokenTtlDays);return p("password",b),I(e,{type:"auth.ok",setupMode:!1,token:ce}),!0}return!1}m?(k.touch(m.tokenHash),p("token",m.name)):(I(e,{type:"auth.required",setupMode:!h.passwordHash}),r.authTimeout=setTimeout(()=>{r.authenticated||(d("auth_timeout",{ip:t}),f("Authentication timeout"),v(e,4e3,"Auth timeout"))},h.security.authTimeoutMs)),e.on("message",async i=>{const o=typeof i=="string"?i:i.toString("utf-8");if(!r.authenticated){if(o.length>1e6){I(e,{type:"auth.failed",message:"Message too large"});return}await S(o)||f("Authentication required");return}if(l)try{l(o)}catch{}}),e.on("close",()=>{r.authTimeout&&clearTimeout(r.authTimeout),P.delete(e),d("ws_disconnected",{ip:t}),_?.(),c&&(c.dispose(),c=null)}),e.on("error",()=>{r.authTimeout&&clearTimeout(r.authTimeout),P.delete(e),_?.(),c&&(c.dispose(),c=null)})});function qe(e,n,s,t,m){const r=B(e),a=new URL(e.url||"/",`http://${e.headers.host}`),c=W(e)||a.searchParams.get("token")||"";if(!(c?k.validateToken(c):null)){d("ws_rejected_remote_auth",{ip:r,agentId:t}),R(n,401,"Unauthorized");return}const p=oe(t);if(!p){d("ws_rejected_remote_offline",{ip:r,agentId:t}),R(n,503,"Agent offline");return}if(p.relayCount>=Fe){R(n,429,"Too many connections to agent");return}const f=e.headers.origin,g=h.security.allowedOrigins;if(f&&!(f===`http://${e.headers.host}`||f===`https://${e.headers.host}`)&&(g.length===0||!g.includes(f))){R(n,403,"Origin not allowed");return}const _=tt(),S=(()=>{try{const i=new URL(e.url||"/",`http://${e.headers.host}`).searchParams,o=Number(i.get("cols")),u=Number(i.get("rows"));if(Number.isInteger(o)&&Number.isInteger(u)&&o>0&&u>0&&o<=1e3&&u<=1e3)return{cols:o,rows:u}}catch{}})();L.handleUpgrade(e,n,s,i=>{if(p.registerRelay(_,m,i),y.bumpRelayConns(t,1),!p.attach(_,m,S)){i.send(JSON.stringify({type:"data",data:`\r
4
+ `),e.destroy()}ot.on("upgrade",(e,n,o)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),f=t.pathname;if(f==="/ws/activity"){const w=H(e),a=j(e)||t.searchParams.get("token")||"";if(!(a?k.validateToken(a):null)){I(n,401,"Unauthorized");return}O.handleUpgrade(e,n,o,c=>{c.on("close",()=>D.delete(c)),D.add(c)});return}if(f==="/agent/ws"){it(e,n,o);return}const s=f.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(s){rt(e,n,o,decodeURIComponent(s[1]),decodeURIComponent(s[2]));return}const i=f.match(/^\/ws\/(.+)$/);if(!i){n.destroy();return}const l=H(e),u=e.headers.origin,p=h.security.allowedOrigins;if(u&&!(u===`http://${e.headers.host}`||u===`https://${e.headers.host}`)&&(p.length===0||!p.includes(u))){d("ws_rejected_origin",{ip:l,origin:u}),I(n,403,"Origin not allowed");return}const m=Qe(l);if(m>=h.security.maxConnectionsPerIp){d("ws_rejected_per_ip_cap",{ip:l,liveFromIp:m}),I(n,429,"Too many connections");return}const g=j(e)||t.searchParams.get("token")||"",_=g?k.validateToken(g):null;O.handleUpgrade(e,n,o,w=>{O.emit("connection",w,e,decodeURIComponent(i[1]),l,_)})}),O.on("connection",(e,n,o,t,f)=>{const s={ws:e,ip:t,authenticated:!1,authTimeout:null},i=(()=>{try{const a=new URL(n.url||"/",`http://${n.headers.host}`).searchParams,r=Number(a.get("cols")),c=Number(a.get("rows"));if(Number.isInteger(r)&&Number.isInteger(c)&&r>0&&c>0&&r<=1e3&&c<=1e3)return{cols:r,rows:c}}catch{}})();R.set(e,s),d("ws_connected",{ip:t});let l=null,u=null;function p(a,r){s.authenticated=!0,s.authTimeout&&(clearTimeout(s.authTimeout),s.authTimeout=null),d("auth_success",{ip:t,method:a,tokenName:r}),P(e,{type:"auth.ok",setupMode:!h.passwordHash}),g()}function m(a,r={}){P(e,{type:"auth.failed",message:a,...r})}function g(){l||(l=Ue(o,{send:a=>P(e,a),onMessage:a=>{u=a},onClose:a=>{_=a}},{terminalBufferConfig:F,getSessionPaneTarget:Q,capturePaneTail:Z,capturePaneHistoryChunk:q,toCrlf:ee,handleClientMessage:V,acquireControlClient:K,initialCols:i?.cols,initialRows:i?.rows,tmuxSocketPath:Re()??void 0,onPtyExit:(a,r)=>b(e,1e3,"pty exited"),onSpawnError:(a,r)=>b(e,1011,"pty spawn failed")}))}let _=null;async function w(a){let r;try{r=JSON.parse(a)}catch{return!1}if(r.type==="auth.token"&&typeof r.token=="string"){const c=k.validateToken(r.token);if(!c){d("token_auth_failed",{ip:t});const T=A.recordFailure(t);return T.permanentLock&&(k.revokeAll(),d("permanent_lock",{ip:t,failures:T.failures})),m("Invalid or expired token",{permanentLock:T.permanentLock,retryAfterMs:T.retryAfterMs}),!0}return k.touch(c.tokenHash),p("token",c.name),!0}if(r.type==="auth"&&typeof r.password=="string"){const c=A.check(t);if(!c.allowed)return d("rate_limited",{ip:t,retryAfterMs:c.retryAfterMs,permanentLock:c.permanentLock}),m("Too many attempts",{permanentLock:c.permanentLock,retryAfterMs:c.retryAfterMs}),!0;if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!lt(t))return d("setup_rejected_remote",{ip:t}),m("First-run setup must be performed from localhost"),!0;if(J)return m("Password setup in progress"),!0;const C=be(r.password);if(C)return m(C),!0;J=!0;try{h.passwordHash=await Ae(r.password),ze(h),d("password_set",{ip:t})}finally{J=!1}const{stored:W,plaintext:me}=k.createAccessToken("setup",h.security.tokenTtlDays);return P(e,{type:"auth.ok",setupMode:!0,token:me}),d("token_created",{ip:t,name:W.name,tokenId:W.id}),d("auth_success",{ip:t,method:"password",tokenName:W.name}),g(),!0}if(!h.passwordHash)return m("Server not configured"),!0;if(!await Ce(r.password,h.passwordHash)){const C=A.recordFailure(t);return d("auth_failed",{ip:t,method:"password",failures:C.failures,permanentLock:C.permanentLock}),C.permanentLock&&(k.revokeAll(),d("permanent_lock",{ip:t,failures:C.failures})),m("Incorrect password",{permanentLock:C.permanentLock,retryAfterMs:C.retryAfterMs}),!0}A.recordSuccess(t);const S=`ws-${t}`,{plaintext:fe}=k.createAccessToken(S,h.security.tokenTtlDays);return p("password",S),P(e,{type:"auth.ok",setupMode:!1,token:fe}),!0}return!1}f?(k.touch(f.tokenHash),p("token",f.name)):(P(e,{type:"auth.required",setupMode:!h.passwordHash}),s.authTimeout=setTimeout(()=>{s.authenticated||(d("auth_timeout",{ip:t}),m("Authentication timeout"),b(e,4e3,"Auth timeout"))},h.security.authTimeoutMs)),e.on("message",async a=>{const r=typeof a=="string"?a:a.toString("utf-8");if(!s.authenticated){if(r.length>1e6){P(e,{type:"auth.failed",message:"Message too large"});return}await w(r)||m("Authentication required");return}if(u)try{u(r)}catch{}}),e.on("close",()=>{s.authTimeout&&clearTimeout(s.authTimeout),R.delete(e),d("ws_disconnected",{ip:t}),_?.(),l&&(l.dispose(),l=null)}),e.on("error",()=>{s.authTimeout&&clearTimeout(s.authTimeout),R.delete(e),_?.(),l&&(l.dispose(),l=null)})});function rt(e,n,o,t,f){const s=H(e),i=new URL(e.url||"/",`http://${e.headers.host}`),l=j(e)||i.searchParams.get("token")||"";if(!(l?k.validateToken(l):null)){d("ws_rejected_remote_auth",{ip:s,agentId:t}),I(n,401,"Unauthorized");return}const p=se(t);if(!p){d("ws_rejected_remote_offline",{ip:s,agentId:t}),I(n,503,"Agent offline");return}if(p.relayCount>=We){I(n,429,"Too many connections to agent");return}const m=e.headers.origin,g=h.security.allowedOrigins;if(m&&!(m===`http://${e.headers.host}`||m===`https://${e.headers.host}`)&&(g.length===0||!g.includes(m))){I(n,403,"Origin not allowed");return}const _=at(),w=(()=>{try{const a=new URL(e.url||"/",`http://${e.headers.host}`).searchParams,r=Number(a.get("cols")),c=Number(a.get("rows"));if(Number.isInteger(r)&&Number.isInteger(c)&&r>0&&c>0&&r<=1e3&&c<=1e3)return{cols:r,rows:c}}catch{}})();O.handleUpgrade(e,n,o,a=>{if(p.registerRelay(_,f,a),y.bumpRelayConns(t,1),!p.attach(_,f,w)){a.send(JSON.stringify({type:"data",data:`\r
5
5
  \x1B[31magent offline\x1B[0m\r
6
- `})),i.close(1011,"agent offline"),p.relayClosed(_),y.bumpRelayConns(t,-1);return}i.on("message",u=>{const w=typeof u=="string"?u:u.toString("utf-8");let b;try{b=JSON.parse(w)}catch{return}(b.type==="input"||b.type==="resize"||b.type==="load_history")&&p.sendToAgent(_,b)});const o=()=>{p.relayClosed(_),y.bumpRelayConns(t,-1)};i.on("close",o),i.on("error",o)})}let et=1;function tt(){return et++}function nt(e,n,s){const t=B(e),m=e.headers.origin;if(m){d("agent_rejected_origin",{ip:t,origin:m}),R(n,403,"Origin not allowed");return}L.handleUpgrade(e,n,s,r=>{let a=null,c=setTimeout(()=>{d("agent_hello_timeout",{ip:t}),v(r,4e3,"hello timeout")},1e4);r.on("message",(l,p)=>{if(a){y.touch(a.agentId),p?a.handleData(Array.isArray(l)?Buffer.concat(l):Buffer.from(l)):a.handleMessage(typeof l=="string"?l:Buffer.from(l).toString("utf-8"));return}const f=typeof l=="string"?l:l.toString("utf-8");let g;try{g=JSON.parse(f)}catch{v(r,1008,"bad hello");return}if(g.type!=="hello"||typeof g.token!="string"){v(r,1008,"expected hello");return}if(!A.check(t).allowed){d("agent_rate_limited",{ip:t}),v(r,429,"Too many attempts");return}const S=He(g.token);if(!S){A.recordFailure(t),d("agent_bad_token",{ip:t}),q(r,{type:"hello_err",reason:"invalid token"}),v(r,4003,"invalid token");return}const i=typeof g.name=="string"&&g.name.trim()?g.name.trim().slice(0,64):"agent",o=S.id;if(!y.register(o,i,r,S.tokenHash)){d("agent_cap_reached",{ip:t}),q(r,{type:"hello_err",reason:"too many agents"}),v(r,4004,"too many agents");return}clearTimeout(c),A.recordSuccess(t),d("agent_connected",{ip:t,agentId:o,name:i}),a=new Ee(r,o,{onSessions:w=>y.setSessions(o,w),onClose:w=>{T.get(o)===w&&T.delete(o)}}),T.set(o,a),y.touch(o),r.send(JSON.stringify({type:"hello_ok",agentId:o})),a.sendSessionsReq()}),r.on("close",()=>{a&&(a.dispose(),T.delete(a.agentId),y.unregister(a.agentId),d("agent_disconnected",{ip:t,agentId:a.agentId})),clearTimeout(c)}),r.on("error",()=>{a&&(a.dispose(),T.delete(a.agentId),y.unregister(a.agentId)),clearTimeout(c)})})}const ot=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=y.pruneOffline(ot);for(const s of e){const t=y.peek(s);t&&t.ws.readyState===H.CLOSED&&(y.unregister(s),T.get(s)?.dispose(),T.delete(s))}const n=new Set(je().map(s=>s.tokenHash));for(const[s,t]of T){const m=y.peek(s);if(!(!m||n.has(m.tokenHash))){d("agent_token_revoked",{agentId:s}),t.dispose(),T.delete(s),y.unregister(s);try{m.ws.close(4003,"token revoked")}catch{}}}k.purgeExpired()},10*1e3).unref();function ie(){se(),$e(),Ne();for(const e of T.values())e.dispose();T.clear();for(const[e,n]of P){try{e.close(1001,"Server shutting down")}catch{}n.authTimeout&&clearTimeout(n.authTimeout)}P.clear();for(const e of te)try{e.kill()}catch{}te.clear(),A.dispose(),process.exit(0)}process.on("SIGINT",ie),process.on("SIGTERM",ie);function rt(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}async function st(e){const{createAgentToken:n,listAgentTokens:s,removeAgentToken:t}=await import("./lib/agent-tokens.js"),[m,...r]=e;switch(m){case"add":{const a=r.find(p=>!p.startsWith("-"))||"agent",{id:c,token:l}=n(a);console.log("\u2713 agent token created"),console.log(` id: ${c}`),console.log(` name: ${a}`),console.log(` token: ${l}`),console.log(`
7
- Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${l} --name "${a}"`);return}case"list":{const a=s();if(!a.length){console.log("No agent tokens. Create one with: tmux-web agent-token add --name <name>");return}console.log("Agent tokens:");for(const c of a)console.log(` ${c.id} ${c.name} (created ${new Date(c.createdAt).toISOString()})`);return}case"remove":{const a=r.find(l=>!l.startsWith("-"));a||(console.error("usage: tmux-web agent-token remove <id>"),process.exit(1)),t(a)||(console.error(`no token with id ${a}`),process.exit(1)),console.log(`\u2713 token ${a} removed`);return}default:console.error("usage: tmux-web agent-token <add|list|remove>"),process.exit(1)}}
6
+ `})),a.close(1011,"agent offline"),p.relayClosed(_),y.bumpRelayConns(t,-1);return}a.on("message",c=>{const T=typeof c=="string"?c:c.toString("utf-8");let S;try{S=JSON.parse(T)}catch{return}(S.type==="input"||S.type==="resize"||S.type==="load_history")&&p.sendToAgent(_,S)});const r=()=>{p.relayClosed(_),y.bumpRelayConns(t,-1)};a.on("close",r),a.on("error",r)})}let st=1;function at(){return st++}function it(e,n,o){const t=H(e),f=e.headers.origin;if(f){d("agent_rejected_origin",{ip:t,origin:f}),I(n,403,"Origin not allowed");return}O.handleUpgrade(e,n,o,s=>{let i=null,l=setTimeout(()=>{d("agent_hello_timeout",{ip:t}),b(s,4e3,"hello timeout")},1e4);s.on("message",(u,p)=>{if(i){y.touch(i.agentId),p?i.handleData(Array.isArray(u)?Buffer.concat(u):Buffer.from(u)):i.handleMessage(typeof u=="string"?u:Buffer.from(u).toString("utf-8"));return}const m=typeof u=="string"?u:u.toString("utf-8");let g;try{g=JSON.parse(m)}catch{b(s,1008,"bad hello");return}if(g.type!=="hello"||typeof g.token!="string"){b(s,1008,"expected hello");return}if(!A.check(t).allowed){d("agent_rate_limited",{ip:t}),b(s,429,"Too many attempts");return}const w=Je(g.token);if(!w){A.recordFailure(t),d("agent_bad_token",{ip:t}),te(s,{type:"hello_err",reason:"invalid token"}),b(s,4003,"invalid token");return}const a=typeof g.name=="string"&&g.name.trim()?g.name.trim().slice(0,64):"agent",r=w.id;if(!y.register(r,a,s,w.tokenHash)){d("agent_cap_reached",{ip:t}),te(s,{type:"hello_err",reason:"too many agents"}),b(s,4004,"too many agents");return}clearTimeout(l),A.recordSuccess(t),d("agent_connected",{ip:t,agentId:r,name:a}),i=new Be(s,r,{onSessions:T=>y.setSessions(r,T),onActivity:T=>{for(const S of T)ue({type:"session.activity",agentId:r,session:S.session,state:S.state})},onClose:T=>{v.get(r)===T&&v.delete(r)}}),v.set(r,i),y.touch(r),s.send(JSON.stringify({type:"hello_ok",agentId:r})),i.sendSessionsReq()}),s.on("close",()=>{i&&(i.dispose(),v.delete(i.agentId),y.unregister(i.agentId),d("agent_disconnected",{ip:t,agentId:i.agentId})),clearTimeout(l)}),s.on("error",()=>{i&&(i.dispose(),v.delete(i.agentId),y.unregister(i.agentId)),clearTimeout(l)})})}const ct=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=y.pruneOffline(ct);for(const o of e){const t=y.peek(o);t&&t.ws.readyState===E.CLOSED&&(y.unregister(o),v.get(o)?.dispose(),v.delete(o))}const n=new Set(De().map(o=>o.tokenHash));for(const[o,t]of v){const f=y.peek(o);if(!(!f||n.has(f.tokenHash))){d("agent_token_revoked",{agentId:o}),t.dispose(),v.delete(o),y.unregister(o);try{f.ws.close(4003,"token revoked")}catch{}}}k.purgeExpired()},10*1e3).unref();function de(){ie(),Fe(),Ee();for(const e of v.values())e.dispose();v.clear();for(const[e,n]of R){try{e.close(1001,"Server shutting down")}catch{}n.authTimeout&&clearTimeout(n.authTimeout)}R.clear();for(const e of oe)try{e.kill()}catch{}oe.clear(),A.dispose(),process.exit(0)}process.on("SIGINT",de),process.on("SIGTERM",de);function lt(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}async function ut(e){const{createAgentToken:n,listAgentTokens:o,removeAgentToken:t}=await import("./lib/agent-tokens.js"),[f,...s]=e;switch(f){case"add":{const i=s.find(p=>!p.startsWith("-"))||"agent",{id:l,token:u}=n(i);console.log("\u2713 agent token created"),console.log(` id: ${l}`),console.log(` name: ${i}`),console.log(` token: ${u}`),console.log(`
7
+ Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${u} --name "${i}"`);return}case"list":{const i=o();if(!i.length){console.log("No agent tokens. Create one with: tmux-web agent-token add --name <name>");return}console.log("Agent tokens:");for(const l of i)console.log(` ${l.id} ${l.name} (created ${new Date(l.createdAt).toISOString()})`);return}case"remove":{const i=s.find(u=>!u.startsWith("-"));i||(console.error("usage: tmux-web agent-token remove <id>"),process.exit(1)),t(i)||(console.error(`no token with id ${i}`),process.exit(1)),console.log(`\u2713 token ${i} removed`);return}default:console.error("usage: tmux-web agent-token <add|list|remove>"),process.exit(1)}}
@@ -0,0 +1,2 @@
1
+ import{execFile as l}from"node:child_process";import{tmuxEnv as f}from"./tmux-env.js";const r=1e4,h=2e3;function u(c,s,e){return c-s<=e?"working":"idle"}function p(c){return new Promise((s,e)=>{l("tmux",c,{env:f(),maxBuffer:8*1024*1024},(i,n)=>{i?e(i):s(n)})})}class _{idleMs;scanMs;execTmux;hashes=new Map;lastActivity=new Map;constructor(s={}){const e=parseInt(process.env.TMUX_WEB_ACTIVITY_IDLE_MS||String(r),10);this.idleMs=s.idleMs??(e||r);const i=parseInt(process.env.TMUX_WEB_ACTIVITY_SCAN_MS||String(h),10);this.scanMs=s.scanMs??(i||h),this.execTmux=s.execTmux??p}async scan(s=Date.now()){const e=await this.listPanes(),i=new Set;for(const t of e){i.add(t.session);let a;try{a=await this.execTmux(["capture-pane","-p","-e","-t",t.target])}catch{continue}const o=this.hashes.get(t.target);o!==void 0&&a!==o&&this.lastActivity.set(t.session,s),this.hashes.set(t.target,a)}for(const t of this.hashes.keys())e.some(a=>a.target===t)||this.hashes.delete(t);for(const t of this.lastActivity.keys())i.has(t)||this.lastActivity.delete(t);const n=new Map;for(const t of i)this.lastActivity.has(t)||this.lastActivity.set(t,s-this.idleMs-1),n.set(t,u(s,this.lastActivity.get(t),this.idleMs));return n}async listPanes(){const s=await this.execTmux(["list-panes","-a","-F","#{session_name} #{window_index}.#{pane_index}"]),e=[];for(const i of s.split(`
2
+ `)){const[n,t]=i.split(" ");n&&t&&e.push({session:n,target:`${n}:${t}`})}return e}}export{_ as ActivityProbe,r as DEFAULT_ACTIVITY_IDLE_MS,h as DEFAULT_ACTIVITY_SCAN_MS,u as computeAgentState};
@@ -1 +1 @@
1
- import{WebSocket as C}from"ws";import{attachTerminal as D}from"./attach-terminal.js";import{encodeBinaryFrame as J,decodeBinaryFrame as j,BIN_KIND_HTTP_BODY as $}from"./agent-channel.js";const K=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),W=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),F=new Set(["content-type","content-disposition","location","cache-control","etag"]);function Q(o){const T=o.log??(e=>console.log(`[agent] ${e}`)),f=e=>console.error(`[agent] ${e}`);let s=null,g=1e3,B=null,m=!1;const d=new Map,h=new Map,y=new Map,w=new Set;function A(e){return w.has(e)?Promise.resolve():new Promise(t=>{y.set(e,t),setTimeout(()=>{y.delete(e)&&t()},3e4)})}let p=null,_=null,E="",b=null;const S={state:"idle",agentId:null,hub:o.hub,name:o.name};function u(e,t){t!==void 0&&(S.lastError=t),S.state=e,S.agentId=b,o.onStatus?.({...S})}function c(e){s&&s.readyState===C.OPEN&&s.send(JSON.stringify(e))}function k(){let e;try{e=o.listSessions()}catch{return}const t=JSON.stringify(e);t!==E&&(E=t,c({type:"sessions",sessions:e}))}function v(){p||(p=setInterval(k,W)),_||(_=setInterval(()=>c({type:"ping",ts:Date.now()}),K))}function H(){p&&(clearInterval(p),p=null),_&&(clearInterval(_),_=null)}async function O(e){let t;if(e.hasBody){await A(e.id),w.delete(e.id);const r=h.get(e.id);h.delete(e.id),t=r&&r.length?Buffer.concat(r):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let i;try{const r={method:e.method,headers:e.headers};t&&t.length&&(r.body=t),i=await Promise.race([o.app.request(new Request(n,r)),new Promise((G,x)=>setTimeout(()=>x(new Error("app.request timed out")),3e4))])}catch(r){f(`tunnel request failed: ${e.method} ${e.path} ${r.message}`),c({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const I={};i.headers.forEach((r,l)=>{F.has(l.toLowerCase())&&(I[l]=r)});const a=Buffer.from(await i.arrayBuffer());if(c({type:"http_resp",id:e.id,status:i.status,headers:I,hasBody:a.length>0}),a.length>0){for(let l=0;l<a.length;l+=65536){if(!s||s.readyState!==C.OPEN)return;s.send(J($,e.id,a.subarray(l,l+65536)))}c({type:"http_body_end",id:e.id})}}function R(e){const t=e.session;let n=!1;try{n=o.listSessions().some(a=>a.name===t)}catch{n=!1}if(!n){c({type:"attach_err",connId:e.connId,session:t,message:"no such session"});return}let i=null;const I=D(t,{send:a=>c({type:"ws_to_hub",connId:e.connId,msg:a}),onMessage:a=>{i=a},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,initialCols:e.cols,initialRows:e.rows,tmuxSocketPath:o.tmuxSocketPath,onPtyExit:(a,r)=>{c({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${r})`})},onSpawnError:(a,r)=>{f(`attach failed for session "${e.session}": ${r}`),c({type:"attach_err",connId:e.connId,session:t,message:r})},onAttached:a=>{c({type:"attach_ok",connId:e.connId,session:t})}});d.set(e.connId,{onMessage:a=>i?.(a),dispose:()=>{d.delete(e.connId),I.dispose()}})}function L(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":b=t.agentId,g=1e3,T(`connected to hub as "${b}"`),u("connected"),k(),v();break;case"hello_err":if(f(`hub rejected us: ${t.reason}`),u("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":k();break;case"http_req":O(t);break;case"http_body_end":{w.add(t.id);const n=y.get(t.id);n&&(y.delete(t.id),n());break}case"attach":R(t);break;case"ws_to_agent":{const n=d.get(t.connId);if(n)try{n.onMessage(JSON.stringify(t.msg))}catch{}break}case"detach":{const n=d.get(t.connId);n&&n.dispose();break}case"pong":break;default:break}}function q(e){const t=j(e);if(t&&t.kind===$){const n=h.get(t.id)??[];n.push(t.payload),h.set(t.id,n)}}function P(){H();for(const e of d.values())try{e.dispose()}catch{}d.clear(),h.clear(),y.clear(),w.clear(),E=""}function M(){if(s){try{s.removeAllListeners()}catch{}s=null}if(P(),m)return;u("connecting");const e=g+Math.random()*500;T(`reconnecting in ${Math.round(e)}ms\u2026`),B=setTimeout(N,e),g=Math.min(g*2,6e4)}function N(){if(m||s)return;const e=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let t;try{t=new C(e)}catch(n){f(`bad hub URL: ${n.message} (${e})`),u("error",`bad hub URL: ${n.message}`);return}s=t,u("connecting"),t.on("open",()=>{T(`connecting to ${e}\u2026`),c({type:"hello",token:o.token,name:o.name,version:o.version})}),t.on("message",(n,i)=>{i?q(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):L(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),t.on("close",()=>{s===t&&M()}),t.on("error",n=>{f(`connection error: ${n.message}`),s===t&&M()})}function U(){if(!m){if(m=!0,B&&clearTimeout(B),P(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}b=null,u("idle")}}return N(),{stop:U,status:()=>({...S})}}export{Q as startAgentClient};
1
+ import{WebSocket as B}from"ws";import{attachTerminal as W}from"./attach-terminal.js";import{encodeBinaryFrame as F,decodeBinaryFrame as G,BIN_KIND_HTTP_BODY as O}from"./agent-channel.js";import{ActivityProbe as X}from"./activity-probe.js";const Y=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),z=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),Q=new Set(["content-type","content-disposition","location","cache-control","etag"]);function se(o){const P=o.log??(e=>console.log(`[agent] ${e}`)),f=e=>console.error(`[agent] ${e}`);let s=null,w=1e3,v=null,b=!1;const u=new Map,h=new Map,y=new Map,I=new Set;function H(e){return I.has(e)?Promise.resolve():new Promise(t=>{y.set(e,t),setTimeout(()=>{y.delete(e)&&t()},3e4)})}let p=null,_=null,S=null,g=null,M=new Map,k="",T=null;const m={state:"idle",agentId:null,hub:o.hub,name:o.name};function d(e,t){t!==void 0&&(m.lastError=t),m.state=e,m.agentId=T,o.onStatus?.({...m})}function i(e){s&&s.readyState===B.OPEN&&s.send(JSON.stringify(e))}function C(){let e;try{e=o.listSessions()}catch{return}const t=JSON.stringify(e);t!==k&&(k=t,i({type:"sessions",sessions:e}))}function R(){p||(p=setInterval(C,z)),_||(_=setInterval(()=>i({type:"ping",ts:Date.now()}),Y)),S||(g=new X,S=setInterval(()=>void q(),g.scanMs))}function L(){p&&(clearInterval(p),p=null),_&&(clearInterval(_),_=null),S&&(clearInterval(S),S=null),g=null}async function q(){if(!(!s||s.readyState!==B.OPEN||!g))try{const e=await g.scan(),t=[];for(const[n,c]of e)M.get(n)!==c&&t.push({session:n,state:c});M=e,t.length&&i({type:"activity",activities:t})}catch{}}async function U(e){let t;if(e.hasBody){await H(e.id),I.delete(e.id);const r=h.get(e.id);h.delete(e.id),t=r&&r.length?Buffer.concat(r):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let c;try{const r={method:e.method,headers:e.headers};t&&t.length&&(r.body=t),c=await Promise.race([o.app.request(new Request(n,r)),new Promise((V,K)=>setTimeout(()=>K(new Error("app.request timed out")),3e4))])}catch(r){f(`tunnel request failed: ${e.method} ${e.path} ${r.message}`),i({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const E={};c.headers.forEach((r,l)=>{Q.has(l.toLowerCase())&&(E[l]=r)});const a=Buffer.from(await c.arrayBuffer());if(i({type:"http_resp",id:e.id,status:c.status,headers:E,hasBody:a.length>0}),a.length>0){for(let l=0;l<a.length;l+=65536){if(!s||s.readyState!==B.OPEN)return;s.send(F(O,e.id,a.subarray(l,l+65536)))}i({type:"http_body_end",id:e.id})}}function x(e){const t=e.session;let n=!1;try{n=o.listSessions().some(a=>a.name===t)}catch{n=!1}if(!n){i({type:"attach_err",connId:e.connId,session:t,message:"no such session"});return}let c=null;const E=W(t,{send:a=>i({type:"ws_to_hub",connId:e.connId,msg:a}),onMessage:a=>{c=a},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,initialCols:e.cols,initialRows:e.rows,tmuxSocketPath:o.tmuxSocketPath,onPtyExit:(a,r)=>{i({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${r})`})},onSpawnError:(a,r)=>{f(`attach failed for session "${e.session}": ${r}`),i({type:"attach_err",connId:e.connId,session:t,message:r})},onAttached:a=>{i({type:"attach_ok",connId:e.connId,session:t})}});u.set(e.connId,{onMessage:a=>c?.(a),dispose:()=>{u.delete(e.connId),E.dispose()}})}function D(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":T=t.agentId,w=1e3,P(`connected to hub as "${T}"`),d("connected"),C(),R();break;case"hello_err":if(f(`hub rejected us: ${t.reason}`),d("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":C();break;case"http_req":U(t);break;case"http_body_end":{I.add(t.id);const n=y.get(t.id);n&&(y.delete(t.id),n());break}case"attach":x(t);break;case"ws_to_agent":{const n=u.get(t.connId);if(n)try{n.onMessage(JSON.stringify(t.msg))}catch{}break}case"detach":{const n=u.get(t.connId);n&&n.dispose();break}case"pong":break;default:break}}function J(e){const t=G(e);if(t&&t.kind===O){const n=h.get(t.id)??[];n.push(t.payload),h.set(t.id,n)}}function A(){L();for(const e of u.values())try{e.dispose()}catch{}u.clear(),h.clear(),y.clear(),I.clear(),k=""}function N(){if(s){try{s.removeAllListeners()}catch{}s=null}if(A(),b)return;d("connecting");const e=w+Math.random()*500;P(`reconnecting in ${Math.round(e)}ms\u2026`),v=setTimeout($,e),w=Math.min(w*2,6e4)}function $(){if(b||s)return;const e=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let t;try{t=new B(e)}catch(n){f(`bad hub URL: ${n.message} (${e})`),d("error",`bad hub URL: ${n.message}`);return}s=t,d("connecting"),t.on("open",()=>{P(`connecting to ${e}\u2026`),i({type:"hello",token:o.token,name:o.name,version:o.version})}),t.on("message",(n,c)=>{c?J(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):D(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),t.on("close",()=>{s===t&&N()}),t.on("error",n=>{f(`connection error: ${n.message}`),s===t&&N()})}function j(){if(!b){if(b=!0,v&&clearTimeout(v),A(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}T=null,d("idle")}}return $(),{stop:j,status:()=>({...m})}}export{se as startAgentClient};
@@ -1,3 +1,3 @@
1
- import{WebSocket as r}from"ws";import{decodeBinaryFrame as f,encodeBinaryFrame as g,BIN_KIND_TERM as u,BIN_KIND_HTTP_BODY as h}from"./agent-channel.js";const b=parseInt(process.env.TMUX_WEB_MAX_AGENT_CONNS||"64",10),_=parseInt(process.env.TMUX_WEB_TUNNEL_TIMEOUT_MS||"30000",10);let E=1,w=1;class O{ws;agentId;pending=new Map;relays=new Map;onSessions;onClose;disposed=!1;constructor(t,e,s){this.ws=t,this.agentId=e,this.onSessions=s.onSessions,this.onClose=s.onClose}send(t){return this.disposed||this.ws.readyState!==r.OPEN?!1:(this.ws.send(JSON.stringify(t)),!0)}attach(t,e,s){return this.send({type:"attach",connId:t,session:e,cols:s?.cols,rows:s?.rows})}detach(t){this.send({type:"detach",connId:t})}sendToAgent(t,e){this.send({type:"ws_to_agent",connId:t,msg:e})}sendSessionsReq(){this.send({type:"sessions_req"})}async httpRequest(t,e,s,n){const a=w++,o=!!n,c=new Promise((d,i)=>{const l=setTimeout(()=>{this.pending.delete(a),i(new Error("tunnel request timed out"))},_);this.pending.set(a,{id:a,resolve:d,reject:i,status:null,headers:null,body:[],hasBody:o,timer:l})});if(!this.send({type:"http_req",id:a,method:t,path:e,headers:s,hasBody:o}))return this.pending.delete(a),Promise.reject(new Error("agent offline"));if(n){const d=n.getReader();(async()=>{for(;;){const{done:l,value:p}=await d.read();if(l)break;const y=Buffer.from(p);this.ws.send(g(h,a,y))}this.send({type:"http_body_end",id:a})})().catch(()=>{this.send({type:"http_body_end",id:a})})}return c}registerRelay(t,e,s){this.relays.set(t,{connId:t,session:e,ws:s,closed:!1})}relayClosed(t){const e=this.relays.get(t);e&&!e.closed&&(e.closed=!0,this.relays.delete(t),this.detach(t))}get relayCount(){return this.relays.size}handleData(t){const e=Buffer.isBuffer(t)?t:Buffer.from(t),s=f(e);if(s){if(s.kind===u){const n=this.relays.get(s.id);n&&!n.closed&&n.ws.readyState===r.OPEN&&n.ws.send(JSON.stringify({type:"data",data:s.payload.toString("utf-8")}));return}if(s.kind===h){const n=this.pending.get(s.id);n&&n.body.push(s.payload)}}}handleMessage(t){let e;try{e=JSON.parse(t)}catch{return}switch(e.type){case"sessions":this.onSessions?.(e.sessions);break;case"http_resp":{const s=this.pending.get(e.id);if(!s)break;s.status=e.status,s.headers=e.headers,e.hasBody||this.finalizeHttp(s.id);break}case"http_body_end":this.finalizeHttp(e.id);break;case"attach_ok":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify({type:"auth.ok",setupMode:!1}));break}case"attach_err":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify({type:"attach_failed",message:`Agent "${this.agentId}": cannot attach to session "${e.session}": ${e.message}`}));break}case"ws_to_hub":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify(e.msg));break}case"ws_close":{const s=this.relays.get(e.connId);if(s&&!s.closed){s.closed=!0;try{s.ws.close(e.code??1e3,e.reason??"agent closed")}catch{}this.relays.delete(e.connId)}break}case"ping":this.send({type:"pong",ts:e.ts});break;default:break}}finalizeHttp(t){const e=this.pending.get(t);if(!e)return;this.pending.delete(t),clearTimeout(e.timer);const s=e.status??500,n=e.headers??{},a=Buffer.concat(e.body),o={};for(const[c,d]of Object.entries(n)){const i=c.toLowerCase();(i==="content-type"||i==="content-disposition"||i==="cache-control"||i==="etag"||i==="location")&&(o[c]=d)}e.resolve(new Response(a,{status:s,headers:o}))}failAll(t){for(const e of this.pending.values())clearTimeout(e.timer),e.reject(new Error(t));this.pending.clear();for(const e of this.relays.values()){if(!e.closed&&e.ws.readyState===r.OPEN){e.ws.send(JSON.stringify({type:"data",data:`\r
2
- \x1B[31m--- agent disconnected (${t}) ---\x1B[0m\r
1
+ import{WebSocket as r}from"ws";import{decodeBinaryFrame as f,encodeBinaryFrame as g,BIN_KIND_TERM as u,BIN_KIND_HTTP_BODY as h}from"./agent-channel.js";const b=parseInt(process.env.TMUX_WEB_MAX_AGENT_CONNS||"64",10),_=parseInt(process.env.TMUX_WEB_TUNNEL_TIMEOUT_MS||"30000",10);let E=1,w=1;class O{ws;agentId;pending=new Map;relays=new Map;onSessions;onActivity;onClose;disposed=!1;constructor(s,e,t){this.ws=s,this.agentId=e,this.onSessions=t.onSessions,this.onActivity=t.onActivity,this.onClose=t.onClose}send(s){return this.disposed||this.ws.readyState!==r.OPEN?!1:(this.ws.send(JSON.stringify(s)),!0)}attach(s,e,t){return this.send({type:"attach",connId:s,session:e,cols:t?.cols,rows:t?.rows})}detach(s){this.send({type:"detach",connId:s})}sendToAgent(s,e){this.send({type:"ws_to_agent",connId:s,msg:e})}sendSessionsReq(){this.send({type:"sessions_req"})}async httpRequest(s,e,t,n){const i=w++,o=!!n,c=new Promise((d,a)=>{const l=setTimeout(()=>{this.pending.delete(i),a(new Error("tunnel request timed out"))},_);this.pending.set(i,{id:i,resolve:d,reject:a,status:null,headers:null,body:[],hasBody:o,timer:l})});if(!this.send({type:"http_req",id:i,method:s,path:e,headers:t,hasBody:o}))return this.pending.delete(i),Promise.reject(new Error("agent offline"));if(n){const d=n.getReader();(async()=>{for(;;){const{done:l,value:p}=await d.read();if(l)break;const y=Buffer.from(p);this.ws.send(g(h,i,y))}this.send({type:"http_body_end",id:i})})().catch(()=>{this.send({type:"http_body_end",id:i})})}return c}registerRelay(s,e,t){this.relays.set(s,{connId:s,session:e,ws:t,closed:!1})}relayClosed(s){const e=this.relays.get(s);e&&!e.closed&&(e.closed=!0,this.relays.delete(s),this.detach(s))}get relayCount(){return this.relays.size}handleData(s){const e=Buffer.isBuffer(s)?s:Buffer.from(s),t=f(e);if(t){if(t.kind===u){const n=this.relays.get(t.id);n&&!n.closed&&n.ws.readyState===r.OPEN&&n.ws.send(JSON.stringify({type:"data",data:t.payload.toString("utf-8")}));return}if(t.kind===h){const n=this.pending.get(t.id);n&&n.body.push(t.payload)}}}handleMessage(s){let e;try{e=JSON.parse(s)}catch{return}switch(e.type){case"sessions":this.onSessions?.(e.sessions);break;case"activity":this.onActivity?.(e.activities);break;case"http_resp":{const t=this.pending.get(e.id);if(!t)break;t.status=e.status,t.headers=e.headers,e.hasBody||this.finalizeHttp(t.id);break}case"http_body_end":this.finalizeHttp(e.id);break;case"attach_ok":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify({type:"auth.ok",setupMode:!1}));break}case"attach_err":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify({type:"attach_failed",message:`Agent "${this.agentId}": cannot attach to session "${e.session}": ${e.message}`}));break}case"ws_to_hub":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify(e.msg));break}case"ws_close":{const t=this.relays.get(e.connId);if(t&&!t.closed){t.closed=!0;try{t.ws.close(e.code??1e3,e.reason??"agent closed")}catch{}this.relays.delete(e.connId)}break}case"ping":this.send({type:"pong",ts:e.ts});break;default:break}}finalizeHttp(s){const e=this.pending.get(s);if(!e)return;this.pending.delete(s),clearTimeout(e.timer);const t=e.status??500,n=e.headers??{},i=Buffer.concat(e.body),o={};for(const[c,d]of Object.entries(n)){const a=c.toLowerCase();(a==="content-type"||a==="content-disposition"||a==="cache-control"||a==="etag"||a==="location")&&(o[c]=d)}e.resolve(new Response(i,{status:t,headers:o}))}failAll(s){for(const e of this.pending.values())clearTimeout(e.timer),e.reject(new Error(s));this.pending.clear();for(const e of this.relays.values()){if(!e.closed&&e.ws.readyState===r.OPEN){e.ws.send(JSON.stringify({type:"data",data:`\r
2
+ \x1B[31m--- agent disconnected (${s}) ---\x1B[0m\r
3
3
  `}));try{e.ws.close(1011,"agent disconnected")}catch{}}e.closed=!0}this.relays.clear()}dispose(){this.disposed||(this.disposed=!0,this.failAll("agent disconnected"),this.onClose?.(this))}}export{O as AgentChannel,b as MAX_RELAY_CONNS_PER_AGENT,_ as TUNNEL_REQUEST_TIMEOUT_MS};
@@ -138,6 +138,8 @@ import{cssVarsStyle as d}from"../theme.js";import{newSessionModalCSS as c,newSes
138
138
  .session-group-body.collapsed { display: none; }
139
139
  .session-group-body .session-item { padding-left: 30px; }
140
140
  .session-item.session-offline { opacity: 0.5; cursor: default; }
141
+ .session-item.working svg { color: #f5a623; }
142
+ .session-item.just-done svg { color: #3fb950; }
141
143
  .session-item .meta { margin-left: auto; }
142
144
  .new-session-sidebar-btn {
143
145
  display: flex; align-items: center; gap: 8px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {