tmux-weblink 2.0.15 → 2.0.16

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/README.md CHANGED
@@ -29,12 +29,6 @@ PORT=8080 tmux-weblink
29
29
  # Use the default xterm.js renderer
30
30
  tmux-weblink
31
31
 
32
- # Optional: use ghostty-web for terminal rendering
33
- tmux-weblink --ghostty
34
-
35
- # Equivalent environment override
36
- TMUX_WEB_TERMINAL_RENDERER=ghostty tmux-weblink
37
-
38
32
  # Optional: tail-first buffer loading (see docs/architecture.md)
39
33
  TMUX_WEB_INITIAL_LINES=1000 TMUX_WEB_HISTORY_CHUNK=500 tmux-weblink
40
34
 
package/dist/agent.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{createRequire as f}from"node:module";import{hostname as p}from"node:os";import{listSessions as l,resolveTmuxSocketPath as u}from"./sessions.js";import{loadSecurityConfig as T}from"./lib/security-config.js";import{TokenStore as d}from"./lib/auth.js";import{RateLimiter as h}from"./lib/rateLimiter.js";import{db as n}from"./lib/db.js";import{handleClientMessage as E}from"./lib/ws-message.js";import{loadDotEnv as _}from"./lib/load-env.js";import{readSettings as g}from"./lib/settings.js";import{readActiveTheme as k}from"./lib/theme-store.js";import{buildApp as A}from"./lib/build-app.js";import{acquireControlClient as N}from"./lib/tmux-control.js";import{getSessionPaneTarget as b,capturePaneTail as C,capturePaneHistoryChunk as S,toCrlf as w}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as B}from"./lib/terminal-config.js";import{startAgentClient as M}from"./lib/agent-client.js";_();const U=f(import.meta.url),{version:W}=U("../package.json");function G(o){let r=process.env.TMUX_WEB_AGENT_HUB||"",i=process.env.TMUX_WEB_AGENT_TOKEN||"",s=process.env.TMUX_WEB_AGENT_NAME||p();for(let t=0;t<o.length;t++){const e=o[t];if(e!=="agent"){if((e==="--hub"||e==="-h")&&o[t+1]){r=o[++t];continue}if((e==="--token"||e==="-t")&&o[t+1]){i=o[++t];continue}if((e==="--name"||e==="-n")&&o[t+1]){s=o[++t];continue}e.startsWith("--hub=")&&(r=e.slice(6)),e.startsWith("--token=")&&(i=e.slice(8)),e.startsWith("--name=")&&(s=e.slice(7))}}return r||(console.error("usage: tmux-web agent --hub wss://HOST[:PORT] --token TOKEN [--name NAME]"),console.error(" (or set TMUX_WEB_AGENT_HUB / TMUX_WEB_AGENT_TOKEN)"),process.exit(1)),i||(console.error("agent token required (--token or TMUX_WEB_AGENT_TOKEN)"),process.exit(1)),{hub:r,token:i,name:s.slice(0,64)}}const{hub:P,token:x,name:O}=G(process.argv.slice(2)),a=B(),R=T(),X=new d,q=new h;await n.read(),n.data.sessionAccess??=[],n.data.pinnedViews??=[],n.data.watchedPanes??=[],n.data.quickCommands??=[];const m=await g(),y=await k(),H=A({mode:"agent",securityConfig:R,tokenStore:X,rateLimiter:q,settings:m,terminalRenderer:m.terminalRenderer==="ghostty"?"ghostty":"xterm",terminalBufferConfig:a,getAgentId:()=>null,state:{activeTheme:y,settingUpPassword:!1}}),I=M({hub:P,token:x,name:O,version:W,app:H,listSessions:l,terminalBufferConfig:a,getSessionPaneTarget:b,capturePaneTail:C,capturePaneHistoryChunk:S,toCrlf:w,handleClientMessage:E,acquireControlClient:N,tmuxSocketPath:u()??void 0});function c(){I.stop(),process.exit(0)}process.on("SIGINT",c),process.on("SIGTERM",c);
2
+ import{createRequire as c}from"node:module";import{hostname as f}from"node:os";import{listSessions as p,resolveTmuxSocketPath as u}from"./sessions.js";import{loadSecurityConfig as l}from"./lib/security-config.js";import{TokenStore as T}from"./lib/auth.js";import{RateLimiter as d}from"./lib/rateLimiter.js";import{db as n}from"./lib/db.js";import{handleClientMessage as h}from"./lib/ws-message.js";import{loadDotEnv as E}from"./lib/load-env.js";import{readSettings as _}from"./lib/settings.js";import{readActiveTheme as g}from"./lib/theme-store.js";import{buildApp as k}from"./lib/build-app.js";import{acquireControlClient as A}from"./lib/tmux-control.js";import{getSessionPaneTarget as N,capturePaneTail as b,capturePaneHistoryChunk as C,toCrlf as S}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as w}from"./lib/terminal-config.js";import{startAgentClient as B}from"./lib/agent-client.js";E();const M=c(import.meta.url),{version:U}=M("../package.json");function W(o){let r=process.env.TMUX_WEB_AGENT_HUB||"",i=process.env.TMUX_WEB_AGENT_TOKEN||"",s=process.env.TMUX_WEB_AGENT_NAME||f();for(let t=0;t<o.length;t++){const e=o[t];if(e!=="agent"){if((e==="--hub"||e==="-h")&&o[t+1]){r=o[++t];continue}if((e==="--token"||e==="-t")&&o[t+1]){i=o[++t];continue}if((e==="--name"||e==="-n")&&o[t+1]){s=o[++t];continue}e.startsWith("--hub=")&&(r=e.slice(6)),e.startsWith("--token=")&&(i=e.slice(8)),e.startsWith("--name=")&&(s=e.slice(7))}}return r||(console.error("usage: tmux-web agent --hub wss://HOST[:PORT] --token TOKEN [--name NAME]"),console.error(" (or set TMUX_WEB_AGENT_HUB / TMUX_WEB_AGENT_TOKEN)"),process.exit(1)),i||(console.error("agent token required (--token or TMUX_WEB_AGENT_TOKEN)"),process.exit(1)),{hub:r,token:i,name:s.slice(0,64)}}const{hub:G,token:P,name:x}=W(process.argv.slice(2)),a=w(),O=l(),X=new T,q=new d;await n.read(),n.data.sessionAccess??=[],n.data.pinnedViews??=[],n.data.watchedPanes??=[],n.data.quickCommands??=[];const H=await _(),I=await g(),K=k({mode:"agent",securityConfig:O,tokenStore:X,rateLimiter:q,settings:H,terminalBufferConfig:a,getAgentId:()=>null,state:{activeTheme:I,settingUpPassword:!1}}),R=B({hub:G,token:P,name:x,version:U,app:K,listSessions:p,terminalBufferConfig:a,getSessionPaneTarget:N,capturePaneTail:b,capturePaneHistoryChunk:C,toCrlf:S,handleClientMessage:h,acquireControlClient:A,tmuxSocketPath:u()??void 0});function m(){R.stop(),process.exit(0)}process.on("SIGINT",m),process.on("SIGTERM",m);
@@ -1,40 +1,38 @@
1
- var Ft="https://esm.sh/ghostty-web@0.4.0",Vt=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function ie(n){return n.replace(Vt,"")}var ye;function _t(){return ye||(document.querySelector("link[data-tmux-web-xterm-css]")?ye=Promise.resolve():(ye=new Promise(n=>{let e=document.createElement("link");e.rel="stylesheet",e.href="/assets/xterm.css",e.setAttribute("data-tmux-web-xterm-css",""),e.addEventListener("load",()=>n(),{once:!0}),e.addEventListener("error",()=>n(),{once:!0}),document.head.appendChild(e)}),ye))}var Ge=class wt{terminal;fitAddon;container;constructor(e,t,i){this.container=e,this.terminal=t,this.fitAddon=i,this.terminal.loadAddon(this.fitAddon),this.terminal.open(e)}static async create(e,t,i){let[{Terminal:a},{FitAddon:l}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),_t()]),c=new a({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:t,convertEol:!1,theme:i});return new wt(e,c,new l)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(e){return new Promise(t=>this.terminal.write(e,t))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(e){this.terminal.scrollToLine(Math.max(0,e))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){let e=Qe(this.container);return e.width<=0||e.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(e){this.terminal.paste(e)}input(e){this.terminal.input(e,!1)}onData(e){this.terminal.onData(e),this.terminal.onBinary(e)}onResize(e){this.terminal.onResize(e)}onScroll(e){this.terminal.onScroll(e)}attachCustomKeyEventHandler(e){this.terminal.attachCustomKeyEventHandler(e)}isFocused(){let e=document.activeElement;return e?e===this.container||e===this.terminal.textarea||this.container.contains(e)||!!e.closest?.(".xterm"):!1}getScreenText(){return this.getRowsText(0,this.terminal.rows-1)}getRowsText(e,t){let i=this.terminal.buffer.active,[a,l]=e<=t?[e,t]:[t,e],c=i.viewportY,r=[];for(let d=a;d<=l;d++){let p=i.getLine(c+d);r.push(p?p.translateToString(!0):"")}return r.join(`
2
- `)}selectRows(e,t){let i=this.terminal.buffer.active,[a,l]=e<=t?[e,t]:[t,e];this.terminal.selectLines(i.viewportY+a,i.viewportY+l)}selectCells(e,t,i,a){let l=this.terminal.buffer.active,c=Math.min(t,a),r=Math.max(t,a);if(c===r){let d=Math.min(e,i),p=Math.max(e,i);this.terminal.select(d,l.viewportY+c,p-d+1)}else this.terminal.selectLines(l.viewportY+c,l.viewportY+r)}getCellsText(e,t,i,a){let l=this.terminal.buffer.active,c=Math.min(t,a),r=Math.max(t,a),d=t<a?e:t>a?i:Math.min(e,i),p=t<a?i:t>a?e:Math.max(e,i),L=l.viewportY,S=[];for(let w=c;w<=r;w++){let E=l.getLine(L+w);if(!E){S.push("");continue}let H=w===c?d:0,A=w===r?p:this.terminal.cols-1,z="";for(let Y=H;Y<=A;Y++){let N=E.getCell(Y);N&&(z+=N.getChars())}S.push(z.replace(/\s+$/,""))}return S.join(`
3
- `)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}},Ut=class bt{terminal;container;colsValue=80;rowsValue=24;charW=0;charH=0;resizeCallbacks=[];constructor(e,t){this.container=e,this.terminal=t,this.terminal.open(e)}static async create(e,t,i){let a=await import(Ft);await a.init();let l=new a.Terminal({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:t,convertEol:!1,theme:i});return new bt(e,l)}get cols(){return this.colsValue}get rows(){return this.rowsValue}write(e){return new Promise(t=>this.terminal.write(e,t))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom?.()}scrollToLine(e){this.terminal.scrollToLine?.(Math.max(0,e))}isNearScrollbackTop(){let e=this.terminal.buffer?.active;return e?e.viewportY>=e.baseY:!1}viewportY(){return this.terminal.buffer?.active?.viewportY??0}baseY(){return this.terminal.buffer?.active?.baseY??0}fit(){let e=Qe(this.container);if(e.width<=0||e.height<=0)return!1;this.updateCellMetrics(!this.charW||!this.charH);let t=Math.floor(e.width/this.charW),i=Math.floor(e.height/this.charH);if(t<10||i<5)return!1;if(t!==this.colsValue||i!==this.rowsValue){this.colsValue=t,this.rowsValue=i,this.terminal.resize(t,i);for(let a of this.resizeCallbacks)a({cols:t,rows:i})}return!0}focus(){(this.terminal.textarea||this.container.querySelector("textarea")||this.container)?.focus?.()}pasteText(e){let t=this.terminal.getMode?.(2004)??!1;this.input(t?"\x1B[200~"+e+"\x1B[201~":e)}input(e){this.terminal.input?.(e,!1)}onData(e){this.terminal.onData(t=>e(t))}onResize(e){this.resizeCallbacks.push(e)}onScroll(e){this.terminal.onScroll(e)}attachCustomKeyEventHandler(e){this.terminal.attachCustomKeyEventHandler(e)}isFocused(){let e=document.activeElement;return!!e&&(e===this.container||e===this.terminal.textarea||this.container.contains(e))}getScreenText(){return this.getRowsText(0,this.rowsValue-1)}getRowsText(e,t){try{let i=this.terminal.buffer?.active;if(!i||typeof i.getLine!="function")return"";let[a,l]=e<=t?[e,t]:[t,e],c=typeof i.viewportY=="number"?i.viewportY:0,r=[];for(let d=a;d<=l;d++){let p=i.getLine(c+d);r.push(typeof p?.translateToString=="function"?p.translateToString(!0):"")}return r.join(`
4
- `)}catch{return""}}selectRows(e,t){try{if(typeof this.terminal?.selectLines!="function")return;let i=this.terminal.buffer?.active,a=typeof i?.viewportY=="number"?i.viewportY:0,[l,c]=e<=t?[e,t]:[t,e];this.terminal.selectLines(a+l,a+c)}catch{}}selectCells(e,t,i,a){try{let l=this.terminal.buffer?.active,c=typeof l?.viewportY=="number"?l.viewportY:0,r=Math.min(t,a),d=Math.max(t,a);if(r===d){if(typeof this.terminal?.select!="function"){this.selectRows(r,d);return}let p=Math.min(e,i),L=Math.max(e,i);this.terminal.select(p,c+r,L-p+1)}else this.selectRows(r,d)}catch{this.selectRows(t,a)}}getCellsText(e,t,i,a){try{let l=this.terminal.buffer?.active;if(!l||typeof l.getLine!="function")return this.getRowsText(t,a);let c=typeof l.viewportY=="number"?l.viewportY:0,r=Math.min(t,a),d=Math.max(t,a),p=t<a?e:t>a?i:Math.min(e,i),L=t<a?i:t>a?e:Math.max(e,i),S=[];for(let w=r;w<=d;w++){let E=l.getLine(c+w);if(!E){S.push("");continue}let H=w===r?p:0,A=w===d?L:this.colsValue-1,z="";if(typeof E.getCell=="function")for(let Y=H;Y<=A;Y++){let N=E.getCell(Y);N&&(z+=typeof N.getChars=="function"?N.getChars():"")}else z=typeof E.translateToString=="function"?E.translateToString(!1):"";S.push(z.replace(/\s+$/,""))}return S.join(`
5
- `)}catch{return this.getRowsText(t,a)}}getSelection(){try{if(typeof this.terminal?.getSelection=="function")return this.terminal.getSelection()}catch{}return""}clearSelection(){try{typeof this.terminal?.clearSelection=="function"&&this.terminal.clearSelection()}catch{}}hasSelectionSupport(){return typeof this.terminal?.selectLines=="function"}updateCellMetrics(e=!1){let t=this.container.querySelector("canvas");if(t instanceof HTMLElement&&t.offsetWidth>0&&t.offsetHeight>0&&this.colsValue>0&&this.rowsValue>0){let i=t.offsetWidth/this.colsValue,a=t.offsetHeight/this.rowsValue;(e||!this.charW||!this.charH)&&(this.charW=i,this.charH=a)}(!this.charW||!this.charH)&&(this.charW=9,this.charH=18)}};function Qe(n){let e=n.getBoundingClientRect(),t=window.visualViewport;return t?{width:Math.min(e.width,t.width),height:Math.max(0,Math.min(e.height,t.height-Math.max(0,e.top)))}:e}function Kt(n,e,t){return n.innerHTML="",n.classList.add("terminal-pending"),(async()=>{let i;if(t.renderer==="ghostty")try{i=await Ut.create(n,t.scrollback,t.theme)}catch{console.error("[tmux-web] ghostty-web failed; falling back to xterm.js"),i=await Ge.create(n,t.scrollback,t.theme)}else i=await Ge.create(n,t.scrollback,t.theme);let a,l=0,c,r=null,d=0,p="connecting",L=0,S=!1,w=[],E="",H=!1,A=1e3,z=location.protocol==="https:"?"wss:":"ws:",Y=t.wsBase||"/ws",N=z+"//"+location.host+Y+"/"+encodeURIComponent(e),Lt="/api/session/"+encodeURIComponent(e)+"/upload",Et=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function xt(){return w.join("")+E}async function kt(s,o=0){let u=s?i.viewportY():0,v=s?i.baseY():0,B=ie(xt());if(i.reset(),!B){s||i.scrollToBottom();return}await i.write(B),s?i.scrollToLine(v+u+o):i.scrollToBottom()}function X(s){a?.readyState===WebSocket.OPEN&&a.send(JSON.stringify(s))}function Tt(s){let o;try{o=JSON.parse(s)}catch{p==="live"&&(E+=s,i.write(ie(s)));return}if(o.type==="auth.required"){let u=localStorage.getItem("tmux-web-token");u?X({type:"auth.token",token:u}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(o.type==="attach_failed"&&typeof o.message=="string"){pe=!0,p="dead",i.reset(),i.write(`\r
6
- \x1B[31m`+ie(o.message)+`\x1B[0m\r
7
- `).then(()=>{me();try{a?.close()}catch{}});return}if(o.type==="auth.ok"){o.token&&localStorage.setItem("tmux-web-token",o.token),A=1e3,J(),X({type:"resize",cols:i.cols,rows:i.rows}),C();return}if(o.type==="auth.failed"){if(o.permanentLock||o.retryAfterMs&&o.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(o.message);return}return}if(o.type==="snapshot"&&typeof o.data=="string"){let u=ie(o.data);w=[u],E="",L=typeof o.lines=="number"?o.lines:t.terminal.initialLines,p="live",i.reset(),i.write(u).then(()=>i.scrollToBottom());return}if(o.type==="window_changed"&&typeof o.activeIndex=="number"){let u=new URL(location.href);u.searchParams.get("window")!==String(o.activeIndex)&&(u.searchParams.set("window",String(o.activeIndex)),history.replaceState(history.state,"",u)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:o.windows}));return}if(o.type==="history"&&typeof o.data=="string"){S=!1,o.lines>0&&o.data&&(w.unshift(o.data),L+=o.lines,kt(!0,o.lines));return}if(o.type==="data"&&typeof o.data=="string"){let u=ie(o.data);if(p==="connecting"){p="live",E=u,i.write(u);return}p==="live"&&(E+=u,i.write(u))}}let pe=!1;function We(){if(pe)return;let s=N,o=localStorage.getItem("tmux-web-token");o&&(s+=(s.includes("?")?"&":"?")+"token="+encodeURIComponent(o)),a=new WebSocket(s),a.onopen=()=>{p="connecting",L=0,S=!1,w=[],E="",i.reset(),A=1e3},a.onmessage=u=>{typeof u.data=="string"&&Tt(u.data)},a.onclose=()=>{H||(p="connecting",!pe&&setTimeout(()=>{A=Math.min(A*2,1e4),!H&&!pe&&We()},A))},a.onerror=()=>a?.close()}function fe(s){X({type:"input",data:s})}function J(){return i.fit()}function me(){n.classList.remove("terminal-pending")}function C(){l&&cancelAnimationFrame(l),c&&clearTimeout(c),J(),l=requestAnimationFrame(()=>{l=0,J()&&me()}),c=setTimeout(()=>{J()&&me()},120)}function Ct(){C(),setTimeout(C,50),setTimeout(C,150),setTimeout(C,300)}function je(s){return typeof s=="string"&&s.startsWith("image/")}function Fe(s){return s?.files?.length?s.files[0]??null:null}function Mt(s){if(!s?.items)return null;for(let o=0;o<s.items.length;o++){let u=s.items[o];if(u?.kind==="file"&&je(u.type))return u.getAsFile()}return null}async function St(s){let o=new FormData,u=s instanceof File&&s.name?s.name:"upload";o.append("file",s,u);let v=localStorage.getItem("tmux-web-token"),B={};v&&(B.Authorization="Bearer "+v);let ne=await fetch(Lt,{method:"POST",body:o,headers:B});if(!ne.ok){let Je="upload failed";try{let Ze=await ne.json();Ze.error&&(Je=Ze.error)}catch{}throw new Error(Je)}let Xe=await ne.json();if(!Xe.path)throw new Error("no path in response");return Xe.path}async function Ie(s){try{let o=await St(s);fe(o)}catch{}}i.onData(s=>fe(s)),i.onResize(({cols:s,rows:o})=>X({type:"resize",cols:s,rows:o})),i.onScroll(()=>{p!=="live"||S||!i.isNearScrollbackTop()||(S=!0,X({type:"load_history",before:L}))});let Z=!1,G=!1,b=null,x=null,Q=document.createElement("div");Q.className="terminal-copy-banner",Q.innerHTML='<strong>Copy mode</strong><span class="hint">drag to copy \xB7 Esc exits</span>',Q.hidden=!0,n.appendChild(Q);let $=document.createElement("div");$.className="terminal-copy-banner terminal-copy-toast",$.textContent="Copied",$.hidden=!0,n.appendChild($);function Ve(s,o){let u=n.querySelector(".xterm-screen, canvas"),v=(u&&u.getBoundingClientRect().width>0?u:n).getBoundingClientRect(),B=v.height>0&&i.rows>0?v.height/i.rows:18,ne=v.width>0&&i.cols>0?v.width/i.cols:9;return{row:Math.max(0,Math.min(i.rows-1,Math.floor((o-v.top)/B))),col:Math.max(0,Math.min(i.cols-1,Math.floor((s-v.left)/ne)))}}async function he(s){if(!s)return;try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(s);return}}catch{}let o=document.createElement("textarea");o.value=s,o.style.position="fixed",o.style.opacity="0",o.style.pointerEvents="none",document.body.appendChild(o),o.focus(),o.select();try{document.execCommand("copy")}catch{}o.remove()}let ee;function ve(){$.hidden=!1,ee&&clearTimeout(ee),ee=setTimeout(()=>{$.hidden=!0},1e3)}function U(s){Z=s,Q.hidden=!s,n.classList.toggle("terminal-copy-mode-active",s),s&&i.focus()}function _e(){U(!Z)}function Bt(s){H||s.button!==0||G||(Z||s.shiftKey)&&(s.preventDefault(),s.stopPropagation(),G=!0,b=Ve(s.clientX,s.clientY),x=b,i.selectCells(b.col,b.row,x.col,x.row),document.addEventListener("mousemove",He,!0),document.addEventListener("mouseup",De,!0))}function He(s){H||!G||!b||(s.preventDefault(),s.stopPropagation(),x=Ve(s.clientX,s.clientY),i.selectCells(b.col,b.row,x.col,x.row))}function De(s){if(H||!G||!b||!x)return;s.preventDefault(),s.stopPropagation(),document.removeEventListener("mousemove",He,!0),document.removeEventListener("mouseup",De,!0),G=!1;let o=i.getCellsText(b.col,b.row,x.col,x.row);b=null,x=null,i.clearSelection(),U(!1),o&&($.textContent="\u5DF2\u590D\u5236",he(o).then(ve)),i.focus()}n.addEventListener("mousedown",Bt,!0),document.addEventListener("keydown",It,!0);function It(s){if(!(H||!i.isFocused()||s.defaultPrevented||s.isComposing)){if(Z){if(s.key==="Escape"){s.preventDefault(),s.stopPropagation(),b=null,x=null,i.clearSelection(),U(!1);return}if(s.key==="Enter"||(s.ctrlKey||s.metaKey)&&s.code==="KeyC"){s.preventDefault(),s.stopPropagation(),he(i.getScreenText()).then(ve),U(!1);return}}if(s.altKey&&s.code==="KeyC"&&!s.ctrlKey&&!s.metaKey){if(s.preventDefault(),s.stopPropagation(),b&&x){let o=i.getCellsText(b.col,b.row,x.col,x.row);b=null,x=null,i.clearSelection(),U(!1),o&&($.textContent="Copied",he(o).then(ve))}else{let o=i.getSelection();o?(i.clearSelection(),$.textContent="Copied",he(o).then(ve)):_e()}return}if((s.ctrlKey||s.metaKey)&&s.code==="KeyV"){s.preventDefault(),s.stopPropagation(),(async()=>{try{if(navigator.clipboard?.read){let o=await navigator.clipboard.read();for(let u of o)for(let v of u.types)if(je(v)){let B=await u.getType(v);await Ie(B);return}}}catch{}try{let o=await navigator.clipboard?.readText();o&&i.pasteText(Ue(o))}catch{}})();return}}}let ge=0;function Ue(s){return s.split(`\r
1
+ var zt=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function ee(e){return e.replace(zt,"")}var ye;function Yt(){return ye||(document.querySelector("link[data-tmux-web-xterm-css]")?ye=Promise.resolve():(ye=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)}),ye))}var jt=class gt{terminal;fitAddon;container;constructor(t,i,a){this.container=t,this.terminal=i,this.fitAddon=a,this.terminal.loadAddon(this.fitAddon),this.terminal.open(t)}static async create(t,i,a){let[{Terminal:o},{FitAddon:r}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),Yt()]),c=new o({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:i,convertEol:!1,theme:a});return new gt(t,c,new r)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(t){return new Promise(i=>this.terminal.write(t,i))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(t){this.terminal.scrollToLine(Math.max(0,t))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){let t=Ft(this.container);return t.width<=0||t.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(t){this.terminal.paste(t)}input(t){this.terminal.input(t,!1)}onData(t){this.terminal.onData(t),this.terminal.onBinary(t)}onResize(t){this.terminal.onResize(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){let t=document.activeElement;return t?t===this.container||t===this.terminal.textarea||this.container.contains(t)||!!t.closest?.(".xterm"):!1}getScreenText(){return this.getRowsText(0,this.terminal.rows-1)}getRowsText(t,i){let a=this.terminal.buffer.active,[o,r]=t<=i?[t,i]:[i,t],c=a.viewportY,l=[];for(let p=o;p<=r;p++){let u=a.getLine(c+p);l.push(u?u.translateToString(!0):"")}return l.join(`
2
+ `)}selectRows(t,i){let a=this.terminal.buffer.active,[o,r]=t<=i?[t,i]:[i,t];this.terminal.selectLines(a.viewportY+o,a.viewportY+r)}selectCells(t,i,a,o){let r=this.terminal.buffer.active,c=Math.min(i,o),l=Math.max(i,o);if(c===l){let p=Math.min(t,a),u=Math.max(t,a);this.terminal.select(p,r.viewportY+c,u-p+1)}else this.terminal.selectLines(r.viewportY+c,r.viewportY+l)}getCellsText(t,i,a,o){let r=this.terminal.buffer.active,c=Math.min(i,o),l=Math.max(i,o),p=i<o?t:i>o?a:Math.min(t,a),u=i<o?a:i>o?t:Math.max(t,a),M=r.viewportY,$=[];for(let B=c;B<=l;B++){let D=r.getLine(M+B);if(!D){$.push("");continue}let H=B===c?p:0,_=B===l?u:this.terminal.cols-1,de="";for(let O=H;O<=_;O++){let pe=D.getCell(O);pe&&(de+=pe.getChars())}$.push(de.replace(/\s+$/,""))}return $.join(`
3
+ `)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}};function Ft(e){let t=e.getBoundingClientRect(),i=window.visualViewport;return i?{width:Math.min(t.width,i.width),height:Math.max(0,Math.min(t.height,i.height-Math.max(0,t.top)))}:t}function Wt(e,t,i){return e.innerHTML="",e.classList.add("terminal-pending"),(async()=>{let a=await jt.create(e,i.scrollback,i.theme),o,r=0,c,l=null,p=0,u="connecting",M=0,$=!1,B=[],D="",H=!1,_=1e3,de=location.protocol==="https:"?"wss:":"ws:",O=i.wsBase||"/ws",pe=de+"//"+location.host+O+"/"+encodeURIComponent(t),yt="/api/session/"+encodeURIComponent(t)+"/upload",wt=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function bt(){return B.join("")+D}async function Lt(n,s=0){let d=n?a.viewportY():0,h=n?a.baseY():0,C=ee(bt());if(a.reset(),!C){n||a.scrollToBottom();return}await a.write(C),n?a.scrollToLine(h+d+s):a.scrollToBottom()}function K(n){o?.readyState===WebSocket.OPEN&&o.send(JSON.stringify(n))}function Et(n){let s;try{s=JSON.parse(n)}catch{u==="live"&&(D+=n,a.write(ee(n)));return}if(s.type==="auth.required"){let d=localStorage.getItem("tmux-web-token");d?K({type:"auth.token",token:d}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(s.type==="attach_failed"&&typeof s.message=="string"){ue=!0,u="dead",a.reset(),a.write(`\r
4
+ \x1B[31m`+ee(s.message)+`\x1B[0m\r
5
+ `).then(()=>{me();try{o?.close()}catch{}});return}if(s.type==="auth.ok"){s.token&&localStorage.setItem("tmux-web-token",s.token),_=1e3,q(),K({type:"resize",cols:a.cols,rows:a.rows}),x();return}if(s.type==="auth.failed"){if(s.permanentLock||s.retryAfterMs&&s.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(s.message);return}return}if(s.type==="snapshot"&&typeof s.data=="string"){let d=ee(s.data);B=[d],D="",M=typeof s.lines=="number"?s.lines:i.terminal.initialLines,u="live",a.reset(),a.write(d).then(()=>a.scrollToBottom());return}if(s.type==="window_changed"&&typeof s.activeIndex=="number"){let d=new URL(location.href);d.searchParams.get("window")!==String(s.activeIndex)&&(d.searchParams.set("window",String(s.activeIndex)),history.replaceState(history.state,"",d)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:s.windows}));return}if(s.type==="history"&&typeof s.data=="string"){$=!1,s.lines>0&&s.data&&(B.unshift(s.data),M+=s.lines,Lt(!0,s.lines));return}if(s.type==="data"&&typeof s.data=="string"){let d=ee(s.data);if(u==="connecting"){u="live",D=d,a.write(d);return}u==="live"&&(D+=d,a.write(d))}}let ue=!1;function Ye(){if(ue)return;let n=pe,s=localStorage.getItem("tmux-web-token");s&&(n+=(n.includes("?")?"&":"?")+"token="+encodeURIComponent(s)),o=new WebSocket(n),o.onopen=()=>{u="connecting",M=0,$=!1,B=[],D="",a.reset(),_=1e3},o.onmessage=d=>{typeof d.data=="string"&&Et(d.data)},o.onclose=()=>{H||(u="connecting",!ue&&setTimeout(()=>{_=Math.min(_*2,1e4),!H&&!ue&&Ye()},_))},o.onerror=()=>o?.close()}function fe(n){K({type:"input",data:n})}function q(){return a.fit()}function me(){e.classList.remove("terminal-pending")}function x(){r&&cancelAnimationFrame(r),c&&clearTimeout(c),q(),r=requestAnimationFrame(()=>{r=0,q()&&me()}),c=setTimeout(()=>{q()&&me()},120)}function xt(){x(),setTimeout(x,50),setTimeout(x,150),setTimeout(x,300)}function je(n){return typeof n=="string"&&n.startsWith("image/")}function Fe(n){return n?.files?.length?n.files[0]??null:null}function kt(n){if(!n?.items)return null;for(let s=0;s<n.items.length;s++){let d=n.items[s];if(d?.kind==="file"&&je(d.type))return d.getAsFile()}return null}async function Ct(n){let s=new FormData,d=n instanceof File&&n.name?n.name:"upload";s.append("file",n,d);let h=localStorage.getItem("tmux-web-token"),C={};h&&(C.Authorization="Bearer "+h);let G=await fetch(yt,{method:"POST",body:s,headers:C});if(!G.ok){let Je="upload failed";try{let Qe=await G.json();Qe.error&&(Je=Qe.error)}catch{}throw new Error(Je)}let Xe=await G.json();if(!Xe.path)throw new Error("no path in response");return Xe.path}async function Se(n){try{let s=await Ct(n);fe(s)}catch{}}a.onData(n=>fe(n)),a.onResize(({cols:n,rows:s})=>K({type:"resize",cols:n,rows:s})),a.onScroll(()=>{u!=="live"||$||!a.isNearScrollbackTop()||($=!0,K({type:"load_history",before:M}))});let V=!1,X=!1,w=null,b=null,J=document.createElement("div");J.className="terminal-copy-banner",J.innerHTML='<strong>Copy mode</strong><span class="hint">drag to copy \xB7 Esc exits</span>',J.hidden=!0,e.appendChild(J);let A=document.createElement("div");A.className="terminal-copy-banner terminal-copy-toast",A.textContent="Copied",A.hidden=!0,e.appendChild(A);function We(n,s){let d=e.querySelector(".xterm-screen, canvas"),h=(d&&d.getBoundingClientRect().width>0?d:e).getBoundingClientRect(),C=h.height>0&&a.rows>0?h.height/a.rows:18,G=h.width>0&&a.cols>0?h.width/a.cols:9;return{row:Math.max(0,Math.min(a.rows-1,Math.floor((s-h.top)/C))),col:Math.max(0,Math.min(a.cols-1,Math.floor((n-h.left)/G)))}}async function ve(n){if(!n)return;try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(n);return}}catch{}let s=document.createElement("textarea");s.value=n,s.style.position="fixed",s.style.opacity="0",s.style.pointerEvents="none",document.body.appendChild(s),s.focus(),s.select();try{document.execCommand("copy")}catch{}s.remove()}let Q;function he(){A.hidden=!1,Q&&clearTimeout(Q),Q=setTimeout(()=>{A.hidden=!0},1e3)}function F(n){V=n,J.hidden=!n,e.classList.toggle("terminal-copy-mode-active",n),n&&a.focus()}function Ue(){F(!V)}function Tt(n){H||n.button!==0||X||(V||n.shiftKey)&&(n.preventDefault(),n.stopPropagation(),X=!0,w=We(n.clientX,n.clientY),b=w,a.selectCells(w.col,w.row,b.col,b.row),document.addEventListener("mousemove",De,!0),document.addEventListener("mouseup",He,!0))}function De(n){H||!X||!w||(n.preventDefault(),n.stopPropagation(),b=We(n.clientX,n.clientY),a.selectCells(w.col,w.row,b.col,b.row))}function He(n){if(H||!X||!w||!b)return;n.preventDefault(),n.stopPropagation(),document.removeEventListener("mousemove",De,!0),document.removeEventListener("mouseup",He,!0),X=!1;let s=a.getCellsText(w.col,w.row,b.col,b.row);w=null,b=null,a.clearSelection(),F(!1),s&&(A.textContent="\u5DF2\u590D\u5236",ve(s).then(he)),a.focus()}e.addEventListener("mousedown",Tt,!0),document.addEventListener("keydown",Mt,!0);function Mt(n){if(!(H||!a.isFocused()||n.defaultPrevented||n.isComposing)){if(V){if(n.key==="Escape"){n.preventDefault(),n.stopPropagation(),w=null,b=null,a.clearSelection(),F(!1);return}if(n.key==="Enter"||(n.ctrlKey||n.metaKey)&&n.code==="KeyC"){n.preventDefault(),n.stopPropagation(),ve(a.getScreenText()).then(he),F(!1);return}}if(n.altKey&&n.code==="KeyC"&&!n.ctrlKey&&!n.metaKey){if(n.preventDefault(),n.stopPropagation(),w&&b){let s=a.getCellsText(w.col,w.row,b.col,b.row);w=null,b=null,a.clearSelection(),F(!1),s&&(A.textContent="Copied",ve(s).then(he))}else{let s=a.getSelection();s?(a.clearSelection(),A.textContent="Copied",ve(s).then(he)):Ue()}return}if((n.ctrlKey||n.metaKey)&&n.code==="KeyV"){n.preventDefault(),n.stopPropagation(),(async()=>{try{if(navigator.clipboard?.read){let s=await navigator.clipboard.read();for(let d of s)for(let h of d.types)if(je(h)){let C=await d.getType(h);await Se(C);return}}}catch{}try{let s=await navigator.clipboard?.readText();s&&a.pasteText(Oe(s))}catch{}})();return}}}let ge=0;function Oe(n){return n.split(`\r
8
6
  `).join(`
9
7
  `).split(`
10
- `).join("\r")}async function Ht(s){let o=Mt(s.clipboardData);if(o){s.preventDefault(),s.stopPropagation();let B=Date.now();if(B-ge<50)return;ge=B,await Ie(o);return}let u=s.clipboardData?.getData("text/plain");if(!u)return;s.preventDefault(),s.stopPropagation();let v=Date.now();v-ge<50||(ge=v,i.pasteText(Ue(u)))}let te=0;function Dt(s){s.preventDefault(),te++,Fe(s.dataTransfer)&&n.classList.add("terminal-drag-over")}function Rt(s){s.preventDefault(),s.dataTransfer&&(s.dataTransfer.dropEffect="copy")}function At(s){s.preventDefault(),te--,te<=0&&(te=0,n.classList.remove("terminal-drag-over"))}async function Yt(s){s.preventDefault(),te=0,n.classList.remove("terminal-drag-over");let o=Fe(s.dataTransfer);o&&await Ie(o)}function Ke(s){!Et||s.key!=="Escape"||Z||s.defaultPrevented||s.repeat||s.metaKey||s.ctrlKey||s.altKey||i.isFocused()&&(s.preventDefault(),s.stopPropagation(),fe("\x1B"))}n.addEventListener("paste",Ht),n.addEventListener("dragenter",Dt),n.addEventListener("dragover",Rt),n.addEventListener("dragleave",At),n.addEventListener("drop",Yt),document.addEventListener("keydown",Ke,!0);function $t(s){if(s.touches.length!==1){r=null;return}let o=s.touches[0];o&&(r={startX:o.clientX,startY:o.clientY,lastX:o.clientX,lastY:o.clientY,scrolling:!1},s.stopPropagation())}function Pt(s){if(!r||s.touches.length!==1)return;let o=s.touches[0];if(!o)return;let u=o.clientY-r.startY,v=o.clientX-r.startX;if(!r.scrolling){if(Math.abs(u)<8||Math.abs(u)<Math.abs(v))return;r.scrolling=!0,d=Date.now()+500}s.preventDefault(),s.stopPropagation(),jt(-(o.clientY-r.lastY),o.clientX,o.clientY),r.lastX=o.clientX,r.lastY=o.clientY}function zt(s){if(!r)return;let o=r.scrolling;r=null,s.stopPropagation(),o?(d=Date.now()+500,s.preventDefault()):i.focus()}function Nt(s){r=null,s.stopPropagation()}function Wt(s){s.pointerType==="touch"&&Date.now()<d&&(s.preventDefault(),s.stopPropagation())}function jt(s,o,u){(n.querySelector(".xterm-screen")??n).dispatchEvent(new WheelEvent("wheel",{deltaY:s,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:o,clientY:u,bubbles:!0,cancelable:!0}))}n.addEventListener("touchstart",$t,{passive:!0,capture:!0}),n.addEventListener("touchmove",Pt,{passive:!1,capture:!0}),n.addEventListener("touchend",zt,{passive:!1,capture:!0}),n.addEventListener("touchcancel",Nt,{passive:!0,capture:!0}),n.addEventListener("pointerup",Wt,!0);function Oe(){C()}window.addEventListener("resize",Oe),window.visualViewport?.addEventListener("resize",C),window.visualViewport?.addEventListener("scroll",C);let qe=new ResizeObserver(()=>C());return qe.observe(n),document.fonts?.ready.then(()=>{C(),setTimeout(C,50),setTimeout(C,200)}),n.addEventListener("focusin",Ct,!0),requestAnimationFrame(()=>{J()&&me(),We(),C()}),{destroy(){if(H=!0,U(!1),ee&&clearTimeout(ee),document.removeEventListener("mousemove",He,!0),document.removeEventListener("mouseup",De,!0),a){try{a.close(1e3,"session switch")}catch{}a=void 0}n.innerHTML="",n.classList.remove("terminal-pending"),window.removeEventListener("resize",Oe),qe.disconnect(),document.removeEventListener("keydown",Ke,!0)},focus(){i.focus()},sendInput(s){fe(s)},toggleCopyMode(){_e()}}})()}var K=window.__TMUX_WEB_SHELL__,h=null,y=null,et=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null,T=et;window.__tmuxWebScopeAgent=et;var Re="sessions",W="",we="",be="",tt="";function M(n){return T?"/a/"+encodeURIComponent(T)+n:n}var m=document.getElementById("sidebar-content"),F=document.getElementById("main-placeholder"),I=document.getElementById("terminal-container"),se=document.getElementById("file-editor"),Ot=document.getElementById("fe-path"),D=document.getElementById("fe-status"),ae=document.getElementById("fe-content"),V=document.getElementById("fe-save"),qt=document.getElementById("fe-delete"),Xt=document.getElementById("fe-back"),nt=document.getElementById("fe-new-name"),Jt=document.getElementById("fe-new-btn"),Ae=document.getElementById("settings-backdrop"),it=document.getElementById("settings-popover"),st=document.querySelector(".sidebar");function Zt(){return window.matchMedia("(max-width: 640px)").matches}function Ye(){Zt()&&st.classList.add("collapsed")}function at(){st.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{at(),ot("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{at(),ot("files")});function ot(n){Re=n,document.querySelectorAll(".mode-btn").forEach(t=>t.classList.remove("active"));let e=document.getElementById(n==="sessions"?"mode-sessions":"mode-files");e&&e.classList.add("active"),n==="sessions"?($e="",O()):rn()}var $e="";async function O(){if(Re!=="sessions")return;try{let e=await(await fetch("/api/sidebar/sessions")).json(),t=JSON.stringify({recent:e.recent,agents:e.agents});if(t===$e)return;$e=t,Gt(e)}catch{m.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let n=document.getElementById("ns-btn");n&&n.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function Gt(n){m.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';let e=n.recent||[],t=document.createElement("div");t.className="sidebar-section-label"+(T?" clickable":""),t.textContent=T?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",T&&(t.title="Back to hub",t.addEventListener("click",()=>{window.location.href="/"})),m.appendChild(t);for(let a of e){let l=document.createElement("div");l.className="session-item"+(a.name===y&&!T?" active":""),l.setAttribute("data-session",a.name),l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${a.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
11
- <span>${f(a.name)}</span>
12
- <button class="session-edit-btn" data-session="${f(a.name)}" title="Edit session"><svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
13
- `,l.addEventListener("click",r=>{r.target.closest(".session-edit-btn")||oe(a.name)}),m.appendChild(l);let c=l.querySelector(".session-edit-btn");c&&c.addEventListener("click",r=>{r.stopPropagation(),fn(a.name,c)})}let i=n.agents||[];for(let a of i){if(!a.sessions||a.sessions.length===0)continue;let l=a.agentId===T,c=document.createElement("div");c.className="sidebar-section-label machine-label"+(a.online?"":" agent-offline-label")+(!l&&a.online?" clickable":""),c.textContent=a.agentName+(a.online?"":" (offline)"),!l&&a.online&&(c.title="\u6253\u5F00 "+a.agentName,c.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(a.agentId)+"/"})),m.appendChild(c);for(let r of a.sessions){let d=document.createElement("div");d.className="session-item"+(r.name===y&&T===a.agentId?" active":"")+(a.online?"":" session-offline"),d.setAttribute("data-session",r.name),d.setAttribute("data-agent",a.agentId),d.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${r.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
14
- <span>${f(r.name)}</span><span class="meta">${a.online?"":"offline"}</span>`,a.online&&d.addEventListener("click",p=>{p.target.closest(".session-edit-btn")||oe(r.name,a.agentId)}),m.appendChild(d)}}}function f(n){return n.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function oe(n,e){if(!(y===n&&h&&T===(e||null))){se.style.display="none",F.style.display="none",I.style.display="",I.classList.add("terminal-pending"),Ye(),h&&(Se(),h.destroy(),h=null),y=n,T=e||null,window.__tmuxWebScopeAgent=T;for(let t of m.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===n&&(t.getAttribute("data-agent")||null)===T);try{h=await Kt(I,n,{terminal:K.terminal,scrollback:K.scrollback,theme:K.theme,renderer:K.renderer,wsBase:T?"/ws/a/"+encodeURIComponent(T):K.wsBase||"/ws"}),vt()}catch(t){console.error("[shell] terminal init failed:",t);let i=t&&typeof t=="object"&&"message"in t?t.message:String(t);I.textContent="Failed to open terminal for "+n+": "+i,I.classList.remove("terminal-pending")}}}var Le=[],g=null,lt=!1;function Qt(n){if(!g||!g.files.length)return null;let e=g.repoRoot;if(!e||!n.startsWith(e))return null;let t=n.slice(e.length+1);for(let i of g.files)if(i.path===t)return i.status;return null}async function en(n){lt=!0;try{let e=await fetch(M("/api/git/status?path="+encodeURIComponent(n)));e.ok?g=await e.json():g=null}catch{g=null}finally{lt=!1}}function tn(){if(!g||!g.branch)return"";let{branch:n,linesAdded:e,linesRemoved:t}=g,i=`<div class="sidebar-git"><div class="git-branch-badge">
8
+ `).join("\r")}async function It(n){let s=kt(n.clipboardData);if(s){n.preventDefault(),n.stopPropagation();let C=Date.now();if(C-ge<50)return;ge=C,await Se(s);return}let d=n.clipboardData?.getData("text/plain");if(!d)return;n.preventDefault(),n.stopPropagation();let h=Date.now();h-ge<50||(ge=h,a.pasteText(Oe(d)))}let Z=0;function Bt(n){n.preventDefault(),Z++,Fe(n.dataTransfer)&&e.classList.add("terminal-drag-over")}function St(n){n.preventDefault(),n.dataTransfer&&(n.dataTransfer.dropEffect="copy")}function Dt(n){n.preventDefault(),Z--,Z<=0&&(Z=0,e.classList.remove("terminal-drag-over"))}async function Ht(n){n.preventDefault(),Z=0,e.classList.remove("terminal-drag-over");let s=Fe(n.dataTransfer);s&&await Se(s)}function Ke(n){!wt||n.key!=="Escape"||V||n.defaultPrevented||n.repeat||n.metaKey||n.ctrlKey||n.altKey||a.isFocused()&&(n.preventDefault(),n.stopPropagation(),fe("\x1B"))}e.addEventListener("paste",It),e.addEventListener("dragenter",Bt),e.addEventListener("dragover",St),e.addEventListener("dragleave",Dt),e.addEventListener("drop",Ht),document.addEventListener("keydown",Ke,!0);function At(n){if(n.touches.length!==1){l=null;return}let s=n.touches[0];s&&(l={startX:s.clientX,startY:s.clientY,lastX:s.clientX,lastY:s.clientY,scrolling:!1},n.stopPropagation())}function Rt(n){if(!l||n.touches.length!==1)return;let s=n.touches[0];if(!s)return;let d=s.clientY-l.startY,h=s.clientX-l.startX;if(!l.scrolling){if(Math.abs(d)<8||Math.abs(d)<Math.abs(h))return;l.scrolling=!0,p=Date.now()+500}n.preventDefault(),n.stopPropagation(),_t(-(s.clientY-l.lastY),s.clientX,s.clientY),l.lastX=s.clientX,l.lastY=s.clientY}function $t(n){if(!l)return;let s=l.scrolling;l=null,n.stopPropagation(),s?(p=Date.now()+500,n.preventDefault()):a.focus()}function Pt(n){l=null,n.stopPropagation()}function Nt(n){n.pointerType==="touch"&&Date.now()<p&&(n.preventDefault(),n.stopPropagation())}function _t(n,s,d){(e.querySelector(".xterm-screen")??e).dispatchEvent(new WheelEvent("wheel",{deltaY:n,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:s,clientY:d,bubbles:!0,cancelable:!0}))}e.addEventListener("touchstart",At,{passive:!0,capture:!0}),e.addEventListener("touchmove",Rt,{passive:!1,capture:!0}),e.addEventListener("touchend",$t,{passive:!1,capture:!0}),e.addEventListener("touchcancel",Pt,{passive:!0,capture:!0}),e.addEventListener("pointerup",Nt,!0);function qe(){x()}window.addEventListener("resize",qe),window.visualViewport?.addEventListener("resize",x),window.visualViewport?.addEventListener("scroll",x);let Ve=new ResizeObserver(()=>x());return Ve.observe(e),document.fonts?.ready.then(()=>{x(),setTimeout(x,50),setTimeout(x,200)}),e.addEventListener("focusin",xt,!0),requestAnimationFrame(()=>{q()&&me(),Ye(),x()}),{destroy(){if(H=!0,F(!1),Q&&clearTimeout(Q),document.removeEventListener("mousemove",De,!0),document.removeEventListener("mouseup",He,!0),o){try{o.close(1e3,"session switch")}catch{}o=void 0}e.innerHTML="",e.classList.remove("terminal-pending"),window.removeEventListener("resize",qe),Ve.disconnect(),document.removeEventListener("keydown",Ke,!0)},focus(){a.focus()},sendInput(n){fe(n)},toggleCopyMode(){Ue()}}})()}var te=window.__TMUX_WEB_SHELL__,v=null,y=null,Ze=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null,E=Ze;window.__tmuxWebScopeAgent=Ze;var Ae="sessions",P="",we="",be="",Ge="";function k(e){return E?"/a/"+encodeURIComponent(E)+e:e}var m=document.getElementById("sidebar-content"),z=document.getElementById("main-placeholder"),T=document.getElementById("terminal-container"),ne=document.getElementById("file-editor"),Ut=document.getElementById("fe-path"),I=document.getElementById("fe-status"),ie=document.getElementById("fe-content"),Y=document.getElementById("fe-save"),Ot=document.getElementById("fe-delete"),Kt=document.getElementById("fe-back"),et=document.getElementById("fe-new-name"),qt=document.getElementById("fe-new-btn"),Re=document.getElementById("settings-backdrop"),tt=document.getElementById("settings-popover"),nt=document.querySelector(".sidebar");function Vt(){return window.matchMedia("(max-width: 640px)").matches}function $e(){Vt()&&nt.classList.add("collapsed")}function it(){nt.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{it(),at("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{it(),at("files")});function at(e){Ae=e,document.querySelectorAll(".mode-btn").forEach(i=>i.classList.remove("active"));let t=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");t&&t.classList.add("active"),e==="sessions"?(Pe="",W()):sn()}var Pe="";async function W(){if(Ae!=="sessions")return;try{let t=await(await fetch("/api/sidebar/sessions")).json(),i=JSON.stringify({recent:t.recent,agents:t.agents});if(i===Pe)return;Pe=i,Xt(t)}catch{m.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function Xt(e){m.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';let t=e.recent||[],i=document.createElement("div");i.className="sidebar-section-label"+(E?" clickable":""),i.textContent=E?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",E&&(i.title="Back to hub",i.addEventListener("click",()=>{window.location.href="/"})),m.appendChild(i);for(let o of t){let r=document.createElement("div");r.className="session-item"+(o.name===y&&!E?" active":""),r.setAttribute("data-session",o.name),r.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${o.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
9
+ <span>${f(o.name)}</span>
10
+ <button class="session-edit-btn" data-session="${f(o.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
+ `,r.addEventListener("click",l=>{l.target.closest(".session-edit-btn")||ae(o.name)}),m.appendChild(r);let c=r.querySelector(".session-edit-btn");c&&c.addEventListener("click",l=>{l.stopPropagation(),dn(o.name,c)})}let a=e.agents||[];for(let o of a){if(!o.sessions||o.sessions.length===0)continue;let r=o.agentId===E,c=document.createElement("div");c.className="sidebar-section-label machine-label"+(o.online?"":" agent-offline-label")+(!r&&o.online?" clickable":""),c.textContent=o.agentName+(o.online?"":" (offline)"),!r&&o.online&&(c.title="\u6253\u5F00 "+o.agentName,c.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(o.agentId)+"/"})),m.appendChild(c);for(let l of o.sessions){let p=document.createElement("div");p.className="session-item"+(l.name===y&&E===o.agentId?" active":"")+(o.online?"":" session-offline"),p.setAttribute("data-session",l.name),p.setAttribute("data-agent",o.agentId),p.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${l.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
12
+ <span>${f(l.name)}</span><span class="meta">${o.online?"":"offline"}</span>`,o.online&&p.addEventListener("click",u=>{u.target.closest(".session-edit-btn")||ae(l.name,o.agentId)}),m.appendChild(p)}}}function f(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function ae(e,t){if(!(y===e&&v&&E===(t||null))){ne.style.display="none",z.style.display="none",T.style.display="",T.classList.add("terminal-pending"),$e(),v&&(Ie(),v.destroy(),v=null),y=e,E=t||null,window.__tmuxWebScopeAgent=E;for(let i of m.querySelectorAll(".session-item"))i.classList.toggle("active",i.getAttribute("data-session")===e&&(i.getAttribute("data-agent")||null)===E);try{v=await Wt(T,e,{terminal:te.terminal,scrollback:te.scrollback,theme:te.theme,wsBase:E?"/ws/a/"+encodeURIComponent(E):te.wsBase||"/ws"}),mt()}catch(i){console.error("[shell] terminal init failed:",i);let a=i&&typeof i=="object"&&"message"in i?i.message:String(i);T.textContent="Failed to open terminal for "+e+": "+a,T.classList.remove("terminal-pending")}}}var Le=[],g=null,st=!1;function Jt(e){if(!g||!g.files.length)return null;let t=g.repoRoot;if(!t||!e.startsWith(t))return null;let i=e.slice(t.length+1);for(let a of g.files)if(a.path===i)return a.status;return null}async function Qt(e){st=!0;try{let t=await fetch(k("/api/git/status?path="+encodeURIComponent(e)));t.ok?g=await t.json():g=null}catch{g=null}finally{st=!1}}function Zt(){if(!g||!g.branch)return"";let{branch:e,linesAdded:t,linesRemoved:i}=g,a=`<div class="sidebar-git"><div class="git-branch-badge">
15
13
  <svg viewBox="0 0 16 16" fill="currentColor"><path d="M11.63 1.22a.75.75 0 00-1.06 0L5.99 5.8 4.12 3.93a.75.75 0 10-1.06 1.06L4.93 6.87A3.5 3.5 0 003 10.09v3.16a.75.75 0 001 0v-3.16a2.5 2.5 0 012.5-2.5h.27l-1.06 1.06a.75.75 0 001.06 1.06l2.37-2.38a.75.75 0 000-1.06L7.59 5.27l4.04-4.05a.75.75 0 000-1.06L11.63 1.22zM3.25 13.94a.75.75 0 01-.75-.75v-.69a.75.75 0 011.5 0v.69c0 .414-.336.75-.75.75z"/></svg>
16
- ${f(n)}</div>`;return(e>0||t>0)&&(i+=`<div class="git-diff-stats">+<span class="add">${e}</span> <span class="del">-${t}</span></div>`),i+"</div>"}m.addEventListener("click",n=>{n.target.closest(".sidebar-git")&&q&&g&&(n.stopPropagation(),pt())});var le=document.getElementById("header-git"),rt=document.getElementById("git-diff-view"),ct=document.getElementById("gd-path"),Ee=document.getElementById("gd-status"),xe=document.getElementById("gd-content"),nn=document.getElementById("gd-back"),q=null;function re(n){if(!n||!n.branch){le.style.display="none",q=null;return}q=n.repoRoot;let{branch:e,linesAdded:t,linesRemoved:i}=n,a=`<span class="branch">${f(e)}</span>`;(t>0||i>0)&&(a+=`<span class="sep">\xB7</span>+<span class="diff-add">${t}</span> <span class="diff-del">-${i}</span>`),le.innerHTML=a,le.style.display="block"}le.addEventListener("click",n=>{q&&g&&(n.stopPropagation(),pt())});var k=document.getElementById("git-popover"),ke=document.getElementById("git-popover-backdrop");function Pe(){let n=le.getBoundingClientRect();k.style.top=n.bottom+4+"px",k.style.left=Math.max(8,Math.min(n.left,window.innerWidth-400))+"px"}function dt(){let n=q,e=document.getElementById("git-popover-browse");e&&e.addEventListener("click",()=>{if(Ce(),n){Re="files",document.querySelectorAll(".mode-btn").forEach(i=>i.classList.remove("active"));let t=document.getElementById("mode-files");t&&t.classList.add("active"),j(n)}})}var ut="";function pt(){if(!g)return;let n=g.files||[];if(!n.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 e=`<div class="git-popover-header">${n.length} file${n.length>1?"s":""} changed</div>`;for(let t=0;t<n.length;t++){let i=n[t],a=i.additions||0,l=i.deletions||0,c="";switch(i.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}e+=`<div class="git-popover-item" data-idx="${t}">
18
- ${c}<span class="file-path">${f(i.path)}</span>
19
- <span class="file-add">${a>0?"+"+a:""}</span>
20
- <span class="file-del">${l>0?"-"+l:""}</span>
21
- </div>`}e+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',ut=e,k.innerHTML=e,k.querySelectorAll(".git-popover-item").forEach(t=>{let i=parseInt(t.dataset.idx||"0",10),a=n[i];!a||a.status==="D"||(t.classList.add("clickable"),t.style.cursor="pointer",t.addEventListener("click",()=>mt(a.path)))})}Pe(),k.classList.add("open"),ke.classList.add("open"),dt()}function sn(){return window.innerWidth>=1024}function ft(n,e){let t=(n+(e&&n?`
22
- `:"")+e).split(`
23
- `),i="",a=0,l=0;for(let c of t){let r=c;if(r.startsWith("@@")){let d=r.match(/@@\s+-(\d+)[^+]*\+(\d+)/);d&&(a=parseInt(d[1],10)-1,l=parseInt(d[2],10)-1),i+=`<div class="diff-line hunk"><span class="ln-body">${f(r)}</span></div>`;continue}if(r.startsWith("diff --git")||r.startsWith("index ")||r.startsWith("--- ")||r.startsWith("+++ ")||r.startsWith("new file")||r.startsWith("deleted file")){r.startsWith("diff --git")&&(i+=`<div class="diff-line header"><span class="ln-body">${f(r)}</span></div>`);continue}if(r.startsWith("+"))l++,i+=`<div class="diff-line add"><span class="ln-no">${l}</span><span class="ln-body">${f(r)}</span></div>`;else if(r.startsWith("-"))a++,i+=`<div class="diff-line del"><span class="ln-no">${a}</span><span class="ln-body">${f(r)}</span></div>`;else if(r.startsWith("\\"))i+=`<div class="diff-line"><span class="ln-body">${f(r)}</span></div>`;else{if(!r&&!a&&!l)continue;a++,l++,i+=`<div class="diff-line"><span class="ln-no">${a}</span><span class="ln-no" style="margin-right:12px">${l}</span><span class="ln-body">${f(r)}</span></div>`}}return{html:i,lineCount:t.length}}var Te=null;async function mt(n){let e=q;if(e)if(Ce(),sn()){I.style.display="none",h&&(Se(),h.destroy(),h=null,Te=y,y=null),re(null),se.style.display="none",F.style.display="none",rt.style.display="flex",ct.textContent=n,Ee.textContent="Loading\u2026",xe.innerHTML="";try{let t=await fetch(M("/api/git/diff?path="+encodeURIComponent(e)+"&file="+encodeURIComponent(n)));if(!t.ok)throw new Error(await t.text());let i=await t.json();on(n,i.diff,i.stagedDiff)}catch(t){let i=t instanceof Error?t.message:String(t);Ee.textContent="Error",xe.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${f(i)}</div>`}}else{k.innerHTML=`<div class="git-diff-view"><div class="diff-header">
14
+ ${f(e)}</div>`;return(t>0||i>0)&&(a+=`<div class="git-diff-stats">+<span class="add">${t}</span> <span class="del">-${i}</span></div>`),a+"</div>"}m.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&U&&g&&(e.stopPropagation(),dt())});var se=document.getElementById("header-git"),ot=document.getElementById("git-diff-view"),lt=document.getElementById("gd-path"),Ee=document.getElementById("gd-status"),xe=document.getElementById("gd-content"),Gt=document.getElementById("gd-back"),U=null;function oe(e){if(!e||!e.branch){se.style.display="none",U=null;return}U=e.repoRoot;let{branch:t,linesAdded:i,linesRemoved:a}=e,o=`<span class="branch">${f(t)}</span>`;(i>0||a>0)&&(o+=`<span class="sep">\xB7</span>+<span class="diff-add">${i}</span> <span class="diff-del">-${a}</span>`),se.innerHTML=o,se.style.display="block"}se.addEventListener("click",e=>{U&&g&&(e.stopPropagation(),dt())});var L=document.getElementById("git-popover"),ke=document.getElementById("git-popover-backdrop");function Ne(){let e=se.getBoundingClientRect();L.style.top=e.bottom+4+"px",L.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function rt(){let e=U,t=document.getElementById("git-popover-browse");t&&t.addEventListener("click",()=>{if(Te(),e){Ae="files",document.querySelectorAll(".mode-btn").forEach(a=>a.classList.remove("active"));let i=document.getElementById("mode-files");i&&i.classList.add("active"),N(e)}})}var ct="";function dt(){if(!g)return;let e=g.files||[];if(!e.length)L.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
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 i=0;i<e.length;i++){let a=e[i],o=a.additions||0,r=a.deletions||0,c="";switch(a.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="${i}">
16
+ ${c}<span class="file-path">${f(a.path)}</span>
17
+ <span class="file-add">${o>0?"+"+o:""}</span>
18
+ <span class="file-del">${r>0?"-"+r:""}</span>
19
+ </div>`}t+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',ct=t,L.innerHTML=t,L.querySelectorAll(".git-popover-item").forEach(i=>{let a=parseInt(i.dataset.idx||"0",10),o=e[a];!o||o.status==="D"||(i.classList.add("clickable"),i.style.cursor="pointer",i.addEventListener("click",()=>ut(o.path)))})}Ne(),L.classList.add("open"),ke.classList.add("open"),rt()}function en(){return window.innerWidth>=1024}function pt(e,t){let i=(e+(t&&e?`
20
+ `:"")+t).split(`
21
+ `),a="",o=0,r=0;for(let c of i){let l=c;if(l.startsWith("@@")){let p=l.match(/@@\s+-(\d+)[^+]*\+(\d+)/);p&&(o=parseInt(p[1],10)-1,r=parseInt(p[2],10)-1),a+=`<div class="diff-line hunk"><span class="ln-body">${f(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">${f(l)}</span></div>`);continue}if(l.startsWith("+"))r++,a+=`<div class="diff-line add"><span class="ln-no">${r}</span><span class="ln-body">${f(l)}</span></div>`;else if(l.startsWith("-"))o++,a+=`<div class="diff-line del"><span class="ln-no">${o}</span><span class="ln-body">${f(l)}</span></div>`;else if(l.startsWith("\\"))a+=`<div class="diff-line"><span class="ln-body">${f(l)}</span></div>`;else{if(!l&&!o&&!r)continue;o++,r++,a+=`<div class="diff-line"><span class="ln-no">${o}</span><span class="ln-no" style="margin-right:12px">${r}</span><span class="ln-body">${f(l)}</span></div>`}}return{html:a,lineCount:i.length}}var Ce=null;async function ut(e){let t=U;if(t)if(Te(),en()){T.style.display="none",v&&(Ie(),v.destroy(),v=null,Ce=y,y=null),oe(null),ne.style.display="none",z.style.display="none",ot.style.display="flex",lt.textContent=e,Ee.textContent="Loading\u2026",xe.innerHTML="";try{let i=await fetch(k("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!i.ok)throw new Error(await i.text());let a=await i.json();nn(e,a.diff,a.stagedDiff)}catch(i){let a=i instanceof Error?i.message:String(i);Ee.textContent="Error",xe.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${f(a)}</div>`}}else{L.innerHTML=`<div class="git-diff-view"><div class="diff-header">
24
22
  <button class="diff-back">\u2190 Back</button>
25
- <span class="diff-filename">${f(n)}</span>
23
+ <span class="diff-filename">${f(e)}</span>
26
24
  <span class="diff-loading">Loading\u2026</span>
27
- </div></div>`,Pe(),k.classList.add("open"),ke.classList.add("open");try{let t=await fetch(M("/api/git/diff?path="+encodeURIComponent(e)+"&file="+encodeURIComponent(n)));if(!t.ok)throw new Error(await t.text());let i=await t.json();an(n,i.diff,i.stagedDiff)}catch(t){let i=t instanceof Error?t.message:String(t);k.innerHTML=`<div class="git-diff-view"><div class="diff-header">
25
+ </div></div>`,Ne(),L.classList.add("open"),ke.classList.add("open");try{let i=await fetch(k("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!i.ok)throw new Error(await i.text());let a=await i.json();tn(e,a.diff,a.stagedDiff)}catch(i){let a=i instanceof Error?i.message:String(i);L.innerHTML=`<div class="git-diff-view"><div class="diff-header">
28
26
  <button class="diff-back">\u2190 Back</button>
29
- <span class="diff-filename">${f(n)}</span>
30
- </div><div class="diff-empty">Error loading diff: ${f(i)}</div></div>`}}}function an(n,e,t){if(!e&&!t){k.innerHTML=`<div class="git-diff-view"><div class="diff-header">
27
+ <span class="diff-filename">${f(e)}</span>
28
+ </div><div class="diff-empty">Error loading diff: ${f(a)}</div></div>`}}}function tn(e,t,i){if(!t&&!i){L.innerHTML=`<div class="git-diff-view"><div class="diff-header">
31
29
  <button class="diff-back">\u2190 Back</button>
32
- <span class="diff-filename">${f(n)}</span>
33
- </div><div class="diff-empty">No diff content available</div></div>`;return}let{html:i,lineCount:a}=ft(e,t);k.innerHTML=`<div class="git-diff-view"><div class="diff-header">
30
+ <span class="diff-filename">${f(e)}</span>
31
+ </div><div class="diff-empty">No diff content available</div></div>`;return}let{html:a,lineCount:o}=pt(t,i);L.innerHTML=`<div class="git-diff-view"><div class="diff-header">
34
32
  <button class="diff-back">\u2190 Back</button>
35
- <span class="diff-filename">${f(n)}</span>
36
- <span class="diff-filesize">${a} lines</span>
37
- </div><div class="diff-content">${i}</div></div>`,k.querySelector(".diff-back")?.addEventListener("click",()=>{k.innerHTML=ut,Pe(),g&&k.querySelectorAll(".git-popover-item").forEach(l=>{let c=parseInt(l.dataset.idx||"0",10),r=g.files[c];!r||r.status==="D"||(l.style.cursor="pointer",l.addEventListener("click",()=>mt(r.path)))}),dt()})}function on(n,e,t){if(!e&&!t){Ee.textContent="No diff",xe.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}let{html:i,lineCount:a}=ft(e,t);ct.textContent=n,Ee.textContent=a+" lines",xe.innerHTML=i}function ln(){rt.style.display="none",Te?(I.style.display="",I.classList.add("terminal-pending"),oe(Te),Te=null):y?(I.style.display="",I.classList.add("terminal-pending"),vt()):F.style.display="flex"}nn.addEventListener("click",ln);function Ce(){k.classList.remove("open"),ke.classList.remove("open")}ke.addEventListener("click",Ce),document.addEventListener("keydown",n=>{n.key==="Escape"&&(Ce(),Be())});async function ht(n){try{let e=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(n))).json();if(e.path){let t=await fetch("/api/git/status?path="+encodeURIComponent(e.path));if(t.ok){let i=await t.json();g=i,re(i);return}}}catch{}g=null,re(null)}var Me=null;function vt(){Se(),y&&(ht(y),Me=setInterval(()=>{y&&ht(y)},3e4))}function Se(){Me&&(clearInterval(Me),Me=null)}function rn(){if(Le=K.fsRoots||[],Le.length===0){if(y){cn(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 n of Le){let e=document.createElement("div");e.className="file-tree-item",e.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${f(n)}`,e.addEventListener("click",()=>j(n)),m.appendChild(e)}}async function cn(n){m.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let e=await(await fetch(M("/api/fs/session-path?session="+encodeURIComponent(n)))).json();e.path?j(e.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 j(n){W=n,m.innerHTML='<div class="file-tree-empty">Loading...</div>';let e=en(n);try{let t=await(await fetch(M("/api/fs/list?path="+encodeURIComponent(n)))).json();await e,m.innerHTML="",re(g),m.innerHTML+=tn();let i=dn(n);if(i&&!Le.some(l=>n===l)){let l=document.createElement("div");l.className="file-tree-item",l.innerHTML="..",l.addEventListener("click",()=>j(i)),m.appendChild(l)}if(t.dirs)for(let l of t.dirs){let c=l.split("/").pop()||l.split("\\").pop(),r=document.createElement("div");r.className="file-tree-item",r.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${f(c)}`,r.addEventListener("click",()=>j(l)),m.appendChild(r)}else m.innerHTML="";let a=t.files||[];for(let l of a){let c=l.split("/").pop()||l.split("\\").pop(),r=Qt(l),d="",p="";if(r)switch(r){case"M":d="git-mod",p='<span class="git-file-status M">M</span>';break;case"A":d="git-add",p='<span class="git-file-status A">A</span>';break;case"D":d="git-del",p='<span class="git-file-status D">D</span>';break;case"?":d="git-untracked",p='<span class="git-file-status ?">?</span>';break;case"R":d="git-mod",p='<span class="git-file-status R">R</span>';break}let L=document.createElement("div");L.className="file-tree-item file"+(d?" "+d:""),L.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${p}${f(c)}`,L.addEventListener("click",()=>un(l)),m.appendChild(L)}}catch{m.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function dn(n){let e=n.lastIndexOf("/");return e<=0?null:n.substring(0,e)}async function un(n){we=n,I.style.display="none",h&&(Se(),h.destroy(),h=null,y=null),re(null),Ye(),F.style.display="none",se.style.display="flex",Ot.textContent=n,D.textContent="Loading...",V.style.display="none";try{let e=await fetch(M("/api/file?path="+encodeURIComponent(n)));if(!e.ok){let i=await e.json();D.textContent=i.error||"Failed to load",ae.value="";return}let t=await e.json();ae.value=t.content,be=t.content,tt=t.content,D.textContent=t.size+" bytes",V.style.display=""}catch{D.textContent="Failed to load",ae.value=""}}ae.addEventListener("input",()=>{be=ae.value}),V.addEventListener("click",async()=>{V.textContent="Saving...",V.disabled=!0;try{let n=await fetch(M("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:we,content:be})});if(!n.ok){let e=await n.json();D.textContent=e.error||"Save failed";return}tt=be,D.textContent="Saved"}catch{D.textContent="Save failed"}finally{V.disabled=!1,V.textContent="Save"}}),qt.addEventListener("click",async()=>{if(confirm("Delete "+we+"?"))try{let n=await fetch(M("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:we})});if(!n.ok){let e=await n.json();D.textContent=e.error||"Delete failed";return}se.style.display="none",F.style.display="flex",W&&j(W)}catch{D.textContent="Delete failed"}}),Xt.addEventListener("click",()=>{se.style.display="none",F.style.display="flex",W&&j(W)}),Jt.addEventListener("click",async()=>{let n=nt.value.trim();if(!n)return;let e=W+"/"+n;try{let t=await fetch(M("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:e})});if(!t.ok){let i=await t.json();D.textContent=i.error||"Create failed";return}nt.value="",W&&j(W)}catch{D.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let n=it.classList.toggle("open");Ae.classList.toggle("open",n)}),Ae.addEventListener("click",()=>{it.classList.remove("open"),Ae.classList.remove("open")});var _="",ce=document.createElement("div");ce.className="session-popover-backdrop",document.body.appendChild(ce);var P=document.createElement("div");P.id="session-popover",P.className="session-popover",P.style.display="none",P.innerHTML=`<div class="sp-field">
33
+ <span class="diff-filename">${f(e)}</span>
34
+ <span class="diff-filesize">${o} lines</span>
35
+ </div><div class="diff-content">${a}</div></div>`,L.querySelector(".diff-back")?.addEventListener("click",()=>{L.innerHTML=ct,Ne(),g&&L.querySelectorAll(".git-popover-item").forEach(r=>{let c=parseInt(r.dataset.idx||"0",10),l=g.files[c];!l||l.status==="D"||(r.style.cursor="pointer",r.addEventListener("click",()=>ut(l.path)))}),rt()})}function nn(e,t,i){if(!t&&!i){Ee.textContent="No diff",xe.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}let{html:a,lineCount:o}=pt(t,i);lt.textContent=e,Ee.textContent=o+" lines",xe.innerHTML=a}function an(){ot.style.display="none",Ce?(T.style.display="",T.classList.add("terminal-pending"),ae(Ce),Ce=null):y?(T.style.display="",T.classList.add("terminal-pending"),mt()):z.style.display="flex"}Gt.addEventListener("click",an);function Te(){L.classList.remove("open"),ke.classList.remove("open")}ke.addEventListener("click",Te),document.addEventListener("keydown",e=>{e.key==="Escape"&&(Te(),Be())});async function ft(e){try{let t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){let i=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(i.ok){let a=await i.json();g=a,oe(a);return}}}catch{}g=null,oe(null)}var Me=null;function mt(){Ie(),y&&(ft(y),Me=setInterval(()=>{y&&ft(y)},3e4))}function Ie(){Me&&(clearInterval(Me),Me=null)}function sn(){if(Le=te.fsRoots||[],Le.length===0){if(y){on(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 Le){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> ${f(e)}`,t.addEventListener("click",()=>N(e)),m.appendChild(t)}}async function on(e){m.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let t=await(await fetch(k("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?N(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 N(e){P=e,m.innerHTML='<div class="file-tree-empty">Loading...</div>';let t=Qt(e);try{let i=await(await fetch(k("/api/fs/list?path="+encodeURIComponent(e)))).json();await t,m.innerHTML="",oe(g),m.innerHTML+=Zt();let a=ln(e);if(a&&!Le.some(r=>e===r)){let r=document.createElement("div");r.className="file-tree-item",r.innerHTML="..",r.addEventListener("click",()=>N(a)),m.appendChild(r)}if(i.dirs)for(let r of i.dirs){let c=r.split("/").pop()||r.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item",l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${f(c)}`,l.addEventListener("click",()=>N(r)),m.appendChild(l)}else m.innerHTML="";let o=i.files||[];for(let r of o){let c=r.split("/").pop()||r.split("\\").pop(),l=Jt(r),p="",u="";if(l)switch(l){case"M":p="git-mod",u='<span class="git-file-status M">M</span>';break;case"A":p="git-add",u='<span class="git-file-status A">A</span>';break;case"D":p="git-del",u='<span class="git-file-status D">D</span>';break;case"?":p="git-untracked",u='<span class="git-file-status ?">?</span>';break;case"R":p="git-mod",u='<span class="git-file-status R">R</span>';break}let M=document.createElement("div");M.className="file-tree-item file"+(p?" "+p:""),M.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${u}${f(c)}`,M.addEventListener("click",()=>rn(r)),m.appendChild(M)}}catch{m.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function ln(e){let t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function rn(e){we=e,T.style.display="none",v&&(Ie(),v.destroy(),v=null,y=null),oe(null),$e(),z.style.display="none",ne.style.display="flex",Ut.textContent=e,I.textContent="Loading...",Y.style.display="none";try{let t=await fetch(k("/api/file?path="+encodeURIComponent(e)));if(!t.ok){let a=await t.json();I.textContent=a.error||"Failed to load",ie.value="";return}let i=await t.json();ie.value=i.content,be=i.content,Ge=i.content,I.textContent=i.size+" bytes",Y.style.display=""}catch{I.textContent="Failed to load",ie.value=""}}ie.addEventListener("input",()=>{be=ie.value}),Y.addEventListener("click",async()=>{Y.textContent="Saving...",Y.disabled=!0;try{let e=await fetch(k("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:we,content:be})});if(!e.ok){let t=await e.json();I.textContent=t.error||"Save failed";return}Ge=be,I.textContent="Saved"}catch{I.textContent="Save failed"}finally{Y.disabled=!1,Y.textContent="Save"}}),Ot.addEventListener("click",async()=>{if(confirm("Delete "+we+"?"))try{let e=await fetch(k("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:we})});if(!e.ok){let t=await e.json();I.textContent=t.error||"Delete failed";return}ne.style.display="none",z.style.display="flex",P&&N(P)}catch{I.textContent="Delete failed"}}),Kt.addEventListener("click",()=>{ne.style.display="none",z.style.display="flex",P&&N(P)}),qt.addEventListener("click",async()=>{let e=et.value.trim();if(!e)return;let t=P+"/"+e;try{let i=await fetch(k("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!i.ok){let a=await i.json();I.textContent=a.error||"Create failed";return}et.value="",P&&N(P)}catch{I.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let e=tt.classList.toggle("open");Re.classList.toggle("open",e)}),Re.addEventListener("click",()=>{tt.classList.remove("open"),Re.classList.remove("open")});var j="",le=document.createElement("div");le.className="session-popover-backdrop",document.body.appendChild(le);var R=document.createElement("div");R.id="session-popover",R.className="session-popover",R.style.display="none",R.innerHTML=`<div class="sp-field">
38
36
  <label>Rename session</label>
39
37
  <input type="text" id="sp-name" />
40
38
  </div>
@@ -43,5 +41,5 @@ var Ft="https://esm.sh/ghostty-web@0.4.0",Vt=/\x1b\](?:10|11|110|111|104)(?:;[^\
43
41
  <button class="btn" id="sp-cancel">Cancel</button>
44
42
  </div>
45
43
  <hr class="sp-divider" />
46
- <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(P);function pn(n){let e=n.getBoundingClientRect();P.style.top=e.bottom+4+"px",P.style.left=Math.max(8,Math.min(e.left,window.innerWidth-240))+"px"}function fn(n,e){_=n;let t=document.getElementById("sp-name");t.value=n,ce.classList.add("open"),P.style.display="block",pn(e),setTimeout(()=>t.focus(),50)}function de(){ce.classList.remove("open"),P.style.display="none"}ce.addEventListener("click",de),document.getElementById("sp-save").addEventListener("click",async()=>{let n=document.getElementById("sp-name").value.trim();if(!n||n===_){de();return}try{if(!(await fetch(M("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:_,newName:n})})).ok)return;de(),O(),y===_&&oe(n)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+_+'"?'))try{if(!(await fetch(M("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:_})})).ok)return;de(),y===_&&(h&&(h.destroy(),h=null),y=null,I.style.display="none",F.style.display="flex"),O()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",de);var mn={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r",exit:"exit\r",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",n=>{let e=n.target.closest("button[data-key]");if(!e||!h)return;let t=mn[e.dataset.key];t&&(h.sendInput(t),h.focus())});var R=document.getElementById("mk-input"),gt=document.getElementById("mk-send");function yt(){if(!R||!h)return;let n=R.value;n&&(h.sendInput(n+"\r"),R.value="",R.rows=1,h.focus())}if(R&&(R.addEventListener("input",()=>{R.rows=1;let n=R.value.split(`
47
- `).length;R.rows=Math.min(n,4)}),R.addEventListener("keydown",n=>{n.key==="Enter"&&!n.shiftKey&&(n.preventDefault(),yt())})),gt&&gt.addEventListener("click",yt),window.visualViewport){let n=document.querySelector(".app-layout"),e=()=>{n&&(n.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",e),window.addEventListener("orientationchange",()=>setTimeout(e,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}var ze=document.getElementById("process-panel"),ue=document.getElementById("proc-list"),hn=document.getElementById("proc-close"),Ne=document.getElementById("proc-backdrop"),vn=document.getElementById("header-ram");function gn(){ue.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(M("/api/system/processes")).then(n=>n.json()).then(n=>{if(!Array.isArray(n)||!n.length){ue.innerHTML='<div class="proc-empty">No processes</div>';return}let e="";for(let t of n){let i=t.rss,a=i<1048576?(i/1024).toFixed(0)+"K":(i/1048576).toFixed(1)+"M";e+='<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="'+f(t.command)+'">'+f(t.command)+'</span><button class="proc-kill" title="Kill PID '+t.pid+'">&times;</button></div>'}ue.innerHTML=e;for(let t of ue.querySelectorAll(".proc-row")){let i=t.querySelector(".proc-kill");i&&i.addEventListener("click",a=>{let l=a.target.closest(".proc-row");!l||!confirm("Kill PID "+l.dataset.pid+"?")||fetch(M("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(l.dataset.pid,10)})}).then(c=>c.json()).then(c=>{c.ok?l.style.opacity="0.3":alert("Failed: "+(c.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{ue.innerHTML='<div class="proc-empty">Failed to load</div>'})}function yn(){ze.classList.add("open"),Ne.classList.add("open"),gn()}function Be(){ze.classList.remove("open"),Ne.classList.remove("open")}vn.addEventListener("click",n=>{n.stopPropagation(),ze.classList.contains("open")?Be():yn()}),hn.addEventListener("click",Be),Ne.addEventListener("click",Be),window.__openSession=oe,window.__refreshSidebar=O,O(),Ye(),setInterval(()=>{document.visibilityState==="visible"&&O()},1e4);
44
+ <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(R);function cn(e){let t=e.getBoundingClientRect();R.style.top=t.bottom+4+"px",R.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function dn(e,t){j=e;let i=document.getElementById("sp-name");i.value=e,le.classList.add("open"),R.style.display="block",cn(t),setTimeout(()=>i.focus(),50)}function re(){le.classList.remove("open"),R.style.display="none"}le.addEventListener("click",re),document.getElementById("sp-save").addEventListener("click",async()=>{let e=document.getElementById("sp-name").value.trim();if(!e||e===j){re();return}try{if(!(await fetch(k("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:j,newName:e})})).ok)return;re(),W(),y===j&&ae(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+j+'"?'))try{if(!(await fetch(k("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:j})})).ok)return;re(),y===j&&(v&&(v.destroy(),v=null),y=null,T.style.display="none",z.style.display="flex"),W()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",re);var pn={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r",exit:"exit\r",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{let t=e.target.closest("button[data-key]");if(!t||!v)return;let i=pn[t.dataset.key];i&&(v.sendInput(i),v.focus())});var S=document.getElementById("mk-input"),vt=document.getElementById("mk-send");function ht(){if(!S||!v)return;let e=S.value;e&&(v.sendInput(e+"\r"),S.value="",S.rows=1,v.focus())}if(S&&(S.addEventListener("input",()=>{S.rows=1;let e=S.value.split(`
45
+ `).length;S.rows=Math.min(e,4)}),S.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),ht())})),vt&&vt.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 _e=document.getElementById("process-panel"),ce=document.getElementById("proc-list"),un=document.getElementById("proc-close"),ze=document.getElementById("proc-backdrop"),fn=document.getElementById("header-ram");function mn(){ce.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(k("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){ce.innerHTML='<div class="proc-empty">No processes</div>';return}let t="";for(let i of e){let a=i.rss,o=a<1048576?(a/1024).toFixed(0)+"K":(a/1048576).toFixed(1)+"M";t+='<div class="proc-row" data-pid="'+i.pid+'"><span class="proc-mem">'+i.mem+'%</span><span class="proc-rss">'+o+'</span><span class="proc-cmd" title="'+f(i.command)+'">'+f(i.command)+'</span><button class="proc-kill" title="Kill PID '+i.pid+'">&times;</button></div>'}ce.innerHTML=t;for(let i of ce.querySelectorAll(".proc-row")){let a=i.querySelector(".proc-kill");a&&a.addEventListener("click",o=>{let r=o.target.closest(".proc-row");!r||!confirm("Kill PID "+r.dataset.pid+"?")||fetch(k("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(r.dataset.pid,10)})}).then(c=>c.json()).then(c=>{c.ok?r.style.opacity="0.3":alert("Failed: "+(c.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{ce.innerHTML='<div class="proc-empty">Failed to load</div>'})}function vn(){_e.classList.add("open"),ze.classList.add("open"),mn()}function Be(){_e.classList.remove("open"),ze.classList.remove("open")}fn.addEventListener("click",e=>{e.stopPropagation(),_e.classList.contains("open")?Be():vn()}),un.addEventListener("click",Be),ze.addEventListener("click",Be),window.__openSession=ae,window.__refreshSidebar=W,W(),$e(),setInterval(()=>{document.visibilityState==="visible"&&W()},1e4);
@@ -1,17 +1,17 @@
1
- import{initTerminal as ke}from"./terminal-core.js";const M=window.__TMUX_WEB_SHELL__;let p=null,u=null;const ie=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null;let v=ie;window.__tmuxWebScopeAgent=ie;let Y="sessions",E="",j="",W="",oe="";function y(e){return v?"/a/"+encodeURIComponent(v)+e:e}const r=document.getElementById("sidebar-content"),C=document.getElementById("main-placeholder"),h=document.getElementById("terminal-container"),H=document.getElementById("file-editor"),Ce=document.getElementById("fe-path"),b=document.getElementById("fe-status"),$=document.getElementById("fe-content"),I=document.getElementById("fe-save"),Ie=document.getElementById("fe-delete"),Be=document.getElementById("fe-back"),ae=document.getElementById("fe-new-name"),xe=document.getElementById("fe-new-btn"),Z=document.getElementById("settings-backdrop"),le=document.getElementById("settings-popover"),ce=document.querySelector(".sidebar");function Me(){return window.matchMedia("(max-width: 640px)").matches}function Q(){Me()&&ce.classList.add("collapsed")}function de(){ce.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{de(),re("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{de(),re("files")});function re(e){Y=e,document.querySelectorAll(".mode-btn").forEach(t=>t.classList.remove("active"));const n=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");n&&n.classList.add("active"),e==="sessions"?(ee="",S()):_e()}let ee="";async function S(){if(Y!=="sessions")return;try{const t=await(await fetch("/api/sidebar/sessions")).json(),s=JSON.stringify({recent:t.recent,agents:t.agents});if(s===ee)return;ee=s,Se(t)}catch{r.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}const e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function Se(e){r.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';const n=e.recent||[],t=document.createElement("div");t.className="sidebar-section-label"+(v?" clickable":""),t.textContent=v?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",v&&(t.title="Back to hub",t.addEventListener("click",()=>{window.location.href="/"})),r.appendChild(t);for(const i of n){const l=document.createElement("div");l.className="session-item"+(i.name===u&&!v?" active":""),l.setAttribute("data-session",i.name),l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${i.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
1
+ import{initTerminal as ke}from"./terminal-core.js";const T=window.__TMUX_WEB_SHELL__;let p=null,u=null;const ie=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null;let v=ie;window.__tmuxWebScopeAgent=ie;let Y="sessions",E="",j="",W="",oe="";function y(e){return v?"/a/"+encodeURIComponent(v)+e:e}const r=document.getElementById("sidebar-content"),C=document.getElementById("main-placeholder"),h=document.getElementById("terminal-container"),H=document.getElementById("file-editor"),Ce=document.getElementById("fe-path"),b=document.getElementById("fe-status"),$=document.getElementById("fe-content"),I=document.getElementById("fe-save"),Ie=document.getElementById("fe-delete"),Be=document.getElementById("fe-back"),ae=document.getElementById("fe-new-name"),xe=document.getElementById("fe-new-btn"),Z=document.getElementById("settings-backdrop"),le=document.getElementById("settings-popover"),ce=document.querySelector(".sidebar");function Me(){return window.matchMedia("(max-width: 640px)").matches}function Q(){Me()&&ce.classList.add("collapsed")}function de(){ce.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{de(),re("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{de(),re("files")});function re(e){Y=e,document.querySelectorAll(".mode-btn").forEach(t=>t.classList.remove("active"));const n=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");n&&n.classList.add("active"),e==="sessions"?(ee="",M()):_e()}let ee="";async function M(){if(Y!=="sessions")return;try{const t=await(await fetch("/api/sidebar/sessions")).json(),s=JSON.stringify({recent:t.recent,agents:t.agents});if(s===ee)return;ee=s,Se(t)}catch{r.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}const e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function Se(e){r.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';const n=e.recent||[],t=document.createElement("div");t.className="sidebar-section-label"+(v?" clickable":""),t.textContent=v?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",v&&(t.title="Back to hub",t.addEventListener("click",()=>{window.location.href="/"})),r.appendChild(t);for(const i of n){const l=document.createElement("div");l.className="session-item"+(i.name===u&&!v?" active":""),l.setAttribute("data-session",i.name),l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${i.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
2
2
  <span>${d(i.name)}</span>
3
3
  <button class="session-edit-btn" data-session="${d(i.name)}" title="Edit session"><svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
4
4
  `,l.addEventListener("click",c=>{c.target.closest(".session-edit-btn")||R(i.name)}),r.appendChild(l);const o=l.querySelector(".session-edit-btn");o&&o.addEventListener("click",c=>{c.stopPropagation(),Oe(i.name,o)})}const s=e.agents||[];for(const i of s){if(!i.sessions||i.sessions.length===0)continue;const l=i.agentId===v,o=document.createElement("div");o.className="sidebar-section-label machine-label"+(i.online?"":" agent-offline-label")+(!l&&i.online?" clickable":""),o.textContent=i.agentName+(i.online?"":" (offline)"),!l&&i.online&&(o.title="\u6253\u5F00 "+i.agentName,o.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(i.agentId)+"/"})),r.appendChild(o);for(const c of i.sessions){const a=document.createElement("div");a.className="session-item"+(c.name===u&&v===i.agentId?" active":"")+(i.online?"":" session-offline"),a.setAttribute("data-session",c.name),a.setAttribute("data-agent",i.agentId),a.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${c.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
5
- <span>${d(c.name)}</span><span class="meta">${i.online?"":"offline"}</span>`,i.online&&a.addEventListener("click",g=>{g.target.closest(".session-edit-btn")||R(c.name,i.agentId)}),r.appendChild(a)}}}function d(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function R(e,n){if(!(u===e&&p&&v===(n||null))){H.style.display="none",C.style.display="none",h.style.display="",h.classList.add("terminal-pending"),Q(),p&&(K(),p.destroy(),p=null),u=e,v=n||null,window.__tmuxWebScopeAgent=v;for(const t of r.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e&&(t.getAttribute("data-agent")||null)===v);try{p=await ke(h,e,{terminal:M.terminal,scrollback:M.scrollback,theme:M.theme,renderer:M.renderer,wsBase:v?"/ws/a/"+encodeURIComponent(v):M.wsBase||"/ws"}),we()}catch(t){console.error("[shell] terminal init failed:",t);const s=t&&typeof t=="object"&&"message"in t?t.message:String(t);h.textContent="Failed to open terminal for "+e+": "+s,h.classList.remove("terminal-pending")}}}let z=[],f=null,pe=!1;function Te(e){if(!f||!f.files.length)return null;const n=f.repoRoot;if(!n||!e.startsWith(n))return null;const t=e.slice(n.length+1);for(const s of f.files)if(s.path===t)return s.status;return null}async function He(e){pe=!0;try{const n=await fetch(y("/api/git/status?path="+encodeURIComponent(e)));n.ok?f=await n.json():f=null}catch{f=null}finally{pe=!1}}function $e(){if(!f||!f.branch)return"";const{branch:e,linesAdded:n,linesRemoved:t}=f;let s=`<div class="sidebar-git"><div class="git-branch-badge">
5
+ <span>${d(c.name)}</span><span class="meta">${i.online?"":"offline"}</span>`,i.online&&a.addEventListener("click",g=>{g.target.closest(".session-edit-btn")||R(c.name,i.agentId)}),r.appendChild(a)}}}function d(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function R(e,n){if(!(u===e&&p&&v===(n||null))){H.style.display="none",C.style.display="none",h.style.display="",h.classList.add("terminal-pending"),Q(),p&&(K(),p.destroy(),p=null),u=e,v=n||null,window.__tmuxWebScopeAgent=v;for(const t of r.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e&&(t.getAttribute("data-agent")||null)===v);try{p=await ke(h,e,{terminal:T.terminal,scrollback:T.scrollback,theme:T.theme,wsBase:v?"/ws/a/"+encodeURIComponent(v):T.wsBase||"/ws"}),we()}catch(t){console.error("[shell] terminal init failed:",t);const s=t&&typeof t=="object"&&"message"in t?t.message:String(t);h.textContent="Failed to open terminal for "+e+": "+s,h.classList.remove("terminal-pending")}}}let z=[],f=null,pe=!1;function Te(e){if(!f||!f.files.length)return null;const n=f.repoRoot;if(!n||!e.startsWith(n))return null;const t=e.slice(n.length+1);for(const s of f.files)if(s.path===t)return s.status;return null}async function He(e){pe=!0;try{const n=await fetch(y("/api/git/status?path="+encodeURIComponent(e)));n.ok?f=await n.json():f=null}catch{f=null}finally{pe=!1}}function $e(){if(!f||!f.branch)return"";const{branch:e,linesAdded:n,linesRemoved:t}=f;let s=`<div class="sidebar-git"><div class="git-branch-badge">
6
6
  <svg viewBox="0 0 16 16" fill="currentColor"><path d="M11.63 1.22a.75.75 0 00-1.06 0L5.99 5.8 4.12 3.93a.75.75 0 10-1.06 1.06L4.93 6.87A3.5 3.5 0 003 10.09v3.16a.75.75 0 001 0v-3.16a2.5 2.5 0 012.5-2.5h.27l-1.06 1.06a.75.75 0 001.06 1.06l2.37-2.38a.75.75 0 000-1.06L7.59 5.27l4.04-4.05a.75.75 0 000-1.06L11.63 1.22zM3.25 13.94a.75.75 0 01-.75-.75v-.69a.75.75 0 011.5 0v.69c0 .414-.336.75-.75.75z"/></svg>
7
- ${d(e)}</div>`;return(n>0||t>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${n}</span> <span class="del">-${t}</span></div>`),s+"</div>"}r.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&T&&f&&(e.stopPropagation(),ge())});const N=document.getElementById("header-git"),fe=document.getElementById("git-diff-view"),ue=document.getElementById("gd-path"),O=document.getElementById("gd-status"),U=document.getElementById("gd-content"),Re=document.getElementById("gd-back");let T=null;function D(e){if(!e||!e.branch){N.style.display="none",T=null;return}T=e.repoRoot;const{branch:n,linesAdded:t,linesRemoved:s}=e;let i=`<span class="branch">${d(n)}</span>`;(t>0||s>0)&&(i+=`<span class="sep">\xB7</span>+<span class="diff-add">${t}</span> <span class="diff-del">-${s}</span>`),N.innerHTML=i,N.style.display="block"}N.addEventListener("click",e=>{T&&f&&(e.stopPropagation(),ge())});const m=document.getElementById("git-popover"),G=document.getElementById("git-popover-backdrop");function te(){const e=N.getBoundingClientRect();m.style.top=e.bottom+4+"px",m.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function me(){const e=T,n=document.getElementById("git-popover-browse");n&&n.addEventListener("click",()=>{if(q(),e){Y="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));const t=document.getElementById("mode-files");t&&t.classList.add("active"),k(e)}})}let ve="";function ge(){if(!f)return;const e=f.files||[];if(!e.length)m.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
7
+ ${d(e)}</div>`;return(n>0||t>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${n}</span> <span class="del">-${t}</span></div>`),s+"</div>"}r.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&S&&f&&(e.stopPropagation(),ge())});const N=document.getElementById("header-git"),fe=document.getElementById("git-diff-view"),ue=document.getElementById("gd-path"),O=document.getElementById("gd-status"),U=document.getElementById("gd-content"),Re=document.getElementById("gd-back");let S=null;function D(e){if(!e||!e.branch){N.style.display="none",S=null;return}S=e.repoRoot;const{branch:n,linesAdded:t,linesRemoved:s}=e;let i=`<span class="branch">${d(n)}</span>`;(t>0||s>0)&&(i+=`<span class="sep">\xB7</span>+<span class="diff-add">${t}</span> <span class="diff-del">-${s}</span>`),N.innerHTML=i,N.style.display="block"}N.addEventListener("click",e=>{S&&f&&(e.stopPropagation(),ge())});const m=document.getElementById("git-popover"),G=document.getElementById("git-popover-backdrop");function te(){const e=N.getBoundingClientRect();m.style.top=e.bottom+4+"px",m.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function me(){const e=S,n=document.getElementById("git-popover-browse");n&&n.addEventListener("click",()=>{if(q(),e){Y="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));const t=document.getElementById("mode-files");t&&t.classList.add("active"),k(e)}})}let ve="";function ge(){if(!f)return;const e=f.files||[];if(!e.length)m.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
8
8
  <div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>`;else{let n=`<div class="git-popover-header">${e.length} file${e.length>1?"s":""} changed</div>`;for(let t=0;t<e.length;t++){const s=e[t],i=s.additions||0,l=s.deletions||0;let o="";switch(s.status){case"M":o='<span class="git-file-status M">M</span>';break;case"A":o='<span class="git-file-status A">A</span>';break;case"D":o='<span class="git-file-status D">D</span>';break;case"?":o='<span class="git-file-status ?">?</span>';break;case"R":o='<span class="git-file-status R">R</span>';break}n+=`<div class="git-popover-item" data-idx="${t}">
9
9
  ${o}<span class="file-path">${d(s.path)}</span>
10
10
  <span class="file-add">${i>0?"+"+i:""}</span>
11
11
  <span class="file-del">${l>0?"-"+l:""}</span>
12
12
  </div>`}n+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',ve=n,m.innerHTML=n,m.querySelectorAll(".git-popover-item").forEach(t=>{const s=parseInt(t.dataset.idx||"0",10),i=e[s];!i||i.status==="D"||(t.classList.add("clickable"),t.style.cursor="pointer",t.addEventListener("click",()=>he(i.path)))})}te(),m.classList.add("open"),G.classList.add("open"),me()}function Ne(){return window.innerWidth>=1024}function ye(e,n){const s=(e+(n&&e?`
13
13
  `:"")+n).split(`
14
- `);let i="",l=0,o=0;for(const c of s){const a=c;if(a.startsWith("@@")){const g=a.match(/@@\s+-(\d+)[^+]*\+(\d+)/);g&&(l=parseInt(g[1],10)-1,o=parseInt(g[2],10)-1),i+=`<div class="diff-line hunk"><span class="ln-body">${d(a)}</span></div>`;continue}if(a.startsWith("diff --git")||a.startsWith("index ")||a.startsWith("--- ")||a.startsWith("+++ ")||a.startsWith("new file")||a.startsWith("deleted file")){a.startsWith("diff --git")&&(i+=`<div class="diff-line header"><span class="ln-body">${d(a)}</span></div>`);continue}if(a.startsWith("+"))o++,i+=`<div class="diff-line add"><span class="ln-no">${o}</span><span class="ln-body">${d(a)}</span></div>`;else if(a.startsWith("-"))l++,i+=`<div class="diff-line del"><span class="ln-no">${l}</span><span class="ln-body">${d(a)}</span></div>`;else if(a.startsWith("\\"))i+=`<div class="diff-line"><span class="ln-body">${d(a)}</span></div>`;else{if(!a&&!l&&!o)continue;l++,o++,i+=`<div class="diff-line"><span class="ln-no">${l}</span><span class="ln-no" style="margin-right:12px">${o}</span><span class="ln-body">${d(a)}</span></div>`}}return{html:i,lineCount:s.length}}let V=null;async function he(e){const n=T;if(n)if(q(),Ne()){h.style.display="none",p&&(K(),p.destroy(),p=null,V=u,u=null),D(null),H.style.display="none",C.style.display="none",fe.style.display="flex",ue.textContent=e,O.textContent="Loading\u2026",U.innerHTML="";try{const t=await fetch(y("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Pe(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);O.textContent="Error",U.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${d(s)}</div>`}}else{m.innerHTML=`<div class="git-diff-view"><div class="diff-header">
14
+ `);let i="",l=0,o=0;for(const c of s){const a=c;if(a.startsWith("@@")){const g=a.match(/@@\s+-(\d+)[^+]*\+(\d+)/);g&&(l=parseInt(g[1],10)-1,o=parseInt(g[2],10)-1),i+=`<div class="diff-line hunk"><span class="ln-body">${d(a)}</span></div>`;continue}if(a.startsWith("diff --git")||a.startsWith("index ")||a.startsWith("--- ")||a.startsWith("+++ ")||a.startsWith("new file")||a.startsWith("deleted file")){a.startsWith("diff --git")&&(i+=`<div class="diff-line header"><span class="ln-body">${d(a)}</span></div>`);continue}if(a.startsWith("+"))o++,i+=`<div class="diff-line add"><span class="ln-no">${o}</span><span class="ln-body">${d(a)}</span></div>`;else if(a.startsWith("-"))l++,i+=`<div class="diff-line del"><span class="ln-no">${l}</span><span class="ln-body">${d(a)}</span></div>`;else if(a.startsWith("\\"))i+=`<div class="diff-line"><span class="ln-body">${d(a)}</span></div>`;else{if(!a&&!l&&!o)continue;l++,o++,i+=`<div class="diff-line"><span class="ln-no">${l}</span><span class="ln-no" style="margin-right:12px">${o}</span><span class="ln-body">${d(a)}</span></div>`}}return{html:i,lineCount:s.length}}let V=null;async function he(e){const n=S;if(n)if(q(),Ne()){h.style.display="none",p&&(K(),p.destroy(),p=null,V=u,u=null),D(null),H.style.display="none",C.style.display="none",fe.style.display="flex",ue.textContent=e,O.textContent="Loading\u2026",U.innerHTML="";try{const t=await fetch(y("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Pe(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);O.textContent="Error",U.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${d(s)}</div>`}}else{m.innerHTML=`<div class="git-diff-view"><div class="diff-header">
15
15
  <button class="diff-back">\u2190 Back</button>
16
16
  <span class="diff-filename">${d(e)}</span>
17
17
  <span class="diff-loading">Loading\u2026</span>
@@ -25,7 +25,7 @@ import{initTerminal as ke}from"./terminal-core.js";const M=window.__TMUX_WEB_SHE
25
25
  <button class="diff-back">\u2190 Back</button>
26
26
  <span class="diff-filename">${d(e)}</span>
27
27
  <span class="diff-filesize">${i} lines</span>
28
- </div><div class="diff-content">${s}</div></div>`,m.querySelector(".diff-back")?.addEventListener("click",()=>{m.innerHTML=ve,te(),f&&m.querySelectorAll(".git-popover-item").forEach(l=>{const o=parseInt(l.dataset.idx||"0",10),c=f.files[o];!c||c.status==="D"||(l.style.cursor="pointer",l.addEventListener("click",()=>he(c.path)))}),me()})}function Pe(e,n,t){if(!n&&!t){O.textContent="No diff",U.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}const{html:s,lineCount:i}=ye(n,t);ue.textContent=e,O.textContent=i+" lines",U.innerHTML=s}function Ae(){fe.style.display="none",V?(h.style.display="",h.classList.add("terminal-pending"),R(V),V=null):u?(h.style.display="",h.classList.add("terminal-pending"),we()):C.style.display="flex"}Re.addEventListener("click",Ae);function q(){m.classList.remove("open"),G.classList.remove("open")}G.addEventListener("click",q),document.addEventListener("keydown",e=>{e.key==="Escape"&&(q(),X())});async function be(e){try{const t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){const s=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(s.ok){const i=await s.json();f=i,D(i);return}}}catch{}f=null,D(null)}let J=null;function we(){K(),u&&(be(u),J=setInterval(()=>{u&&be(u)},3e4))}function K(){J&&(clearInterval(J),J=null)}function _e(){if(z=M.fsRoots||[],z.length===0){if(u){Fe(u);return}r.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}r.innerHTML="";for(const e of z){const n=document.createElement("div");n.className="file-tree-item",n.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${d(e)}`,n.addEventListener("click",()=>k(e)),r.appendChild(n)}}async function Fe(e){r.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const t=await(await fetch(y("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?k(t.path):r.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{r.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function k(e){E=e,r.innerHTML='<div class="file-tree-empty">Loading...</div>';const n=He(e);try{const s=await(await fetch(y("/api/fs/list?path="+encodeURIComponent(e)))).json();await n,r.innerHTML="",D(f),r.innerHTML+=$e();const i=je(e);if(i&&!z.some(c=>e===c)){const c=document.createElement("div");c.className="file-tree-item",c.innerHTML="..",c.addEventListener("click",()=>k(i)),r.appendChild(c)}if(s.dirs)for(const o of s.dirs){const c=o.split("/").pop()||o.split("\\").pop(),a=document.createElement("div");a.className="file-tree-item",a.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${d(c)}`,a.addEventListener("click",()=>k(o)),r.appendChild(a)}else r.innerHTML="";const l=s.files||[];for(const o of l){const c=o.split("/").pop()||o.split("\\").pop(),a=Te(o);let g="",x="";if(a)switch(a){case"M":g="git-mod",x='<span class="git-file-status M">M</span>';break;case"A":g="git-add",x='<span class="git-file-status A">A</span>';break;case"D":g="git-del",x='<span class="git-file-status D">D</span>';break;case"?":g="git-untracked",x='<span class="git-file-status ?">?</span>';break;case"R":g="git-mod",x='<span class="git-file-status R">R</span>';break}const F=document.createElement("div");F.className="file-tree-item file"+(g?" "+g:""),F.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${x}${d(c)}`,F.addEventListener("click",()=>We(o)),r.appendChild(F)}}catch{r.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function je(e){const n=e.lastIndexOf("/");return n<=0?null:e.substring(0,n)}async function We(e){j=e,h.style.display="none",p&&(K(),p.destroy(),p=null,u=null),D(null),Q(),C.style.display="none",H.style.display="flex",Ce.textContent=e,b.textContent="Loading...",I.style.display="none";try{const n=await fetch(y("/api/file?path="+encodeURIComponent(e)));if(!n.ok){const s=await n.json();b.textContent=s.error||"Failed to load",$.value="";return}const t=await n.json();$.value=t.content,W=t.content,oe=t.content,b.textContent=t.size+" bytes",I.style.display=""}catch{b.textContent="Failed to load",$.value=""}}$.addEventListener("input",()=>{W=$.value}),I.addEventListener("click",async()=>{I.textContent="Saving...",I.disabled=!0;try{const e=await fetch(y("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:j,content:W})});if(!e.ok){const n=await e.json();b.textContent=n.error||"Save failed";return}oe=W,b.textContent="Saved"}catch{b.textContent="Save failed"}finally{I.disabled=!1,I.textContent="Save"}}),Ie.addEventListener("click",async()=>{if(confirm("Delete "+j+"?"))try{const e=await fetch(y("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:j})});if(!e.ok){const n=await e.json();b.textContent=n.error||"Delete failed";return}H.style.display="none",C.style.display="flex",E&&k(E)}catch{b.textContent="Delete failed"}}),Be.addEventListener("click",()=>{H.style.display="none",C.style.display="flex",E&&k(E)}),xe.addEventListener("click",async()=>{const e=ae.value.trim();if(!e)return;const n=E+"/"+e;try{const t=await fetch(y("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:n})});if(!t.ok){const s=await t.json();b.textContent=s.error||"Create failed";return}ae.value="",E&&k(E)}catch{b.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=le.classList.toggle("open");Z.classList.toggle("open",e)}),Z.addEventListener("click",()=>{le.classList.remove("open"),Z.classList.remove("open")});let B="";const P=document.createElement("div");P.className="session-popover-backdrop",document.body.appendChild(P);const L=document.createElement("div");L.id="session-popover",L.className="session-popover",L.style.display="none",L.innerHTML=`<div class="sp-field">
28
+ </div><div class="diff-content">${s}</div></div>`,m.querySelector(".diff-back")?.addEventListener("click",()=>{m.innerHTML=ve,te(),f&&m.querySelectorAll(".git-popover-item").forEach(l=>{const o=parseInt(l.dataset.idx||"0",10),c=f.files[o];!c||c.status==="D"||(l.style.cursor="pointer",l.addEventListener("click",()=>he(c.path)))}),me()})}function Pe(e,n,t){if(!n&&!t){O.textContent="No diff",U.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}const{html:s,lineCount:i}=ye(n,t);ue.textContent=e,O.textContent=i+" lines",U.innerHTML=s}function Ae(){fe.style.display="none",V?(h.style.display="",h.classList.add("terminal-pending"),R(V),V=null):u?(h.style.display="",h.classList.add("terminal-pending"),we()):C.style.display="flex"}Re.addEventListener("click",Ae);function q(){m.classList.remove("open"),G.classList.remove("open")}G.addEventListener("click",q),document.addEventListener("keydown",e=>{e.key==="Escape"&&(q(),X())});async function be(e){try{const t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){const s=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(s.ok){const i=await s.json();f=i,D(i);return}}}catch{}f=null,D(null)}let J=null;function we(){K(),u&&(be(u),J=setInterval(()=>{u&&be(u)},3e4))}function K(){J&&(clearInterval(J),J=null)}function _e(){if(z=T.fsRoots||[],z.length===0){if(u){Fe(u);return}r.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}r.innerHTML="";for(const e of z){const n=document.createElement("div");n.className="file-tree-item",n.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${d(e)}`,n.addEventListener("click",()=>k(e)),r.appendChild(n)}}async function Fe(e){r.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const t=await(await fetch(y("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?k(t.path):r.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{r.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function k(e){E=e,r.innerHTML='<div class="file-tree-empty">Loading...</div>';const n=He(e);try{const s=await(await fetch(y("/api/fs/list?path="+encodeURIComponent(e)))).json();await n,r.innerHTML="",D(f),r.innerHTML+=$e();const i=je(e);if(i&&!z.some(c=>e===c)){const c=document.createElement("div");c.className="file-tree-item",c.innerHTML="..",c.addEventListener("click",()=>k(i)),r.appendChild(c)}if(s.dirs)for(const o of s.dirs){const c=o.split("/").pop()||o.split("\\").pop(),a=document.createElement("div");a.className="file-tree-item",a.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${d(c)}`,a.addEventListener("click",()=>k(o)),r.appendChild(a)}else r.innerHTML="";const l=s.files||[];for(const o of l){const c=o.split("/").pop()||o.split("\\").pop(),a=Te(o);let g="",x="";if(a)switch(a){case"M":g="git-mod",x='<span class="git-file-status M">M</span>';break;case"A":g="git-add",x='<span class="git-file-status A">A</span>';break;case"D":g="git-del",x='<span class="git-file-status D">D</span>';break;case"?":g="git-untracked",x='<span class="git-file-status ?">?</span>';break;case"R":g="git-mod",x='<span class="git-file-status R">R</span>';break}const F=document.createElement("div");F.className="file-tree-item file"+(g?" "+g:""),F.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${x}${d(c)}`,F.addEventListener("click",()=>We(o)),r.appendChild(F)}}catch{r.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function je(e){const n=e.lastIndexOf("/");return n<=0?null:e.substring(0,n)}async function We(e){j=e,h.style.display="none",p&&(K(),p.destroy(),p=null,u=null),D(null),Q(),C.style.display="none",H.style.display="flex",Ce.textContent=e,b.textContent="Loading...",I.style.display="none";try{const n=await fetch(y("/api/file?path="+encodeURIComponent(e)));if(!n.ok){const s=await n.json();b.textContent=s.error||"Failed to load",$.value="";return}const t=await n.json();$.value=t.content,W=t.content,oe=t.content,b.textContent=t.size+" bytes",I.style.display=""}catch{b.textContent="Failed to load",$.value=""}}$.addEventListener("input",()=>{W=$.value}),I.addEventListener("click",async()=>{I.textContent="Saving...",I.disabled=!0;try{const e=await fetch(y("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:j,content:W})});if(!e.ok){const n=await e.json();b.textContent=n.error||"Save failed";return}oe=W,b.textContent="Saved"}catch{b.textContent="Save failed"}finally{I.disabled=!1,I.textContent="Save"}}),Ie.addEventListener("click",async()=>{if(confirm("Delete "+j+"?"))try{const e=await fetch(y("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:j})});if(!e.ok){const n=await e.json();b.textContent=n.error||"Delete failed";return}H.style.display="none",C.style.display="flex",E&&k(E)}catch{b.textContent="Delete failed"}}),Be.addEventListener("click",()=>{H.style.display="none",C.style.display="flex",E&&k(E)}),xe.addEventListener("click",async()=>{const e=ae.value.trim();if(!e)return;const n=E+"/"+e;try{const t=await fetch(y("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:n})});if(!t.ok){const s=await t.json();b.textContent=s.error||"Create failed";return}ae.value="",E&&k(E)}catch{b.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=le.classList.toggle("open");Z.classList.toggle("open",e)}),Z.addEventListener("click",()=>{le.classList.remove("open"),Z.classList.remove("open")});let B="";const P=document.createElement("div");P.className="session-popover-backdrop",document.body.appendChild(P);const L=document.createElement("div");L.id="session-popover",L.className="session-popover",L.style.display="none",L.innerHTML=`<div class="sp-field">
29
29
  <label>Rename session</label>
30
30
  <input type="text" id="sp-name" />
31
31
  </div>
@@ -34,5 +34,5 @@ import{initTerminal as ke}from"./terminal-core.js";const M=window.__TMUX_WEB_SHE
34
34
  <button class="btn" id="sp-cancel">Cancel</button>
35
35
  </div>
36
36
  <hr class="sp-divider" />
37
- <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(L);function ze(e){const n=e.getBoundingClientRect();L.style.top=n.bottom+4+"px",L.style.left=Math.max(8,Math.min(n.left,window.innerWidth-240))+"px"}function Oe(e,n){B=e;const t=document.getElementById("sp-name");t.value=e,P.classList.add("open"),L.style.display="block",ze(n),setTimeout(()=>t.focus(),50)}function A(){P.classList.remove("open"),L.style.display="none"}P.addEventListener("click",A),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===B){A();return}try{if(!(await fetch(y("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:B,newName:e})})).ok)return;A(),S(),u===B&&R(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+B+'"?'))try{if(!(await fetch(y("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:B})})).ok)return;A(),u===B&&(p&&(p.destroy(),p=null),u=null,h.style.display="none",C.style.display="flex"),S()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",A);const Ue={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r",exit:"exit\r",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{const n=e.target.closest("button[data-key]");if(!n||!p)return;const t=Ue[n.dataset.key];t&&(p.sendInput(t),p.focus())});const w=document.getElementById("mk-input"),Le=document.getElementById("mk-send");function Ee(){if(!w||!p)return;const e=w.value;e&&(p.sendInput(e+"\r"),w.value="",w.rows=1,p.focus())}if(w&&(w.addEventListener("input",()=>{w.rows=1;const e=w.value.split(`
38
- `).length;w.rows=Math.min(e,4)}),w.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),Ee())})),Le&&Le.addEventListener("click",Ee),window.visualViewport){const e=document.querySelector(".app-layout"),n=()=>{e&&(e.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",n),window.addEventListener("orientationchange",()=>setTimeout(n,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}const ne=document.getElementById("process-panel"),_=document.getElementById("proc-list"),Ge=document.getElementById("proc-close"),se=document.getElementById("proc-backdrop"),Ve=document.getElementById("header-ram");function qe(){_.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(y("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){_.innerHTML='<div class="proc-empty">No processes</div>';return}let n="";for(const t of e){const s=t.rss,i=s<1048576?(s/1024).toFixed(0)+"K":(s/1048576).toFixed(1)+"M";n+='<div class="proc-row" data-pid="'+t.pid+'"><span class="proc-mem">'+t.mem+'%</span><span class="proc-rss">'+i+'</span><span class="proc-cmd" title="'+d(t.command)+'">'+d(t.command)+'</span><button class="proc-kill" title="Kill PID '+t.pid+'">&times;</button></div>'}_.innerHTML=n;for(const t of _.querySelectorAll(".proc-row")){const s=t.querySelector(".proc-kill");s&&s.addEventListener("click",i=>{const l=i.target.closest(".proc-row");!l||!confirm("Kill PID "+l.dataset.pid+"?")||fetch(y("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(l.dataset.pid,10)})}).then(o=>o.json()).then(o=>{o.ok?l.style.opacity="0.3":alert("Failed: "+(o.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{_.innerHTML='<div class="proc-empty">Failed to load</div>'})}function Je(){ne.classList.add("open"),se.classList.add("open"),qe()}function X(){ne.classList.remove("open"),se.classList.remove("open")}Ve.addEventListener("click",e=>{e.stopPropagation(),ne.classList.contains("open")?X():Je()}),Ge.addEventListener("click",X),se.addEventListener("click",X),window.__openSession=R,window.__refreshSidebar=S,S(),Q(),setInterval(()=>{document.visibilityState==="visible"&&S()},1e4);
37
+ <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(L);function ze(e){const n=e.getBoundingClientRect();L.style.top=n.bottom+4+"px",L.style.left=Math.max(8,Math.min(n.left,window.innerWidth-240))+"px"}function Oe(e,n){B=e;const t=document.getElementById("sp-name");t.value=e,P.classList.add("open"),L.style.display="block",ze(n),setTimeout(()=>t.focus(),50)}function A(){P.classList.remove("open"),L.style.display="none"}P.addEventListener("click",A),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===B){A();return}try{if(!(await fetch(y("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:B,newName:e})})).ok)return;A(),M(),u===B&&R(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+B+'"?'))try{if(!(await fetch(y("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:B})})).ok)return;A(),u===B&&(p&&(p.destroy(),p=null),u=null,h.style.display="none",C.style.display="flex"),M()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",A);const Ue={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r",exit:"exit\r",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{const n=e.target.closest("button[data-key]");if(!n||!p)return;const t=Ue[n.dataset.key];t&&(p.sendInput(t),p.focus())});const w=document.getElementById("mk-input"),Le=document.getElementById("mk-send");function Ee(){if(!w||!p)return;const e=w.value;e&&(p.sendInput(e+"\r"),w.value="",w.rows=1,p.focus())}if(w&&(w.addEventListener("input",()=>{w.rows=1;const e=w.value.split(`
38
+ `).length;w.rows=Math.min(e,4)}),w.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),Ee())})),Le&&Le.addEventListener("click",Ee),window.visualViewport){const e=document.querySelector(".app-layout"),n=()=>{e&&(e.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",n),window.addEventListener("orientationchange",()=>setTimeout(n,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}const ne=document.getElementById("process-panel"),_=document.getElementById("proc-list"),Ge=document.getElementById("proc-close"),se=document.getElementById("proc-backdrop"),Ve=document.getElementById("header-ram");function qe(){_.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(y("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){_.innerHTML='<div class="proc-empty">No processes</div>';return}let n="";for(const t of e){const s=t.rss,i=s<1048576?(s/1024).toFixed(0)+"K":(s/1048576).toFixed(1)+"M";n+='<div class="proc-row" data-pid="'+t.pid+'"><span class="proc-mem">'+t.mem+'%</span><span class="proc-rss">'+i+'</span><span class="proc-cmd" title="'+d(t.command)+'">'+d(t.command)+'</span><button class="proc-kill" title="Kill PID '+t.pid+'">&times;</button></div>'}_.innerHTML=n;for(const t of _.querySelectorAll(".proc-row")){const s=t.querySelector(".proc-kill");s&&s.addEventListener("click",i=>{const l=i.target.closest(".proc-row");!l||!confirm("Kill PID "+l.dataset.pid+"?")||fetch(y("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(l.dataset.pid,10)})}).then(o=>o.json()).then(o=>{o.ok?l.style.opacity="0.3":alert("Failed: "+(o.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{_.innerHTML='<div class="proc-empty">Failed to load</div>'})}function Je(){ne.classList.add("open"),se.classList.add("open"),qe()}function X(){ne.classList.remove("open"),se.classList.remove("open")}Ve.addEventListener("click",e=>{e.stopPropagation(),ne.classList.contains("open")?X():Je()}),Ge.addEventListener("click",X),se.addEventListener("click",X),window.__openSession=R,window.__refreshSidebar=M,M(),Q(),setInterval(()=>{document.visibilityState==="visible"&&M()},1e4);
@@ -1,10 +1,8 @@
1
- const kt="https://esm.sh/ghostty-web@0.4.0",Pt=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function K(a){return a.replace(Pt,"")}let V;function Ft(){return V||(document.querySelector("link[data-tmux-web-xterm-css]")?V=Promise.resolve():(V=new Promise(e=>{const r=document.createElement("link");r.rel="stylesheet",r.href="/assets/xterm.css",r.setAttribute("data-tmux-web-xterm-css",""),r.addEventListener("load",()=>e(),{once:!0}),r.addEventListener("error",()=>e(),{once:!0}),document.head.appendChild(r)}),V))}class N{terminal;fitAddon;container;constructor(e,r,i){this.container=e,this.terminal=r,this.fitAddon=i,this.terminal.loadAddon(this.fitAddon),this.terminal.open(e)}static async create(e,r,i){const[{Terminal:o},{FitAddon:c}]=await Promise.all([import("@xterm/xterm"),import("@xterm/addon-fit"),Ft()]),h=new o({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:r,convertEol:!1,theme:i});return new N(e,h,new c)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(e){return new Promise(r=>this.terminal.write(e,r))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(e){this.terminal.scrollToLine(Math.max(0,e))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){const e=lt(this.container);return e.width<=0||e.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(e){this.terminal.paste(e)}input(e){this.terminal.input(e,!1)}onData(e){this.terminal.onData(e),this.terminal.onBinary(e)}onResize(e){this.terminal.onResize(e)}onScroll(e){this.terminal.onScroll(e)}attachCustomKeyEventHandler(e){this.terminal.attachCustomKeyEventHandler(e)}isFocused(){const e=document.activeElement;return e?e===this.container||e===this.terminal.textarea||this.container.contains(e)||!!e.closest?.(".xterm"):!1}getScreenText(){return this.getRowsText(0,this.terminal.rows-1)}getRowsText(e,r){const i=this.terminal.buffer.active,[o,c]=e<=r?[e,r]:[r,e],h=i.viewportY,l=[];for(let f=o;f<=c;f++){const u=i.getLine(h+f);l.push(u?u.translateToString(!0):"")}return l.join(`
2
- `)}selectRows(e,r){const i=this.terminal.buffer.active,[o,c]=e<=r?[e,r]:[r,e];this.terminal.selectLines(i.viewportY+o,i.viewportY+c)}selectCells(e,r,i,o){const c=this.terminal.buffer.active,h=Math.min(r,o),l=Math.max(r,o);if(h===l){const f=Math.min(e,i),u=Math.max(e,i);this.terminal.select(f,c.viewportY+h,u-f+1)}else this.terminal.selectLines(c.viewportY+h,c.viewportY+l)}getCellsText(e,r,i,o){const c=this.terminal.buffer.active,h=Math.min(r,o),l=Math.max(r,o),f=r<o?e:r>o?i:Math.min(e,i),u=r<o?i:r>o?e:Math.max(e,i),T=c.viewportY,b=[];for(let p=h;p<=l;p++){const g=c.getLine(T+p);if(!g){b.push("");continue}const x=p===h?f:0,S=p===l?u:this.terminal.cols-1;let M="";for(let L=x;L<=S;L++){const C=g.getCell(L);C&&(M+=C.getChars())}b.push(M.replace(/\s+$/,""))}return b.join(`
3
- `)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}}class ${terminal;container;colsValue=80;rowsValue=24;charW=0;charH=0;resizeCallbacks=[];constructor(e,r){this.container=e,this.terminal=r,this.terminal.open(e)}static async create(e,r,i){const o=await import(kt);await o.init();const c=new o.Terminal({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:r,convertEol:!1,theme:i});return new $(e,c)}get cols(){return this.colsValue}get rows(){return this.rowsValue}write(e){return new Promise(r=>this.terminal.write(e,r))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom?.()}scrollToLine(e){this.terminal.scrollToLine?.(Math.max(0,e))}isNearScrollbackTop(){const e=this.terminal.buffer?.active;return e?e.viewportY>=e.baseY:!1}viewportY(){return this.terminal.buffer?.active?.viewportY??0}baseY(){return this.terminal.buffer?.active?.baseY??0}fit(){const e=lt(this.container);if(e.width<=0||e.height<=0)return!1;this.updateCellMetrics(!this.charW||!this.charH);const r=Math.floor(e.width/this.charW),i=Math.floor(e.height/this.charH);if(r<10||i<5)return!1;if(r!==this.colsValue||i!==this.rowsValue){this.colsValue=r,this.rowsValue=i,this.terminal.resize(r,i);for(const o of this.resizeCallbacks)o({cols:r,rows:i})}return!0}focus(){(this.terminal.textarea||this.container.querySelector("textarea")||this.container)?.focus?.()}pasteText(e){const r=this.terminal.getMode?.(2004)??!1;this.input(r?"\x1B[200~"+e+"\x1B[201~":e)}input(e){this.terminal.input?.(e,!1)}onData(e){this.terminal.onData(r=>e(r))}onResize(e){this.resizeCallbacks.push(e)}onScroll(e){this.terminal.onScroll(e)}attachCustomKeyEventHandler(e){this.terminal.attachCustomKeyEventHandler(e)}isFocused(){const e=document.activeElement;return!!e&&(e===this.container||e===this.terminal.textarea||this.container.contains(e))}getScreenText(){return this.getRowsText(0,this.rowsValue-1)}getRowsText(e,r){try{const i=this.terminal.buffer?.active;if(!i||typeof i.getLine!="function")return"";const[o,c]=e<=r?[e,r]:[r,e],h=typeof i.viewportY=="number"?i.viewportY:0,l=[];for(let f=o;f<=c;f++){const u=i.getLine(h+f);l.push(typeof u?.translateToString=="function"?u.translateToString(!0):"")}return l.join(`
4
- `)}catch{return""}}selectRows(e,r){try{if(typeof this.terminal?.selectLines!="function")return;const i=this.terminal.buffer?.active,o=typeof i?.viewportY=="number"?i.viewportY:0,[c,h]=e<=r?[e,r]:[r,e];this.terminal.selectLines(o+c,o+h)}catch{}}selectCells(e,r,i,o){try{const c=this.terminal.buffer?.active,h=typeof c?.viewportY=="number"?c.viewportY:0,l=Math.min(r,o),f=Math.max(r,o);if(l===f){if(typeof this.terminal?.select!="function"){this.selectRows(l,f);return}const u=Math.min(e,i),T=Math.max(e,i);this.terminal.select(u,h+l,T-u+1)}else this.selectRows(l,f)}catch{this.selectRows(r,o)}}getCellsText(e,r,i,o){try{const c=this.terminal.buffer?.active;if(!c||typeof c.getLine!="function")return this.getRowsText(r,o);const h=typeof c.viewportY=="number"?c.viewportY:0,l=Math.min(r,o),f=Math.max(r,o),u=r<o?e:r>o?i:Math.min(e,i),T=r<o?i:r>o?e:Math.max(e,i),b=[];for(let p=l;p<=f;p++){const g=c.getLine(h+p);if(!g){b.push("");continue}const x=p===l?u:0,S=p===f?T:this.colsValue-1;let M="";if(typeof g.getCell=="function")for(let L=x;L<=S;L++){const C=g.getCell(L);C&&(M+=typeof C.getChars=="function"?C.getChars():"")}else M=typeof g.translateToString=="function"?g.translateToString(!1):"";b.push(M.replace(/\s+$/,""))}return b.join(`
5
- `)}catch{return this.getRowsText(r,o)}}getSelection(){try{if(typeof this.terminal?.getSelection=="function")return this.terminal.getSelection()}catch{}return""}clearSelection(){try{typeof this.terminal?.clearSelection=="function"&&this.terminal.clearSelection()}catch{}}hasSelectionSupport(){return typeof this.terminal?.selectLines=="function"}updateCellMetrics(e=!1){const r=this.container.querySelector("canvas");if(r instanceof HTMLElement&&r.offsetWidth>0&&r.offsetHeight>0&&this.colsValue>0&&this.rowsValue>0){const i=r.offsetWidth/this.colsValue,o=r.offsetHeight/this.rowsValue;(e||!this.charW||!this.charH)&&(this.charW=i,this.charH=o)}(!this.charW||!this.charH)&&(this.charW=9,this.charH=18)}}function lt(a){const e=a.getBoundingClientRect(),r=window.visualViewport;return r?{width:Math.min(e.width,r.width),height:Math.max(0,Math.min(e.height,r.height-Math.max(0,e.top)))}:e}function Bt(a,e,r){return a.innerHTML="",a.classList.add("terminal-pending"),(async()=>{let i;if(r.renderer==="ghostty")try{i=await $.create(a,r.scrollback,r.theme)}catch{console.error("[tmux-web] ghostty-web failed; falling back to xterm.js"),i=await N.create(a,r.scrollback,r.theme)}else i=await N.create(a,r.scrollback,r.theme);let o,c=0,h,l=null,f=0,u="connecting",T=0,b=!1,p=[],g="",x=!1,S=1e3;const M=location.protocol==="https:"?"wss:":"ws:",L=r.wsBase||"/ws",C=M+"//"+location.host+L+"/"+encodeURIComponent(e),ct="/api/session/"+encodeURIComponent(e)+"/upload",ut=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function ft(){return p.join("")+g}async function ht(t,n=0){const s=t?i.viewportY():0,w=t?i.baseY():0,m=K(ft());if(i.reset(),!m){t||i.scrollToBottom();return}await i.write(m),t?i.scrollToLine(w+s+n):i.scrollToBottom()}function Y(t){o?.readyState===WebSocket.OPEN&&o.send(JSON.stringify(t))}function mt(t){let n;try{n=JSON.parse(t)}catch{u==="live"&&(g+=t,i.write(K(t)));return}if(n.type==="auth.required"){const s=localStorage.getItem("tmux-web-token");s?Y({type:"auth.token",token:s}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(n.type==="attach_failed"&&typeof n.message=="string"){A=!0,u="dead",i.reset(),i.write(`\r
6
- \x1B[31m`+K(n.message)+`\x1B[0m\r
7
- `).then(()=>{O();try{o?.close()}catch{}});return}if(n.type==="auth.ok"){n.token&&localStorage.setItem("tmux-web-token",n.token),S=1e3,k(),Y({type:"resize",cols:i.cols,rows:i.rows}),v();return}if(n.type==="auth.failed"){if(n.permanentLock||n.retryAfterMs&&n.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(n.message);return}return}if(n.type==="snapshot"&&typeof n.data=="string"){const s=K(n.data);p=[s],g="",T=typeof n.lines=="number"?n.lines:r.terminal.initialLines,u="live",i.reset(),i.write(s).then(()=>i.scrollToBottom());return}if(n.type==="window_changed"&&typeof n.activeIndex=="number"){const s=new URL(location.href);s.searchParams.get("window")!==String(n.activeIndex)&&(s.searchParams.set("window",String(n.activeIndex)),history.replaceState(history.state,"",s)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:n.windows}));return}if(n.type==="history"&&typeof n.data=="string"){b=!1,n.lines>0&&n.data&&(p.unshift(n.data),T+=n.lines,ht(!0,n.lines));return}if(n.type==="data"&&typeof n.data=="string"){const s=K(n.data);if(u==="connecting"){u="live",g=s,i.write(s);return}u==="live"&&(g+=s,i.write(s))}}let A=!1;function Q(){if(A)return;let t=C;const n=localStorage.getItem("tmux-web-token");n&&(t+=(t.includes("?")?"&":"?")+"token="+encodeURIComponent(n)),o=new WebSocket(t),o.onopen=()=>{u="connecting",T=0,b=!1,p=[],g="",i.reset(),S=1e3},o.onmessage=s=>{typeof s.data=="string"&&mt(s.data)},o.onclose=()=>{x||(u="connecting",!A&&setTimeout(()=>{S=Math.min(S*2,1e4),!x&&!A&&Q()},S))},o.onerror=()=>o?.close()}function W(t){Y({type:"input",data:t})}function k(){return i.fit()}function O(){a.classList.remove("terminal-pending")}function v(){c&&cancelAnimationFrame(c),h&&clearTimeout(h),k(),c=requestAnimationFrame(()=>{c=0,k()&&O()}),h=setTimeout(()=>{k()&&O()},120)}function pt(){v(),setTimeout(v,50),setTimeout(v,150),setTimeout(v,300)}function Z(t){return typeof t=="string"&&t.startsWith("image/")}function R(t){return t?.files?.length?t.files[0]??null:null}function dt(t){if(!t?.items)return null;for(let n=0;n<t.items.length;n++){const s=t.items[n];if(s?.kind==="file"&&Z(s.type))return s.getAsFile()}return null}async function gt(t){const n=new FormData,s=t instanceof File&&t.name?t.name:"upload";n.append("file",t,s);const w=localStorage.getItem("tmux-web-token"),m={};w&&(m.Authorization="Bearer "+w);const I=await fetch(ct,{method:"POST",body:n,headers:m});if(!I.ok){let st="upload failed";try{const at=await I.json();at.error&&(st=at.error)}catch{}throw new Error(st)}const q=await I.json();if(!q.path)throw new Error("no path in response");return q.path}async function _(t){try{const n=await gt(t);W(n)}catch{}}i.onData(t=>W(t)),i.onResize(({cols:t,rows:n})=>Y({type:"resize",cols:t,rows:n})),i.onScroll(()=>{u!=="live"||b||!i.isNearScrollbackTop()||(b=!0,Y({type:"load_history",before:T}))});let P=!1,F=!1,d=null,y=null;const B=document.createElement("div");B.className="terminal-copy-banner",B.innerHTML='<strong>Copy mode</strong><span class="hint">drag to copy \xB7 Esc exits</span>',B.hidden=!0,a.appendChild(B);const E=document.createElement("div");E.className="terminal-copy-banner terminal-copy-toast",E.textContent="Copied",E.hidden=!0,a.appendChild(E);function tt(t,n){const s=a.querySelector(".xterm-screen, canvas"),m=(s&&s.getBoundingClientRect().width>0?s:a).getBoundingClientRect(),I=m.height>0&&i.rows>0?m.height/i.rows:18,q=m.width>0&&i.cols>0?m.width/i.cols:9;return{row:Math.max(0,Math.min(i.rows-1,Math.floor((n-m.top)/I))),col:Math.max(0,Math.min(i.cols-1,Math.floor((t-m.left)/q)))}}async function X(t){if(!t)return;try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(t);return}}catch{}const n=document.createElement("textarea");n.value=t,n.style.position="fixed",n.style.opacity="0",n.style.pointerEvents="none",document.body.appendChild(n),n.focus(),n.select();try{document.execCommand("copy")}catch{}n.remove()}let z;function j(){E.hidden=!1,z&&clearTimeout(z),z=setTimeout(()=>{E.hidden=!0},1e3)}function D(t){P=t,B.hidden=!t,a.classList.toggle("terminal-copy-mode-active",t),t&&i.focus()}function et(){D(!P)}function yt(t){x||t.button!==0||F||(P||t.shiftKey)&&(t.preventDefault(),t.stopPropagation(),F=!0,d=tt(t.clientX,t.clientY),y=d,i.selectCells(d.col,d.row,y.col,y.row),document.addEventListener("mousemove",J,!0),document.addEventListener("mouseup",G,!0))}function J(t){x||!F||!d||(t.preventDefault(),t.stopPropagation(),y=tt(t.clientX,t.clientY),i.selectCells(d.col,d.row,y.col,y.row))}function G(t){if(x||!F||!d||!y)return;t.preventDefault(),t.stopPropagation(),document.removeEventListener("mousemove",J,!0),document.removeEventListener("mouseup",G,!0),F=!1;const n=i.getCellsText(d.col,d.row,y.col,y.row);d=null,y=null,i.clearSelection(),D(!1),n&&(E.textContent="\u5DF2\u590D\u5236",X(n).then(j)),i.focus()}a.addEventListener("mousedown",yt,!0),document.addEventListener("keydown",wt,!0);function wt(t){if(!(x||!i.isFocused()||t.defaultPrevented||t.isComposing)){if(P){if(t.key==="Escape"){t.preventDefault(),t.stopPropagation(),d=null,y=null,i.clearSelection(),D(!1);return}if(t.key==="Enter"||(t.ctrlKey||t.metaKey)&&t.code==="KeyC"){t.preventDefault(),t.stopPropagation(),X(i.getScreenText()).then(j),D(!1);return}}if(t.altKey&&t.code==="KeyC"&&!t.ctrlKey&&!t.metaKey){if(t.preventDefault(),t.stopPropagation(),d&&y){const n=i.getCellsText(d.col,d.row,y.col,y.row);d=null,y=null,i.clearSelection(),D(!1),n&&(E.textContent="Copied",X(n).then(j))}else{const n=i.getSelection();n?(i.clearSelection(),E.textContent="Copied",X(n).then(j)):et()}return}if((t.ctrlKey||t.metaKey)&&t.code==="KeyV"){t.preventDefault(),t.stopPropagation(),(async()=>{try{if(navigator.clipboard?.read){const n=await navigator.clipboard.read();for(const s of n)for(const w of s.types)if(Z(w)){const m=await s.getType(w);await _(m);return}}}catch{}try{const n=await navigator.clipboard?.readText();n&&i.pasteText(nt(n))}catch{}})();return}}}let U=0;function nt(t){return t.split(`\r
1
+ const Me=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function A(a){return a.replace(Me,"")}let B;function Pe(){return B||(document.querySelector("link[data-tmux-web-xterm-css]")?B=Promise.resolve():(B=new Promise(o=>{const i=document.createElement("link");i.rel="stylesheet",i.href="/assets/xterm.css",i.setAttribute("data-tmux-web-xterm-css",""),i.addEventListener("load",()=>o(),{once:!0}),i.addEventListener("error",()=>o(),{once:!0}),document.head.appendChild(i)}),B))}class J{terminal;fitAddon;container;constructor(o,i,n){this.container=o,this.terminal=i,this.fitAddon=n,this.terminal.loadAddon(this.fitAddon),this.terminal.open(o)}static async create(o,i,n){const[{Terminal:s},{FitAddon:c}]=await Promise.all([import("@xterm/xterm"),import("@xterm/addon-fit"),Pe()]),p=new s({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:i,convertEol:!1,theme:n});return new J(o,p,new c)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(o){return new Promise(i=>this.terminal.write(o,i))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(o){this.terminal.scrollToLine(Math.max(0,o))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){const o=ke(this.container);return o.width<=0||o.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(o){this.terminal.paste(o)}input(o){this.terminal.input(o,!1)}onData(o){this.terminal.onData(o),this.terminal.onBinary(o)}onResize(o){this.terminal.onResize(o)}onScroll(o){this.terminal.onScroll(o)}attachCustomKeyEventHandler(o){this.terminal.attachCustomKeyEventHandler(o)}isFocused(){const o=document.activeElement;return o?o===this.container||o===this.terminal.textarea||this.container.contains(o)||!!o.closest?.(".xterm"):!1}getScreenText(){return this.getRowsText(0,this.terminal.rows-1)}getRowsText(o,i){const n=this.terminal.buffer.active,[s,c]=o<=i?[o,i]:[i,o],p=n.viewportY,l=[];for(let w=s;w<=c;w++){const f=n.getLine(p+w);l.push(f?f.translateToString(!0):"")}return l.join(`
2
+ `)}selectRows(o,i){const n=this.terminal.buffer.active,[s,c]=o<=i?[o,i]:[i,o];this.terminal.selectLines(n.viewportY+s,n.viewportY+c)}selectCells(o,i,n,s){const c=this.terminal.buffer.active,p=Math.min(i,s),l=Math.max(i,s);if(p===l){const w=Math.min(o,n),f=Math.max(o,n);this.terminal.select(w,c.viewportY+p,f-w+1)}else this.terminal.selectLines(c.viewportY+p,c.viewportY+l)}getCellsText(o,i,n,s){const c=this.terminal.buffer.active,p=Math.min(i,s),l=Math.max(i,s),w=i<s?o:i>s?n:Math.min(o,n),f=i<s?n:i>s?o:Math.max(o,n),L=c.viewportY,b=[];for(let y=p;y<=l;y++){const v=c.getLine(L+y);if(!v){b.push("");continue}const x=y===p?w:0,E=y===l?f:this.terminal.cols-1;let z="";for(let S=x;S<=E;S++){const X=v.getCell(S);X&&(z+=X.getChars())}b.push(z.replace(/\s+$/,""))}return b.join(`
3
+ `)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}}function ke(a){const o=a.getBoundingClientRect(),i=window.visualViewport;return i?{width:Math.min(o.width,i.width),height:Math.max(0,Math.min(o.height,i.height-Math.max(0,o.top)))}:o}function Ye(a,o,i){return a.innerHTML="",a.classList.add("terminal-pending"),(async()=>{const n=await J.create(a,i.scrollback,i.theme);let s,c=0,p,l=null,w=0,f="connecting",L=0,b=!1,y=[],v="",x=!1,E=1e3;const z=location.protocol==="https:"?"wss:":"ws:",S=i.wsBase||"/ws",X=z+"//"+location.host+S+"/"+encodeURIComponent(o),se="/api/session/"+encodeURIComponent(o)+"/upload",le=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function ce(){return y.join("")+v}async function ue(e,t=0){const r=e?n.viewportY():0,h=e?n.baseY():0,u=A(ce());if(n.reset(),!u){e||n.scrollToBottom();return}await n.write(u),e?n.scrollToLine(h+r+t):n.scrollToBottom()}function D(e){s?.readyState===WebSocket.OPEN&&s.send(JSON.stringify(e))}function fe(e){let t;try{t=JSON.parse(e)}catch{f==="live"&&(v+=e,n.write(A(e)));return}if(t.type==="auth.required"){const r=localStorage.getItem("tmux-web-token");r?D({type:"auth.token",token:r}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(t.type==="attach_failed"&&typeof t.message=="string"){O=!0,f="dead",n.reset(),n.write(`\r
4
+ \x1B[31m`+A(t.message)+`\x1B[0m\r
5
+ `).then(()=>{j();try{s?.close()}catch{}});return}if(t.type==="auth.ok"){t.token&&localStorage.setItem("tmux-web-token",t.token),E=1e3,M(),D({type:"resize",cols:n.cols,rows:n.rows}),g();return}if(t.type==="auth.failed"){if(t.permanentLock||t.retryAfterMs&&t.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(t.message);return}return}if(t.type==="snapshot"&&typeof t.data=="string"){const r=A(t.data);y=[r],v="",L=typeof t.lines=="number"?t.lines:i.terminal.initialLines,f="live",n.reset(),n.write(r).then(()=>n.scrollToBottom());return}if(t.type==="window_changed"&&typeof t.activeIndex=="number"){const r=new URL(location.href);r.searchParams.get("window")!==String(t.activeIndex)&&(r.searchParams.set("window",String(t.activeIndex)),history.replaceState(history.state,"",r)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:t.windows}));return}if(t.type==="history"&&typeof t.data=="string"){b=!1,t.lines>0&&t.data&&(y.unshift(t.data),L+=t.lines,ue(!0,t.lines));return}if(t.type==="data"&&typeof t.data=="string"){const r=A(t.data);if(f==="connecting"){f="live",v=r,n.write(r);return}f==="live"&&(v+=r,n.write(r))}}let O=!1;function G(){if(O)return;let e=X;const t=localStorage.getItem("tmux-web-token");t&&(e+=(e.includes("?")?"&":"?")+"token="+encodeURIComponent(t)),s=new WebSocket(e),s.onopen=()=>{f="connecting",L=0,b=!1,y=[],v="",n.reset(),E=1e3},s.onmessage=r=>{typeof r.data=="string"&&fe(r.data)},s.onclose=()=>{x||(f="connecting",!O&&setTimeout(()=>{E=Math.min(E*2,1e4),!x&&!O&&G()},E))},s.onerror=()=>s?.close()}function U(e){D({type:"input",data:e})}function M(){return n.fit()}function j(){a.classList.remove("terminal-pending")}function g(){c&&cancelAnimationFrame(c),p&&clearTimeout(p),M(),c=requestAnimationFrame(()=>{c=0,M()&&j()}),p=setTimeout(()=>{M()&&j()},120)}function de(){g(),setTimeout(g,50),setTimeout(g,150),setTimeout(g,300)}function $(e){return typeof e=="string"&&e.startsWith("image/")}function Q(e){return e?.files?.length?e.files[0]??null:null}function me(e){if(!e?.items)return null;for(let t=0;t<e.items.length;t++){const r=e.items[t];if(r?.kind==="file"&&$(r.type))return r.getAsFile()}return null}async function pe(e){const t=new FormData,r=e instanceof File&&e.name?e.name:"upload";t.append("file",e,r);const h=localStorage.getItem("tmux-web-token"),u={};h&&(u.Authorization="Bearer "+h);const K=await fetch(se,{method:"POST",body:t,headers:u});if(!K.ok){let ie="upload failed";try{const ae=await K.json();ae.error&&(ie=ae.error)}catch{}throw new Error(ie)}const H=await K.json();if(!H.path)throw new Error("no path in response");return H.path}async function W(e){try{const t=await pe(e);U(t)}catch{}}n.onData(e=>U(e)),n.onResize(({cols:e,rows:t})=>D({type:"resize",cols:e,rows:t})),n.onScroll(()=>{f!=="live"||b||!n.isNearScrollbackTop()||(b=!0,D({type:"load_history",before:L}))});let P=!1,k=!1,d=null,m=null;const Y=document.createElement("div");Y.className="terminal-copy-banner",Y.innerHTML='<strong>Copy mode</strong><span class="hint">drag to copy \xB7 Esc exits</span>',Y.hidden=!0,a.appendChild(Y);const T=document.createElement("div");T.className="terminal-copy-banner terminal-copy-toast",T.textContent="Copied",T.hidden=!0,a.appendChild(T);function Z(e,t){const r=a.querySelector(".xterm-screen, canvas"),u=(r&&r.getBoundingClientRect().width>0?r:a).getBoundingClientRect(),K=u.height>0&&n.rows>0?u.height/n.rows:18,H=u.width>0&&n.cols>0?u.width/n.cols:9;return{row:Math.max(0,Math.min(n.rows-1,Math.floor((t-u.top)/K))),col:Math.max(0,Math.min(n.cols-1,Math.floor((e-u.left)/H)))}}async function q(e){if(!e)return;try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(e);return}}catch{}const t=document.createElement("textarea");t.value=e,t.style.position="fixed",t.style.opacity="0",t.style.pointerEvents="none",document.body.appendChild(t),t.focus(),t.select();try{document.execCommand("copy")}catch{}t.remove()}let F;function N(){T.hidden=!1,F&&clearTimeout(F),F=setTimeout(()=>{T.hidden=!0},1e3)}function C(e){P=e,Y.hidden=!e,a.classList.toggle("terminal-copy-mode-active",e),e&&n.focus()}function ee(){C(!P)}function he(e){x||e.button!==0||k||(P||e.shiftKey)&&(e.preventDefault(),e.stopPropagation(),k=!0,d=Z(e.clientX,e.clientY),m=d,n.selectCells(d.col,d.row,m.col,m.row),document.addEventListener("mousemove",R,!0),document.addEventListener("mouseup",V,!0))}function R(e){x||!k||!d||(e.preventDefault(),e.stopPropagation(),m=Z(e.clientX,e.clientY),n.selectCells(d.col,d.row,m.col,m.row))}function V(e){if(x||!k||!d||!m)return;e.preventDefault(),e.stopPropagation(),document.removeEventListener("mousemove",R,!0),document.removeEventListener("mouseup",V,!0),k=!1;const t=n.getCellsText(d.col,d.row,m.col,m.row);d=null,m=null,n.clearSelection(),C(!1),t&&(T.textContent="\u5DF2\u590D\u5236",q(t).then(N)),n.focus()}a.addEventListener("mousedown",he,!0),document.addEventListener("keydown",ge,!0);function ge(e){if(!(x||!n.isFocused()||e.defaultPrevented||e.isComposing)){if(P){if(e.key==="Escape"){e.preventDefault(),e.stopPropagation(),d=null,m=null,n.clearSelection(),C(!1);return}if(e.key==="Enter"||(e.ctrlKey||e.metaKey)&&e.code==="KeyC"){e.preventDefault(),e.stopPropagation(),q(n.getScreenText()).then(N),C(!1);return}}if(e.altKey&&e.code==="KeyC"&&!e.ctrlKey&&!e.metaKey){if(e.preventDefault(),e.stopPropagation(),d&&m){const t=n.getCellsText(d.col,d.row,m.col,m.row);d=null,m=null,n.clearSelection(),C(!1),t&&(T.textContent="Copied",q(t).then(N))}else{const t=n.getSelection();t?(n.clearSelection(),T.textContent="Copied",q(t).then(N)):ee()}return}if((e.ctrlKey||e.metaKey)&&e.code==="KeyV"){e.preventDefault(),e.stopPropagation(),(async()=>{try{if(navigator.clipboard?.read){const t=await navigator.clipboard.read();for(const r of t)for(const h of r.types)if($(h)){const u=await r.getType(h);await W(u);return}}}catch{}try{const t=await navigator.clipboard?.readText();t&&n.pasteText(te(t))}catch{}})();return}}}let _=0;function te(e){return e.split(`\r
8
6
  `).join(`
9
7
  `).split(`
10
- `).join("\r")}async function vt(t){const n=dt(t.clipboardData);if(n){t.preventDefault(),t.stopPropagation();const m=Date.now();if(m-U<50)return;U=m,await _(n);return}const s=t.clipboardData?.getData("text/plain");if(!s)return;t.preventDefault(),t.stopPropagation();const w=Date.now();w-U<50||(U=w,i.pasteText(nt(s)))}let H=0;function bt(t){t.preventDefault(),H++,R(t.dataTransfer)&&a.classList.add("terminal-drag-over")}function xt(t){t.preventDefault(),t.dataTransfer&&(t.dataTransfer.dropEffect="copy")}function Tt(t){t.preventDefault(),H--,H<=0&&(H=0,a.classList.remove("terminal-drag-over"))}async function St(t){t.preventDefault(),H=0,a.classList.remove("terminal-drag-over");const n=R(t.dataTransfer);n&&await _(n)}function it(t){!ut||t.key!=="Escape"||P||t.defaultPrevented||t.repeat||t.metaKey||t.ctrlKey||t.altKey||i.isFocused()&&(t.preventDefault(),t.stopPropagation(),W("\x1B"))}a.addEventListener("paste",vt),a.addEventListener("dragenter",bt),a.addEventListener("dragover",xt),a.addEventListener("dragleave",Tt),a.addEventListener("drop",St),document.addEventListener("keydown",it,!0);function Lt(t){if(t.touches.length!==1){l=null;return}const n=t.touches[0];n&&(l={startX:n.clientX,startY:n.clientY,lastX:n.clientX,lastY:n.clientY,scrolling:!1},t.stopPropagation())}function Et(t){if(!l||t.touches.length!==1)return;const n=t.touches[0];if(!n)return;const s=n.clientY-l.startY,w=n.clientX-l.startX;if(!l.scrolling){if(Math.abs(s)<8||Math.abs(s)<Math.abs(w))return;l.scrolling=!0,f=Date.now()+500}t.preventDefault(),t.stopPropagation(),Yt(-(n.clientY-l.lastY),n.clientX,n.clientY),l.lastX=n.clientX,l.lastY=n.clientY}function Mt(t){if(!l)return;const n=l.scrolling;l=null,t.stopPropagation(),n?(f=Date.now()+500,t.preventDefault()):i.focus()}function Ct(t){l=null,t.stopPropagation()}function Dt(t){t.pointerType==="touch"&&Date.now()<f&&(t.preventDefault(),t.stopPropagation())}function Yt(t,n,s){(a.querySelector(".xterm-screen")??a).dispatchEvent(new WheelEvent("wheel",{deltaY:t,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:n,clientY:s,bubbles:!0,cancelable:!0}))}a.addEventListener("touchstart",Lt,{passive:!0,capture:!0}),a.addEventListener("touchmove",Et,{passive:!1,capture:!0}),a.addEventListener("touchend",Mt,{passive:!1,capture:!0}),a.addEventListener("touchcancel",Ct,{passive:!0,capture:!0}),a.addEventListener("pointerup",Dt,!0);function rt(){v()}window.addEventListener("resize",rt),window.visualViewport?.addEventListener("resize",v),window.visualViewport?.addEventListener("scroll",v);const ot=new ResizeObserver(()=>v());return ot.observe(a),document.fonts?.ready.then(()=>{v(),setTimeout(v,50),setTimeout(v,200)}),a.addEventListener("focusin",pt,!0),requestAnimationFrame(()=>{k()&&O(),Q(),v()}),{destroy(){if(x=!0,D(!1),z&&clearTimeout(z),document.removeEventListener("mousemove",J,!0),document.removeEventListener("mouseup",G,!0),o){try{o.close(1e3,"session switch")}catch{}o=void 0}a.innerHTML="",a.classList.remove("terminal-pending"),window.removeEventListener("resize",rt),ot.disconnect(),document.removeEventListener("keydown",it,!0)},focus(){i.focus()},sendInput(t){W(t)},toggleCopyMode(){et()}}})()}export{Bt as initTerminal};
8
+ `).join("\r")}async function we(e){const t=me(e.clipboardData);if(t){e.preventDefault(),e.stopPropagation();const u=Date.now();if(u-_<50)return;_=u,await W(t);return}const r=e.clipboardData?.getData("text/plain");if(!r)return;e.preventDefault(),e.stopPropagation();const h=Date.now();h-_<50||(_=h,n.pasteText(te(r)))}let I=0;function ye(e){e.preventDefault(),I++,Q(e.dataTransfer)&&a.classList.add("terminal-drag-over")}function ve(e){e.preventDefault(),e.dataTransfer&&(e.dataTransfer.dropEffect="copy")}function xe(e){e.preventDefault(),I--,I<=0&&(I=0,a.classList.remove("terminal-drag-over"))}async function Te(e){e.preventDefault(),I=0,a.classList.remove("terminal-drag-over");const t=Q(e.dataTransfer);t&&await W(t)}function ne(e){!le||e.key!=="Escape"||P||e.defaultPrevented||e.repeat||e.metaKey||e.ctrlKey||e.altKey||n.isFocused()&&(e.preventDefault(),e.stopPropagation(),U("\x1B"))}a.addEventListener("paste",we),a.addEventListener("dragenter",ye),a.addEventListener("dragover",ve),a.addEventListener("dragleave",xe),a.addEventListener("drop",Te),document.addEventListener("keydown",ne,!0);function be(e){if(e.touches.length!==1){l=null;return}const t=e.touches[0];t&&(l={startX:t.clientX,startY:t.clientY,lastX:t.clientX,lastY:t.clientY,scrolling:!1},e.stopPropagation())}function Ee(e){if(!l||e.touches.length!==1)return;const t=e.touches[0];if(!t)return;const r=t.clientY-l.startY,h=t.clientX-l.startX;if(!l.scrolling){if(Math.abs(r)<8||Math.abs(r)<Math.abs(h))return;l.scrolling=!0,w=Date.now()+500}e.preventDefault(),e.stopPropagation(),De(-(t.clientY-l.lastY),t.clientX,t.clientY),l.lastX=t.clientX,l.lastY=t.clientY}function Le(e){if(!l)return;const t=l.scrolling;l=null,e.stopPropagation(),t?(w=Date.now()+500,e.preventDefault()):n.focus()}function Ce(e){l=null,e.stopPropagation()}function Se(e){e.pointerType==="touch"&&Date.now()<w&&(e.preventDefault(),e.stopPropagation())}function De(e,t,r){(a.querySelector(".xterm-screen")??a).dispatchEvent(new WheelEvent("wheel",{deltaY:e,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:t,clientY:r,bubbles:!0,cancelable:!0}))}a.addEventListener("touchstart",be,{passive:!0,capture:!0}),a.addEventListener("touchmove",Ee,{passive:!1,capture:!0}),a.addEventListener("touchend",Le,{passive:!1,capture:!0}),a.addEventListener("touchcancel",Ce,{passive:!0,capture:!0}),a.addEventListener("pointerup",Se,!0);function oe(){g()}window.addEventListener("resize",oe),window.visualViewport?.addEventListener("resize",g),window.visualViewport?.addEventListener("scroll",g);const re=new ResizeObserver(()=>g());return re.observe(a),document.fonts?.ready.then(()=>{g(),setTimeout(g,50),setTimeout(g,200)}),a.addEventListener("focusin",de,!0),requestAnimationFrame(()=>{M()&&j(),G(),g()}),{destroy(){if(x=!0,C(!1),F&&clearTimeout(F),document.removeEventListener("mousemove",R,!0),document.removeEventListener("mouseup",V,!0),s){try{s.close(1e3,"session switch")}catch{}s=void 0}a.innerHTML="",a.classList.remove("terminal-pending"),window.removeEventListener("resize",oe),re.disconnect(),document.removeEventListener("keydown",ne,!0)},focus(){n.focus()},sendInput(e){U(e)},toggleCopyMode(){ee()}}})()}export{Ye as initTerminal};
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import{chmodSync as le,existsSync as D,readdirSync as ue,statSync as de}from"node:fs";import{createRequire as J}from"node:module";import{hostname as L}from"node:os";const me=J(import.meta.url),{version:fe}=me("../package.json");import O from"node:path";import{fileURLToPath as pe}from"node:url";import{serve as he}from"@hono/node-server";import{WebSocketServer as ge,WebSocket as H}from"ws";try{const e=O.dirname(J(import.meta.url).resolve("node-pty/package.json")),n=O.join(e,"prebuilds");if(D(n))for(const r of ue(n)){const t=O.join(n,r,"spawn-helper");D(t)&&!(de(t).mode&73)&&le(t,493)}}catch{}import{loadSecurityConfig as ye}from"./lib/security-config.js";import{TokenStore as ke,hashPassword as _e,verifyPassword as Te,validatePassword as ve}from"./lib/auth.js";import{RateLimiter as Ae}from"./lib/rateLimiter.js";import{audit as l}from"./lib/auditLog.js";import{db as P}from"./lib/db.js";import{handleClientMessage as G}from"./lib/ws-message.js";import{loadDotEnv as Ce}from"./lib/load-env.js";import{listSessions as Se,resolveTmuxSocketPath as we}from"./sessions.js";import{cmdSetup as xe,cmdTheme as Re,printUsage as z,printVersion as be}from"./lib/cli.js";import{readSettings as Ie}from"./lib/settings.js";import{readActiveTheme as Me}from"./lib/theme-store.js";import{buildApp as V}from"./lib/build-app.js";import{attachTerminal as Oe,killAllAttachedPtys as Pe}from"./lib/attach-terminal.js";import{acquireControlClient as X,killAllControlClients as Ee}from"./lib/tmux-control.js";import{getSessionPaneTarget as Y,capturePaneTail as K,capturePaneHistoryChunk as Q,toCrlf as Z}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Le}from"./lib/terminal-config.js";import{AgentRegistry as $e}from"./lib/agent-registry.js";import{AgentChannel as Ne,MAX_RELAY_CONNS_PER_AGENT as Ue}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as Fe,listAgentTokens as He}from"./lib/agent-tokens.js";import{saveSecurityConfig as je}from"./lib/security-config.js";import{readFederationConfig as Be,writeFederationConfig as We}from"./lib/federation-config.js";import{startAgentClient as De}from"./lib/agent-client.js";Ce();const $=Le(),h=ye(),y=new ke,A=new Ae;let j=!1;const Je="tmux-web-token";function B(e){if(h.security.trustProxy){const n=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(n)return n}return e.socket.remoteAddress||"unknown"}function W(e){const n=e.headers.authorization;if(typeof n=="string"&&n.startsWith("Bearer "))return n.slice(7);const r=e.headers.cookie;if(typeof r!="string")return null;const t=r.match(new RegExp(`(?:^|;\\s*)${Je}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function Lt(e){const n=W(e);return n?y.validateToken(n):null}const I=new Map;function Ge(e){let n=0;for(const r of I.values())r.ip===e&&n++;return n}function S(e,n,r){try{e.close(n,r)}catch{}}function q(e,n){e.readyState===H.OPEN&&e.send(JSON.stringify(n))}const N=process.argv.slice(2);{const e=N.filter(n=>n!=="--ghostty"&&n!=="--xterm");if(e.length>0){const[n,r]=e;switch(n){case"setup":await xe(e),process.exit(0);case"theme":await Re(e.slice(1)),process.exit(0);case"agent":{const t=O.join(O.dirname(pe(import.meta.url)),"agent.js"),{spawn:u}=await import("node:child_process"),s=u(process.execPath,[t,...N],{stdio:"inherit"});s.on("error",o=>{console.error(o),process.exit(1)}),s.on("exit",o=>process.exit(o??0)),await new Promise(()=>{})}case"agent-token":await rt(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":z(),process.exit(0);case"-V":case"--version":case"-v":be(),process.exit(0);default:console.error(`unknown argument: ${n}`),z(),process.exit(1)}}}function R(e,n){e.readyState===H.OPEN&&e.send(JSON.stringify(n))}const ee=new Set;await P.read(),P.data.sessionAccess??=[],P.data.pinnedViews??=[],P.data.watchedPanes??=[],P.data.quickCommands??=[];const U=await Ie();let ze=await Me();const g=new $e({onChange:()=>{}}),T=new Map;function te(e){return g.get(e)?T.get(e)??null:null}const ne={activeTheme:ze,settingUpPassword:!1},Ve=V({mode:"hub",securityConfig:h,tokenStore:y,rateLimiter:A,settings:U,terminalRenderer:ae(N,U.terminalRenderer),terminalBufferConfig:$,state:ne,federation:{getConfig:()=>x,setConfig:async e=>{x=e,await We(e),re(e)},status:Ye,hostname:L()},hub:{agents:g,getChannel:te}}),Xe=V({mode:"agent",securityConfig:h,tokenStore:y,rateLimiter:A,settings:U,terminalRenderer:ae(N,U.terminalRenderer),terminalBufferConfig:$,state:ne});let x=await Be(),M=null;function oe(){M&&(M.stop(),M=null)}function re(e){oe(),!(!e.enabled||!e.hub||!e.token)&&(M=De({hub:e.hub,token:e.token,name:e.name?.trim()||L(),version:fe,app:Xe,listSessions:Se,terminalBufferConfig:$,getSessionPaneTarget:Y,capturePaneTail:K,capturePaneHistoryChunk:Q,toCrlf:Z,handleClientMessage:G,acquireControlClient:X}))}function Ye(){return x.enabled?M?M.status():{state:"idle",agentId:null,hub:x.hub??"",name:x.name??L()}:{state:"idle",agentId:null,hub:x.hub??"",name:x.name??L()}}re(x);const Ke=parseInt(process.env.PORT||"21000",10),Qe=he({fetch:Ve.fetch,port:Ke,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),E=new ge({noServer:!0});function b(e,n,r){e.write(`HTTP/1.1 ${n} ${r}\r
2
+ import{chmodSync as le,existsSync as W,readdirSync as ue,statSync as de}from"node:fs";import{createRequire as D}from"node:module";import{hostname as E}from"node:os";const me=D(import.meta.url),{version:fe}=me("../package.json");import O from"node:path";import{fileURLToPath as pe}from"node:url";import{serve as he}from"@hono/node-server";import{WebSocketServer as ge,WebSocket as F}from"ws";try{const e=O.dirname(D(import.meta.url).resolve("node-pty/package.json")),n=O.join(e,"prebuilds");if(W(n))for(const s of ue(n)){const t=O.join(n,s,"spawn-helper");W(t)&&!(de(t).mode&73)&&le(t,493)}}catch{}import{loadSecurityConfig as ye}from"./lib/security-config.js";import{TokenStore as ke,hashPassword as _e,verifyPassword as Te,validatePassword as Se}from"./lib/auth.js";import{RateLimiter as Ae}from"./lib/rateLimiter.js";import{audit as l}from"./lib/auditLog.js";import{db as P}from"./lib/db.js";import{handleClientMessage as J}from"./lib/ws-message.js";import{loadDotEnv as Ce}from"./lib/load-env.js";import{listSessions as ve,resolveTmuxSocketPath as we}from"./sessions.js";import{cmdSetup as xe,cmdTheme as be,printUsage as G,printVersion as Ie}from"./lib/cli.js";import{readSettings as Me}from"./lib/settings.js";import{readActiveTheme as Re}from"./lib/theme-store.js";import{buildApp as z}from"./lib/build-app.js";import{attachTerminal as Oe,killAllAttachedPtys as Pe}from"./lib/attach-terminal.js";import{acquireControlClient as V,killAllControlClients as $e}from"./lib/tmux-control.js";import{getSessionPaneTarget as X,capturePaneTail as Y,capturePaneHistoryChunk as K,toCrlf as Q}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Le}from"./lib/terminal-config.js";import{AgentRegistry as Ee}from"./lib/agent-registry.js";import{AgentChannel as Ue,MAX_RELAY_CONNS_PER_AGENT as Ne}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as Fe,listAgentTokens as He}from"./lib/agent-tokens.js";import{saveSecurityConfig as je}from"./lib/security-config.js";import{readFederationConfig as Be,writeFederationConfig as We}from"./lib/federation-config.js";import{startAgentClient as De}from"./lib/agent-client.js";Ce();const U=Le(),h=ye(),y=new ke,A=new Ae;let H=!1;const Je="tmux-web-token";function j(e){if(h.security.trustProxy){const n=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(n)return n}return e.socket.remoteAddress||"unknown"}function B(e){const n=e.headers.authorization;if(typeof n=="string"&&n.startsWith("Bearer "))return n.slice(7);const s=e.headers.cookie;if(typeof s!="string")return null;const t=s.match(new RegExp(`(?:^|;\\s*)${Je}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function Lt(e){const n=B(e);return n?y.validateToken(n):null}const M=new Map;function Ge(e){let n=0;for(const s of M.values())s.ip===e&&n++;return n}function v(e,n,s){try{e.close(n,s)}catch{}}function Z(e,n){e.readyState===F.OPEN&&e.send(JSON.stringify(n))}const q=process.argv.slice(2),$=q;if($.length>0){const[e,n]=$;switch(e){case"setup":await xe($),process.exit(0);case"theme":await be($.slice(1)),process.exit(0);case"agent":{const s=O.join(O.dirname(pe(import.meta.url)),"agent.js"),{spawn:t}=await import("node:child_process"),m=t(process.execPath,[s,...q],{stdio:"inherit"});m.on("error",r=>{console.error(r),process.exit(1)}),m.on("exit",r=>process.exit(r??0)),await new Promise(()=>{})}case"agent-token":await rt($.slice(1)),process.exit(0);case"help":case"--help":case"-h":G(),process.exit(0);case"-V":case"--version":case"-v":Ie(),process.exit(0);default:console.error(`unknown argument: ${e}`),G(),process.exit(1)}}function b(e,n){e.readyState===F.OPEN&&e.send(JSON.stringify(n))}const ee=new Set;await P.read(),P.data.sessionAccess??=[],P.data.pinnedViews??=[],P.data.watchedPanes??=[],P.data.quickCommands??=[];const te=await Me();let ze=await Re();const g=new Ee({onChange:()=>{}}),T=new Map;function ne(e){return g.get(e)?T.get(e)??null:null}const oe={activeTheme:ze,settingUpPassword:!1},Ve=z({mode:"hub",securityConfig:h,tokenStore:y,rateLimiter:A,settings:te,terminalBufferConfig:U,state:oe,federation:{getConfig:()=>x,setConfig:async e=>{x=e,await We(e),se(e)},status:Ye,hostname:E()},hub:{agents:g,getChannel:ne}}),Xe=z({mode:"agent",securityConfig:h,tokenStore:y,rateLimiter:A,settings:te,terminalBufferConfig:U,state:oe});let x=await Be(),R=null;function re(){R&&(R.stop(),R=null)}function se(e){re(),!(!e.enabled||!e.hub||!e.token)&&(R=De({hub:e.hub,token:e.token,name:e.name?.trim()||E(),version:fe,app:Xe,listSessions:ve,terminalBufferConfig:U,getSessionPaneTarget:X,capturePaneTail:Y,capturePaneHistoryChunk:K,toCrlf:Q,handleClientMessage:J,acquireControlClient:V}))}function Ye(){return x.enabled?R?R.status():{state:"idle",agentId:null,hub:x.hub??"",name:x.name??E()}:{state:"idle",agentId:null,hub:x.hub??"",name:x.name??E()}}se(x);const Ke=parseInt(process.env.PORT||"21000",10),Qe=he({fetch:Ve.fetch,port:Ke,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),L=new ge({noServer:!0});function I(e,n,s){e.write(`HTTP/1.1 ${n} ${s}\r
3
3
  \r
4
- `),e.destroy()}Qe.on("upgrade",(e,n,r)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),u=t.pathname;if(u==="/agent/ws"){tt(e,n,r);return}const s=u.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(s){Ze(e,n,r,decodeURIComponent(s[1]),decodeURIComponent(s[2]));return}const o=u.match(/^\/ws\/(.+)$/);if(!o){n.destroy();return}const f=B(e),i=e.headers.origin,d=h.security.allowedOrigins;if(i&&!(i===`http://${e.headers.host}`||i===`https://${e.headers.host}`)&&(d.length===0||!d.includes(i))){l("ws_rejected_origin",{ip:f,origin:i}),b(n,403,"Origin not allowed");return}const k=Ge(f);if(k>=h.security.maxConnectionsPerIp){l("ws_rejected_per_ip_cap",{ip:f,liveFromIp:k}),b(n,429,"Too many connections");return}const p=W(e)||t.searchParams.get("token")||"",v=p?y.validateToken(p):null;E.handleUpgrade(e,n,r,a=>{E.emit("connection",a,e,decodeURIComponent(o[1]),f,v)})}),E.on("connection",(e,n,r,t,u)=>{const s={ws:e,ip:t,authenticated:!1,authTimeout:null};I.set(e,s),l("ws_connected",{ip:t});let o=null,f=null;function i(a,c){s.authenticated=!0,s.authTimeout&&(clearTimeout(s.authTimeout),s.authTimeout=null),l("auth_success",{ip:t,method:a,tokenName:c}),R(e,{type:"auth.ok",setupMode:!h.passwordHash}),k()}function d(a,c={}){R(e,{type:"auth.failed",message:a,...c})}function k(){o||(o=Oe(r,{send:a=>R(e,a),onMessage:a=>{f=a},onClose:a=>{p=a}},{terminalBufferConfig:$,getSessionPaneTarget:Y,capturePaneTail:K,capturePaneHistoryChunk:Q,toCrlf:Z,handleClientMessage:G,acquireControlClient:X,tmuxSocketPath:we()??void 0,onPtyExit:(a,c)=>S(e,1e3,"pty exited"),onSpawnError:(a,c)=>S(e,1011,"pty spawn failed")}))}let p=null;async function v(a){let c;try{c=JSON.parse(a)}catch{return!1}if(c.type==="auth.token"&&typeof c.token=="string"){const m=y.validateToken(c.token);if(!m){l("token_auth_failed",{ip:t});const w=A.recordFailure(t);return w.permanentLock&&(y.revokeAll(),l("permanent_lock",{ip:t,failures:w.failures})),d("Invalid or expired token",{permanentLock:w.permanentLock,retryAfterMs:w.retryAfterMs}),!0}return y.touch(m.tokenHash),i("token",m.name),!0}if(c.type==="auth"&&typeof c.password=="string"){const m=A.check(t);if(!m.allowed)return l("rate_limited",{ip:t,retryAfterMs:m.retryAfterMs,permanentLock:m.permanentLock}),d("Too many attempts",{permanentLock:m.permanentLock,retryAfterMs:m.retryAfterMs}),!0;if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!ot(t))return l("setup_rejected_remote",{ip:t}),d("First-run setup must be performed from localhost"),!0;if(j)return d("Password setup in progress"),!0;const C=ve(c.password);if(C)return d(C),!0;j=!0;try{h.passwordHash=await _e(c.password),je(h),l("password_set",{ip:t})}finally{j=!1}const{stored:F,plaintext:ce}=y.createAccessToken("setup",h.security.tokenTtlDays);return R(e,{type:"auth.ok",setupMode:!0,token:ce}),l("token_created",{ip:t,name:F.name,tokenId:F.id}),l("auth_success",{ip:t,method:"password",tokenName:F.name}),k(),!0}if(!h.passwordHash)return d("Server not configured"),!0;if(!await Te(c.password,h.passwordHash)){const C=A.recordFailure(t);return l("auth_failed",{ip:t,method:"password",failures:C.failures,permanentLock:C.permanentLock}),C.permanentLock&&(y.revokeAll(),l("permanent_lock",{ip:t,failures:C.failures})),d("Incorrect password",{permanentLock:C.permanentLock,retryAfterMs:C.retryAfterMs}),!0}A.recordSuccess(t);const _=`ws-${t}`,{plaintext:ie}=y.createAccessToken(_,h.security.tokenTtlDays);return i("password",_),R(e,{type:"auth.ok",setupMode:!1,token:ie}),!0}return!1}u?(y.touch(u.tokenHash),i("token",u.name)):(R(e,{type:"auth.required",setupMode:!h.passwordHash}),s.authTimeout=setTimeout(()=>{s.authenticated||(l("auth_timeout",{ip:t}),d("Authentication timeout"),S(e,4e3,"Auth timeout"))},h.security.authTimeoutMs)),e.on("message",async a=>{const c=typeof a=="string"?a:a.toString("utf-8");if(!s.authenticated){if(c.length>1e6){R(e,{type:"auth.failed",message:"Message too large"});return}await v(c)||d("Authentication required");return}if(f)try{f(c)}catch{}}),e.on("close",()=>{s.authTimeout&&clearTimeout(s.authTimeout),I.delete(e),l("ws_disconnected",{ip:t}),p?.(),o&&(o.dispose(),o=null)}),e.on("error",()=>{s.authTimeout&&clearTimeout(s.authTimeout),I.delete(e),p?.(),o&&(o.dispose(),o=null)})});function Ze(e,n,r,t,u){const s=B(e),o=new URL(e.url||"/",`http://${e.headers.host}`),f=W(e)||o.searchParams.get("token")||"";if(!(f?y.validateToken(f):null)){l("ws_rejected_remote_auth",{ip:s,agentId:t}),b(n,401,"Unauthorized");return}const d=te(t);if(!d){l("ws_rejected_remote_offline",{ip:s,agentId:t}),b(n,503,"Agent offline");return}if(d.relayCount>=Ue){b(n,429,"Too many connections to agent");return}const k=e.headers.origin,p=h.security.allowedOrigins;if(k&&!(k===`http://${e.headers.host}`||k===`https://${e.headers.host}`)&&(p.length===0||!p.includes(k))){b(n,403,"Origin not allowed");return}const v=et();E.handleUpgrade(e,n,r,a=>{if(d.registerRelay(v,u,a),g.bumpRelayConns(t,1),!d.attach(v,u)){a.send(JSON.stringify({type:"data",data:`\r
4
+ `),e.destroy()}Qe.on("upgrade",(e,n,s)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),m=t.pathname;if(m==="/agent/ws"){tt(e,n,s);return}const r=m.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(r){Ze(e,n,s,decodeURIComponent(r[1]),decodeURIComponent(r[2]));return}const o=m.match(/^\/ws\/(.+)$/);if(!o){n.destroy();return}const f=j(e),i=e.headers.origin,u=h.security.allowedOrigins;if(i&&!(i===`http://${e.headers.host}`||i===`https://${e.headers.host}`)&&(u.length===0||!u.includes(i))){l("ws_rejected_origin",{ip:f,origin:i}),I(n,403,"Origin not allowed");return}const k=Ge(f);if(k>=h.security.maxConnectionsPerIp){l("ws_rejected_per_ip_cap",{ip:f,liveFromIp:k}),I(n,429,"Too many connections");return}const p=B(e)||t.searchParams.get("token")||"",S=p?y.validateToken(p):null;L.handleUpgrade(e,n,s,a=>{L.emit("connection",a,e,decodeURIComponent(o[1]),f,S)})}),L.on("connection",(e,n,s,t,m)=>{const r={ws:e,ip:t,authenticated:!1,authTimeout:null};M.set(e,r),l("ws_connected",{ip:t});let o=null,f=null;function i(a,c){r.authenticated=!0,r.authTimeout&&(clearTimeout(r.authTimeout),r.authTimeout=null),l("auth_success",{ip:t,method:a,tokenName:c}),b(e,{type:"auth.ok",setupMode:!h.passwordHash}),k()}function u(a,c={}){b(e,{type:"auth.failed",message:a,...c})}function k(){o||(o=Oe(s,{send:a=>b(e,a),onMessage:a=>{f=a},onClose:a=>{p=a}},{terminalBufferConfig:U,getSessionPaneTarget:X,capturePaneTail:Y,capturePaneHistoryChunk:K,toCrlf:Q,handleClientMessage:J,acquireControlClient:V,tmuxSocketPath:we()??void 0,onPtyExit:(a,c)=>v(e,1e3,"pty exited"),onSpawnError:(a,c)=>v(e,1011,"pty spawn failed")}))}let p=null;async function S(a){let c;try{c=JSON.parse(a)}catch{return!1}if(c.type==="auth.token"&&typeof c.token=="string"){const d=y.validateToken(c.token);if(!d){l("token_auth_failed",{ip:t});const w=A.recordFailure(t);return w.permanentLock&&(y.revokeAll(),l("permanent_lock",{ip:t,failures:w.failures})),u("Invalid or expired token",{permanentLock:w.permanentLock,retryAfterMs:w.retryAfterMs}),!0}return y.touch(d.tokenHash),i("token",d.name),!0}if(c.type==="auth"&&typeof c.password=="string"){const d=A.check(t);if(!d.allowed)return l("rate_limited",{ip:t,retryAfterMs:d.retryAfterMs,permanentLock:d.permanentLock}),u("Too many attempts",{permanentLock:d.permanentLock,retryAfterMs:d.retryAfterMs}),!0;if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!ot(t))return l("setup_rejected_remote",{ip:t}),u("First-run setup must be performed from localhost"),!0;if(H)return u("Password setup in progress"),!0;const C=Se(c.password);if(C)return u(C),!0;H=!0;try{h.passwordHash=await _e(c.password),je(h),l("password_set",{ip:t})}finally{H=!1}const{stored:N,plaintext:ce}=y.createAccessToken("setup",h.security.tokenTtlDays);return b(e,{type:"auth.ok",setupMode:!0,token:ce}),l("token_created",{ip:t,name:N.name,tokenId:N.id}),l("auth_success",{ip:t,method:"password",tokenName:N.name}),k(),!0}if(!h.passwordHash)return u("Server not configured"),!0;if(!await Te(c.password,h.passwordHash)){const C=A.recordFailure(t);return l("auth_failed",{ip:t,method:"password",failures:C.failures,permanentLock:C.permanentLock}),C.permanentLock&&(y.revokeAll(),l("permanent_lock",{ip:t,failures:C.failures})),u("Incorrect password",{permanentLock:C.permanentLock,retryAfterMs:C.retryAfterMs}),!0}A.recordSuccess(t);const _=`ws-${t}`,{plaintext:ie}=y.createAccessToken(_,h.security.tokenTtlDays);return i("password",_),b(e,{type:"auth.ok",setupMode:!1,token:ie}),!0}return!1}m?(y.touch(m.tokenHash),i("token",m.name)):(b(e,{type:"auth.required",setupMode:!h.passwordHash}),r.authTimeout=setTimeout(()=>{r.authenticated||(l("auth_timeout",{ip:t}),u("Authentication timeout"),v(e,4e3,"Auth timeout"))},h.security.authTimeoutMs)),e.on("message",async a=>{const c=typeof a=="string"?a:a.toString("utf-8");if(!r.authenticated){if(c.length>1e6){b(e,{type:"auth.failed",message:"Message too large"});return}await S(c)||u("Authentication required");return}if(f)try{f(c)}catch{}}),e.on("close",()=>{r.authTimeout&&clearTimeout(r.authTimeout),M.delete(e),l("ws_disconnected",{ip:t}),p?.(),o&&(o.dispose(),o=null)}),e.on("error",()=>{r.authTimeout&&clearTimeout(r.authTimeout),M.delete(e),p?.(),o&&(o.dispose(),o=null)})});function Ze(e,n,s,t,m){const r=j(e),o=new URL(e.url||"/",`http://${e.headers.host}`),f=B(e)||o.searchParams.get("token")||"";if(!(f?y.validateToken(f):null)){l("ws_rejected_remote_auth",{ip:r,agentId:t}),I(n,401,"Unauthorized");return}const u=ne(t);if(!u){l("ws_rejected_remote_offline",{ip:r,agentId:t}),I(n,503,"Agent offline");return}if(u.relayCount>=Ne){I(n,429,"Too many connections to agent");return}const k=e.headers.origin,p=h.security.allowedOrigins;if(k&&!(k===`http://${e.headers.host}`||k===`https://${e.headers.host}`)&&(p.length===0||!p.includes(k))){I(n,403,"Origin not allowed");return}const S=et();L.handleUpgrade(e,n,s,a=>{if(u.registerRelay(S,m,a),g.bumpRelayConns(t,1),!u.attach(S,m)){a.send(JSON.stringify({type:"data",data:`\r
5
5
  \x1B[31magent offline\x1B[0m\r
6
- `})),a.close(1011,"agent offline"),d.relayClosed(v),g.bumpRelayConns(t,-1);return}a.on("message",m=>{const w=typeof m=="string"?m:m.toString("utf-8");let _;try{_=JSON.parse(w)}catch{return}(_.type==="input"||_.type==="resize"||_.type==="load_history")&&d.sendToAgent(v,_)});const c=()=>{d.relayClosed(v),g.bumpRelayConns(t,-1)};a.on("close",c),a.on("error",c)})}let qe=1;function et(){return qe++}function tt(e,n,r){const t=B(e),u=e.headers.origin;if(u){l("agent_rejected_origin",{ip:t,origin:u}),b(n,403,"Origin not allowed");return}E.handleUpgrade(e,n,r,s=>{let o=null,f=setTimeout(()=>{l("agent_hello_timeout",{ip:t}),S(s,4e3,"hello timeout")},1e4);s.on("message",(i,d)=>{if(o){g.touch(o.agentId),d?o.handleData(Array.isArray(i)?Buffer.concat(i):Buffer.from(i)):o.handleMessage(typeof i=="string"?i:Buffer.from(i).toString("utf-8"));return}const k=typeof i=="string"?i:i.toString("utf-8");let p;try{p=JSON.parse(k)}catch{S(s,1008,"bad hello");return}if(p.type!=="hello"||typeof p.token!="string"){S(s,1008,"expected hello");return}if(!A.check(t).allowed){l("agent_rate_limited",{ip:t}),S(s,429,"Too many attempts");return}const a=Fe(p.token);if(!a){A.recordFailure(t),l("agent_bad_token",{ip:t}),q(s,{type:"hello_err",reason:"invalid token"}),S(s,4003,"invalid token");return}const c=typeof p.name=="string"&&p.name.trim()?p.name.trim().slice(0,64):"agent",m=a.id;if(!g.register(m,c,s,a.tokenHash)){l("agent_cap_reached",{ip:t}),q(s,{type:"hello_err",reason:"too many agents"}),S(s,4004,"too many agents");return}clearTimeout(f),A.recordSuccess(t),l("agent_connected",{ip:t,agentId:m,name:c}),o=new Ne(s,m,{onSessions:_=>g.setSessions(m,_),onClose:_=>{T.get(m)===_&&T.delete(m)}}),T.set(m,o),g.touch(m),s.send(JSON.stringify({type:"hello_ok",agentId:m})),o.sendSessionsReq()}),s.on("close",()=>{o&&(o.dispose(),T.delete(o.agentId),g.unregister(o.agentId),l("agent_disconnected",{ip:t,agentId:o.agentId})),clearTimeout(f)}),s.on("error",()=>{o&&(o.dispose(),T.delete(o.agentId),g.unregister(o.agentId)),clearTimeout(f)})})}const nt=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=g.pruneOffline(nt);for(const r of e){const t=g.peek(r);t&&t.ws.readyState===H.CLOSED&&(g.unregister(r),T.get(r)?.dispose(),T.delete(r))}const n=new Set(He().map(r=>r.tokenHash));for(const[r,t]of T){const u=g.peek(r);if(!(!u||n.has(u.tokenHash))){l("agent_token_revoked",{agentId:r}),t.dispose(),T.delete(r),g.unregister(r);try{u.ws.close(4003,"token revoked")}catch{}}}y.purgeExpired()},10*1e3).unref();function se(){oe(),Ee(),Pe();for(const e of T.values())e.dispose();T.clear();for(const[e,n]of I){try{e.close(1001,"Server shutting down")}catch{}n.authTimeout&&clearTimeout(n.authTimeout)}I.clear();for(const e of ee)try{e.kill()}catch{}ee.clear(),A.dispose(),process.exit(0)}process.on("SIGINT",se),process.on("SIGTERM",se);function ot(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function ae(e,n){let r="xterm";(n==="ghostty"||n==="xterm")&&(r=n);const t=process.env.TMUX_WEB_TERMINAL_RENDERER?.trim().toLowerCase();(t==="ghostty"||t==="xterm")&&(r=t);for(const u of e)u==="--ghostty"&&(r="ghostty"),u==="--xterm"&&(r="xterm");return r}async function rt(e){const{createAgentToken:n,listAgentTokens:r,removeAgentToken:t}=await import("./lib/agent-tokens.js"),[u,...s]=e;switch(u){case"add":{const o=s.find(d=>!d.startsWith("-"))||"agent",{id:f,token:i}=n(o);console.log("\u2713 agent token created"),console.log(` id: ${f}`),console.log(` name: ${o}`),console.log(` token: ${i}`),console.log(`
7
- Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${i} --name "${o}"`);return}case"list":{const o=r();if(!o.length){console.log("No agent tokens. Create one with: tmux-web agent-token add --name <name>");return}console.log("Agent tokens:");for(const f of o)console.log(` ${f.id} ${f.name} (created ${new Date(f.createdAt).toISOString()})`);return}case"remove":{const o=s.find(i=>!i.startsWith("-"));o||(console.error("usage: tmux-web agent-token remove <id>"),process.exit(1)),t(o)||(console.error(`no token with id ${o}`),process.exit(1)),console.log(`\u2713 token ${o} removed`);return}default:console.error("usage: tmux-web agent-token <add|list|remove>"),process.exit(1)}}
6
+ `})),a.close(1011,"agent offline"),u.relayClosed(S),g.bumpRelayConns(t,-1);return}a.on("message",d=>{const w=typeof d=="string"?d:d.toString("utf-8");let _;try{_=JSON.parse(w)}catch{return}(_.type==="input"||_.type==="resize"||_.type==="load_history")&&u.sendToAgent(S,_)});const c=()=>{u.relayClosed(S),g.bumpRelayConns(t,-1)};a.on("close",c),a.on("error",c)})}let qe=1;function et(){return qe++}function tt(e,n,s){const t=j(e),m=e.headers.origin;if(m){l("agent_rejected_origin",{ip:t,origin:m}),I(n,403,"Origin not allowed");return}L.handleUpgrade(e,n,s,r=>{let o=null,f=setTimeout(()=>{l("agent_hello_timeout",{ip:t}),v(r,4e3,"hello timeout")},1e4);r.on("message",(i,u)=>{if(o){g.touch(o.agentId),u?o.handleData(Array.isArray(i)?Buffer.concat(i):Buffer.from(i)):o.handleMessage(typeof i=="string"?i:Buffer.from(i).toString("utf-8"));return}const k=typeof i=="string"?i:i.toString("utf-8");let p;try{p=JSON.parse(k)}catch{v(r,1008,"bad hello");return}if(p.type!=="hello"||typeof p.token!="string"){v(r,1008,"expected hello");return}if(!A.check(t).allowed){l("agent_rate_limited",{ip:t}),v(r,429,"Too many attempts");return}const a=Fe(p.token);if(!a){A.recordFailure(t),l("agent_bad_token",{ip:t}),Z(r,{type:"hello_err",reason:"invalid token"}),v(r,4003,"invalid token");return}const c=typeof p.name=="string"&&p.name.trim()?p.name.trim().slice(0,64):"agent",d=a.id;if(!g.register(d,c,r,a.tokenHash)){l("agent_cap_reached",{ip:t}),Z(r,{type:"hello_err",reason:"too many agents"}),v(r,4004,"too many agents");return}clearTimeout(f),A.recordSuccess(t),l("agent_connected",{ip:t,agentId:d,name:c}),o=new Ue(r,d,{onSessions:_=>g.setSessions(d,_),onClose:_=>{T.get(d)===_&&T.delete(d)}}),T.set(d,o),g.touch(d),r.send(JSON.stringify({type:"hello_ok",agentId:d})),o.sendSessionsReq()}),r.on("close",()=>{o&&(o.dispose(),T.delete(o.agentId),g.unregister(o.agentId),l("agent_disconnected",{ip:t,agentId:o.agentId})),clearTimeout(f)}),r.on("error",()=>{o&&(o.dispose(),T.delete(o.agentId),g.unregister(o.agentId)),clearTimeout(f)})})}const nt=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=g.pruneOffline(nt);for(const s of e){const t=g.peek(s);t&&t.ws.readyState===F.CLOSED&&(g.unregister(s),T.get(s)?.dispose(),T.delete(s))}const n=new Set(He().map(s=>s.tokenHash));for(const[s,t]of T){const m=g.peek(s);if(!(!m||n.has(m.tokenHash))){l("agent_token_revoked",{agentId:s}),t.dispose(),T.delete(s),g.unregister(s);try{m.ws.close(4003,"token revoked")}catch{}}}y.purgeExpired()},10*1e3).unref();function ae(){re(),$e(),Pe();for(const e of T.values())e.dispose();T.clear();for(const[e,n]of M){try{e.close(1001,"Server shutting down")}catch{}n.authTimeout&&clearTimeout(n.authTimeout)}M.clear();for(const e of ee)try{e.kill()}catch{}ee.clear(),A.dispose(),process.exit(0)}process.on("SIGINT",ae),process.on("SIGTERM",ae);function ot(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}async function rt(e){const{createAgentToken:n,listAgentTokens:s,removeAgentToken:t}=await import("./lib/agent-tokens.js"),[m,...r]=e;switch(m){case"add":{const o=r.find(u=>!u.startsWith("-"))||"agent",{id:f,token:i}=n(o);console.log("\u2713 agent token created"),console.log(` id: ${f}`),console.log(` name: ${o}`),console.log(` token: ${i}`),console.log(`
7
+ Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${i} --name "${o}"`);return}case"list":{const o=s();if(!o.length){console.log("No agent tokens. Create one with: tmux-web agent-token add --name <name>");return}console.log("Agent tokens:");for(const f of o)console.log(` ${f.id} ${f.name} (created ${new Date(f.createdAt).toISOString()})`);return}case"remove":{const o=r.find(i=>!i.startsWith("-"));o||(console.error("usage: tmux-web agent-token remove <id>"),process.exit(1)),t(o)||(console.error(`no token with id ${o}`),process.exit(1)),console.log(`\u2713 token ${o} removed`);return}default:console.error("usage: tmux-web agent-token <add|list|remove>"),process.exit(1)}}
@@ -1,8 +1,8 @@
1
- import{existsSync as L,readdirSync as ae,statSync as A,unlinkSync as de,writeFileSync as ue,mkdirSync as ce,readFileSync as G}from"node:fs";import{readFile as le}from"node:fs/promises";import{execSync as _,execFileSync as z}from"node:child_process";import p from"node:path";import{fileURLToPath as me}from"node:url";import{Hono as fe}from"hono";import{listSessions as D}from"../sessions.js";import{renderLoginPage as pe,renderNotesIndex as he,renderNotesPage as ge,renderSettings as ye,renderThemeSettings as je,renderHistoryIndex as we,renderQuickCommandsPage as ke,renderFilesIndex as ve,renderShell as qe}from"../frontend.js";import{renderFederationPage as Se}from"./pages/federation.js";import{createAgentToken as Te,removeAgentToken as Ae,listAgentTokens as B}from"./agent-tokens.js";import{hashPassword as be,verifyPassword as xe,validatePassword as _e}from"./auth.js";import{atomicWriteFileSync as Ce}from"./atomicWrite.js";import{resolveFsPath as b,resolveFsRoots as V,MAX_FILE_BYTES as Q,walkRecursive as Oe}from"./fs-access.js";import{captureSessionWindowsWithPath as Ee}from"./tmux-windows.js";import{audit as j}from"./auditLog.js";import{db as S}from"./db.js";import{getSessionAccessMap as Ie}from"./session-access.js";import{listWindowHistory as Re,clearWindowHistory as Ne}from"./window-history.js";import{saveSecurityConfig as Pe}from"./security-config.js";import{readSettings as K,writeSettings as Le}from"./settings.js";import{setActiveThemeTemplate as X}from"./theme-store.js";import{isThemeTemplateId as Y,THEME_TEMPLATE_IDS as De}from"./themes/index.js";import{pinView as Fe,unpinView as He,listPinnedViews as Ue}from"./pinned-views.js";import{getStoredWindows as Me}from"./session-windows.js";import{buildSidebarSessions as We}from"./sessions-sidebar.js";import{createQuickCommand as $e,deleteQuickCommand as Ge,listQuickCommands as Z,updateQuickCommand as ze}from"./quick-commands.js";import{getSystemStatus as Be,getTopProcesses as Ve,killProcess as Qe}from"./system-monitor.js";import{newTmuxSession as Ke,renameSession as Xe,killSession as Ye,TmuxWindowsError as Ze}from"./tmux-windows.js";import{registerAgentTunnel as Je}from"./agent-tunnel.js";import{injectScopeScript as et}from"./scope-script.js";const F="tmux-web-token",tt=365;function rt(x){return x==="127.0.0.1"||x==="::1"||x==="::ffff:127.0.0.1"}function Pt(x){const{mode:J,securityConfig:h,tokenStore:g,rateLimiter:O,settings:nt,terminalRenderer:E,terminalBufferConfig:I,getAgentId:H,state:u,federation:w,hub:q}=x,k=J==="agent";function C(e){if(h.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 R(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*)${F}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function st(e){const t=R(e);return t?g.validateToken(t):null}function U(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${F}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${tt*86400}${r?"; Secure":""}`)}function ee(e){e.header("Set-Cookie",`${F}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const o=()=>k?async(e,t)=>t():async(e,t)=>{const r=R(e);return!r||!g.validateToken(r)?(j("http_unauthorized",{ip:C(e)}),e.json({error:"unauthorized"},401)):t()},y=()=>k?async(e,t)=>t():async(e,t)=>{const r=R(e);if(!r||!g.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function N(e){const t=We(D(),Ie(),Ue()),r=[];if(q)for(const n of q.agents.list()){const i=q.agents.get(n.agentId);r.push({agentId:n.agentId,agentName:n.name,online:n.online,sessions:i?i.sessions:[]})}return{...t,currentSession:e??null,agents:r}}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 fe;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()}),k||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(et(n,{prefix:"",agentId:null}),e.res)});const W=p.dirname(me(import.meta.url)),te=[p.join(W,"..","assets"),p.join(W,"assets"),p.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 te){const n=p.join(r,t);if(!L(n))continue;const i=await le(n),a=p.extname(t),c={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(i,200,{"Content-Type":c[a]??"application/octet-stream","Cache-Control":a===".js"||a===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const re=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1
+ import{existsSync as P,readdirSync as ie,statSync as A,unlinkSync as ae,writeFileSync as de,mkdirSync as ue,readFileSync as $}from"node:fs";import{readFile as ce}from"node:fs/promises";import{execSync as _,execFileSync as G}from"node:child_process";import p from"node:path";import{fileURLToPath as le}from"node:url";import{Hono as fe}from"hono";import{listSessions as L}from"../sessions.js";import{renderLoginPage as me,renderNotesIndex as pe,renderNotesPage as he,renderSettings as ge,renderThemeSettings as ye,renderHistoryIndex as je,renderQuickCommandsPage as we,renderFilesIndex as ke,renderShell as ve}from"../frontend.js";import{renderFederationPage as qe}from"./pages/federation.js";import{createAgentToken as Se,removeAgentToken as Te,listAgentTokens as z}from"./agent-tokens.js";import{hashPassword as Ae,verifyPassword as be,validatePassword as xe}from"./auth.js";import{atomicWriteFileSync as _e}from"./atomicWrite.js";import{resolveFsPath as b,resolveFsRoots as B,MAX_FILE_BYTES as V,walkRecursive as Ce}from"./fs-access.js";import{captureSessionWindowsWithPath as Oe}from"./tmux-windows.js";import{audit as j}from"./auditLog.js";import{db as S}from"./db.js";import{getSessionAccessMap as Ee}from"./session-access.js";import{listWindowHistory as Ie,clearWindowHistory as Ne}from"./window-history.js";import{saveSecurityConfig as Re}from"./security-config.js";import{readSettings as Q,writeSettings as Pe}from"./settings.js";import{setActiveThemeTemplate as K}from"./theme-store.js";import{isThemeTemplateId as X,THEME_TEMPLATE_IDS as Le}from"./themes/index.js";import{pinView as De,unpinView as Fe,listPinnedViews as He}from"./pinned-views.js";import{getStoredWindows as Ue}from"./session-windows.js";import{buildSidebarSessions as Me}from"./sessions-sidebar.js";import{createQuickCommand as We,deleteQuickCommand as $e,listQuickCommands as Y,updateQuickCommand as Ge}from"./quick-commands.js";import{getSystemStatus as ze,getTopProcesses as Be,killProcess as Ve}from"./system-monitor.js";import{newTmuxSession as Qe,renameSession as Ke,killSession as Xe,TmuxWindowsError as Ye}from"./tmux-windows.js";import{registerAgentTunnel as Ze}from"./agent-tunnel.js";import{injectScopeScript as Je}from"./scope-script.js";const D="tmux-web-token",et=365;function tt(x){return x==="127.0.0.1"||x==="::1"||x==="::ffff:127.0.0.1"}function Rt(x){const{mode:Z,securityConfig:h,tokenStore:g,rateLimiter:O,settings:rt,terminalBufferConfig:E,getAgentId:F,state:u,federation:w,hub:q}=x,k=Z==="agent";function C(e){if(h.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 I(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 nt(e){const t=I(e);return t?g.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=${et*86400}${r?"; Secure":""}`)}function J(e){e.header("Set-Cookie",`${D}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const o=()=>k?async(e,t)=>t():async(e,t)=>{const r=I(e);return!r||!g.validateToken(r)?(j("http_unauthorized",{ip:C(e)}),e.json({error:"unauthorized"},401)):t()},y=()=>k?async(e,t)=>t():async(e,t)=>{const r=I(e);if(!r||!g.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function N(e){const t=Me(L(),Ee(),He()),r=[];if(q)for(const n of q.agents.list()){const i=q.agents.get(n.agentId);r.push({agentId:n.agentId,agentName:n.name,online:n.online,sessions:i?i.sessions:[]})}return{...t,currentSession:e??null,agents:r}}function U(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 fe;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()}),k||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(Je(n,{prefix:"",agentId:null}),e.res)});const M=p.dirname(le(import.meta.url)),ee=[p.join(M,"..","assets"),p.join(M,"assets"),p.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 ee){const n=p.join(r,t);if(!P(n))continue;const i=await ce(n),a=p.extname(t),c={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(i,200,{"Content-Type":c[a]??"application/octet-stream","Cache-Control":a===".js"||a===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const te=`<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>`,$=e=>e.body(re,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",$),s.get("/favicon.ico",$);const ne={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(ne,200,{"Cache-Control":"public, max-age=3600"}));const se=`// tmux-weblink Service Worker
5
+ </svg>`,W=e=>e.body(te,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 re={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(re,200,{"Cache-Control":"public, max-age=3600"}));const ne=`// 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(se,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(pe({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:u.activeTheme}))}),s.get("/",y(),e=>{const t=V();return e.html(qe({theme:u.activeTheme,fsRoots:t,terminalCfg:I,renderer:E,scrollback:I.initialLines+2*I.historyChunk,wsBase:k&&H?.()?`/ws/a/${H()}`:void 0}))}),s.get("/notes",y(),e=>e.html(he(S.data.notes,u.activeTheme))),s.get("/notes/:session",y(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(ge(t,u.activeTheme))}),s.get("/history",y(),e=>{const t=D(),r=new Set(t.map(n=>n.name));return e.html(we(Re(),u.activeTheme,r))}),s.get("/quick-commands",y(),e=>e.html(ke(Z(),u.activeTheme))),s.get("/files",y(),e=>{const t=V();return e.html(ve(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(Z())),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 $e(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=>Ge(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&&!rt(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=_e(i);if(l)return e.json({error:l},400);u.settingUpPassword=!0;try{h.passwordHash=await be(i),Pe(h),j("password_set",{ip:t})}finally{u.settingUpPassword=!1}const{plaintext:d}=g.createAccessToken("setup",h.security.tokenTtlDays);return U(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await xe(i,h.passwordHash)){const l=O.recordFailure(t);return j("auth_failed",{ip:t,method:"password",failures:l.failures,permanentLock:l.permanentLock}),l.permanentLock?(g.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}=g.createAccessToken(c,h.security.tokenTtlDays);return U(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}=g.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(g.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):g.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=>(ee(e),e.json({ok:!0}))),s.get("/settings",y(),async e=>{const t=await K(),r=t.terminalRenderer??"xterm";return e.html(ye({settings:t,renderer:E,rendererOverridden:E!==r,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 K(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",i=t.defaultView==="recent"?"recent":"default";return await Le({...r,terminalRenderer:n,defaultView:i}),e.redirect("/settings?saved=1",303)}),s.get("/settings/theme",y(),e=>e.html(je({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"||!Y(r)?e.redirect("/settings/theme",303):(u.activeTheme=await X(r),e.redirect("/settings/theme?saved=1",303))});const oe={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:De.map(t=>({id:t,name:oe[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"||!Y(r)?e.json({error:"invalid theme template"},400):(u.activeTheme=await X(r),e.json({ok:!0,active:u.activeTheme.template}))}),s.get("/api/system/status",o(),e=>e.json(Be())),s.get("/api/system/processes",o(),e=>e.json(Ve())),w&&(s.get("/settings/federation",y(),async e=>e.html(Se({config:w.getConfig(),status:w.status(),tokens:B().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:w.hostname},u.activeTheme))),s.get("/api/federation",o(),e=>e.json({config:w.getConfig(),status:w.status()})),s.post("/api/federation",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(B().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}=Te(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 Ae(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=Qe(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(D().some(a=>a.name===r))return e.json({error:"session already exists"},409);try{return Ke(r,n),e.json({ok:!0})}catch(a){const c=a instanceof Ze?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)?(Xe(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?(Ye(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({error:"session is required"},400);try{const r=Ee(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=ae(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&&Oe(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 ie(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(),P=f!==" "?f:m;a.push({path:T,status:P,staged:m,unstaged:f,additions:0,deletions:0})}const c=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,P=parseInt(f[1],10)||0;c.set(f[2],{added:T,deleted:P})}}}catch{}for(const d of a){const m=c.get(d.path);m&&(d.additions=m.added,d.deletions=m.deleted)}let v=0,l=0;for(const d of a)v+=d.additions,l+=d.deletions;for(const d of a)if(d.status==="?"&&d.additions===0)try{const m=p.join(r,d.path),T=G(m,"utf-8").split(`
19
- `).length;d.additions=T,v+=T}catch{}return{repoRoot:r,branch:n,files:a,linesAdded:v,linesRemoved:l}}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=ie(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(!L(n)||!A(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const i=z("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),a=z("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:i,stagedDiff:a})}catch(i){throw console.error("[git/diff] execFileSync failed:",i?.code,i.message),i}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",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>Q)return e.json({error:"file too large",size:n,maxBytes:Q},413);const i=G(r,"utf-8");return e.json({path:r,content:i,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",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 Ce(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()?(de(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 L(r)?e.json({error:"file already exists"},409):(ce(p.dirname(r),{recursive:!0}),ue(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(Me(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 Fe(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 He(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()},i=S.data.notes.findIndex(a=>a.scope===t);return i>=0?S.data.notes[i]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),q&&(s.use("/a/:agentId/*",o()),Je(s,{getChannel:q.getChannel})),s}export{Pt as buildApp};
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(me({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:u.activeTheme}))}),s.get("/",y(),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",y(),e=>e.html(pe(S.data.notes,u.activeTheme))),s.get("/notes/:session",y(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(he(t,u.activeTheme))}),s.get("/history",y(),e=>{const t=L(),r=new Set(t.map(n=>n.name));return e.html(je(Ie(),u.activeTheme,r))}),s.get("/quick-commands",y(),e=>e.html(we(Y(),u.activeTheme))),s.get("/files",y(),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}=g.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?(g.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}=g.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}=g.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(g.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):g.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",y(),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",y(),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",y(),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/federation",o(),e=>e.json({config:w.getConfig(),status:w.status()})),s.post("/api/federation",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({error:"session is required"},400);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 f of v)if(!f.startsWith(".")&&!(a&&!f.toLowerCase().startsWith(a))){try{const m=p.join(i,f);A(m).isDirectory()?(l.push(m),n&&Ce(m,l,d,0)):d.push(m)}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 f=d[0],m=d[1],T=d.substring(3).trim(),R=m!==" "?m:f;a.push({path:T,status:R,staged:f,unstaged:m,additions:0,deletions:0})}const c=new Map;try{const d=_("git diff --numstat HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3});for(const f of d.split(`
18
+ `)){if(!f.trim())continue;const m=f.split(" ");if(m.length>=3){const T=parseInt(m[0],10)||0,R=parseInt(m[1],10)||0;c.set(m[2],{added:T,deleted:R})}}}catch{}for(const d of a){const f=c.get(d.path);f&&(d.additions=f.added,d.deletions=f.deleted)}let v=0,l=0;for(const d of a)v+=d.additions,l+=d.deletions;for(const d of a)if(d.status==="?"&&d.additions===0)try{const f=p.join(r,d.path),T=$(f,"utf-8").split(`
19
+ `).length;d.additions=T,v+=T}catch{}return{repoRoot:r,branch:n,files:a,linesAdded:v,linesRemoved:l}}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=oe(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 i=G("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),a=G("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:i,stagedDiff:a})}catch(i){throw console.error("[git/diff] execFileSync failed:",i?.code,i.message),i}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",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 i=$(r,"utf-8");return e.json({path:r,content:i,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",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 _e(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()?(ae(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):(ue(p.dirname(r),{recursive:!0}),de(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(Ue(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=U(t);return"error"in r?e.json({error:r.error},400):(await De(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=U(t);return"error"in r?e.json({error:r.error},400):(await Fe(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()},i=S.data.notes.findIndex(a=>a.scope===t);return i>=0?S.data.notes[i]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),q&&(s.use("/a/:agentId/*",o()),Ze(s,{getChannel:q.getChannel})),s}export{Rt as buildApp};
package/dist/lib/cli.js CHANGED
@@ -1,12 +1,10 @@
1
- import{existsSync as w}from"node:fs";import{createRequire as b}from"node:module";import{getDataRoot as d,getSettingsPath as x,getThemePath as l}from"./state-paths.js";import{readSettings as g,writeSettings as E}from"./settings.js";import{readActiveTheme as T,setActiveThemeTemplate as y}from"./theme-store.js";import{isThemeTemplateId as _,THEME_TEMPLATE_IDS as v}from"./themes/index.js";import{getEnvFilePath as m}from"./load-env.js";import{SETUP_FEATURES as h}from"./setup-features.js";import{promptYesNo as A,promptChoice as S,requireTty as k}from"./setup-prompts.js";const M=d(),f=x(),R=b(import.meta.url),{version:N}=R("../../package.json");function $(o){const s={};for(const e of o)switch(e){case"--yes":case"-y":s.yes=!0;break;case"--agents":s.agents=!0;break;case"--no-agents":s.agents=!1;break}return s}function U(o){switch(o){case"agents":return"agents";default:return"agents"}}async function F(o){const s=o[0]==="setup"?o.slice(1):o,e=$(s),n=e.yes||e.agents!==void 0;n||k();const c=await g();console.log(`tmux-weblink setup
1
+ import{existsSync as h}from"node:fs";import{createRequire as p}from"node:module";import{getDataRoot as b,getSettingsPath as w,getThemePath as i}from"./state-paths.js";import{readSettings as d}from"./settings.js";import{readActiveTheme as x,setActiveThemeTemplate as T}from"./theme-store.js";import{isThemeTemplateId as E,THEME_TEMPLATE_IDS as v}from"./themes/index.js";import{getEnvFilePath as l}from"./load-env.js";import{SETUP_FEATURES as m}from"./setup-features.js";import{promptYesNo as _,requireTty as y}from"./setup-prompts.js";const A=b(),u=w(),k=p(import.meta.url),{version:S}=k("../../package.json");function M(t){const o={};for(const e of t)switch(e){case"--yes":case"-y":o.yes=!0;break;case"--agents":o.agents=!0;break;case"--no-agents":o.agents=!1;break}return o}function $(t){switch(t){case"agents":return"agents";default:return"agents"}}async function O(t){const o=t[0]==="setup"?t.slice(1):t,e=M(o);e.yes||e.agents!==void 0||y();const g=await d();console.log(`tmux-weblink setup
2
2
  `),console.log(`Configure optional features.
3
- `);const u=new Map;for(const t of h){const i=U(t.id);let a;if(e.yes)a=!0;else if(e[i]!==void 0)a=e[i];else{const p=t.isEnabled(c);a=await A(`${t.label} (${t.description})`,p)}u.set(t.id,a)}let r;if(!n){const t=c.terminalRenderer==="ghostty"?"ghostty":"xterm";r=await S("Terminal renderer (xterm.js is the default; ghostty-web is experimental)",["xterm","ghostty"],t)}for(const t of h)u.get(t.id)?await t.enable():await t.disable();if(r){const t=await g();await E({...t,terminalRenderer:r}),console.log(`\u2713 terminal renderer set to ${r}`)}console.log(`
4
- Done. Settings: ${f}`),w(m())&&console.log(`Env file: ${m()}`),console.log("Restart tmux-web to apply UI changes.")}async function L(o){const[s,e]=o;switch(s){case"list":{console.log("Available themes:");for(const n of v)console.log(` ${n}`);return}case"set":{e||(console.error("usage: tmux-web theme set <vscode|ghostty>"),process.exit(1)),_(e)||(console.error(`unknown theme: ${e}`),console.error("Run: tmux-web theme list"),process.exit(1)),await y(e),console.log(`\u2713 theme set to ${e}`),console.log(` ${l()}`),console.log("Restart tmux-web to apply.");return}case"show":{const n=await T();console.log(`Active theme: ${n.template}`),console.log(`Config file: ${l()}`);return}default:console.error("usage: tmux-web theme <list|set|show>"),process.exit(1)}}function j(){console.log(N)}function q(){const o=M,s=m();console.log(`tmux-web \u2014 terminal-in-the-browser for tmux
3
+ `);const c=new Map;for(const s of m){const r=$(s.id);let a;if(e.yes)a=!0;else if(e[r]!==void 0)a=e[r];else{const f=s.isEnabled(g);a=await _(`${s.label} (${s.description})`,f)}c.set(s.id,a)}for(const s of m)c.get(s.id)?await s.enable():await s.disable();console.log(`
4
+ Done. Settings: ${u}`),h(l())&&console.log(`Env file: ${l()}`),console.log("Restart tmux-web to apply UI changes.")}async function F(t){const[o,e]=t;switch(o){case"list":{console.log("Available themes:");for(const n of v)console.log(` ${n}`);return}case"set":{e||(console.error("usage: tmux-web theme set <vscode|ghostty>"),process.exit(1)),E(e)||(console.error(`unknown theme: ${e}`),console.error("Run: tmux-web theme list"),process.exit(1)),await T(e),console.log(`\u2713 theme set to ${e}`),console.log(` ${i()}`),console.log("Restart tmux-web to apply.");return}case"show":{const n=await x();console.log(`Active theme: ${n.template}`),console.log(`Config file: ${i()}`);return}default:console.error("usage: tmux-web theme <list|set|show>"),process.exit(1)}}function W(){console.log(S)}function C(){const t=A,o=l();console.log(`tmux-web \u2014 terminal-in-the-browser for tmux
5
5
 
6
6
  Usage:
7
7
  tmux-web Start the server (PORT env var, default 3000)
8
- tmux-web --ghostty Start with ghostty-web instead of xterm.js
9
- tmux-web --xterm Start with xterm.js explicitly
10
8
  tmux-web -V, --version Print version and exit
11
9
  tmux-web -h, --help Show this help
12
10
  tmux-web setup Interactive feature setup
@@ -23,15 +21,14 @@ Usage:
23
21
  /settings/federation to join a hub or to create/revoke agent tokens.
24
22
 
25
23
  Files:
26
- ${f} settings (agents)
27
- ${l()} active theme (shell + terminal colors)
28
- ${s} secrets (loaded automatically)
29
- ${o}/ runtime state
24
+ ${u} settings (agents)
25
+ ${i()} active theme (shell + terminal colors)
26
+ ${o} secrets (loaded automatically)
27
+ ${t}/ runtime state
30
28
 
31
29
  Most of these are also editable from the browser at /settings and /settings/theme.
32
30
 
33
31
  Env:
34
- TMUX_WEB_TERMINAL_RENDERER=xterm|ghostty (also persistable via /settings)
35
32
  TMUX_WEB_AGENT_HUB / TMUX_WEB_AGENT_TOKEN / TMUX_WEB_AGENT_NAME (agent mode)
36
33
  TMUX_WEB_MAX_AGENTS / TMUX_WEB_MAX_AGENT_CONNS / TMUX_WEB_AGENT_OFFLINE_MS
37
- `)}export{F as cmdSetup,L as cmdTheme,q as printUsage,j as printVersion};
34
+ `)}export{O as cmdSetup,F as cmdTheme,C as printUsage,W as printVersion};
@@ -1,12 +1,12 @@
1
- import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_TEMPLATE_IDS as v}from"../themes/index.js";function s(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function p(a,e){return`<!DOCTYPE html>
1
+ import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_TEMPLATE_IDS as v}from"../themes/index.js";function o(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function c(e,a){return`<!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
- <title>${s(a)} - tmux-web</title>
7
+ <title>${o(e)} - tmux-web</title>
8
8
  <style>
9
- ${m(e.shell)}
9
+ ${m(a.shell)}
10
10
  html, body { background: var(--page-bg); color: var(--page-fg); min-height: 100%; font-family: var(--font-sans); }
11
11
  .container { max-width: 680px; margin: 80px auto; padding: 0 24px; }
12
12
  .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; }
@@ -77,7 +77,7 @@ import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_T
77
77
  .num-input { width: 100%; }
78
78
  }
79
79
  </style>
80
- </head>`}function h(a,e){let t="";return a&&(t+='<div class="saved-flash">\u2713 Saved. Restart tmux-web to apply.</div>'),e&&(t+=`<div class="error-flash">${s(e)}</div>`),t}function y(a){const{settings:e,renderer:t,rendererOverridden:i,theme:l,saved:r=!1,error:c}=a,n=e.terminalRenderer??"xterm",o=e.defaultView??"default";return`${p("Settings",l)}
80
+ </head>`}function h(e,a){let r="";return e&&(r+='<div class="saved-flash">\u2713 Saved. Restart tmux-web to apply.</div>'),a&&(r+=`<div class="error-flash">${o(a)}</div>`),r}function y(e){const{settings:a,theme:r,saved:n=!1,error:s}=e,t=a.defaultView??"default";return`${c("Settings",r)}
81
81
  <body>
82
82
  <div class="container">
83
83
  <div class="page-header">
@@ -90,25 +90,15 @@ import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_T
90
90
  <strong>restart the tmux-web process</strong> after making changes for them to take effect.
91
91
  </div>
92
92
 
93
- ${h(r,c)}
93
+ ${h(n,s)}
94
94
 
95
95
  <form method="POST" action="/settings">
96
- <div class="section">
97
- <h2>Terminal library</h2>
98
- <p class="desc">Rendering engine for the terminal view.</p>
99
- <div class="radios">
100
- <label class="row"><input type="radio" name="terminalRenderer" value="xterm" ${n==="xterm"?"checked":""} /> xterm.js (default)</label>
101
- <label class="row"><input type="radio" name="terminalRenderer" value="ghostty" ${n==="ghostty"?"checked":""} /> ghostty-web</label>
102
- </div>
103
- ${i?`<p class="override-note">\u26A0 A CLI flag or <code>TMUX_WEB_TERMINAL_RENDERER</code> env var is currently forcing <strong>${t}</strong>, overriding this setting for the running process.</p>`:""}
104
- </div>
105
-
106
96
  <div class="section">
107
97
  <h2>Default home tab</h2>
108
98
  <p class="desc">Which tab the home page opens on.</p>
109
99
  <div class="radios">
110
- <label class="row"><input type="radio" name="defaultView" value="default" ${o==="default"?"checked":""} /> Default</label>
111
- <label class="row"><input type="radio" name="defaultView" value="recent" ${o==="recent"?"checked":""} /> Last Updated</label>
100
+ <label class="row"><input type="radio" name="defaultView" value="default" ${t==="default"?"checked":""} /> Default</label>
101
+ <label class="row"><input type="radio" name="defaultView" value="recent" ${t==="recent"?"checked":""} /> Last Updated</label>
112
102
  </div>
113
103
  </div>
114
104
 
@@ -130,14 +120,14 @@ import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_T
130
120
  </div>
131
121
  </div>
132
122
  </body>
133
- </html>`}const x={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"},b=["background","foreground","red","green","yellow","blue","magenta","cyan"];function w(a){const{theme:e,saved:t=!1}=a,i=g(),l=v.map(r=>{const c=i[r],n=e.template===r,o=b.map(d=>`<span class="swatch" style="background:${s(c.terminal[d])}" title="${d}"></span>`).join("");return`<label class="theme-card${n?" active":""}">
123
+ </html>`}const x={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"},b=["background","foreground","red","green","yellow","blue","magenta","cyan"];function w(e){const{theme:a,saved:r=!1}=e,n=g(),s=v.map(t=>{const d=n[t],i=a.template===t,p=b.map(l=>`<span class="swatch" style="background:${o(d.terminal[l])}" title="${l}"></span>`).join("");return`<label class="theme-card${i?" active":""}">
134
124
  <div class="tname">
135
- <input type="radio" name="template" value="${r}" ${n?"checked":""} style="accent-color:var(--panel-success)" />
136
- ${s(x[r]??r)}
125
+ <input type="radio" name="template" value="${t}" ${i?"checked":""} style="accent-color:var(--panel-success)" />
126
+ ${o(x[t]??t)}
137
127
  </div>
138
- <div class="swatches">${o}</div>
128
+ <div class="swatches">${p}</div>
139
129
  </label>`}).join(`
140
- `);return`${p("Theme",e)}
130
+ `);return`${c("Theme",a)}
141
131
  <body>
142
132
  <div class="container">
143
133
  <div class="page-header">
@@ -149,11 +139,11 @@ import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_T
149
139
  <strong>Note:</strong> the active theme is applied immediately to new page renders.
150
140
  </div>
151
141
 
152
- ${t?'<div class="saved-flash">\u2713 Theme saved.</div>':""}
142
+ ${r?'<div class="saved-flash">\u2713 Theme saved.</div>':""}
153
143
 
154
144
  <form method="POST" action="/settings/theme">
155
145
  <div class="theme-grid">
156
- ${l}
146
+ ${s}
157
147
  </div>
158
148
  <div class="form-actions" style="margin-top:16px">
159
149
  <button type="submit" class="btn primary">Save theme</button>
@@ -1,4 +1,4 @@
1
- import{cssVarsStyle as c}from"../theme.js";import{newSessionModalCSS as f,newSessionModalHTML as g,newSessionModalScript as x,reducedMotion as b}from"../shared-layout.js";import{icon as i}from"../icons.js";function y(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function o(t="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${t}; outline: none;`}function k(t){const{theme:e,fsRoots:r,terminalCfg:a,renderer:n,scrollback:s,wsBase:p}=t,l=`
1
+ import{cssVarsStyle as d}from"../theme.js";import{newSessionModalCSS as c,newSessionModalHTML as f,newSessionModalScript as g,reducedMotion as x}from"../shared-layout.js";import{icon as i}from"../icons.js";function h(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function o(t="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${t}; outline: none;`}function y(t){const{theme:e,fsRoots:n,terminalCfg:r,scrollback:a,wsBase:s}=t,p=`
2
2
  *, *::before, *::after { box-sizing: border-box; }
3
3
  html, body {
4
4
  background: var(--page-bg); color: var(--page-fg);
@@ -580,8 +580,8 @@ import{cssVarsStyle as c}from"../theme.js";import{newSessionModalCSS as f,newSes
580
580
  .file-roots-list li:hover { border-color: var(--panel-accent); }
581
581
  .file-roots-list li svg { width: 20px; height: 20px; flex-shrink: 0; fill: var(--panel-accent); }
582
582
 
583
- ${b()}
584
- ${f()}
583
+ ${x()}
584
+ ${c()}
585
585
  @media (max-width: 640px) {
586
586
  .app-layout { flex-direction: column; }
587
587
  .sidebar { flex: 0 0 auto; border-right: none; border-bottom: 1px solid var(--panel-border); transition: max-height 0.2s; }
@@ -592,7 +592,7 @@ import{cssVarsStyle as c}from"../theme.js";import{newSessionModalCSS as f,newSes
592
592
  .mobile-keys .mk-buttons { display: none; }
593
593
  .mobile-keys.mk-focused .mk-buttons { display: flex; flex-wrap: wrap; gap: 4px; }
594
594
  }
595
- `,m=JSON.stringify(e.terminal).replace(/</g,"\\u003c"),d=JSON.stringify({terminal:a,scrollback:s,theme:e.terminal,renderer:n,fsRoots:r,wsBase:p||""}).replace(/</g,"\\u003c");return`<!DOCTYPE html>
595
+ `,b=JSON.stringify(e.terminal).replace(/</g,"\\u003c"),l=JSON.stringify({terminal:r,scrollback:a,theme:e.terminal,fsRoots:n,wsBase:s||""}).replace(/</g,"\\u003c");return`<!DOCTYPE html>
596
596
  <html lang="en">
597
597
  <head>
598
598
  <meta charset="UTF-8" />
@@ -605,8 +605,8 @@ import{cssVarsStyle as c}from"../theme.js";import{newSessionModalCSS as f,newSes
605
605
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
606
606
  <title>tmux-weblink</title>
607
607
  <style>
608
- ${c(e.shell)}
609
- ${l}
608
+ ${d(e.shell)}
609
+ ${p}
610
610
  </style>
611
611
  </head>
612
612
  <body>
@@ -701,8 +701,6 @@ import{cssVarsStyle as c}from"../theme.js";import{newSessionModalCSS as f,newSes
701
701
  <div class="popover-backdrop" id="settings-backdrop"></div>
702
702
  <div class="settings-popover" id="settings-popover">
703
703
  <div class="settings-item">
704
- <span>Terminal renderer</span>
705
- <span style="font-size:var(--text-xs);color:var(--panel-muted)">${n==="ghostty"?"Ghostty":"xterm.js"}</span>
706
704
  </div>
707
705
  <hr class="settings-divider" />
708
706
  <div style="padding:4px 0">
@@ -750,10 +748,10 @@ import{cssVarsStyle as c}from"../theme.js";import{newSessionModalCSS as f,newSes
750
748
  })();
751
749
  </script>
752
750
 
753
- ${g()}
751
+ ${f()}
754
752
 
755
753
  <script>
756
- window.__TMUX_WEB_SHELL__ = ${d};
754
+ window.__TMUX_WEB_SHELL__ = ${l};
757
755
  </script>
758
756
  <script type="module">
759
757
  await import('/assets/shell-client.js');
@@ -762,7 +760,7 @@ window.__onSessionCreated = async function(name, agentId) {
762
760
  await window.__refreshSidebar();
763
761
  window.__openSession(name, agentId || undefined);
764
762
  };
765
- ${x("__onSessionCreated")}
763
+ ${g("__onSessionCreated")}
766
764
  </script>
767
765
  <script>
768
766
  // Sync app-layout height to visual viewport so mobile keyboard doesn't overlap
@@ -829,4 +827,4 @@ if ('serviceWorker' in navigator) {
829
827
  }
830
828
  </script>
831
829
  </body>
832
- </html>`}export{k as renderShell};
830
+ </html>`}export{y as renderShell};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {