tmux-weblink 2.2.1 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/shell-client.js +24 -21
- package/dist/browser/shell-client.js +26 -23
- package/dist/index.js +3 -3
- package/dist/lib/agent-channel.js +1 -1
- package/dist/lib/agent-client.js +1 -1
- package/dist/lib/agent-relay.js +3 -3
- package/dist/lib/build-app.js +6 -6
- package/dist/lib/db.js +1 -1
- package/dist/lib/pages/shell.js +4 -0
- package/dist/lib/session-state.js +1 -0
- package/dist/lib/tmux-windows.js +1 -1
- package/package.json +1 -1
|
@@ -1,38 +1,40 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)}selectRows(t,n){let s=this.terminal.buffer.active,[o
|
|
3
|
-
`)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}};function
|
|
4
|
-
\x1B[31m`+
|
|
5
|
-
`).then(()=>{
|
|
1
|
+
var rn=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function Ye(e){return e.replace(/\x1b\[\?1049[hl]/g,"")}function re(e){return e.replace(rn,"")}var Se;function cn(){return Se||(document.querySelector("link[data-tmux-web-xterm-css]")?Se=Promise.resolve():(Se=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)}),Se))}var dn=class Rt{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:r},{FitAddon:o}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),cn()]),c=new r({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,smoothScrollDuration:120,theme:s});return new Rt(t,c,new o)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(t,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=un(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,r=n.height,o=this.terminal.element?.querySelector(".xterm-viewport"),c=o?Math.max(0,o.offsetWidth-o.clientWidth):0,l=Math.max(2,Math.floor((t.width-c)/s)),u=Math.max(2,Math.floor(t.height/r));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,[r,o]=t<=n?[t,n]:[n,t],c=s.viewportY,l=[];for(let u=r;u<=o;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,[r,o]=t<=n?[t,n]:[n,t];this.terminal.selectLines(s.viewportY+r,s.viewportY+o)}selectCells(t,n,s,r){let o=this.terminal.buffer.active,c=Math.min(n,r),l=Math.max(n,r);if(c===l){let u=Math.min(t,s),p=Math.max(t,s);this.terminal.select(u,o.viewportY+c,p-u+1)}else this.terminal.selectLines(o.viewportY+c,o.viewportY+l)}getCellsText(t,n,s,r){let o=this.terminal.buffer.active,c=Math.min(n,r),l=Math.max(n,r),u=n<r?t:n>r?s:Math.min(t,s),p=n<r?s:n>r?t:Math.max(t,s),w=o.viewportY,S=[];for(let A=c;A<=l;A++){let ye=o.getLine(w+A);if(!ye){S.push("");continue}let J=A===c?u:0,_e=A===l?p:this.terminal.cols-1,X="";for(let T=J;T<=_e;T++){let W=ye.getCell(T);W&&(X+=W.getChars())}S.push(X.replace(/\s+$/,""))}return S.join(`
|
|
3
|
+
`)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}};function un(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 pn(e,t,n){return e.innerHTML="",e.classList.add("terminal-pending"),(async()=>{let s=await dn.create(e,n.scrollback,n.theme),r,o=0,c,l=null,u=0,p=0,w=.2,S=10,A=.95,ye=.04;function J(){u&&(cancelAnimationFrame(u),u=0),p=0}function _e(a){if(J(),Math.abs(a)<w)return;p=Math.max(-S,Math.min(S,a));let i=()=>{if(P){u=0;return}let d=e.querySelector(".xterm-rows > div"),f=d?d.getBoundingClientRect().height:16,L=f>0?Math.round(p*16/f):p>0?1:-1;L===0&&(L=p>0?1:-1);let D=s.viewportY()-L,Y=s.baseY(),le=Math.max(0,Math.min(D,Y));if(s.scrollToLine(le,!0),p*=A,le!==D){u=0;return}if(Math.abs(p)<ye){u=0;return}u=requestAnimationFrame(i)};u=requestAnimationFrame(i)}let X=0,T="connecting",W=0,ee=!1,we=[],G="",P=!1,te=1e3,Nt=location.protocol==="https:"?"wss:":"ws:",$t=n.wsBase||"/ws",Pt=Nt+"//"+location.host+$t+"/"+encodeURIComponent(t),zt="/api/session/"+encodeURIComponent(t)+"/upload",jt=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function _t(){return we.join("")+G}async function Wt(a,i=0){let d=a?s.viewportY():0,f=re(_t());if(s.reset(),!f){a||s.scrollToBottom();return}await s.write(f),a?s.scrollToLine(i+d):s.scrollToBottom()}function O(a){r?.readyState===WebSocket.OPEN&&r.send(JSON.stringify(a))}function Ot(a){let i;try{i=JSON.parse(a)}catch{T==="live"&&(G+=a,s.write(re(a)));return}if(i.type==="auth.required"){let d=localStorage.getItem("tmux-web-token");d?O({type:"auth.token",token:d}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(i.type==="attach_failed"&&typeof i.message=="string"){be=!0,T="dead",s.reset(),s.write(`\r
|
|
4
|
+
\x1B[31m`+re(i.message)+`\x1B[0m\r
|
|
5
|
+
`).then(()=>{Ee();try{r?.close()}catch{}});return}if(i.type==="auth.ok"){i.token&&localStorage.setItem("tmux-web-token",i.token),te=1e3,U(),O({type:"resize",cols:s.cols,rows:s.rows}),M();return}if(i.type==="auth.failed"){if(i.permanentLock||i.retryAfterMs&&i.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(i.message);return}return}if(i.type==="snapshot"&&typeof i.data=="string"){let d=Ye(re(i.data));we=[d],G="",W=typeof i.lines=="number"?i.lines:n.terminal.initialLines,T="live",s.reset(),U(),O({type:"resize",cols:s.cols,rows:s.rows}),s.write(d).then(()=>s.scrollToBottom());return}if(i.type==="window_changed"&&typeof i.activeIndex=="number"){let d=new URL(location.href);d.searchParams.get("window")!==String(i.activeIndex)&&(d.searchParams.set("window",String(i.activeIndex)),history.replaceState(history.state,"",d)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:i.windows}));return}if(i.type==="history"&&typeof i.data=="string"){i.lines>0&&i.data?(we.unshift(Ye(i.data)),W+=i.lines,Wt(!0,i.lines).finally(()=>{ee=!1})):ee=!1;return}if(i.type==="data"&&typeof i.data=="string"){let d=Ye(re(i.data));if(T==="connecting"){T="live",G=d,s.write(d);return}T==="live"&&(G+=d,s.write(d))}}let be=!1;function tt(){if(be)return;let a=Pt,i=localStorage.getItem("tmux-web-token");i&&(a+=(a.includes("?")?"&":"?")+"token="+encodeURIComponent(i)),a+=(a.includes("?")?"&":"?")+"cols="+s.cols+"&rows="+s.rows,r=new WebSocket(a),r.onopen=()=>{T="connecting",W=0,ee=!1,we=[],G="",s.reset(),te=1e3},r.onmessage=d=>{typeof d.data=="string"&&Ot(d.data)},r.onclose=()=>{P||(T="connecting",!be&&setTimeout(()=>{te=Math.min(te*2,1e4),!P&&!be&&tt()},te))},r.onerror=()=>r?.close()}function Le(a){O({type:"input",data:a})}function U(){return s.fit()}function Ee(){e.classList.remove("terminal-pending")}function M(){o&&cancelAnimationFrame(o),c&&clearTimeout(c),U(),o=requestAnimationFrame(()=>{o=0,U()&&Ee()}),c=setTimeout(()=>{U()&&Ee()},120)}function Ut(){M(),setTimeout(M,50),setTimeout(M,150),setTimeout(M,300)}function nt(a){return typeof a=="string"&&a.startsWith("image/")}function st(a){return a?.files?.length?a.files[0]??null:null}function Yt(a){if(!a?.items)return null;for(let i=0;i<a.items.length;i++){let d=a.items[i];if(d?.kind==="file"&&nt(d.type))return d.getAsFile()}return null}async function qt(a){let i=new FormData,d=a instanceof File&&a.name?a.name:"upload";i.append("file",a,d);let f=localStorage.getItem("tmux-web-token"),L={};f&&(L.Authorization="Bearer "+f);let D=await fetch(zt,{method:"POST",body:i,headers:L});if(!D.ok){let le="upload failed";try{let dt=await D.json();dt.error&&(le=dt.error)}catch{}throw new Error(le)}let Y=await D.json();if(!Y.path)throw new Error("no path in response");return Y.path}async function We(a){try{let i=await qt(a);Le(i)}catch{}}s.onData(a=>Le(a)),s.onResize(({cols:a,rows:i})=>O({type:"resize",cols:a,rows:i})),s.onScroll(()=>{T!=="live"||ee||!s.isNearScrollbackTop()||(ee=!0,O({type:"load_history",before:W}))});let ne=!1,se=!1,b=null,E=null,ae=document.createElement("div");ae.className="terminal-copy-banner",ae.innerHTML='<strong>Copy mode</strong><span class="hint">drag to copy \xB7 Esc exits</span>',ae.hidden=!0,e.appendChild(ae);let N=document.createElement("div");N.className="terminal-copy-banner terminal-copy-toast",N.textContent="Copied",N.hidden=!0,e.appendChild(N);function at(a,i){let d=e.querySelector(".xterm-screen, canvas"),f=(d&&d.getBoundingClientRect().width>0?d:e).getBoundingClientRect(),L=f.height>0&&s.rows>0?f.height/s.rows:18,D=f.width>0&&s.cols>0?f.width/s.cols:9;return{row:Math.max(0,Math.min(s.rows-1,Math.floor((i-f.top)/L))),col:Math.max(0,Math.min(s.cols-1,Math.floor((a-f.left)/D)))}}async function xe(a){if(!a)return;try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(a);return}}catch{}let i=document.createElement("textarea");i.value=a,i.style.position="fixed",i.style.opacity="0",i.style.pointerEvents="none",document.body.appendChild(i),i.focus(),i.select();try{document.execCommand("copy")}catch{}i.remove()}let ie;function ke(){N.hidden=!1,ie&&clearTimeout(ie),ie=setTimeout(()=>{N.hidden=!0},1e3)}function Z(a){ne=a,ae.hidden=!a,e.classList.toggle("terminal-copy-mode-active",a),a&&s.focus()}function it(){Z(!ne)}function Kt(a){P||a.button!==0||se||(ne||a.shiftKey)&&(a.preventDefault(),a.stopPropagation(),se=!0,b=at(a.clientX,a.clientY),E=b,s.selectCells(b.col,b.row,E.col,E.row),document.addEventListener("mousemove",Oe,!0),document.addEventListener("mouseup",Ue,!0))}function Oe(a){P||!se||!b||(a.preventDefault(),a.stopPropagation(),E=at(a.clientX,a.clientY),s.selectCells(b.col,b.row,E.col,E.row))}function Ue(a){if(P||!se||!b||!E)return;a.preventDefault(),a.stopPropagation(),document.removeEventListener("mousemove",Oe,!0),document.removeEventListener("mouseup",Ue,!0),se=!1;let i=s.getCellsText(b.col,b.row,E.col,E.row);b=null,E=null,s.clearSelection(),Z(!1),i&&(N.textContent="\u5DF2\u590D\u5236",xe(i).then(ke)),s.focus()}e.addEventListener("mousedown",Kt,!0),document.addEventListener("keydown",Vt,!0);function Vt(a){if(!(P||!s.isFocused()||a.defaultPrevented||a.isComposing)){if(ne){if(a.key==="Escape"){a.preventDefault(),a.stopPropagation(),b=null,E=null,s.clearSelection(),Z(!1);return}if(a.key==="Enter"||(a.ctrlKey||a.metaKey)&&a.code==="KeyC"){a.preventDefault(),a.stopPropagation(),xe(s.getScreenText()).then(ke),Z(!1);return}}if(a.altKey&&a.code==="KeyC"&&!a.ctrlKey&&!a.metaKey){if(a.preventDefault(),a.stopPropagation(),b&&E){let i=s.getCellsText(b.col,b.row,E.col,E.row);b=null,E=null,s.clearSelection(),Z(!1),i&&(N.textContent="Copied",xe(i).then(ke))}else{let i=s.getSelection();i?(s.clearSelection(),N.textContent="Copied",xe(i).then(ke)):it()}return}if((a.ctrlKey||a.metaKey)&&a.code==="KeyV"){a.preventDefault(),a.stopPropagation(),(async()=>{try{if(navigator.clipboard?.read){let i=await navigator.clipboard.read();for(let d of i)for(let f of d.types)if(nt(f)){let L=await d.getType(f);await We(L);return}}}catch{}try{let i=await navigator.clipboard?.readText();i&&s.pasteText(ot(i))}catch{}})();return}}}let Ce=0;function ot(a){return a.split(`\r
|
|
6
6
|
`).join(`
|
|
7
7
|
`).split(`
|
|
8
|
-
`).join("\r")}async function
|
|
8
|
+
`).join("\r")}async function Jt(a){let i=Yt(a.clipboardData);if(i){a.preventDefault(),a.stopPropagation();let L=Date.now();if(L-Ce<50)return;Ce=L,await We(i);return}let d=a.clipboardData?.getData("text/plain");if(!d)return;a.preventDefault(),a.stopPropagation();let f=Date.now();f-Ce<50||(Ce=f,s.pasteText(ot(d)))}let oe=0;function Xt(a){a.preventDefault(),oe++,st(a.dataTransfer)&&e.classList.add("terminal-drag-over")}function Gt(a){a.preventDefault(),a.dataTransfer&&(a.dataTransfer.dropEffect="copy")}function Zt(a){a.preventDefault(),oe--,oe<=0&&(oe=0,e.classList.remove("terminal-drag-over"))}async function Qt(a){a.preventDefault(),oe=0,e.classList.remove("terminal-drag-over");let i=st(a.dataTransfer);i&&await We(i)}function lt(a){!jt||a.key!=="Escape"||ne||a.defaultPrevented||a.repeat||a.metaKey||a.ctrlKey||a.altKey||s.isFocused()&&(a.preventDefault(),a.stopPropagation(),Le("\x1B"))}e.addEventListener("paste",Jt),e.addEventListener("dragenter",Xt),e.addEventListener("dragover",Gt),e.addEventListener("dragleave",Zt),e.addEventListener("drop",Qt),document.addEventListener("keydown",lt,!0);function en(a){if(a.touches.length!==1){l=null;return}let i=a.touches[0];i&&(J(),l={startX:i.clientX,startY:i.clientY,lastX:i.clientX,lastY:i.clientY,lastT:performance.now(),velocity:0,scrolling:!1},a.stopPropagation())}function tn(a){if(!l||a.touches.length!==1)return;let i=a.touches[0];if(!i)return;let d=i.clientY-l.startY,f=i.clientX-l.startX;if(!l.scrolling){if(Math.abs(d)<8||Math.abs(d)<Math.abs(f))return;l.scrolling=!0,X=Date.now()+500}a.preventDefault(),a.stopPropagation();let L=performance.now(),D=i.clientY-l.lastY,Y=L-l.lastT;Y>0&&(l.velocity=D/Y),on(-D),l.lastX=i.clientX,l.lastY=i.clientY,l.lastT=L}function nn(a){if(!l)return;let i=l.scrolling,d=l.velocity;if(l=null,a.stopPropagation(),!i){s.focus();return}X=Date.now()+500,a.preventDefault(),d&&_e(d)}function sn(a){l=null,J(),a.stopPropagation()}function an(a){a.pointerType==="touch"&&Date.now()<X&&(a.preventDefault(),a.stopPropagation())}function on(a){let i=e.querySelector(".xterm-rows > div"),d=i?i.getBoundingClientRect().height:16,f=d>0?Math.round(a/d):a>0?1:-1;f===0&&(f=a>0?1:-1),s.scrollLines(f)}e.addEventListener("touchstart",en,{passive:!0,capture:!0}),e.addEventListener("touchmove",tn,{passive:!1,capture:!0}),e.addEventListener("touchend",nn,{passive:!1,capture:!0}),e.addEventListener("touchcancel",sn,{passive:!0,capture:!0}),e.addEventListener("pointerup",an,!0);function rt(){M()}window.addEventListener("resize",rt),window.visualViewport?.addEventListener("resize",M),window.visualViewport?.addEventListener("scroll",M);let ct=new ResizeObserver(()=>M());ct.observe(e),document.fonts?.ready.then(()=>{M(),setTimeout(M,50),setTimeout(M,200)}),e.addEventListener("focusin",Ut,!0);async function ln(){if(document.fonts?.ready)try{await Promise.race([document.fonts.ready,new Promise(a=>setTimeout(a,1e3))])}catch{}}return requestAnimationFrame(()=>{U()&&Ee(),(async()=>(await ln(),U()&&O({type:"resize",cols:s.cols,rows:s.rows}),tt(),M()))()}),{destroy(){if(P=!0,J(),Z(!1),ie&&clearTimeout(ie),document.removeEventListener("mousemove",Oe,!0),document.removeEventListener("mouseup",Ue,!0),r){try{r.close(1e3,"session switch")}catch{}r=void 0}e.innerHTML="",e.classList.remove("terminal-pending"),window.removeEventListener("resize",rt),ct.disconnect(),document.removeEventListener("keydown",lt,!0)},focus(){s.focus()},sendInput(a){Le(a)},toggleCopyMode(){it()}}})()}var ce=window.__TMUX_WEB_SHELL__,v=null,y=null,ut=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null,C=ut;window.__tmuxWebScopeAgent=ut;var qe="sessions",z="",Te="",Me="",pt="";function x(e){return C?"/a/"+encodeURIComponent(C)+e:e}var m=document.getElementById("sidebar-content"),q=document.getElementById("main-placeholder"),B=document.getElementById("terminal-container"),de=document.getElementById("file-editor"),fn=document.getElementById("fe-path"),H=document.getElementById("fe-status"),ue=document.getElementById("fe-content"),K=document.getElementById("fe-save"),mn=document.getElementById("fe-delete"),hn=document.getElementById("fe-back"),ft=document.getElementById("fe-new-name"),vn=document.getElementById("fe-new-btn"),Ke=document.getElementById("settings-backdrop"),mt=document.getElementById("settings-popover"),ht=document.querySelector(".sidebar");function gn(){return window.matchMedia("(max-width: 640px)").matches}function Ve(){gn()&&ht.classList.add("collapsed")}function vt(){ht.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{vt(),gt("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{vt(),gt("files")});function gt(e){qe=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"?(Je="",j()):Dn()}var Je="";async function j(){if(qe!=="sessions")return;try{let t=await(await fetch("/api/sidebar/sessions")).json(),n=JSON.stringify({recent:t.recent,agents:t.agents,tombstones:t.tombstones});if(n===Je)return;Je=n,En(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")}),et()}var yt="tmux-web-sidebar-group-collapsed";function wt(){try{return new Set(JSON.parse(localStorage.getItem(yt)||"[]"))}catch{return new Set}}function yn(e){try{localStorage.setItem(yt,JSON.stringify([...e]))}catch{}}function wn(e,t,n){if(!e)return"\u2026";let s=n&&(e===n||e.startsWith(n+"/"))?"~"+e.slice(n.length):e,r=s.split("/").filter(Boolean);if(r.length===0)return e;for(let o=1;o<=r.length;o++){let c=r.slice(-o).join("/");if(!t.some(l=>!l||l===e?!1:(n&&(l===n||l.startsWith(n+"/"))?"~"+l.slice(n.length):l).split("/").filter(Boolean).slice(-o).join("/")===c))return c}return s}function bn(e,t){let n=new Map;for(let o of e){let c=o.path||"";n.has(c)||n.set(c,[]),n.get(c).push(o)}let s=[...n.keys()],r=wt();for(let[o,c]of n){let l=wn(o,s,t),u=r.has(o),p=document.createElement("div");p.className="session-group-header"+(u?" collapsed":""),p.title=o||"No working directory",p.innerHTML=`<span class="sg-folder">📁</span><span class="sg-label">${h(l)}</span>`,p.addEventListener("click",()=>{let S=wt(),A=p.nextElementSibling;S.has(o)?(S.delete(o),p.classList.remove("collapsed"),A?.classList.remove("collapsed")):(S.add(o),p.classList.add("collapsed"),A?.classList.add("collapsed")),yn(S)});let w=document.createElement("div");w.className="session-group-body"+(u?" collapsed":"");for(let S of c)w.appendChild(Ln(S));m.appendChild(p),m.appendChild(w)}}function Ln(e){let t=document.createElement("div");t.className="session-item"+(e.name===y&&!C?" 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
9
|
<span>${h(e.name)}</span>
|
|
10
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")||
|
|
12
|
-
<span>${h(
|
|
11
|
+
`,t.addEventListener("click",s=>{s.target.closest(".session-edit-btn")||pe(e.name)});let n=t.querySelector(".session-edit-btn");return n&&n.addEventListener("click",s=>{s.stopPropagation(),It(e.name,n)}),t}function En(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"+(C?" clickable":""),n.textContent=C?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",C&&(n.title="Back to hub",n.addEventListener("click",()=>{window.location.href="/"})),m.appendChild(n),bn(t,e.home);let s=e.agents||[];for(let o of s){if(!o.sessions||o.sessions.length===0)continue;let c=o.agentId===C,l=document.createElement("div");l.className="sidebar-section-label machine-label"+(o.online?"":" agent-offline-label")+(!c&&o.online?" clickable":""),l.textContent=o.agentName+(o.online?"":" (offline)"),!c&&o.online&&(l.title="\u6253\u5F00 "+o.agentName,l.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(o.agentId)+"/"})),m.appendChild(l);for(let u of o.sessions){let p=document.createElement("div");p.className="session-item"+(u.name===y&&C===o.agentId?" active":"")+(o.online?"":" session-offline"),p.setAttribute("data-session",u.name),p.setAttribute("data-agent",o.agentId),p.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${u.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(u.name)}</span><span class="meta">${o.online?"":"offline"}</span>`,o.online&&p.addEventListener("click",w=>{w.target.closest(".session-edit-btn")||pe(u.name,o.agentId)}),m.appendChild(p)}}let r=e.tombstones||[];if(r.length>0){let o=document.createElement("div");o.className="sidebar-section-label tombstone-label",o.textContent="\u5931\u6548\u4F1A\u8BDD",m.appendChild(o);for(let c of r){let l=document.createElement("div");l.className="session-item session-tombstone",l.setAttribute("data-session",c.name),c.agentId&&l.setAttribute("data-agent",c.agentId);let u=c.agentId?c.agentOnline?" (agent)":" (agent \u79BB\u7EBF)":"";l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"></svg>
|
|
13
|
+
<span>${h(c.name)}</span><span class="meta">\u5931\u6548${u}</span>
|
|
14
|
+
<button class="session-edit-btn" data-session="${h(c.name)}" title="\u6062\u590D\u4F1A\u8BDD"><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>`;let p=l.querySelector(".session-edit-btn");p&&p.addEventListener("click",w=>{w.stopPropagation(),It(c.name,p,{tombstone:!0,agentId:c.agentId})}),m.appendChild(l)}}}function h(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function pe(e,t){if(!(y===e&&v&&C===(t||null))){de.style.display="none",q.style.display="none",B.style.display="",B.classList.add("terminal-pending"),Ve(),v&&(Ne(),v.destroy(),v=null),y=e,C=t||null,window.__tmuxWebScopeAgent=C,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)===C);try{v=await pn(B,e,{terminal:ce.terminal,scrollback:ce.scrollback,theme:ce.theme,wsBase:C?"/ws/a/"+encodeURIComponent(C):ce.wsBase||"/ws"}),Bt()}catch(n){console.error("[shell] terminal init failed:",n);let s=n&&typeof n=="object"&&"message"in n?n.message:String(n);B.textContent="Failed to open terminal for "+e+": "+s,B.classList.remove("terminal-pending")}}}var Be=[],g=null,bt=!1;function xn(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 kn(e){bt=!0;try{let t=await fetch(x("/api/git/status?path="+encodeURIComponent(e)));t.ok?g=await t.json():g=null}catch{g=null}finally{bt=!1}}function Cn(){if(!g||!g.branch)return"";let{branch:e,linesAdded:t,linesRemoved:n}=g,s=`<div class="sidebar-git"><div class="git-branch-badge">
|
|
13
15
|
<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
|
-
${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")&&
|
|
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],
|
|
16
|
+
${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")&&Q&&g&&(e.stopPropagation(),Ct())});var fe=document.getElementById("header-git"),Lt=document.getElementById("git-diff-view"),Et=document.getElementById("gd-path"),Ie=document.getElementById("gd-status"),De=document.getElementById("gd-content"),Sn=document.getElementById("gd-back"),Q=null;function me(e){if(!e||!e.branch){fe.style.display="none",Q=null;return}Q=e.repoRoot;let{branch:t,linesAdded:n,linesRemoved:s}=e,r=`<span class="branch">${h(t)}</span>`;(n>0||s>0)&&(r+=`<span class="sep">\xB7</span>+<span class="diff-add">${n}</span> <span class="diff-del">-${s}</span>`),fe.innerHTML=r,fe.style.display="block"}fe.addEventListener("click",e=>{Q&&g&&(e.stopPropagation(),Ct())});var k=document.getElementById("git-popover"),He=document.getElementById("git-popover-backdrop");function Xe(){let e=fe.getBoundingClientRect();k.style.top=e.bottom+4+"px",k.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function xt(){let e=Q,t=document.getElementById("git-popover-browse");t&&t.addEventListener("click",()=>{if(Fe(),e){qe="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));let n=document.getElementById("mode-files");n&&n.classList.add("active"),_(e)}})}var kt="";function Ct(){if(!g)return;let e=g.files||[];if(!e.length)k.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
|
|
17
|
+
<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],r=s.additions||0,o=s.deletions||0,c="";switch(s.status){case"M":c='<span class="git-file-status M">M</span>';break;case"A":c='<span class="git-file-status A">A</span>';break;case"D":c='<span class="git-file-status D">D</span>';break;case"?":c='<span class="git-file-status ?">?</span>';break;case"R":c='<span class="git-file-status R">R</span>';break}t+=`<div class="git-popover-item" data-idx="${n}">
|
|
16
18
|
${c}<span class="file-path">${h(s.path)}</span>
|
|
17
|
-
<span class="file-add">${
|
|
18
|
-
<span class="file-del">${
|
|
19
|
-
</div>`}t+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',
|
|
19
|
+
<span class="file-add">${r>0?"+"+r:""}</span>
|
|
20
|
+
<span class="file-del">${o>0?"-"+o:""}</span>
|
|
21
|
+
</div>`}t+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',kt=t,k.innerHTML=t,k.querySelectorAll(".git-popover-item").forEach(n=>{let s=parseInt(n.dataset.idx||"0",10),r=e[s];!r||r.status==="D"||(n.classList.add("clickable"),n.style.cursor="pointer",n.addEventListener("click",()=>Tt(r.path)))})}Xe(),k.classList.add("open"),He.classList.add("open"),xt()}function Tn(){return window.innerWidth>=1024}function St(e,t){let n=(e+(t&&e?`
|
|
20
22
|
`:"")+t).split(`
|
|
21
|
-
`),s="",
|
|
23
|
+
`),s="",r=0,o=0;for(let c of n){let l=c;if(l.startsWith("@@")){let u=l.match(/@@\s+-(\d+)[^+]*\+(\d+)/);u&&(r=parseInt(u[1],10)-1,o=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("+"))o++,s+=`<div class="diff-line add"><span class="ln-no">${o}</span><span class="ln-body">${h(l)}</span></div>`;else if(l.startsWith("-"))r++,s+=`<div class="diff-line del"><span class="ln-no">${r}</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&&!r&&!o)continue;r++,o++,s+=`<div class="diff-line"><span class="ln-no">${r}</span><span class="ln-no" style="margin-right:12px">${o}</span><span class="ln-body">${h(l)}</span></div>`}}return{html:s,lineCount:n.length}}var Ae=null;async function Tt(e){let t=Q;if(t)if(Fe(),Tn()){B.style.display="none",v&&(Ne(),v.destroy(),v=null,Ae=y,y=null),me(null),de.style.display="none",q.style.display="none",Lt.style.display="flex",Et.textContent=e,Ie.textContent="Loading\u2026",De.innerHTML="";try{let n=await fetch(x("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!n.ok)throw new Error(await n.text());let s=await n.json();Bn(e,s.diff,s.stagedDiff)}catch(n){let s=n instanceof Error?n.message:String(n);Ie.textContent="Error",De.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${h(s)}</div>`}}else{k.innerHTML=`<div class="git-diff-view"><div class="diff-header">
|
|
22
24
|
<button class="diff-back">\u2190 Back</button>
|
|
23
25
|
<span class="diff-filename">${h(e)}</span>
|
|
24
26
|
<span class="diff-loading">Loading\u2026</span>
|
|
25
|
-
</div></div>`,
|
|
27
|
+
</div></div>`,Xe(),k.classList.add("open"),He.classList.add("open");try{let n=await fetch(x("/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);k.innerHTML=`<div class="git-diff-view"><div class="diff-header">
|
|
26
28
|
<button class="diff-back">\u2190 Back</button>
|
|
27
29
|
<span class="diff-filename">${h(e)}</span>
|
|
28
|
-
</div><div class="diff-empty">Error loading diff: ${h(s)}</div></div>`}}}function
|
|
30
|
+
</div><div class="diff-empty">Error loading diff: ${h(s)}</div></div>`}}}function Mn(e,t,n){if(!t&&!n){k.innerHTML=`<div class="git-diff-view"><div class="diff-header">
|
|
29
31
|
<button class="diff-back">\u2190 Back</button>
|
|
30
32
|
<span class="diff-filename">${h(e)}</span>
|
|
31
|
-
</div><div class="diff-empty">No diff content available</div></div>`;return}let{html:s,lineCount:
|
|
33
|
+
</div><div class="diff-empty">No diff content available</div></div>`;return}let{html:s,lineCount:r}=St(t,n);k.innerHTML=`<div class="git-diff-view"><div class="diff-header">
|
|
32
34
|
<button class="diff-back">\u2190 Back</button>
|
|
33
35
|
<span class="diff-filename">${h(e)}</span>
|
|
34
|
-
<span class="diff-filesize">${
|
|
35
|
-
</div><div class="diff-content">${s}</div></div>`,
|
|
36
|
+
<span class="diff-filesize">${r} lines</span>
|
|
37
|
+
</div><div class="diff-content">${s}</div></div>`,k.querySelector(".diff-back")?.addEventListener("click",()=>{k.innerHTML=kt,Xe(),g&&k.querySelectorAll(".git-popover-item").forEach(o=>{let c=parseInt(o.dataset.idx||"0",10),l=g.files[c];!l||l.status==="D"||(o.style.cursor="pointer",o.addEventListener("click",()=>Tt(l.path)))}),xt()})}function Bn(e,t,n){if(!t&&!n){Ie.textContent="No diff",De.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:r}=St(t,n);Et.textContent=e,Ie.textContent=r+" lines",De.innerHTML=s}function In(){Lt.style.display="none",Ae?(B.style.display="",B.classList.add("terminal-pending"),pe(Ae),Ae=null):y?(B.style.display="",B.classList.add("terminal-pending"),Bt()):q.style.display="flex"}Sn.addEventListener("click",In);function Fe(){k.classList.remove("open"),He.classList.remove("open")}He.addEventListener("click",Fe),document.addEventListener("keydown",e=>{e.key==="Escape"&&(Fe(),Pe())});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,me(s);return}}}catch{}g=null,me(null)}var Re=null;function Bt(){Ne(),y&&(Mt(y),Re=setInterval(()=>{y&&Mt(y)},3e4))}function Ne(){Re&&(clearInterval(Re),Re=null)}function Dn(){if(Be=ce.fsRoots||[],Be.length===0){if(y){Hn(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 Be){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 Hn(e){m.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let t=await(await fetch(x("/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){z=e,m.innerHTML='<div class="file-tree-empty">Loading...</div>';let t=kn(e);try{let n=await(await fetch(x("/api/fs/list?path="+encodeURIComponent(e)))).json();await t,m.innerHTML="",me(g),m.innerHTML+=Cn();let s=An(e);if(s&&!Be.some(o=>e===o)){let o=document.createElement("div");o.className="file-tree-item",o.innerHTML="..",o.addEventListener("click",()=>_(s)),m.appendChild(o)}if(n.dirs)for(let o of n.dirs){let 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> ${h(c)}`,l.addEventListener("click",()=>_(o)),m.appendChild(l)}else m.innerHTML="";let r=n.files||[];for(let o of r){let c=o.split("/").pop()||o.split("\\").pop(),l=xn(o),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 w=document.createElement("div");w.className="file-tree-item file"+(u?" "+u:""),w.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${p}${h(c)}`,w.addEventListener("click",()=>Fn(o)),m.appendChild(w)}}catch{m.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function An(e){let t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function Fn(e){Te=e,B.style.display="none",v&&(Ne(),v.destroy(),v=null,y=null),me(null),Ve(),q.style.display="none",de.style.display="flex",fn.textContent=e,H.textContent="Loading...",K.style.display="none";try{let t=await fetch(x("/api/file?path="+encodeURIComponent(e)));if(!t.ok){let s=await t.json();H.textContent=s.error||"Failed to load",ue.value="";return}let n=await t.json();ue.value=n.content,Me=n.content,pt=n.content,H.textContent=n.size+" bytes",K.style.display=""}catch{H.textContent="Failed to load",ue.value=""}}ue.addEventListener("input",()=>{Me=ue.value}),K.addEventListener("click",async()=>{K.textContent="Saving...",K.disabled=!0;try{let e=await fetch(x("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:Te,content:Me})});if(!e.ok){let t=await e.json();H.textContent=t.error||"Save failed";return}pt=Me,H.textContent="Saved"}catch{H.textContent="Save failed"}finally{K.disabled=!1,K.textContent="Save"}}),mn.addEventListener("click",async()=>{if(confirm("Delete "+Te+"?"))try{let e=await fetch(x("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:Te})});if(!e.ok){let t=await e.json();H.textContent=t.error||"Delete failed";return}de.style.display="none",q.style.display="flex",z&&_(z)}catch{H.textContent="Delete failed"}}),hn.addEventListener("click",()=>{de.style.display="none",q.style.display="flex",z&&_(z)}),vn.addEventListener("click",async()=>{let e=ft.value.trim();if(!e)return;let t=z+"/"+e;try{let n=await fetch(x("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok){let s=await n.json();H.textContent=s.error||"Create failed";return}ft.value="",z&&_(z)}catch{H.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let e=mt.classList.toggle("open");Ke.classList.toggle("open",e)}),Ke.addEventListener("click",()=>{mt.classList.remove("open"),Ke.classList.remove("open")});var I="",$=!1,$e,he=document.createElement("div");he.className="session-popover-backdrop",document.body.appendChild(he);var F=document.createElement("div");F.id="session-popover",F.className="session-popover",F.style.display="none",F.innerHTML=`<div class="sp-field">
|
|
36
38
|
<label>Rename session</label>
|
|
37
39
|
<input type="text" id="sp-name" />
|
|
38
40
|
</div>
|
|
@@ -41,5 +43,6 @@ var an=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function
|
|
|
41
43
|
<button class="btn" id="sp-cancel">Cancel</button>
|
|
42
44
|
</div>
|
|
43
45
|
<hr class="sp-divider" />
|
|
44
|
-
<button class="btn
|
|
45
|
-
|
|
46
|
+
<button class="btn primary" id="sp-rebuild" style="display:none">\u91CD\u5EFA\u540C\u540D\u540C\u8DEF\u5F84 session</button>
|
|
47
|
+
<button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(F);function Rn(e){let t=e.getBoundingClientRect();F.style.top=t.bottom+4+"px",F.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function It(e,t,n){I=e,$=!!n?.tombstone,$e=n?.agentId;let s=document.getElementById("sp-name");s.value=e;let r=F.querySelector(".sp-field label");r.textContent=$?"\u91CD\u547D\u540D (\u4EC5\u8BB0\u5F55\uFF0C\u4E0D\u91CD\u5EFA)":"Rename session";let o=document.getElementById("sp-rebuild");o.style.display=$?"":"none";let c=document.getElementById("sp-delete");c.textContent=$?"\u5220\u9664\u5931\u6548\u8BB0\u5F55 (\u5893\u7891\u6D88\u5931)":"Delete session";let l=document.getElementById("sp-save");l.textContent=$?"\u91CD\u547D\u540D":"Save",he.classList.add("open"),F.style.display="block",Rn(t),setTimeout(()=>s.focus(),50)}function V(){he.classList.remove("open"),F.style.display="none"}he.addEventListener("click",V),document.getElementById("sp-save").addEventListener("click",async()=>{let e=document.getElementById("sp-name").value.trim();if(!e||e===I){V();return}try{let t=$?{oldName:I,newName:e,agentId:$e}:{oldName:I,newName:e};if(!(await fetch(x($?"/api/sessions/rename-tombstone":"/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).ok)return;V(),j(),!$&&y===I&&pe(e)}catch{}}),document.getElementById("sp-rebuild").addEventListener("click",async()=>{if(confirm('\u91CD\u5EFA\u4F1A\u8BDD "'+I+'" (\u540C\u540D\u540C\u8DEF\u5F84\uFF0C\u5185\u5BB9\u4E3A\u7A7A)\uFF1F'))try{let e=await fetch(x("/api/sessions/rebuild"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:I,agentId:$e})});if(!e.ok){let t=await e.json();alert(t.error||"\u91CD\u5EFA\u5931\u8D25");return}V(),j()}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if($){if(!confirm('\u5220\u9664\u5931\u6548\u8BB0\u5F55 "'+I+'"\uFF1F'))return;try{if(!(await fetch(x("/api/sessions/dismiss"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:I,agentId:$e})})).ok)return;V(),j()}catch{}return}if(confirm('Delete session "'+I+'"?'))try{if(!(await fetch(x("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:I})})).ok)return;V(),y===I&&(v&&(v.destroy(),v=null),y=null,B.style.display="none",q.style.display="flex"),j()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",V);var Nn={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=Nn[t.dataset.key];n&&(v.sendInput(n),v.focus())});var R=document.getElementById("mk-input"),Dt=document.getElementById("mk-send");function Ht(){if(!R||!v)return;let e=R.value;e&&(v.sendInput(e+"\r"),R.value="",R.rows=1,v.focus())}if(R&&(R.addEventListener("input",()=>{R.rows=1;let e=R.value.split(`
|
|
48
|
+
`).length;R.rows=Math.min(e,4)}),R.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),Ht())})),Dt&&Dt.addEventListener("click",Ht),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 Ge=document.getElementById("process-panel"),ve=document.getElementById("proc-list"),$n=document.getElementById("proc-close"),Ze=document.getElementById("proc-backdrop"),Pn=document.getElementById("header-ram");function zn(){ve.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(x("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){ve.innerHTML='<div class="proc-empty">No processes</div>';return}let t="";for(let n of e){let s=n.rss,r=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">'+r+'</span><span class="proc-cmd" title="'+h(n.command)+'">'+h(n.command)+'</span><button class="proc-kill" title="Kill PID '+n.pid+'">×</button></div>'}ve.innerHTML=t;for(let n of ve.querySelectorAll(".proc-row")){let s=n.querySelector(".proc-kill");s&&s.addEventListener("click",r=>{let o=r.target.closest(".proc-row");!o||!confirm("Kill PID "+o.dataset.pid+"?")||fetch(x("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(o.dataset.pid,10)})}).then(c=>c.json()).then(c=>{c.ok?o.style.opacity="0.3":alert("Failed: "+(c.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{ve.innerHTML='<div class="proc-empty">Failed to load</div>'})}function jn(){Ge.classList.add("open"),Ze.classList.add("open"),zn()}function Pe(){Ge.classList.remove("open"),Ze.classList.remove("open")}Pn.addEventListener("click",e=>{e.stopPropagation(),Ge.classList.contains("open")?Pe():jn()}),$n.addEventListener("click",Pe),Ze.addEventListener("click",Pe);var _n=2e3,ge=null,Qe=null,ze=new Map,je=new Map;function Wn(e,t){return(t?"a:"+t+":":"l:")+e}function On(e,t,n){let s=Wn(e,t),r=je.get(s);je.set(s,{session:e,agentId:t,state:n,doneUntil:n==="idle"&&r?.state==="working"?Date.now()+_n:void 0});let o=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(o);c&&At(c,s,je.get(s))}function At(e,t,n){let s=ze.get(t);s&&(clearTimeout(s),ze.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"),ze.set(t,setTimeout(()=>{e.classList.remove("just-done"),ze.delete(t)},Math.max(0,n.doneUntil-Date.now()))))}function et(){for(let[e,t]of je){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&&At(s,e,t)}}function Ft(){if(ge||Qe)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);ge=n,n.onmessage=s=>{try{let r=JSON.parse(String(s.data));r&&r.type==="session.activity"&&typeof r.session=="string"&&On(r.session,r.agentId||void 0,r.state==="working"?"working":"idle")}catch{}},n.onclose=()=>{ge===n&&(ge=null,Qe=setTimeout(()=>{Qe=null,Ft()},5e3))},n.onerror=()=>{try{n.close()}catch{}}}catch{ge=null}}window.__openSession=pe,window.__refreshSidebar=j,j(),Ve(),et(),Ft(),document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&et()}),setInterval(()=>{document.visibilityState==="visible"&&j()},1e4);
|
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
import{initTerminal as
|
|
2
|
-
<span>${
|
|
3
|
-
<button class="session-edit-btn" data-session="${
|
|
4
|
-
`,n.addEventListener("click",s=>{s.target.closest(".session-edit-btn")||
|
|
5
|
-
<span>${
|
|
1
|
+
import{initTerminal as _e}from"./terminal-core.js";const R=window.__TMUX_WEB_SHELL__;let f=null,m=null;const fe=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null;let h=fe;window.__tmuxWebScopeAgent=fe;let ne="sessions",I="",z="",F="",ue="";function v(e){return h?"/a/"+encodeURIComponent(h)+e:e}const d=document.getElementById("sidebar-content"),M=document.getElementById("main-placeholder"),b=document.getElementById("terminal-container"),$=document.getElementById("file-editor"),De=document.getElementById("fe-path"),E=document.getElementById("fe-status"),A=document.getElementById("fe-content"),T=document.getElementById("fe-save"),Pe=document.getElementById("fe-delete"),je=document.getElementById("fe-back"),me=document.getElementById("fe-new-name"),Oe=document.getElementById("fe-new-btn"),se=document.getElementById("settings-backdrop"),ve=document.getElementById("settings-popover"),ge=document.querySelector(".sidebar");function We(){return window.matchMedia("(max-width: 640px)").matches}function ie(){We()&&ge.classList.add("collapsed")}function ye(){ge.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{ye(),he("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{ye(),he("files")});function he(e){ne=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"?(oe="",B()):tt()}let oe="";async function B(){if(ne!=="sessions")return;try{const t=await(await fetch("/api/sidebar/sessions")).json(),s=JSON.stringify({recent:t.recent,agents:t.agents,tombstones:t.tombstones});if(s===oe)return;oe=s,qe(t)}catch{d.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")}),de()}const be="tmux-web-sidebar-group-collapsed";function we(){try{return new Set(JSON.parse(localStorage.getItem(be)||"[]"))}catch{return new Set}}function ze(e){try{localStorage.setItem(be,JSON.stringify([...e]))}catch{}}function Fe(e,n,t){if(!e)return"\u2026";const s=t&&(e===t||e.startsWith(t+"/"))?"~"+e.slice(t.length):e,a=s.split("/").filter(Boolean);if(a.length===0)return e;for(let i=1;i<=a.length;i++){const o=a.slice(-i).join("/");if(!n.some(l=>!l||l===e?!1:(t&&(l===t||l.startsWith(t+"/"))?"~"+l.slice(t.length):l).split("/").filter(Boolean).slice(-i).join("/")===o))return o}return s}function Ue(e,n){const t=new Map;for(const i of e){const o=i.path||"";t.has(o)||t.set(o,[]),t.get(o).push(i)}const s=[...t.keys()],a=we();for(const[i,o]of t){const c=Fe(i,s,n),l=a.has(i),r=document.createElement("div");r.className="session-group-header"+(l?" collapsed":""),r.title=i||"No working directory",r.innerHTML=`<span class="sg-folder">📁</span><span class="sg-label">${p(c)}</span>`,r.addEventListener("click",()=>{const L=we(),pe=r.nextElementSibling;L.has(i)?(L.delete(i),r.classList.remove("collapsed"),pe?.classList.remove("collapsed")):(L.add(i),r.classList.add("collapsed"),pe?.classList.add("collapsed")),ze(L)});const y=document.createElement("div");y.className="session-group-body"+(l?" collapsed":"");for(const L of o)y.appendChild(Ge(L));d.appendChild(r),d.appendChild(y)}}function Ge(e){const n=document.createElement("div");n.className="session-item"+(e.name===m&&!h?" 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
|
+
<span>${p(e.name)}</span>
|
|
3
|
+
<button class="session-edit-btn" data-session="${p(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")||_(e.name)});const t=n.querySelector(".session-edit-btn");return t&&t.addEventListener("click",s=>{s.stopPropagation(),He(e.name,t)}),n}function qe(e){d.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"+(h?" clickable":""),t.textContent=h?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",h&&(t.title="Back to hub",t.addEventListener("click",()=>{window.location.href="/"})),d.appendChild(t),Ue(n,e.home);const s=e.agents||[];for(const i of s){if(!i.sessions||i.sessions.length===0)continue;const o=i.agentId===h,c=document.createElement("div");c.className="sidebar-section-label machine-label"+(i.online?"":" agent-offline-label")+(!o&&i.online?" clickable":""),c.textContent=i.agentName+(i.online?"":" (offline)"),!o&&i.online&&(c.title="\u6253\u5F00 "+i.agentName,c.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(i.agentId)+"/"})),d.appendChild(c);for(const l of i.sessions){const r=document.createElement("div");r.className="session-item"+(l.name===m&&h===i.agentId?" active":"")+(i.online?"":" session-offline"),r.setAttribute("data-session",l.name),r.setAttribute("data-agent",i.agentId),r.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>
|
|
5
|
+
<span>${p(l.name)}</span><span class="meta">${i.online?"":"offline"}</span>`,i.online&&r.addEventListener("click",y=>{y.target.closest(".session-edit-btn")||_(l.name,i.agentId)}),d.appendChild(r)}}const a=e.tombstones||[];if(a.length>0){const i=document.createElement("div");i.className="sidebar-section-label tombstone-label",i.textContent="\u5931\u6548\u4F1A\u8BDD",d.appendChild(i);for(const o of a){const c=document.createElement("div");c.className="session-item session-tombstone",c.setAttribute("data-session",o.name),o.agentId&&c.setAttribute("data-agent",o.agentId);const l=o.agentId?o.agentOnline?" (agent)":" (agent \u79BB\u7EBF)":"";c.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"></svg>
|
|
6
|
+
<span>${p(o.name)}</span><span class="meta">\u5931\u6548${l}</span>
|
|
7
|
+
<button class="session-edit-btn" data-session="${p(o.name)}" title="\u6062\u590D\u4F1A\u8BDD"><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>`;const r=c.querySelector(".session-edit-btn");r&&r.addEventListener("click",y=>{y.stopPropagation(),He(o.name,r,{tombstone:!0,agentId:o.agentId})}),d.appendChild(c)}}}function p(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function _(e,n){if(!(m===e&&f&&h===(n||null))){$.style.display="none",M.style.display="none",b.style.display="",b.classList.add("terminal-pending"),ie(),f&&(Y(),f.destroy(),f=null),m=e,h=n||null,window.__tmuxWebScopeAgent=h,window.__tmuxWebCurrentSession=m;for(const t of d.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e&&(t.getAttribute("data-agent")||null)===h);try{f=await _e(b,e,{terminal:R.terminal,scrollback:R.scrollback,theme:R.theme,wsBase:h?"/ws/a/"+encodeURIComponent(h):R.wsBase||"/ws"}),Te()}catch(t){console.error("[shell] terminal init failed:",t);const s=t&&typeof t=="object"&&"message"in t?t.message:String(t);b.textContent="Failed to open terminal for "+e+": "+s,b.classList.remove("terminal-pending")}}}let U=[],u=null,Le=!1;function Ve(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 Je(e){Le=!0;try{const n=await fetch(v("/api/git/status?path="+encodeURIComponent(e)));n.ok?u=await n.json():u=null}catch{u=null}finally{Le=!1}}function Ke(){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
8
|
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M11.63 1.22a.75.75 0 00-1.06 0L5.99 5.8 4.12 3.93a.75.75 0 10-1.06 1.06L4.93 6.87A3.5 3.5 0 003 10.09v3.16a.75.75 0 001 0v-3.16a2.5 2.5 0 012.5-2.5h.27l-1.06 1.06a.75.75 0 001.06 1.06l2.37-2.38a.75.75 0 000-1.06L7.59 5.27l4.04-4.05a.75.75 0 000-1.06L11.63 1.22zM3.25 13.94a.75.75 0 01-.75-.75v-.69a.75.75 0 011.5 0v.69c0 .414-.336.75-.75.75z"/></svg>
|
|
7
|
-
${
|
|
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],
|
|
9
|
-
${o}<span class="file-path">${
|
|
10
|
-
<span class="file-add">${
|
|
11
|
-
<span class="file-del">${
|
|
12
|
-
</div>`}n+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',
|
|
9
|
+
${p(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>"}d.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&N&&u&&(e.stopPropagation(),Ie())});const D=document.getElementById("header-git"),Ee=document.getElementById("git-diff-view"),ke=document.getElementById("gd-path"),G=document.getElementById("gd-status"),q=document.getElementById("gd-content"),Xe=document.getElementById("gd-back");let N=null;function P(e){if(!e||!e.branch){D.style.display="none",N=null;return}N=e.repoRoot;const{branch:n,linesAdded:t,linesRemoved:s}=e;let a=`<span class="branch">${p(n)}</span>`;(t>0||s>0)&&(a+=`<span class="sep">\xB7</span>+<span class="diff-add">${t}</span> <span class="diff-del">-${s}</span>`),D.innerHTML=a,D.style.display="block"}D.addEventListener("click",e=>{N&&u&&(e.stopPropagation(),Ie())});const g=document.getElementById("git-popover"),V=document.getElementById("git-popover-backdrop");function ae(){const e=D.getBoundingClientRect();g.style.top=e.bottom+4+"px",g.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function Ce(){const e=N,n=document.getElementById("git-popover-browse");n&&n.addEventListener("click",()=>{if(K(),e){ne="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));const t=document.getElementById("mode-files");t&&t.classList.add("active"),x(e)}})}let Se="";function Ie(){if(!u)return;const e=u.files||[];if(!e.length)g.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
|
|
10
|
+
<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],a=s.additions||0,i=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}">
|
|
11
|
+
${o}<span class="file-path">${p(s.path)}</span>
|
|
12
|
+
<span class="file-add">${a>0?"+"+a:""}</span>
|
|
13
|
+
<span class="file-del">${i>0?"-"+i:""}</span>
|
|
14
|
+
</div>`}n+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',Se=n,g.innerHTML=n,g.querySelectorAll(".git-popover-item").forEach(t=>{const s=parseInt(t.dataset.idx||"0",10),a=e[s];!a||a.status==="D"||(t.classList.add("clickable"),t.style.cursor="pointer",t.addEventListener("click",()=>xe(a.path)))})}ae(),g.classList.add("open"),V.classList.add("open"),Ce()}function Ye(){return window.innerWidth>=1024}function Be(e,n){const s=(e+(n&&e?`
|
|
13
15
|
`:"")+n).split(`
|
|
14
|
-
`);let
|
|
16
|
+
`);let a="",i=0,o=0;for(const c of s){const l=c;if(l.startsWith("@@")){const r=l.match(/@@\s+-(\d+)[^+]*\+(\d+)/);r&&(i=parseInt(r[1],10)-1,o=parseInt(r[2],10)-1),a+=`<div class="diff-line hunk"><span class="ln-body">${p(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")&&(a+=`<div class="diff-line header"><span class="ln-body">${p(l)}</span></div>`);continue}if(l.startsWith("+"))o++,a+=`<div class="diff-line add"><span class="ln-no">${o}</span><span class="ln-body">${p(l)}</span></div>`;else if(l.startsWith("-"))i++,a+=`<div class="diff-line del"><span class="ln-no">${i}</span><span class="ln-body">${p(l)}</span></div>`;else if(l.startsWith("\\"))a+=`<div class="diff-line"><span class="ln-body">${p(l)}</span></div>`;else{if(!l&&!i&&!o)continue;i++,o++,a+=`<div class="diff-line"><span class="ln-no">${i}</span><span class="ln-no" style="margin-right:12px">${o}</span><span class="ln-body">${p(l)}</span></div>`}}return{html:a,lineCount:s.length}}let J=null;async function xe(e){const n=N;if(n)if(K(),Ye()){b.style.display="none",f&&(Y(),f.destroy(),f=null,J=m,m=null),P(null),$.style.display="none",M.style.display="none",Ee.style.display="flex",ke.textContent=e,G.textContent="Loading\u2026",q.innerHTML="";try{const t=await fetch(v("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Qe(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);G.textContent="Error",q.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${p(s)}</div>`}}else{g.innerHTML=`<div class="git-diff-view"><div class="diff-header">
|
|
15
17
|
<button class="diff-back">\u2190 Back</button>
|
|
16
|
-
<span class="diff-filename">${
|
|
18
|
+
<span class="diff-filename">${p(e)}</span>
|
|
17
19
|
<span class="diff-loading">Loading\u2026</span>
|
|
18
|
-
</div></div>`,
|
|
20
|
+
</div></div>`,ae(),g.classList.add("open"),V.classList.add("open");try{const t=await fetch(v("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Ze(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);g.innerHTML=`<div class="git-diff-view"><div class="diff-header">
|
|
19
21
|
<button class="diff-back">\u2190 Back</button>
|
|
20
|
-
<span class="diff-filename">${
|
|
21
|
-
</div><div class="diff-empty">Error loading diff: ${
|
|
22
|
+
<span class="diff-filename">${p(e)}</span>
|
|
23
|
+
</div><div class="diff-empty">Error loading diff: ${p(s)}</div></div>`}}}function Ze(e,n,t){if(!n&&!t){g.innerHTML=`<div class="git-diff-view"><div class="diff-header">
|
|
22
24
|
<button class="diff-back">\u2190 Back</button>
|
|
23
|
-
<span class="diff-filename">${
|
|
24
|
-
</div><div class="diff-empty">No diff content available</div></div>`;return}const{html:s,lineCount:
|
|
25
|
+
<span class="diff-filename">${p(e)}</span>
|
|
26
|
+
</div><div class="diff-empty">No diff content available</div></div>`;return}const{html:s,lineCount:a}=Be(n,t);g.innerHTML=`<div class="git-diff-view"><div class="diff-header">
|
|
25
27
|
<button class="diff-back">\u2190 Back</button>
|
|
26
|
-
<span class="diff-filename">${
|
|
27
|
-
<span class="diff-filesize">${
|
|
28
|
-
</div><div class="diff-content">${s}</div></div>`,
|
|
28
|
+
<span class="diff-filename">${p(e)}</span>
|
|
29
|
+
<span class="diff-filesize">${a} lines</span>
|
|
30
|
+
</div><div class="diff-content">${s}</div></div>`,g.querySelector(".diff-back")?.addEventListener("click",()=>{g.innerHTML=Se,ae(),u&&g.querySelectorAll(".git-popover-item").forEach(i=>{const o=parseInt(i.dataset.idx||"0",10),c=u.files[o];!c||c.status==="D"||(i.style.cursor="pointer",i.addEventListener("click",()=>xe(c.path)))}),Ce()})}function Qe(e,n,t){if(!n&&!t){G.textContent="No diff",q.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:a}=Be(n,t);ke.textContent=e,G.textContent=a+" lines",q.innerHTML=s}function et(){Ee.style.display="none",J?(b.style.display="",b.classList.add("terminal-pending"),_(J),J=null):m?(b.style.display="",b.classList.add("terminal-pending"),Te()):M.style.display="flex"}Xe.addEventListener("click",et);function K(){g.classList.remove("open"),V.classList.remove("open")}V.addEventListener("click",K),document.addEventListener("keydown",e=>{e.key==="Escape"&&(K(),Q())});async function Me(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 a=await s.json();u=a,P(a);return}}}catch{}u=null,P(null)}let X=null;function Te(){Y(),m&&(Me(m),X=setInterval(()=>{m&&Me(m)},3e4))}function Y(){X&&(clearInterval(X),X=null)}function tt(){if(U=R.fsRoots||[],U.length===0){if(m){nt(m);return}d.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}d.innerHTML="";for(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> ${p(e)}`,n.addEventListener("click",()=>x(e)),d.appendChild(n)}}async function nt(e){d.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const t=await(await fetch(v("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?x(t.path):d.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{d.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function x(e){I=e,d.innerHTML='<div class="file-tree-empty">Loading...</div>';const n=Je(e);try{const s=await(await fetch(v("/api/fs/list?path="+encodeURIComponent(e)))).json();await n,d.innerHTML="",P(u),d.innerHTML+=Ke();const a=st(e);if(a&&!U.some(c=>e===c)){const c=document.createElement("div");c.className="file-tree-item",c.innerHTML="..",c.addEventListener("click",()=>x(a)),d.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> ${p(c)}`,l.addEventListener("click",()=>x(o)),d.appendChild(l)}else d.innerHTML="";const i=s.files||[];for(const o of i){const c=o.split("/").pop()||o.split("\\").pop(),l=Ve(o);let r="",y="";if(l)switch(l){case"M":r="git-mod",y='<span class="git-file-status M">M</span>';break;case"A":r="git-add",y='<span class="git-file-status A">A</span>';break;case"D":r="git-del",y='<span class="git-file-status D">D</span>';break;case"?":r="git-untracked",y='<span class="git-file-status ?">?</span>';break;case"R":r="git-mod",y='<span class="git-file-status R">R</span>';break}const L=document.createElement("div");L.className="file-tree-item file"+(r?" "+r:""),L.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${y}${p(c)}`,L.addEventListener("click",()=>it(o)),d.appendChild(L)}}catch{d.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function st(e){const n=e.lastIndexOf("/");return n<=0?null:e.substring(0,n)}async function it(e){z=e,b.style.display="none",f&&(Y(),f.destroy(),f=null,m=null),P(null),ie(),M.style.display="none",$.style.display="flex",De.textContent=e,E.textContent="Loading...",T.style.display="none";try{const n=await fetch(v("/api/file?path="+encodeURIComponent(e)));if(!n.ok){const s=await n.json();E.textContent=s.error||"Failed to load",A.value="";return}const t=await n.json();A.value=t.content,F=t.content,ue=t.content,E.textContent=t.size+" bytes",T.style.display=""}catch{E.textContent="Failed to load",A.value=""}}A.addEventListener("input",()=>{F=A.value}),T.addEventListener("click",async()=>{T.textContent="Saving...",T.disabled=!0;try{const e=await fetch(v("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:z,content:F})});if(!e.ok){const n=await e.json();E.textContent=n.error||"Save failed";return}ue=F,E.textContent="Saved"}catch{E.textContent="Save failed"}finally{T.disabled=!1,T.textContent="Save"}}),Pe.addEventListener("click",async()=>{if(confirm("Delete "+z+"?"))try{const e=await fetch(v("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:z})});if(!e.ok){const n=await e.json();E.textContent=n.error||"Delete failed";return}$.style.display="none",M.style.display="flex",I&&x(I)}catch{E.textContent="Delete failed"}}),je.addEventListener("click",()=>{$.style.display="none",M.style.display="flex",I&&x(I)}),Oe.addEventListener("click",async()=>{const e=me.value.trim();if(!e)return;const n=I+"/"+e;try{const t=await fetch(v("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:n})});if(!t.ok){const s=await t.json();E.textContent=s.error||"Create failed";return}me.value="",I&&x(I)}catch{E.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=ve.classList.toggle("open");se.classList.toggle("open",e)}),se.addEventListener("click",()=>{ve.classList.remove("open"),se.classList.remove("open")});let w="",S=!1,Z;const j=document.createElement("div");j.className="session-popover-backdrop",document.body.appendChild(j);const k=document.createElement("div");k.id="session-popover",k.className="session-popover",k.style.display="none",k.innerHTML=`<div class="sp-field">
|
|
29
31
|
<label>Rename session</label>
|
|
30
32
|
<input type="text" id="sp-name" />
|
|
31
33
|
</div>
|
|
@@ -34,5 +36,6 @@ import{initTerminal as $e}from"./terminal-core.js";const H=window.__TMUX_WEB_SHE
|
|
|
34
36
|
<button class="btn" id="sp-cancel">Cancel</button>
|
|
35
37
|
</div>
|
|
36
38
|
<hr class="sp-divider" />
|
|
37
|
-
<button class="btn
|
|
38
|
-
|
|
39
|
+
<button class="btn primary" id="sp-rebuild" style="display:none">\u91CD\u5EFA\u540C\u540D\u540C\u8DEF\u5F84 session</button>
|
|
40
|
+
<button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(k);function ot(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 He(e,n,t){w=e,S=!!t?.tombstone,Z=t?.agentId;const s=document.getElementById("sp-name");s.value=e;const a=k.querySelector(".sp-field label");a.textContent=S?"\u91CD\u547D\u540D (\u4EC5\u8BB0\u5F55\uFF0C\u4E0D\u91CD\u5EFA)":"Rename session";const i=document.getElementById("sp-rebuild");i.style.display=S?"":"none";const o=document.getElementById("sp-delete");o.textContent=S?"\u5220\u9664\u5931\u6548\u8BB0\u5F55 (\u5893\u7891\u6D88\u5931)":"Delete session";const c=document.getElementById("sp-save");c.textContent=S?"\u91CD\u547D\u540D":"Save",j.classList.add("open"),k.style.display="block",ot(n),setTimeout(()=>s.focus(),50)}function H(){j.classList.remove("open"),k.style.display="none"}j.addEventListener("click",H),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===w){H();return}try{const n=S?{oldName:w,newName:e,agentId:Z}:{oldName:w,newName:e};if(!(await fetch(v(S?"/api/sessions/rename-tombstone":"/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).ok)return;H(),B(),!S&&m===w&&_(e)}catch{}}),document.getElementById("sp-rebuild").addEventListener("click",async()=>{if(confirm('\u91CD\u5EFA\u4F1A\u8BDD "'+w+'" (\u540C\u540D\u540C\u8DEF\u5F84\uFF0C\u5185\u5BB9\u4E3A\u7A7A)\uFF1F'))try{const e=await fetch(v("/api/sessions/rebuild"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:w,agentId:Z})});if(!e.ok){const n=await e.json();alert(n.error||"\u91CD\u5EFA\u5931\u8D25");return}H(),B()}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(S){if(!confirm('\u5220\u9664\u5931\u6548\u8BB0\u5F55 "'+w+'"\uFF1F'))return;try{if(!(await fetch(v("/api/sessions/dismiss"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:w,agentId:Z})})).ok)return;H(),B()}catch{}return}if(confirm('Delete session "'+w+'"?'))try{if(!(await fetch(v("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:w})})).ok)return;H(),m===w&&(f&&(f.destroy(),f=null),m=null,b.style.display="none",M.style.display="flex"),B()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",H);const at={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=at[n.dataset.key];t&&(f.sendInput(t),f.focus())});const C=document.getElementById("mk-input"),Ne=document.getElementById("mk-send");function Re(){if(!C||!f)return;const e=C.value;e&&(f.sendInput(e+"\r"),C.value="",C.rows=1,f.focus())}if(C&&(C.addEventListener("input",()=>{C.rows=1;const e=C.value.split(`
|
|
41
|
+
`).length;C.rows=Math.min(e,4)}),C.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),Re())})),Ne&&Ne.addEventListener("click",Re),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 le=document.getElementById("process-panel"),O=document.getElementById("proc-list"),lt=document.getElementById("proc-close"),ce=document.getElementById("proc-backdrop"),ct=document.getElementById("header-ram");function rt(){O.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(v("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){O.innerHTML='<div class="proc-empty">No processes</div>';return}let n="";for(const t of e){const s=t.rss,a=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">'+a+'</span><span class="proc-cmd" title="'+p(t.command)+'">'+p(t.command)+'</span><button class="proc-kill" title="Kill PID '+t.pid+'">×</button></div>'}O.innerHTML=n;for(const t of O.querySelectorAll(".proc-row")){const s=t.querySelector(".proc-kill");s&&s.addEventListener("click",a=>{const i=a.target.closest(".proc-row");!i||!confirm("Kill PID "+i.dataset.pid+"?")||fetch(v("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(i.dataset.pid,10)})}).then(o=>o.json()).then(o=>{o.ok?i.style.opacity="0.3":alert("Failed: "+(o.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{O.innerHTML='<div class="proc-empty">Failed to load</div>'})}function dt(){le.classList.add("open"),ce.classList.add("open"),rt()}function Q(){le.classList.remove("open"),ce.classList.remove("open")}ct.addEventListener("click",e=>{e.stopPropagation(),le.classList.contains("open")?Q():dt()}),lt.addEventListener("click",Q),ce.addEventListener("click",Q);const pt=2e3;let W=null,re=null;const ee=new Map,te=new Map;function ft(e,n){return(n?"a:"+n+":":"l:")+e}function ut(e,n,t){const s=ft(e,n),a=te.get(s);te.set(s,{session:e,agentId:n,state:t,doneUntil:t==="idle"&&a?.state==="working"?Date.now()+pt:void 0});const i=n?'.session-item[data-agent="'+CSS.escape(n)+'"][data-session="'+CSS.escape(e)+'"]':'.session-item[data-session="'+CSS.escape(e)+'"]:not([data-agent])',o=d.querySelector(i);o&&$e(o,s,te.get(s))}function $e(e,n,t){const s=ee.get(n);s&&(clearTimeout(s),ee.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"),ee.set(n,setTimeout(()=>{e.classList.remove("just-done"),ee.delete(n)},Math.max(0,t.doneUntil-Date.now()))))}function de(){for(const[e,n]of te){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=d.querySelector(t);s&&$e(s,e,n)}}function Ae(){if(W||re)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=a=>{try{const i=JSON.parse(String(a.data));i&&i.type==="session.activity"&&typeof i.session=="string"&&ut(i.session,i.agentId||void 0,i.state==="working"?"working":"idle")}catch{}},s.onclose=()=>{W===s&&(W=null,re=setTimeout(()=>{re=null,Ae()},5e3))},s.onerror=()=>{try{s.close()}catch{}}}catch{W=null}}window.__openSession=_,window.__refreshSidebar=B,B(),ie(),de(),Ae(),document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&de()}),setInterval(()=>{document.visibilityState==="visible"&&B()},1e4);
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{chmodSync as
|
|
2
|
+
import{chmodSync as he,existsSync as z,readdirSync as ge,statSync as ye}from"node:fs";import{createRequire as G}from"node:module";import{hostname as U}from"node:os";const ke=G(import.meta.url),{version:Te}=ke("../package.json");import $ from"node:path";import{fileURLToPath as ve}from"node:url";import{serve as _e}from"@hono/node-server";import{WebSocketServer as we,WebSocket as E}from"ws";try{const e=$.dirname(G(import.meta.url).resolve("node-pty/package.json")),n=$.join(e,"prebuilds");if(z(n))for(const o of ge(n)){const t=$.join(n,o,"spawn-helper");z(t)&&!(ye(t).mode&73)&&he(t,493)}}catch{}import{loadSecurityConfig as Se}from"./lib/security-config.js";import{TokenStore as Ae,hashPassword as Ce,verifyPassword as be,validatePassword as xe}from"./lib/auth.js";import{RateLimiter as Ie}from"./lib/rateLimiter.js";import{audit as d}from"./lib/auditLog.js";import{db as N}from"./lib/db.js";import{handleClientMessage as V}from"./lib/ws-message.js";import{loadDotEnv as Pe}from"./lib/load-env.js";import{listSessions as Re,resolveTmuxSocketPath as Me}from"./sessions.js";import{cmdSetup as Oe,cmdTheme as $e,printUsage as X,printVersion as Ne}from"./lib/cli.js";import{readSettings as Le}from"./lib/settings.js";import{readActiveTheme as Ue}from"./lib/theme-store.js";import{buildApp as Y}from"./lib/build-app.js";import{attachTerminal as Ee,killAllAttachedPtys as Fe}from"./lib/attach-terminal.js";import{acquireControlClient as K,killAllControlClients as He}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 je}from"./lib/terminal-config.js";import{AgentRegistry as Be}from"./lib/agent-registry.js";import{AgentChannel as We,MAX_RELAY_CONNS_PER_AGENT as Je}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as De,listAgentTokens as ze}from"./lib/agent-tokens.js";import{saveSecurityConfig as Ge}from"./lib/security-config.js";import{readFederationConfig as Ve,writeFederationConfig as Xe}from"./lib/federation-config.js";import{startAgentClient as Ye}from"./lib/agent-client.js";import{ActivityProbe as Ke}from"./lib/activity-probe.js";import{upsertSessionState as te}from"./lib/session-state.js";import{sessionWorkingPath as Qe}from"./lib/sessions-sidebar.js";Pe();const F=je(),g=Se(),T=new Ae,A=new Ie;let J=!1;const Ze="tmux-web-token";function H(e){if(g.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*)${Ze}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function zt(e){const n=j(e);return n?T.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 ne(e,n){e.readyState===E.OPEN&&e.send(JSON.stringify(n))}const oe=process.argv.slice(2),L=oe;if(L.length>0){const[e,n]=L;switch(e){case"setup":await Oe(L),process.exit(0);case"theme":await $e(L.slice(1)),process.exit(0);case"agent":{const o=$.join($.dirname(ve(import.meta.url)),"agent.js"),{spawn:t}=await import("node:child_process"),f=t(process.execPath,[o,...oe],{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 ft(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 re=new Set;await N.read(),N.data.sessionAccess??=[],N.data.pinnedViews??=[],N.data.watchedPanes??=[],N.data.quickCommands??=[];const se=await Le();let et=await Ue();const k=new Be({onChange:()=>{}}),_=new Map;function ae(e){return k.get(e)?_.get(e)??null:null}const ie={activeTheme:et,settingUpPassword:!1},tt=Y({mode:"hub",securityConfig:g,tokenStore:T,rateLimiter:A,settings:se,terminalBufferConfig:F,state:ie,federation:{getConfig:()=>x,setConfig:async e=>{x=e,await Xe(e),le(e)},status:ot,hostname:U()},hub:{agents:k,getChannel:ae}}),nt=Y({mode:"agent",securityConfig:g,tokenStore:T,rateLimiter:A,settings:se,terminalBufferConfig:F,state:ie});let x=await Ve(),M=null;function ce(){M&&(M.stop(),M=null)}function le(e){ce(),!(!e.enabled||!e.hub||!e.token)&&(M=Ye({hub:e.hub,token:e.token,name:e.name?.trim()||U(),version:Te,app:nt,listSessions:Re,terminalBufferConfig:F,getSessionPaneTarget:Q,capturePaneTail:Z,capturePaneHistoryChunk:q,toCrlf:ee,handleClientMessage:V,acquireControlClient:K}))}function ot(){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()}}le(x);const ue=new Ke,B=new Map;setInterval(()=>{(async()=>{try{const e=await ue.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)de({type:"session.activity",session:o.session,state:o.state})}catch{}})()},ue.scanMs);const rt=parseInt(process.env.PORT||"21000",10),st=_e({fetch:tt.fetch,port:rt,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),O=new we({noServer:!0}),D=new Set;function de(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()}st.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?T.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"){lt(e,n,o);return}const s=f.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(s){at(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=g.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>=g.security.maxConnectionsPerIp){d("ws_rejected_per_ip_cap",{ip:l,liveFromIp:m}),I(n,429,"Too many connections");return}const y=j(e)||t.searchParams.get("token")||"",v=y?T.validateToken(y):null;O.handleUpgrade(e,n,o,w=>{O.emit("connection",w,e,decodeURIComponent(i[1]),l,v)})}),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:!g.passwordHash}),y()}function m(a,r={}){P(e,{type:"auth.failed",message:a,...r})}function y(){l||(te(o,{path:Qe(o)}),l=Ee(o,{send:a=>P(e,a),onMessage:a=>{u=a},onClose:a=>{v=a}},{terminalBufferConfig:F,getSessionPaneTarget:Q,capturePaneTail:Z,capturePaneHistoryChunk:q,toCrlf:ee,handleClientMessage:V,acquireControlClient:K,initialCols:i?.cols,initialRows:i?.rows,tmuxSocketPath:Me()??void 0,onPtyExit:(a,r)=>b(e,1e3,"pty exited"),onSpawnError:(a,r)=>b(e,1011,"pty spawn failed")}))}let v=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=T.validateToken(r.token);if(!c){d("token_auth_failed",{ip:t});const h=A.recordFailure(t);return h.permanentLock&&(T.revokeAll(),d("permanent_lock",{ip:t,failures:h.failures})),m("Invalid or expired token",{permanentLock:h.permanentLock,retryAfterMs:h.retryAfterMs}),!0}return T.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(!g.passwordHash){if(!g.security.allowRemoteSetup&&!dt(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=xe(r.password);if(C)return m(C),!0;J=!0;try{g.passwordHash=await Ce(r.password),Ge(g),d("password_set",{ip:t})}finally{J=!1}const{stored:W,plaintext:pe}=T.createAccessToken("setup",g.security.tokenTtlDays);return P(e,{type:"auth.ok",setupMode:!0,token:pe}),d("token_created",{ip:t,name:W.name,tokenId:W.id}),d("auth_success",{ip:t,method:"password",tokenName:W.name}),y(),!0}if(!g.passwordHash)return m("Server not configured"),!0;if(!await be(r.password,g.passwordHash)){const C=A.recordFailure(t);return d("auth_failed",{ip:t,method:"password",failures:C.failures,permanentLock:C.permanentLock}),C.permanentLock&&(T.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:me}=T.createAccessToken(S,g.security.tokenTtlDays);return p("password",S),P(e,{type:"auth.ok",setupMode:!1,token:me}),!0}return!1}f?(T.touch(f.tokenHash),p("token",f.name)):(P(e,{type:"auth.required",setupMode:!g.passwordHash}),s.authTimeout=setTimeout(()=>{s.authenticated||(d("auth_timeout",{ip:t}),m("Authentication timeout"),b(e,4e3,"Auth timeout"))},g.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}),v?.(),l&&(l.dispose(),l=null)}),e.on("error",()=>{s.authTimeout&&clearTimeout(s.authTimeout),R.delete(e),v?.(),l&&(l.dispose(),l=null)})});function at(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?T.validateToken(l):null)){d("ws_rejected_remote_auth",{ip:s,agentId:t}),I(n,401,"Unauthorized");return}const p=ae(t);if(!p){d("ws_rejected_remote_offline",{ip:s,agentId:t}),I(n,503,"Agent offline");return}if(p.relayCount>=Je){I(n,429,"Too many connections to agent");return}const m=e.headers.origin,y=g.security.allowedOrigins;if(m&&!(m===`http://${e.headers.host}`||m===`https://${e.headers.host}`)&&(y.length===0||!y.includes(m))){I(n,403,"Origin not allowed");return}const v=ct(),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(v,f,a),k.bumpRelayConns(t,1),te(f,{agentId:t}),!p.attach(v,f,w)){a.send(JSON.stringify({type:"data",data:`\r
|
|
5
5
|
\x1B[31magent offline\x1B[0m\r
|
|
6
|
-
`})),a.close(1011,"agent offline"),p.relayClosed(
|
|
6
|
+
`})),a.close(1011,"agent offline"),p.relayClosed(v),k.bumpRelayConns(t,-1);return}a.on("message",c=>{const h=typeof c=="string"?c:c.toString("utf-8");let S;try{S=JSON.parse(h)}catch{return}(S.type==="input"||S.type==="resize"||S.type==="load_history")&&p.sendToAgent(v,S)});const r=()=>{p.relayClosed(v),k.bumpRelayConns(t,-1)};a.on("close",r),a.on("error",r)})}let it=1;function ct(){return it++}function lt(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){k.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 y;try{y=JSON.parse(m)}catch{b(s,1008,"bad hello");return}if(y.type!=="hello"||typeof y.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=De(y.token);if(!w){A.recordFailure(t),d("agent_bad_token",{ip:t}),ne(s,{type:"hello_err",reason:"invalid token"}),b(s,4003,"invalid token");return}const a=typeof y.name=="string"&&y.name.trim()?y.name.trim().slice(0,64):"agent",r=w.id;if(!k.register(r,a,s,w.tokenHash)){d("agent_cap_reached",{ip:t}),ne(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 We(s,r,{onSessions:h=>k.setSessions(r,h),onActivity:h=>{for(const S of h)de({type:"session.activity",agentId:r,session:S.session,state:S.state})},onRebuildResult:h=>{console.log(`[agent ${r}] rebuild ${h.session}: ${h.ok?"ok":h.message}`)},onClose:h=>{_.get(r)===h&&_.delete(r)}}),_.set(r,i),k.touch(r),s.send(JSON.stringify({type:"hello_ok",agentId:r})),i.sendSessionsReq()}),s.on("close",()=>{i&&(i.dispose(),_.delete(i.agentId),k.unregister(i.agentId),d("agent_disconnected",{ip:t,agentId:i.agentId})),clearTimeout(l)}),s.on("error",()=>{i&&(i.dispose(),_.delete(i.agentId),k.unregister(i.agentId)),clearTimeout(l)})})}const ut=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=k.pruneOffline(ut);for(const o of e){const t=k.peek(o);t&&t.ws.readyState===E.CLOSED&&(k.unregister(o),_.get(o)?.dispose(),_.delete(o))}const n=new Set(ze().map(o=>o.tokenHash));for(const[o,t]of _){const f=k.peek(o);if(!(!f||n.has(f.tokenHash))){d("agent_token_revoked",{agentId:o}),t.dispose(),_.delete(o),k.unregister(o);try{f.ws.close(4003,"token revoked")}catch{}}}T.purgeExpired()},10*1e3).unref();function fe(){ce(),He(),Fe();for(const e of _.values())e.dispose();_.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 re)try{e.kill()}catch{}re.clear(),A.dispose(),process.exit(0)}process.on("SIGINT",fe),process.on("SIGTERM",fe);function dt(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}async function ft(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
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)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const i=1,f=2;function u(t,e,n){const s=typeof n=="string"?Buffer.from(n,"utf-8"):Buffer.isBuffer(n)?n:Buffer.from(n),r=Buffer.allocUnsafe(5+s.length);return r[0]=t,r.writeUInt32BE(e>>>0,1),s.copy(r,5),r}function c(t){return t.length<5?null:{kind:t[0],id:t.readUInt32BE(1),payload:t.subarray(5)}}function l(t){if(typeof t!="object"||t===null)return!1;const e=t.type;return typeof e=="string"&&o.has(e)}function a(t){if(typeof t!="object"||t===null)return!1;const e=t.type;return typeof e=="string"&&_.has(e)}const o=new Set(["hello","sessions","http_resp","http_body_end","attach_ok","attach_err","ws_to_hub","ws_close","activity","rebuild_session_result","ping"]),_=new Set(["hello_ok","hello_err","sessions_req","http_req","http_body_end","attach","ws_to_agent","rebuild_session","detach","pong"]);export{f as BIN_KIND_HTTP_BODY,i as BIN_KIND_TERM,c as decodeBinaryFrame,u as encodeBinaryFrame,l as isAgentToHubMessage,a as isHubToAgentMessage};
|
package/dist/lib/agent-client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocket as
|
|
1
|
+
import{WebSocket as w}from"ws";import{attachTerminal as F}from"./attach-terminal.js";import{encodeBinaryFrame as G,decodeBinaryFrame as X,BIN_KIND_HTTP_BODY as O}from"./agent-channel.js";import{ActivityProbe as Y}from"./activity-probe.js";import{newTmuxSession as z}from"./tmux-windows.js";const Q=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),V=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),Z=new Set(["content-type","content-disposition","location","cache-control","etag"]);function ae(r){const k=r.log??(e=>console.log(`[agent] ${e}`)),h=e=>console.error(`[agent] ${e}`);let s=null,I=1e3,v=null,T=!1;const u=new Map,y=new Map,p=new Map,E=new Set;function H(e){return E.has(e)?Promise.resolve():new Promise(t=>{p.set(e,t),setTimeout(()=>{p.delete(e)&&t()},3e4)})}let _=null,S=null,g=null,m=null,M=new Map,C="",B=null;const b={state:"idle",agentId:null,hub:r.hub,name:r.name};function f(e,t){t!==void 0&&(b.lastError=t),b.state=e,b.agentId=B,r.onStatus?.({...b})}function c(e){s&&s.readyState===w.OPEN&&s.send(JSON.stringify(e))}function P(){let e;try{e=r.listSessions()}catch{return}const t=JSON.stringify(e);t!==C&&(C=t,c({type:"sessions",sessions:e}))}function R(){_||(_=setInterval(P,V)),S||(S=setInterval(()=>c({type:"ping",ts:Date.now()}),Q)),g||(m=new Y,g=setInterval(()=>void q(),m.scanMs))}function L(){_&&(clearInterval(_),_=null),S&&(clearInterval(S),S=null),g&&(clearInterval(g),g=null),m=null}async function q(){if(!(!s||s.readyState!==w.OPEN||!m))try{const e=await m.scan(),t=[];for(const[n,a]of e)M.get(n)!==a&&t.push({session:n,state:a});M=e,t.length&&c({type:"activity",activities:t})}catch{}}async function U(e){let t;if(e.hasBody){await H(e.id),E.delete(e.id);const o=y.get(e.id);y.delete(e.id),t=o&&o.length?Buffer.concat(o):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let a;try{const o={method:e.method,headers:e.headers};t&&t.length&&(o.body=t),a=await Promise.race([r.app.request(new Request(n,o)),new Promise((ee,W)=>setTimeout(()=>W(new Error("app.request timed out")),3e4))])}catch(o){h(`tunnel request failed: ${e.method} ${e.path} ${o.message}`),c({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const d={};a.headers.forEach((o,l)=>{Z.has(l.toLowerCase())&&(d[l]=o)});const i=Buffer.from(await a.arrayBuffer());if(c({type:"http_resp",id:e.id,status:a.status,headers:d,hasBody:i.length>0}),i.length>0){for(let l=0;l<i.length;l+=65536){if(!s||s.readyState!==w.OPEN)return;s.send(G(O,e.id,i.subarray(l,l+65536)))}c({type:"http_body_end",id:e.id})}}function x(e,t){let n=!1,a="";try{z(e,t),n=!0}catch(d){a=d instanceof Error?d.message:"rebuild failed"}s&&s.readyState===w.OPEN&&s.send(JSON.stringify({type:"rebuild_session_result",session:e,ok:n,message:a})),P()}function J(e){const t=e.session;let n=!1;try{n=r.listSessions().some(i=>i.name===t)}catch{n=!1}if(!n){c({type:"attach_err",connId:e.connId,session:t,message:"no such session"});return}let a=null;const d=F(t,{send:i=>c({type:"ws_to_hub",connId:e.connId,msg:i}),onMessage:i=>{a=i},onClose:()=>{}},{terminalBufferConfig:r.terminalBufferConfig,getSessionPaneTarget:r.getSessionPaneTarget,capturePaneTail:r.capturePaneTail,capturePaneHistoryChunk:r.capturePaneHistoryChunk,toCrlf:r.toCrlf,handleClientMessage:r.handleClientMessage,acquireControlClient:r.acquireControlClient,initialCols:e.cols,initialRows:e.rows,tmuxSocketPath:r.tmuxSocketPath,onPtyExit:(i,o)=>{c({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${o})`})},onSpawnError:(i,o)=>{h(`attach failed for session "${e.session}": ${o}`),c({type:"attach_err",connId:e.connId,session:t,message:o})},onAttached:i=>{c({type:"attach_ok",connId:e.connId,session:t})}});u.set(e.connId,{onMessage:i=>a?.(i),dispose:()=>{u.delete(e.connId),d.dispose()}})}function D(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":B=t.agentId,I=1e3,k(`connected to hub as "${B}"`),f("connected"),P(),R();break;case"hello_err":if(h(`hub rejected us: ${t.reason}`),f("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":P();break;case"http_req":U(t);break;case"http_body_end":{E.add(t.id);const n=p.get(t.id);n&&(p.delete(t.id),n());break}case"attach":J(t);break;case"rebuild_session":x(t.session,t.dir);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=X(e);if(t&&t.kind===O){const n=y.get(t.id)??[];n.push(t.payload),y.set(t.id,n)}}function N(){L();for(const e of u.values())try{e.dispose()}catch{}u.clear(),y.clear(),p.clear(),E.clear(),C=""}function A(){if(s){try{s.removeAllListeners()}catch{}s=null}if(N(),T)return;f("connecting");const e=I+Math.random()*500;k(`reconnecting in ${Math.round(e)}ms\u2026`),v=setTimeout($,e),I=Math.min(I*2,6e4)}function $(){if(T||s)return;const e=r.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let t;try{t=new w(e)}catch(n){h(`bad hub URL: ${n.message} (${e})`),f("error",`bad hub URL: ${n.message}`);return}s=t,f("connecting"),t.on("open",()=>{k(`connecting to ${e}\u2026`),c({type:"hello",token:r.token,name:r.name,version:r.version})}),t.on("message",(n,a)=>{a?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&&A()}),t.on("error",n=>{h(`connection error: ${n.message}`),s===t&&A()})}function K(){if(!T){if(T=!0,v&&clearTimeout(v),N(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}B=null,f("idle")}}return $(),{stop:K,status:()=>({...b})}}export{ae as startAgentClient};
|
package/dist/lib/agent-relay.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{WebSocket as r}from"ws";import{decodeBinaryFrame as
|
|
2
|
-
\x1B[31m--- agent disconnected (${
|
|
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{
|
|
1
|
+
import{WebSocket as r}from"ws";import{decodeBinaryFrame as u,encodeBinaryFrame as f,BIN_KIND_TERM as g,BIN_KIND_HTTP_BODY as h}from"./agent-channel.js";const S=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 k{ws;agentId;pending=new Map;relays=new Map;onSessions;onActivity;onRebuildResult;onClose;disposed=!1;constructor(t,e,s){this.ws=t,this.agentId=e,this.onSessions=s.onSessions,this.onActivity=s.onActivity,this.onRebuildResult=s.onRebuildResult,this.onClose=s.onClose}send(t){return this.disposed||this.ws.readyState!==r.OPEN?!1:(this.ws.send(JSON.stringify(t)),!0)}attach(t,e,s){return this.send({type:"attach",connId:t,session:e,cols:s?.cols,rows:s?.rows})}detach(t){this.send({type:"detach",connId:t})}sendToAgent(t,e){this.send({type:"ws_to_agent",connId:t,msg:e})}sendSessionsReq(){this.send({type:"sessions_req"})}rebuildSession(t,e){return this.send({type:"rebuild_session",session:t,dir:e})}async httpRequest(t,e,s,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:t,path:e,headers:s,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(f(h,i,y))}this.send({type:"http_body_end",id:i})})().catch(()=>{this.send({type:"http_body_end",id:i})})}return c}registerRelay(t,e,s){this.relays.set(t,{connId:t,session:e,ws:s,closed:!1})}relayClosed(t){const e=this.relays.get(t);e&&!e.closed&&(e.closed=!0,this.relays.delete(t),this.detach(t))}get relayCount(){return this.relays.size}handleData(t){const e=Buffer.isBuffer(t)?t:Buffer.from(t),s=u(e);if(s){if(s.kind===g){const n=this.relays.get(s.id);n&&!n.closed&&n.ws.readyState===r.OPEN&&n.ws.send(JSON.stringify({type:"data",data:s.payload.toString("utf-8")}));return}if(s.kind===h){const n=this.pending.get(s.id);n&&n.body.push(s.payload)}}}handleMessage(t){let e;try{e=JSON.parse(t)}catch{return}switch(e.type){case"sessions":this.onSessions?.(e.sessions);break;case"activity":this.onActivity?.(e.activities);break;case"rebuild_session_result":this.onRebuildResult?.({session:e.session,ok:e.ok,message:e.message});break;case"http_resp":{const s=this.pending.get(e.id);if(!s)break;s.status=e.status,s.headers=e.headers,e.hasBody||this.finalizeHttp(s.id);break}case"http_body_end":this.finalizeHttp(e.id);break;case"attach_ok":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify({type:"auth.ok",setupMode:!1}));break}case"attach_err":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify({type:"attach_failed",message:`Agent "${this.agentId}": cannot attach to session "${e.session}": ${e.message}`}));break}case"ws_to_hub":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify(e.msg));break}case"ws_close":{const s=this.relays.get(e.connId);if(s&&!s.closed){s.closed=!0;try{s.ws.close(e.code??1e3,e.reason??"agent closed")}catch{}this.relays.delete(e.connId)}break}case"ping":this.send({type:"pong",ts:e.ts});break;default:break}}finalizeHttp(t){const e=this.pending.get(t);if(!e)return;this.pending.delete(t),clearTimeout(e.timer);const s=e.status??500,n=e.headers??{},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:s,headers:o}))}failAll(t){for(const e of this.pending.values())clearTimeout(e.timer),e.reject(new Error(t));this.pending.clear();for(const e of this.relays.values()){if(!e.closed&&e.ws.readyState===r.OPEN){e.ws.send(JSON.stringify({type:"data",data:`\r
|
|
2
|
+
\x1B[31m--- agent disconnected (${t}) ---\x1B[0m\r
|
|
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{k as AgentChannel,S as MAX_RELAY_CONNS_PER_AGENT,_ as TUNNEL_REQUEST_TIMEOUT_MS};
|
package/dist/lib/build-app.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{existsSync as P,readdirSync as
|
|
1
|
+
import{existsSync as P,readdirSync as le,statSync as A,unlinkSync as me,writeFileSync as fe,mkdirSync as pe,readFileSync as $}from"node:fs";import{readFile as he}from"node:fs/promises";import{execSync as _,execFileSync as G}from"node:child_process";import h from"node:path";import{fileURLToPath as ge}from"node:url";import{Hono as ye}from"hono";import{listSessions as L}from"../sessions.js";import{renderLoginPage as je,renderNotesIndex as we,renderNotesPage as ve,renderSettings as ke,renderThemeSettings as qe,renderHistoryIndex as Se,renderQuickCommandsPage as Te,renderFilesIndex as Ae,renderShell as be}from"../frontend.js";import{renderFederationPage as xe}from"./pages/federation.js";import{createAgentToken as _e,removeAgentToken as Ce,listAgentTokens as z}from"./agent-tokens.js";import{hashPassword as Ie,verifyPassword as Oe,validatePassword as Ee}from"./auth.js";import{atomicWriteFileSync as Ne}from"./atomicWrite.js";import{resolveFsPath as b,resolveFsRoots as B,MAX_FILE_BYTES as V,walkRecursive as Re}from"./fs-access.js";import{captureSessionWindowsWithPath as Pe}from"./tmux-windows.js";import{audit as v}from"./auditLog.js";import{db as S}from"./db.js";import{getSessionAccessMap as Le}from"./session-access.js";import{listWindowHistory as De,clearWindowHistory as Fe}from"./window-history.js";import{saveSecurityConfig as He}from"./security-config.js";import{readSettings as Q,writeSettings as Me}from"./settings.js";import{setActiveThemeTemplate as K}from"./theme-store.js";import{isThemeTemplateId as X,THEME_TEMPLATE_IDS as Ue}from"./themes/index.js";import{pinView as We,unpinView as $e,listPinnedViews as Ge}from"./pinned-views.js";import{getStoredWindows as ze}from"./session-windows.js";import{buildSidebarSessions as Be}from"./sessions-sidebar.js";import{upsertSessionState as Z,removeSessionState as Y,renameSessionState as J,computeTombstones as Ve,getSessionState as Qe}from"./session-state.js";import{createQuickCommand as Ke,deleteQuickCommand as Xe,listQuickCommands as ee,updateQuickCommand as Ze}from"./quick-commands.js";import{getSystemStatus as Ye,getTopProcesses as Je,killProcess as et}from"./system-monitor.js";import{newTmuxSession as te,renameSession as tt,killSession as rt,sessionExists as nt,TmuxWindowsError as re}from"./tmux-windows.js";import{registerAgentTunnel as st}from"./agent-tunnel.js";import{injectScopeScript as ot}from"./scope-script.js";const D="tmux-web-token",it=365;function at(x){return x==="127.0.0.1"||x==="::1"||x==="::ffff:127.0.0.1"}function Ut(x){const{mode:ne,securityConfig:g,tokenStore:j,rateLimiter:I,settings:dt,terminalBufferConfig:O,getAgentId:F,state:l,federation:k,hub:w}=x,q=ne==="agent";function C(e){if(g.security.trustProxy){const t=(e.req.header("x-forwarded-for")||"").split(",")[0].trim();if(t)return t}return e.env?.incoming?.socket?.remoteAddress||"unknown"}function E(e){const t=e.req.header("authorization");if(t?.startsWith("Bearer "))return t.slice(7);const r=e.req.header("cookie");if(!r)return null;const n=r.match(new RegExp(`(?:^|;\\s*)${D}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function ut(e){const t=E(e);return t?j.validateToken(t):null}function H(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${D}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${it*86400}${r?"; Secure":""}`)}function se(e){e.header("Set-Cookie",`${D}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const o=()=>q?async(e,t)=>t():async(e,t)=>{const r=E(e);return!r||!j.validateToken(r)?(v("http_unauthorized",{ip:C(e)}),e.json({error:"unauthorized"},401)):t()},y=()=>q?async(e,t)=>t():async(e,t)=>{const r=E(e);if(!r||!j.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function N(e){const t=L(),r=new Set(t.map(c=>c.name)),n=Be(t,Le(),Ge()),a=[],i=new Map,u=new Map;if(w)for(const c of w.agents.list()){const d=w.agents.get(c.agentId);i.set(c.agentId,new Set((d?d.sessions:[]).map(m=>m.name))),u.set(c.agentId,c.online),a.push({agentId:c.agentId,agentName:c.name,online:c.online,sessions:d?d.sessions:[]})}const p=Ve(r,i,u);return{...n,currentSession:e??null,agents:a,tombstones:p}}function M(e){const t=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!t)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:t};const r=e.windowIndex;return typeof r!="number"||!Number.isInteger(r)||r<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:t,windowIndex:r}}const s=new ye;s.use("*",async(e,t)=>{await t(),e.header("X-Content-Type-Options","nosniff"),e.header("X-Frame-Options","DENY"),e.header("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline'","style-src 'self' 'unsafe-inline' https://fonts.googleapis.com","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self' https://fonts.gstatic.com","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),s.use("*",async(e,t)=>{const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS")return t();const n=e.req.header("sec-fetch-site");return n&&n!=="same-origin"&&n!=="none"?e.text("cross-site request blocked",403):t()}),q||s.use("*",async(e,t)=>{if(await t(),e.req.method!=="GET"||e.req.path==="/login"||!(e.res.headers.get("content-type")??"").includes("text/html"))return;const n=await e.res.text();e.res=new Response(ot(n,{prefix:"",agentId:null}),e.res)});const U=h.dirname(ge(import.meta.url)),oe=[h.join(U,"..","assets"),h.join(U,"assets"),h.join(process.cwd(),"dist","assets")];s.get("/assets/:file",async e=>{const t=e.req.param("file");if(!/^[a-zA-Z0-9._-]+$/.test(t))return e.notFound();for(const r of oe){const n=h.join(r,t);if(!P(n))continue;const a=await he(n),i=h.extname(t),u={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(a,200,{"Content-Type":u[i]??"application/octet-stream","Cache-Control":i===".js"||i===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const ie=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
2
|
<rect x="2.5" y="5.5" width="27" height="21" rx="4" fill="#0d1117" stroke="#7dd3fc" stroke-width="2"/>
|
|
3
3
|
<path d="M8 13l4.2 3.1L8 19.2" fill="none" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
4
|
<line x1="15.5" y1="19.6" x2="22.5" y2="19.6" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round"/>
|
|
5
|
-
</svg>`,W=e=>e.body(
|
|
5
|
+
</svg>`,W=e=>e.body(ie,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",W),s.get("/favicon.ico",W);const ae={name:"tmux-weblink",short_name:"tmux-web",description:"Access your tmux sessions from the browser",start_url:"/",display:"standalone",background_color:"#0d1117",theme_color:"#0d1117",icons:[{src:"/assets/icon-192.png",sizes:"192x192",type:"image/png"},{src:"/assets/icon-512.png",sizes:"512x512",type:"image/png"}]};s.get("/manifest.json",e=>e.json(ae,200,{"Cache-Control":"public, max-age=3600"}));const de=`// tmux-weblink Service Worker
|
|
6
6
|
const CACHE = "tmux-weblink-v1";
|
|
7
7
|
const ASSETS = ["/", "/favicon.svg"];
|
|
8
8
|
self.addEventListener("install", (e) => {
|
|
@@ -13,7 +13,7 @@ self.addEventListener("activate", (e) => { e.waitUntil(clients.claim()); });
|
|
|
13
13
|
self.addEventListener("fetch", (e) => {
|
|
14
14
|
e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
|
|
15
15
|
});
|
|
16
|
-
`;s.get("/sw.js",e=>e.body(ne,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!h.passwordHash,n=e.req.query("error");return e.html(fe({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:u.activeTheme}))}),s.get("/",g(),e=>{const t=B();return e.html(ve({theme:u.activeTheme,fsRoots:t,terminalCfg:E,scrollback:E.initialLines+2*E.historyChunk,wsBase:k&&F?.()?`/ws/a/${F()}`:void 0}))}),s.get("/notes",g(),e=>e.html(pe(S.data.notes,u.activeTheme))),s.get("/notes/:session",g(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(he(t,u.activeTheme))}),s.get("/history",g(),e=>{const t=L(),r=new Set(t.map(n=>n.name));return e.html(je(Ie(),u.activeTheme,r))}),s.get("/quick-commands",g(),e=>e.html(we(Y(),u.activeTheme))),s.get("/files",g(),e=>{const t=B();return e.html(ke(u.activeTheme,t))}),s.post("/api/history/clear",o(),async e=>(await Ne(),e.json({ok:!0}))),s.get("/api/quick-commands",o(),e=>e.json(Y())),s.post("/api/quick-commands",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await We(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Ge(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",o(),e=>$e(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(k)return e.json({error:"hub only"},403);const t=C(e),r=O.check(t);if(!r.allowed)return j("rate_limited",{ip:t,retryAfterMs:r.retryAfterMs,permanentLock:r.permanentLock}),r.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:r.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const i=typeof n.password=="string"?n.password:"";if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!tt(t))return j("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(u.settingUpPassword)return e.json({error:"Password setup in progress"},409);const l=xe(i);if(l)return e.json({error:l},400);u.settingUpPassword=!0;try{h.passwordHash=await Ae(i),Re(h),j("password_set",{ip:t})}finally{u.settingUpPassword=!1}const{plaintext:d}=y.createAccessToken("setup",h.security.tokenTtlDays);return H(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await be(i,h.passwordHash)){const l=O.recordFailure(t);return j("auth_failed",{ip:t,method:"password",failures:l.failures,permanentLock:l.permanentLock}),l.permanentLock?(y.revokeAll(),j("permanent_lock",{ip:t,failures:l.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):l.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:l.retryAfterMs},429)}O.recordSuccess(t);const c=`browser-${t}`,{plaintext:v}=y.createAccessToken(c,h.security.tokenTtlDays);return H(e,v),j("auth_success",{ip:t,method:"password",tokenName:c}),e.json({ok:!0,token:v})}),s.post("/api/auth/token",o(),async e=>{if(k)return e.json({error:"hub only"},403);const t=C(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,i=typeof r.ttlDays=="number"?r.ttlDays:h.security.tokenTtlDays,{stored:a,plaintext:c}=y.createAccessToken(n,i);return j("token_created",{ip:t,name:a.name,tokenId:a.id}),e.json({id:a.id,name:a.name,token:c,expiresAt:a.expiresAt})}),s.get("/api/auth/tokens",o(),e=>k?e.json({error:"hub only"},403):e.json(y.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",o(),e=>k?e.json({error:"hub only"},403):y.revoke(e.req.param("id"))?(j("token_revoked",{ip:C(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",o(),e=>(J(e),e.json({ok:!0}))),s.get("/settings",g(),async e=>{const t=await Q();return e.html(ge({settings:t,theme:u.activeTheme,saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Q(),n=t.defaultView==="recent"?"recent":"default";return await Pe({...r,defaultView:n}),e.redirect("/settings?saved=1",303)}),s.get("/settings/theme",g(),e=>e.html(ye({theme:u.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!X(r)?e.redirect("/settings/theme",303):(u.activeTheme=await K(r),e.redirect("/settings/theme?saved=1",303))});const se={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",o(),e=>e.json({active:u.activeTheme.template,templates:Le.map(t=>({id:t,name:se[t]}))})),s.post("/api/theme",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!X(r)?e.json({error:"invalid theme template"},400):(u.activeTheme=await K(r),e.json({ok:!0,active:u.activeTheme.template}))}),s.get("/api/system/status",o(),e=>e.json(ze())),s.get("/api/system/processes",o(),e=>e.json(Be())),w&&(s.get("/settings/federation",g(),e=>e.redirect("/settings/machines",301)),s.get("/settings/machines",g(),async e=>e.html(qe({config:w.getConfig(),status:w.status(),tokens:z().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:w.hostname},u.activeTheme))),s.get("/api/machines",o(),e=>e.json({config:w.getConfig(),status:w.status()})),s.post("/api/machines",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.hub=="string"?t.hub.trim():"",n=typeof t.token=="string"?t.token.trim():"",i=typeof t.name=="string"?t.name.trim().slice(0,64):"",a=t.enabled===!0;if(a){if(!r)return e.json({error:"hub URL is required"},400);if(!/^wss?:\/\//i.test(r))return e.json({error:"hub URL must start with ws:// or wss://"},400);if(!n)return e.json({error:"registration token is required"},400)}const c={hub:r||void 0,token:n||void 0,name:i||void 0,enabled:a};return await w.setConfig(c),e.json({ok:!0,status:w.status()})}),s.get("/api/agent-tokens",o(),e=>e.json(z().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim().slice(0,64):"";if(!r)return e.json({error:"name is required"},400);const{id:n,token:i}=Se(r);return e.json({ok:!0,id:n,name:r,token:i})}),s.delete("/api/agent-tokens/:id",o(),e=>{const t=e.req.param("id");return Te(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",o(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=Ve(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",o(),e=>q?e.json(q.agents.list()):e.json([])),s.post("/api/sessions/new",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(L().some(a=>a.name===r))return e.json({error:"session already exists"},409);try{return Qe(r,n),e.json({ok:!0})}catch(a){const c=a instanceof Ye?a.message:"failed to create session";return e.json({error:c},500)}}),s.post("/api/sessions/rename",o(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(Ke(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",o(),async e=>{try{const{name:t}=await e.req.json();return t?(Xe(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",o(),e=>{const t=e.req.query("session");if(!t)return e.json({path:process.env.HOME??"/"});try{const r=Oe(t),i=r.find(a=>a.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:i})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",o(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=p.join(t,r));const n=e.req.query("recursive")==="true";let i=r,a="",c=r.endsWith("/");if(!c)try{c=A(r).isDirectory()}catch{}c||(i=p.dirname(r),a=p.basename(r).toLowerCase());try{const v=ie(i),l=[],d=[];for(const m of v)if(!m.startsWith(".")&&!(a&&!m.toLowerCase().startsWith(a))){try{const f=p.join(i,m);A(f).isDirectory()?(l.push(f),n&&Ce(f,l,d,0)):d.push(f)}catch{}if(l.length+d.length>=5e3)break}return e.json({dirs:l,files:d})}catch{return e.json({dirs:[],files:[]})}});function oe(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=_("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=_("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=_("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let i="";try{i=_("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const a=[];for(const d of i.split(`
|
|
17
|
-
`)){if(!d.trim())continue;const m=d[0],f=d[1],T=d.substring(3).trim(),R=f!==" "?f:m;
|
|
18
|
-
`)){if(!m.trim())continue;const f=m.split(" ");if(f.length>=3){const T=parseInt(f[0],10)||0,R=parseInt(f[1],10)||0;
|
|
19
|
-
`).length;d.additions=T,
|
|
16
|
+
`;s.get("/sw.js",e=>e.body(de,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!g.passwordHash,n=e.req.query("error");return e.html(je({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:l.activeTheme}))}),s.get("/",y(),e=>{const t=B();return e.html(be({theme:l.activeTheme,fsRoots:t,terminalCfg:O,scrollback:O.initialLines+2*O.historyChunk,wsBase:q&&F?.()?`/ws/a/${F()}`:void 0}))}),s.get("/notes",y(),e=>e.html(we(S.data.notes,l.activeTheme))),s.get("/notes/:session",y(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(ve(t,l.activeTheme))}),s.get("/history",y(),e=>{const t=L(),r=new Set(t.map(n=>n.name));return e.html(Se(De(),l.activeTheme,r))}),s.get("/quick-commands",y(),e=>e.html(Te(ee(),l.activeTheme))),s.get("/files",y(),e=>{const t=B();return e.html(Ae(l.activeTheme,t))}),s.post("/api/history/clear",o(),async e=>(await Fe(),e.json({ok:!0}))),s.get("/api/quick-commands",o(),e=>e.json(ee())),s.post("/api/quick-commands",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Ke(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Ze(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",o(),e=>Xe(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(q)return e.json({error:"hub only"},403);const t=C(e),r=I.check(t);if(!r.allowed)return v("rate_limited",{ip:t,retryAfterMs:r.retryAfterMs,permanentLock:r.permanentLock}),r.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:r.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const a=typeof n.password=="string"?n.password:"";if(!g.passwordHash){if(!g.security.allowRemoteSetup&&!at(t))return v("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(l.settingUpPassword)return e.json({error:"Password setup in progress"},409);const c=Ee(a);if(c)return e.json({error:c},400);l.settingUpPassword=!0;try{g.passwordHash=await Ie(a),He(g),v("password_set",{ip:t})}finally{l.settingUpPassword=!1}const{plaintext:d}=j.createAccessToken("setup",g.security.tokenTtlDays);return H(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await Oe(a,g.passwordHash)){const c=I.recordFailure(t);return v("auth_failed",{ip:t,method:"password",failures:c.failures,permanentLock:c.permanentLock}),c.permanentLock?(j.revokeAll(),v("permanent_lock",{ip:t,failures:c.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):c.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:c.retryAfterMs},429)}I.recordSuccess(t);const u=`browser-${t}`,{plaintext:p}=j.createAccessToken(u,g.security.tokenTtlDays);return H(e,p),v("auth_success",{ip:t,method:"password",tokenName:u}),e.json({ok:!0,token:p})}),s.post("/api/auth/token",o(),async e=>{if(q)return e.json({error:"hub only"},403);const t=C(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,a=typeof r.ttlDays=="number"?r.ttlDays:g.security.tokenTtlDays,{stored:i,plaintext:u}=j.createAccessToken(n,a);return v("token_created",{ip:t,name:i.name,tokenId:i.id}),e.json({id:i.id,name:i.name,token:u,expiresAt:i.expiresAt})}),s.get("/api/auth/tokens",o(),e=>q?e.json({error:"hub only"},403):e.json(j.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",o(),e=>q?e.json({error:"hub only"},403):j.revoke(e.req.param("id"))?(v("token_revoked",{ip:C(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",o(),e=>(se(e),e.json({ok:!0}))),s.get("/settings",y(),async e=>{const t=await Q();return e.html(ke({settings:t,theme:l.activeTheme,saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Q(),n=t.defaultView==="recent"?"recent":"default";return await Me({...r,defaultView:n}),e.redirect("/settings?saved=1",303)}),s.get("/settings/theme",y(),e=>e.html(qe({theme:l.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!X(r)?e.redirect("/settings/theme",303):(l.activeTheme=await K(r),e.redirect("/settings/theme?saved=1",303))});const ue={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",o(),e=>e.json({active:l.activeTheme.template,templates:Ue.map(t=>({id:t,name:ue[t]}))})),s.post("/api/theme",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!X(r)?e.json({error:"invalid theme template"},400):(l.activeTheme=await K(r),e.json({ok:!0,active:l.activeTheme.template}))}),s.get("/api/system/status",o(),e=>e.json(Ye())),s.get("/api/system/processes",o(),e=>e.json(Je())),k&&(s.get("/settings/federation",y(),e=>e.redirect("/settings/machines",301)),s.get("/settings/machines",y(),async e=>e.html(xe({config:k.getConfig(),status:k.status(),tokens:z().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:k.hostname},l.activeTheme))),s.get("/api/machines",o(),e=>e.json({config:k.getConfig(),status:k.status()})),s.post("/api/machines",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.hub=="string"?t.hub.trim():"",n=typeof t.token=="string"?t.token.trim():"",a=typeof t.name=="string"?t.name.trim().slice(0,64):"",i=t.enabled===!0;if(i){if(!r)return e.json({error:"hub URL is required"},400);if(!/^wss?:\/\//i.test(r))return e.json({error:"hub URL must start with ws:// or wss://"},400);if(!n)return e.json({error:"registration token is required"},400)}const u={hub:r||void 0,token:n||void 0,name:a||void 0,enabled:i};return await k.setConfig(u),e.json({ok:!0,status:k.status()})}),s.get("/api/agent-tokens",o(),e=>e.json(z().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim().slice(0,64):"";if(!r)return e.json({error:"name is required"},400);const{id:n,token:a}=_e(r);return e.json({ok:!0,id:n,name:r,token:a})}),s.delete("/api/agent-tokens/:id",o(),e=>{const t=e.req.param("id");return Ce(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",o(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=et(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",o(),e=>w?e.json(w.agents.list()):e.json([])),s.post("/api/sessions/new",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(L().some(i=>i.name===r))return e.json({error:"session already exists"},409);try{return te(r,n),Z(r,{path:n}),e.json({ok:!0})}catch(i){const u=i instanceof re?i.message:"failed to create session";return e.json({error:u},500)}}),s.post("/api/sessions/rename",o(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(tt(t,r),J(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",o(),async e=>{try{const{name:t}=await e.req.json();return t?(rt(t),Y(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.post("/api/sessions/rebuild",o(),async e=>{try{const t=await e.req.json(),r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name required"},400);const n=typeof t.agentId=="string"?t.agentId:void 0,i=Qe().find(u=>u.name===r&&(u.agentId??null)===(n??null))?.path;if(n){if(!w)return e.json({error:"not a hub"},400);const u=w.getChannel(n);return u?u.rebuildSession(r,i)?e.json({ok:!0,async:!0}):e.json({error:"agent offline"},409):e.json({error:"agent offline"},409)}if(nt(r))return e.json({error:"session already exists"},409);try{te(r,i)}catch(u){const p=u instanceof re?u.message:"rebuild failed";return e.json({error:p},500)}return Z(r,{path:i}),e.json({ok:!0})}catch{return e.json({error:"rebuild failed"},500)}}),s.post("/api/sessions/dismiss",o(),async e=>{try{const t=await e.req.json(),r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name required"},400);const n=typeof t.agentId=="string"?t.agentId:void 0;return Y(r,n),e.json({ok:!0})}catch{return e.json({error:"dismiss failed"},500)}}),s.post("/api/sessions/rename-tombstone",o(),async e=>{try{const t=await e.req.json(),r=typeof t.oldName=="string"?t.oldName.trim():"",n=typeof t.newName=="string"?t.newName.trim():"";if(!r||!n)return e.json({error:"oldName and newName required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(n))return e.json({error:"invalid characters in name"},400);const a=typeof t.agentId=="string"?t.agentId:void 0;return J(r,n,a),e.json({ok:!0})}catch{return e.json({error:"rename failed"},500)}}),s.get("/api/fs/session-path",o(),e=>{const t=e.req.query("session");if(!t)return e.json({path:process.env.HOME??"/"});try{const r=Pe(t),a=r.find(i=>i.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:a})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",o(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=h.join(t,r));const n=e.req.query("recursive")==="true";let a=r,i="",u=r.endsWith("/");if(!u)try{u=A(r).isDirectory()}catch{}u||(a=h.dirname(r),i=h.basename(r).toLowerCase());try{const p=le(a),c=[],d=[];for(const m of p)if(!m.startsWith(".")&&!(i&&!m.toLowerCase().startsWith(i))){try{const f=h.join(a,m);A(f).isDirectory()?(c.push(f),n&&Re(f,c,d,0)):d.push(f)}catch{}if(c.length+d.length>=5e3)break}return e.json({dirs:c,files:d})}catch{return e.json({dirs:[],files:[]})}});function ce(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=_("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=_("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=_("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let a="";try{a=_("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const i=[];for(const d of a.split(`
|
|
17
|
+
`)){if(!d.trim())continue;const m=d[0],f=d[1],T=d.substring(3).trim(),R=f!==" "?f:m;i.push({path:T,status:R,staged:m,unstaged:f,additions:0,deletions:0})}const u=new Map;try{const d=_("git diff --numstat HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3});for(const m of d.split(`
|
|
18
|
+
`)){if(!m.trim())continue;const f=m.split(" ");if(f.length>=3){const T=parseInt(f[0],10)||0,R=parseInt(f[1],10)||0;u.set(f[2],{added:T,deleted:R})}}}catch{}for(const d of i){const m=u.get(d.path);m&&(d.additions=m.added,d.deletions=m.deleted)}let p=0,c=0;for(const d of i)p+=d.additions,c+=d.deletions;for(const d of i)if(d.status==="?"&&d.additions===0)try{const m=h.join(r,d.path),T=$(m,"utf-8").split(`
|
|
19
|
+
`).length;d.additions=T,p+=T}catch{}return{repoRoot:r,branch:n,files:i,linesAdded:p,linesRemoved:c}}catch{return t}}return s.get("/api/git/status",o(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=b(t),n=ce(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",o(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=b(t);if(!P(n)||!A(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const a=G("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),i=G("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:a,stagedDiff:i})}catch(a){throw console.error("[git/diff] execFileSync failed:",a?.code,a.message),a}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",o(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=b(t);if(!A(r).isFile())return e.json({error:"not a file"},400);const n=A(r).size;if(n>V)return e.json({error:"file too large",size:n,maxBytes:V},413);const a=$(r,"utf-8");return e.json({path:r,content:a,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",o(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=b(t.path);return Ne(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=b(t.path);return A(r).isFile()?(me(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=b(t.path);return P(r)?e.json({error:"file already exists"},409):(pe(h.dirname(r),{recursive:!0}),fe(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}}),s.get("/api/sidebar/sessions",o(),e=>{const t=e.req.query("currentSession");return e.json(N(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",o(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(ze(t))}),s.post("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=M(t);return"error"in r?e.json({error:r.error},400):(await We(r.sessionName,r.windowIndex),e.json(N()))}),s.delete("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=M(t);return"error"in r?e.json({error:r.error},400):(await $e(r.sessionName,r.windowIndex),e.json(N()))}),s.get("/api/notes",o(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",o(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=S.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",o(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},a=S.data.notes.findIndex(i=>i.scope===t);return a>=0?S.data.notes[a]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),w&&(s.use("/a/:agentId/*",o()),st(s,{getChannel:w.getChannel})),s}export{Ut as buildApp};
|
package/dist/lib/db.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Low as
|
|
1
|
+
import{Low as s}from"lowdb";import{JSONFile as i}from"lowdb/node";import{mkdirSync as e}from"node:fs";import{join as n}from"node:path";import{getDataRoot as r}from"./state-paths.js";const o=r();e(o,{recursive:!0});const a=new s(new i(n(o,"db.json")),{notes:[],sessionAccess:[],pinnedViews:[],watchedPanes:[],windowLabels:[],sessionWindows:[],windowHistory:[],quickCommands:[],sessionState:[]});export{a as db};
|
package/dist/lib/pages/shell.js
CHANGED
|
@@ -138,6 +138,10 @@ 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.session-tombstone { opacity: 0.55; cursor: default; }
|
|
142
|
+
.session-item.session-tombstone span { text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--panel-muted) 70%, transparent); text-underline-offset: 3px; }
|
|
143
|
+
.session-item.session-tombstone:hover { background: transparent; }
|
|
144
|
+
.tombstone-label { opacity: 0.8; }
|
|
141
145
|
.session-item.working svg { color: #f5a623; }
|
|
142
146
|
.session-item.just-done svg { color: #3fb950; }
|
|
143
147
|
.session-item .meta { margin-left: auto; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{db as s}from"./db.js";const S=Number(process.env.TMUX_WEB_TOMBSTONE_TTL_DAYS||"30");function c(){return s.data.sessionState??=[],s.data.sessionState}async function u(d,n={}){const i=Date.now();s.data.sessionState??=[];const e=s.data.sessionState,a=e.findIndex(o=>o.name===d&&(o.agentId??null)===(n.agentId??null));a>=0?(e[a].lastSeenAt=i,n.path&&(e[a].path=n.path)):e.push({name:d,agentId:n.agentId,path:n.path,firstSeenAt:i,lastSeenAt:i}),await s.write()}async function f(d,n){s.data.sessionState??=[];const i=s.data.sessionState,e=i.findIndex(a=>a.name===d&&(a.agentId??null)===(n??null));e<0||(i.splice(e,1),await s.write())}async function g(d,n,i){s.data.sessionState??=[];const e=s.data.sessionState,a=e.findIndex(o=>o.name===d&&(o.agentId??null)===(i??null));a<0||(e[a].name=n,await s.write())}function p(d,n,i){const e=Date.now(),a=S*24*60*60*1e3,o=[];for(const t of c()){if(e-t.lastSeenAt>a)continue;let l=!1;t.agentId===void 0?l=d.has(t.name):l=n.get(t.agentId)?.has(t.name)??!1,!l&&o.push({name:t.name,path:t.path,agentId:t.agentId,agentOnline:t.agentId===void 0?!0:i.get(t.agentId)??!1,lastSeenAt:t.lastSeenAt})}return o.sort((t,l)=>l.lastSeenAt-t.lastSeenAt),o}export{p as computeTombstones,c as getSessionState,f as removeSessionState,g as renameSessionState,u as upsertSessionState};
|
package/dist/lib/tmux-windows.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import{execFileSync as r}from"node:child_process";import{tmuxEnv as a}from"./tmux-env.js";class s extends Error{status;constructor(e,n){super(e),this.status=n,this.name="TmuxWindowsError"}}function m(t){try{return r("tmux",["list-windows","-t",t,"-F","#{window_index} #{window_name} #{window_active}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
|
|
2
2
|
`).filter(Boolean).map(n=>{const[o,i,w]=n.split(" ");return{index:parseInt(o,10),name:i,active:w==="1"}})}catch{return[]}}function x(t){try{return r("tmux",["list-windows","-t",t,"-F","#{window_index} #{window_name} #{window_active} #{pane_current_path}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
|
|
3
3
|
`).filter(Boolean).map(n=>{const[o,i,w,c]=n.split(" ");return{index:parseInt(o,10),name:i,active:w==="1",path:c??""}})}catch{return[]}}function p(t){if(!t)return!1;try{const e=r("git",["-C",t,"rev-parse","--git-dir","--git-common-dir"],{encoding:"utf-8",timeout:3e3,stdio:["ignore","pipe","ignore"]}),[n,o]=e.trim().split(`
|
|
4
|
-
`);return!n||!o?!1:n!==o}catch{return!1}}function u(t){try{return r("tmux",["has-session","-t",t],{timeout:3e3,env:a()}),!0}catch{return!1}}function l(t){if(!u(t))throw new s("session not found",404);try{r("tmux",["new-window","-t",t],{timeout:3e3,env:a()})}catch(e){const n=e instanceof Error?e.message:"new-window failed";throw new s(n,500)}}function h(t,e){const n=["new-session","-d","-s",t];e&&n.push("-c",e);try{r("tmux",n,{timeout:5e3,env:a()})}catch(o){const i=o instanceof Error?o.message:"new-session failed";throw new s(i,500)}}function g(t,e){r("tmux",["rename-session","-t",t,e],{timeout:5e3,env:a()})}function v(t){r("tmux",["kill-session","-t",t],{timeout:5e3,env:a()})}function y(t,e){if(!u(t))throw new s("session not found",404);if(!m(t).some(o=>o.index===e))throw new s("window not found",404);try{r("tmux",["select-window","-t",`${t}:${e}`],{timeout:3e3})}catch(o){const i=o instanceof Error?o.message:"select-window failed";throw new s(i,500)}}function E(t,e,n){if(!u(t))throw new s("session not found",404);if(!m(t).some(w=>w.index===e))throw new s("window not found",404);const i=n.trim();if(!i)throw new s("name is required",500);try{r("tmux",["rename-window","-t",`${t}:${e}`,i],{timeout:3e3})}catch(w){const c=w instanceof Error?w.message:"rename-window failed";throw new s(c,500)}}export{s as TmuxWindowsError,x as captureSessionWindowsWithPath,p as isGitWorktree,v as killSession,m as listSessionWindows,l as newSessionWindow,h as newTmuxSession,g as renameSession,E as renameSessionWindow,y as selectSessionWindow};
|
|
4
|
+
`);return!n||!o?!1:n!==o}catch{return!1}}function u(t){try{return r("tmux",["has-session","-t",t],{timeout:3e3,env:a()}),!0}catch{return!1}}function l(t){if(!u(t))throw new s("session not found",404);try{r("tmux",["new-window","-t",t],{timeout:3e3,env:a()})}catch(e){const n=e instanceof Error?e.message:"new-window failed";throw new s(n,500)}}function h(t,e){const n=["new-session","-d","-s",t];e&&n.push("-c",e);try{r("tmux",n,{timeout:5e3,env:a()})}catch(o){const i=o instanceof Error?o.message:"new-session failed";throw new s(i,500)}}function g(t,e){r("tmux",["rename-session","-t",t,e],{timeout:5e3,env:a()})}function v(t){r("tmux",["kill-session","-t",t],{timeout:5e3,env:a()})}function y(t,e){if(!u(t))throw new s("session not found",404);if(!m(t).some(o=>o.index===e))throw new s("window not found",404);try{r("tmux",["select-window","-t",`${t}:${e}`],{timeout:3e3})}catch(o){const i=o instanceof Error?o.message:"select-window failed";throw new s(i,500)}}function E(t,e,n){if(!u(t))throw new s("session not found",404);if(!m(t).some(w=>w.index===e))throw new s("window not found",404);const i=n.trim();if(!i)throw new s("name is required",500);try{r("tmux",["rename-window","-t",`${t}:${e}`,i],{timeout:3e3})}catch(w){const c=w instanceof Error?w.message:"rename-window failed";throw new s(c,500)}}export{s as TmuxWindowsError,x as captureSessionWindowsWithPath,p as isGitWorktree,v as killSession,m as listSessionWindows,l as newSessionWindow,h as newTmuxSession,g as renameSession,E as renameSessionWindow,y as selectSessionWindow,u as sessionExists};
|