tmux-weblink 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
var
|
|
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,
|
|
3
|
-
`)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}};function
|
|
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(()=>{
|
|
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 Qe(){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&&Qe()},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 et(i){return typeof i=="string"&&i.startsWith("image/")}function tt(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"&&et(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 rt=await B.json();rt.error&&(ae=rt.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 nt(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 st(){J(!ee)}function Yt(i){N||i.button!==0||te||(ee||i.shiftKey)&&(i.preventDefault(),i.stopPropagation(),te=!0,w=nt(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=nt(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)):st()}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(et(f)){let b=await d.getType(f);await ze(b);return}}}catch{}try{let a=await navigator.clipboard?.readText();a&&s.pasteText(it(a))}catch{}})();return}}}let xe=0;function it(i){return i.split(`\r
|
|
6
6
|
`).join(`
|
|
7
7
|
`).split(`
|
|
8
|
-
`).join("\r")}async function
|
|
9
|
-
<span>${
|
|
10
|
-
<button class="session-edit-btn" data-session="${
|
|
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(),
|
|
12
|
-
<span>${
|
|
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(it(d)))}let ie=0;function Kt(i){i.preventDefault(),ie++,tt(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=tt(i.dataTransfer);a&&await ze(a)}function at(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",at,!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 ot(){M()}window.addEventListener("resize",ot),window.visualViewport?.addEventListener("resize",M),window.visualViewport?.addEventListener("scroll",M);let lt=new ResizeObserver(()=>M());lt.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}),Qe(),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",ot),lt.disconnect(),document.removeEventListener("keydown",at,!0)},focus(){s.focus()},sendInput(i){be(i)},toggleCopyMode(){st()}}})()}var le=window.__TMUX_WEB_SHELL__,v=null,y=null,ct=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null,k=ct;window.__tmuxWebScopeAgent=ct;var Ye="sessions",P="",Me="",Te="",dt="";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"),ut=document.getElementById("fe-new-name"),fn=document.getElementById("fe-new-btn"),qe=document.getElementById("settings-backdrop"),pt=document.getElementById("settings-popover"),ft=document.querySelector(".sidebar");function mn(){return window.matchMedia("(max-width: 640px)").matches}function Oe(){mn()&&ft.classList.add("collapsed")}function mt(){ft.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{mt(),ht("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{mt(),ht("files")});function ht(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")}),Ge()}var vt="tmux-web-sidebar-group-collapsed";function gt(){try{return new Set(JSON.parse(localStorage.getItem(vt)||"[]"))}catch{return new Set}}function hn(e){try{localStorage.setItem(vt,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=gt();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">📁</span><span class="sg-label">${h(l)}</span>`,p.addEventListener("click",()=>{let x=gt(),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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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"}),Tt()}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,yt=!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){yt=!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{yt=!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
|
-
${
|
|
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(),kt())});var ue=document.getElementById("header-git"),wt=document.getElementById("git-diff-view"),bt=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(),kt())});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 Lt(){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 Et="";function kt(){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">${
|
|
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>',
|
|
19
|
+
</div>`}t+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',Et=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",()=>Ct(o.path)))})}Ve(),E.classList.add("open"),De.classList.add("open"),Lt()}function xn(){return window.innerWidth>=1024}function xt(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">${
|
|
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 Ct(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",wt.style.display="flex",bt.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">${
|
|
23
|
+
<span class="diff-filename">${h(e)}</span>
|
|
24
24
|
<span class="diff-loading">Loading\u2026</span>
|
|
25
|
-
</div></div>`,
|
|
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">${
|
|
28
|
-
</div><div class="diff-empty">Error loading diff: ${
|
|
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">${
|
|
31
|
-
</div><div class="diff-empty">No diff content available</div></div>`;return}let{html:s,lineCount:o}=
|
|
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}=xt(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">${
|
|
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=
|
|
35
|
+
</div><div class="diff-content">${s}</div></div>`,E.querySelector(".diff-back")?.addEventListener("click",()=>{E.innerHTML=Et,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",()=>Ct(l.path)))}),Lt()})}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}=xt(t,n);bt.textContent=e,Ie.textContent=o+" lines",Be.innerHTML=s}function Tn(){wt.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"),Tt()):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 Mt(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 Tt(){$e(),y&&(Mt(y),Re=setInterval(()=>{y&&Mt(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,dt=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}dt=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=ut.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}ut.value="",P&&_(P)}catch{D.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let e=pt.classList.toggle("open");qe.classList.toggle("open",e)}),qe.addEventListener("click",()=>{pt.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
|
|
45
|
-
`).length;A.rows=Math.min(e,4)}),A.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),
|
|
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"),St=document.getElementById("mk-send");function It(){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(),It())})),St&&St.addEventListener("click",It),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+'">×</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&&Bt(c,s,_e.get(s))}function Bt(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 Ge(){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&&Bt(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(),Ge(),Dt(),document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&Ge()}),setInterval(()=>{document.visibilityState==="visible"&&X()},1e4);
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import{initTerminal as
|
|
1
|
+
import{initTerminal as $e}from"./terminal-core.js";const H=window.__TMUX_WEB_SHELL__;let f=null,m=null;const de=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null;let g=de;window.__tmuxWebScopeAgent=de;let ee="sessions",S="",F="",O="",pe="";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"),w=document.getElementById("fe-status"),$=document.getElementById("fe-content"),x=document.getElementById("fe-save"),_e=document.getElementById("fe-delete"),Ae=document.getElementById("fe-back"),fe=document.getElementById("fe-new-name"),De=document.getElementById("fe-new-btn"),te=document.getElementById("settings-backdrop"),ue=document.getElementById("settings-popover"),me=document.querySelector(".sidebar");function Pe(){return window.matchMedia("(max-width: 640px)").matches}function ne(){Pe()&&me.classList.add("collapsed")}function ve(){me.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{ve(),ge("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{ve(),ge("files")});function ge(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")}),ce()}const ye="tmux-web-sidebar-group-collapsed";function he(){try{return new Set(JSON.parse(localStorage.getItem(ye)||"[]"))}catch{return new Set}}function je(e){try{localStorage.setItem(ye,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=he();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">📁</span><span class="sg-label">${d(c)}</span>`,p.addEventListener("click",()=>{const b=he(),re=p.nextElementSibling;b.has(a)?(b.delete(a),p.classList.remove("collapsed"),re?.classList.remove("collapsed")):(b.add(a),p.classList.add("collapsed"),re?.classList.add("collapsed")),je(b)});const L=document.createElement("div");L.className="session-group-body"+(l?" collapsed":"");for(const b of o)L.appendChild(Oe(b));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(),
|
|
5
|
-
<span>${d(c.name)}</span><span class="meta">${i.online?"":"offline"}</span>`,i.online&&
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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"}),Be()}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,be=!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){be=!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{be=!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>"}
|
|
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,
|
|
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(),Se())});const _=document.getElementById("header-git"),we=document.getElementById("git-diff-view"),Le=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(),Se())});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 Ee(){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"),C(e)}})}let ke="";function Se(){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">${
|
|
12
|
-
</div>`}n+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',
|
|
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>',ke=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",()=>Ie(i.path)))})}ie(),v.classList.add("open"),q.classList.add("open"),Ee()}function Je(){return window.innerWidth>=1024}function Ce(e,n){const s=(e+(n&&e?`
|
|
13
13
|
`:"")+n).split(`
|
|
14
|
-
`);let i="",
|
|
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 Ie(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",Le.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>`,
|
|
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
|
|
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}=
|
|
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=
|
|
28
|
+
</div><div class="diff-content">${s}</div></div>`,v.querySelector(".diff-back")?.addEventListener("click",()=>{v.innerHTML=ke,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",()=>Ie(c.path)))}),Ee()})}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);Le.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"),Be()):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 xe(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 Be(){X(),m&&(xe(m),K=setInterval(()=>{m&&xe(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",()=>C(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?C(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 C(e){S=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",()=>C(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",()=>C(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 b=document.createElement("div");b.className="file-tree-item file"+(p?" "+p:""),b.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${L}${d(c)}`,b.addEventListener("click",()=>tt(o)),r.appendChild(b)}}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,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",$.value="";return}const t=await n.json();$.value=t.content,O=t.content,pe=t.content,w.textContent=t.size+" bytes",x.style.display=""}catch{w.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();w.textContent=n.error||"Save failed";return}pe=O,w.textContent="Saved"}catch{w.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();w.textContent=n.error||"Delete failed";return}R.style.display="none",I.style.display="flex",S&&C(S)}catch{w.textContent="Delete failed"}}),Ae.addEventListener("click",()=>{R.style.display="none",I.style.display="flex",S&&C(S)}),De.addEventListener("click",async()=>{const e=fe.value.trim();if(!e)return;const n=S+"/"+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}fe.value="",S&&C(S)}catch{w.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=ue.classList.toggle("open");te.classList.toggle("open",e)}),te.addEventListener("click",()=>{ue.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
|
|
38
|
-
`).length;E.rows=Math.min(e,4)}),E.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),
|
|
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"),Me=document.getElementById("mk-send");function Te(){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(),Te())})),Me&&Me.addEventListener("click",Te),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+'">×</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&&He(o,s,Q.get(s))}function He(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 ce(){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&&He(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(),ce(),Re(),document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&ce()}),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
|
|
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()}
|
|
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
|
-
`})),
|
|
7
|
-
Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${
|
|
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};
|
package/dist/lib/agent-client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocket as
|
|
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};
|
package/dist/lib/agent-relay.js
CHANGED
|
@@ -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(
|
|
2
|
-
\x1B[31m--- agent disconnected (${
|
|
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};
|
package/dist/lib/pages/shell.js
CHANGED
|
@@ -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;
|