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 +0 -6
- package/dist/agent.js +1 -1
- package/dist/assets/shell-client.js +29 -31
- package/dist/browser/shell-client.js +7 -7
- package/dist/browser/terminal-core.js +6 -8
- package/dist/index.js +4 -4
- package/dist/lib/build-app.js +6 -6
- package/dist/lib/cli.js +8 -11
- package/dist/lib/pages/settings.js +14 -24
- package/dist/lib/pages/shell.js +10 -12
- package/package.json +1 -1
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
|
|
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
|
|
2
|
-
`)}selectRows(
|
|
3
|
-
`)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}}
|
|
4
|
-
|
|
5
|
-
`)}catch{return
|
|
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
|
|
11
|
-
<span>${f(
|
|
12
|
-
<button class="session-edit-btn" data-session="${f(
|
|
13
|
-
`,
|
|
14
|
-
<span>${f(
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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(
|
|
17
|
-
<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>`;else{let
|
|
18
|
-
${c}<span class="file-path">${f(
|
|
19
|
-
<span class="file-add">${
|
|
20
|
-
<span class="file-del">${
|
|
21
|
-
</div>`}
|
|
22
|
-
`:"")+
|
|
23
|
-
`),
|
|
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(
|
|
23
|
+
<span class="diff-filename">${f(e)}</span>
|
|
26
24
|
<span class="diff-loading">Loading\u2026</span>
|
|
27
|
-
</div></div>`,
|
|
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(
|
|
30
|
-
</div><div class="diff-empty">Error loading diff: ${f(
|
|
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(
|
|
33
|
-
</div><div class="diff-empty">No diff content available</div></div>`;return}let{html:
|
|
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(
|
|
36
|
-
<span class="diff-filesize">${
|
|
37
|
-
</div><div class="diff-content">${
|
|
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(
|
|
47
|
-
`).length;
|
|
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+'">×</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
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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:
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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")&&
|
|
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=
|
|
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=
|
|
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(),
|
|
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+'">×</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=
|
|
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+'">×</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
|
|
2
|
-
`)}selectRows(
|
|
3
|
-
`)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}}
|
|
4
|
-
|
|
5
|
-
`)}catch{return
|
|
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
|
|
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
|
|
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,
|
|
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"),
|
|
7
|
-
Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${i} --name "${o}"`);return}case"list":{const o=
|
|
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)}}
|
package/dist/lib/build-app.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{existsSync as
|
|
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
|
|
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
|
|
18
|
-
`)){if(!
|
|
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=
|
|
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
|
|
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
|
|
4
|
-
Done. Settings: ${
|
|
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
|
-
${
|
|
27
|
-
${
|
|
28
|
-
${
|
|
29
|
-
${
|
|
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{
|
|
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
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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>${
|
|
7
|
+
<title>${o(e)} - tmux-web</title>
|
|
8
8
|
<style>
|
|
9
|
-
${m(
|
|
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
|
|
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(
|
|
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" ${
|
|
111
|
-
<label class="row"><input type="radio" name="defaultView" value="recent" ${
|
|
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(
|
|
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="${
|
|
136
|
-
${
|
|
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">${
|
|
128
|
+
<div class="swatches">${p}</div>
|
|
139
129
|
</label>`}).join(`
|
|
140
|
-
`);return`${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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>
|
package/dist/lib/pages/shell.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cssVarsStyle as
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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
|
-
${
|
|
584
|
-
${
|
|
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
|
-
`,
|
|
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
|
-
${
|
|
609
|
-
${
|
|
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
|
-
${
|
|
751
|
+
${f()}
|
|
754
752
|
|
|
755
753
|
<script>
|
|
756
|
-
window.__TMUX_WEB_SHELL__ = ${
|
|
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
|
-
${
|
|
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{
|
|
830
|
+
</html>`}export{y as renderShell};
|