tmux-weblink 2.0.33 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # tmux-weblink
2
2
 
3
+ **English** | [中文](README.zh-CN.md)
4
+
3
5
  Access your tmux sessions from the browser. A lightweight web server that lists running tmux sessions and lets you attach through a full terminal in your browser — with built-in notes and a sidebar.
4
6
 
5
7
  ## Install
@@ -0,0 +1,112 @@
1
+ # tmux-weblink
2
+
3
+ [English](README.md) | **中文**
4
+
5
+ 在浏览器里访问你的 tmux 会话。一个轻量级 Web 服务器:列出正在运行的 tmux 会话,并让你通过浏览器里的完整终端接入——内置笔记与侧边栏,移动端与桌面端都能用。
6
+
7
+ ## 特性
8
+
9
+ - **完整终端**:xterm.js 渲染,支持 256 色、复制粘贴、粘贴图片到 CLI 工具
10
+ - **触摸优化**:移动端惯性滚动、Shift+拖选松手自动复制、虚拟键盘工具栏
11
+ - **PWA**:手机"添加到主屏幕"、桌面浏览器"安装应用"即为独立窗口应用
12
+ - **侧边栏会话分组**:按工作目录自动聚类(显示文件夹名),可折叠
13
+ - **New Session 目录树选择器**:层层展开目录树,直接定位到当前会话所在路径
14
+ - **笔记(Notes)**:每个会话独立的 Markdown 便签
15
+ - **文件浏览/编辑(Files)**:浏览、编辑本地文件(默认仅本机,路径受限)
16
+ - **Machines 联邦**:把 NAT 后的机器加入公共 hub(hub 无需开放端口),多机终端矩阵
17
+ - **窗口抽屉**:在终端头部切换 tmux 窗口(移动端友好的标签选择器)
18
+ - **安全**:scrypt 哈希 token 认证、登录限流、审计日志
19
+
20
+ ## 安装
21
+
22
+ ```bash
23
+ npm install -g tmux-weblink
24
+ ```
25
+
26
+ 或者直接用 npx 运行:
27
+
28
+ ```bash
29
+ npx tmux-weblink
30
+ ```
31
+
32
+ ## 使用
33
+
34
+ ```bash
35
+ # 交互式初始化(创建 token 等)
36
+ tmux-weblink setup
37
+
38
+ # 默认端口 21000 启动
39
+ tmux-weblink
40
+
41
+ # 自定义端口
42
+ PORT=8080 tmux-weblink
43
+
44
+ # 可选:先读尾部缓冲再加载历史(见 docs/architecture.md)
45
+ TMUX_WEB_INITIAL_LINES=1000 TMUX_WEB_HISTORY_CHUNK=500 tmux-weblink
46
+
47
+ # Machines:把本机加入一个 hub(无需开放端口)
48
+ tmux-weblink agent --hub wss://hub.example.com --token <agent-token> --name laptop
49
+ ```
50
+
51
+ > **每台机器都用同一条启动命令。** 加入 hub:在要接入的机器上打开
52
+ > `http://localhost:21000/settings/machines`,粘贴 hub 地址 + token,点
53
+ > **Save & Connect**——agent 客户端与普通服务器同进程运行。hub 自己的
54
+ > `/settings/machines` 页面负责创建/吊销 token。
55
+
56
+ 然后浏览器打开 `http://localhost:21000`,你会看到活跃的 tmux 会话列表——点击即可接入。
57
+
58
+ ## 手机端 / PWA
59
+
60
+ - **iOS**:Safari 分享 → "添加到主屏幕",全屏独立运行
61
+ - **Android / Chrome**:菜单 → "添加到主屏幕" / "安装应用"
62
+ - 支持软键盘自动适配、触摸惯性滚动、拖选复制
63
+
64
+ ## 桌面端(Linux / Windows / macOS)
65
+
66
+ 桌面浏览器(Chrome / Edge / Chromium)地址栏右侧点"安装"图标即可生成**独立窗口应用**:
67
+
68
+ - 本机 `http://localhost:端口` 或 HTTPS 均可;内网 HTTP IP 不会出现安装提示(非 secure context)
69
+ - Windows 上服务端原生运行受限(node-pty 需 WSL2),可用 WSL2 或接入远程 hub 使用
70
+
71
+ ## 文档
72
+
73
+ - [文档中心](docs/index.md)
74
+ - [笔记](docs/notes.md) — 每个会话独立的 Markdown 便签
75
+ - [文件](docs/files.md) — 浏览/编辑本地文件
76
+ - [Machines / Agents](docs/agents.md) — 把 NAT 机器接入公共 hub
77
+ - [架构](docs/architecture.md) — 服务器、终端、侧边栏如何协作
78
+
79
+ ## 环境变量(常用)
80
+
81
+ | 变量 | 默认 | 说明 |
82
+ | --- | --- | --- |
83
+ | `PORT` | `21000` | HTTP 端口 |
84
+ | `TMUX_WEB_MODE` | `production` | `development` 用 dev 数据目录 |
85
+ | `TMUX_WEB_INITIAL_LINES` | — | 打开会话时先读尾部 N 行 |
86
+ | `TMUX_WEB_HISTORY_CHUNK` | — | 历史分批加载的块大小 |
87
+ | `TMUX_WEB_AGENT_HUB` / `TMUX_WEB_AGENT_TOKEN` / `TMUX_WEB_AGENT_NAME` | — | agent 连接参数 |
88
+ | `FS_ROOTS` | `~` | 文件浏览可访问的根目录(逗号分隔) |
89
+
90
+ ## 前提条件
91
+
92
+ - **Node.js** >= 22
93
+ - **tmux** 已安装且在 PATH 中
94
+ - 可写的 `~/.tmux-web/` 和 `~/.config/tmux-web/`(dev 模式路径见 [docs](docs/index.md))
95
+
96
+ ## Windows 说明
97
+
98
+ - 服务端在 Windows 上原生运行受限(node-pty 在 Windows 无 linux-x64 类 prebuild,需源码编译;推荐 **WSL2** 内运行服务)
99
+ - 浏览器客户端(PWA / 网页)在 Windows 上完全正常,可接入本机 WSL2 或远程 hub 的会话
100
+
101
+ ## 致谢
102
+
103
+ 本项目建立在以下项目的想法和代码之上:
104
+
105
+ - [tmux-web](https://github.com/ashutoshpw/tmux-web) by [@ashutoshpw](https://github.com/ashutoshpw)
106
+ - [persalink](https://github.com/brobata/persalink) by [@brobata](https://github.com/brobata)
107
+
108
+ 它是这些实验的延续与重新打包,目标是做一个单用户、浏览器优先、易于安装运行的 tmux 伴侣。
109
+
110
+ ## 许可证
111
+
112
+ MIT
@@ -1,38 +1,38 @@
1
- var Zt=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function je(e){return e.replace(/\x1b\[\?1049[hl]/g,"")}function oe(e){return e.replace(Zt,"")}var ke;function Gt(){return ke||(document.querySelector("link[data-tmux-web-xterm-css]")?ke=Promise.resolve():(ke=new Promise(e=>{let t=document.createElement("link");t.rel="stylesheet",t.href="/assets/xterm.css",t.setAttribute("data-tmux-web-xterm-css",""),t.addEventListener("load",()=>e(),{once:!0}),t.addEventListener("error",()=>e(),{once:!0}),document.head.appendChild(t)}),ke))}var Qt=class Ct{terminal;fitAddon;container;constructor(t,n,s){this.container=t,this.terminal=n,this.fitAddon=s,this.terminal.loadAddon(this.fitAddon),this.terminal.open(t)}static async create(t,n,s){let[{Terminal:o},{FitAddon:r}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),Gt()]),c=new o({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,smoothScrollDuration:120,theme:s});return new Ct(t,c,new r)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(t,n=!1){this.terminal.scrollToLine(Math.max(0,t),n)}scrollLines(t){let n=this.terminal.buffer?.active;this.terminal.scrollLines(t)}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){let t=en(this.container);if(t.width<=0||t.height<=0)return!1;let n=this.terminal._core?._renderService?.dimensions?.css?.cell;if(!n?.width||!n?.height)return this.fitAddon.fit(),!0;let s=n.width,o=n.height,r=this.terminal.element?.querySelector(".xterm-viewport"),c=r?Math.max(0,r.offsetWidth-r.clientWidth):0,l=Math.max(2,Math.floor((t.width-c)/s)),u=Math.max(2,Math.floor(t.height/o));return(l!==this.terminal.cols||u!==this.terminal.rows)&&this.terminal.resize(l,u),!0}focus(){this.terminal.focus()}pasteText(t){this.terminal.paste(t)}input(t){this.terminal.input(t,!1)}onData(t){this.terminal.onData(t),this.terminal.onBinary(t)}onResize(t){this.terminal.onResize(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){let t=document.activeElement;return t?t===this.container||t===this.terminal.textarea||this.container.contains(t)||!!t.closest?.(".xterm"):!1}getScreenText(){return this.getRowsText(0,this.terminal.rows-1)}getRowsText(t,n){let s=this.terminal.buffer.active,[o,r]=t<=n?[t,n]:[n,t],c=s.viewportY,l=[];for(let u=o;u<=r;u++){let p=s.getLine(c+u);l.push(p?p.translateToString(!0):"")}return l.join(`
2
- `)}selectRows(t,n){let s=this.terminal.buffer.active,[o,r]=t<=n?[t,n]:[n,t];this.terminal.selectLines(s.viewportY+o,s.viewportY+r)}selectCells(t,n,s,o){let r=this.terminal.buffer.active,c=Math.min(n,o),l=Math.max(n,o);if(c===l){let u=Math.min(t,s),p=Math.max(t,s);this.terminal.select(u,r.viewportY+c,p-u+1)}else this.terminal.selectLines(r.viewportY+c,r.viewportY+l)}getCellsText(t,n,s,o){let r=this.terminal.buffer.active,c=Math.min(n,o),l=Math.max(n,o),u=n<o?t:n>o?s:Math.min(t,s),p=n<o?s:n>o?t:Math.max(t,s),S=r.viewportY,k=[];for(let H=c;H<=l;H++){let ve=r.getLine(S+H);if(!ve){k.push("");continue}let O=H===c?u:0,Ne=H===l?p:this.terminal.cols-1,K="";for(let C=O;C<=Ne;C++){let z=ve.getCell(C);z&&(K+=z.getChars())}k.push(K.replace(/\s+$/,""))}return k.join(`
3
- `)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}};function en(e){let t=e.getBoundingClientRect(),n=window.visualViewport;return n?{width:Math.min(t.width,n.width),height:Math.max(0,Math.min(t.height,n.height-Math.max(0,t.top)))}:t}function tn(e,t,n){return e.innerHTML="",e.classList.add("terminal-pending"),(async()=>{let s=await Qt.create(e,n.scrollback,n.theme),o,r=0,c,l=null,u=0,p=0,S=.2,k=10,H=.95,ve=.04;function O(){u&&(cancelAnimationFrame(u),u=0),p=0}function Ne(i){if(O(),Math.abs(i)<S)return;p=Math.max(-k,Math.min(k,i));let a=()=>{if(N){u=0;return}let d=e.querySelector(".xterm-rows > div"),f=d?d.getBoundingClientRect().height:16,b=f>0?Math.round(p*16/f):p>0?1:-1;b===0&&(b=p>0?1:-1);let I=s.viewportY()-b,W=s.baseY(),ae=Math.max(0,Math.min(I,W));if(s.scrollToLine(ae,!0),p*=H,ae!==I){u=0;return}if(Math.abs(p)<ve){u=0;return}u=requestAnimationFrame(a)};u=requestAnimationFrame(a)}let K=0,C="connecting",z=0,G=!1,ge=[],V="",N=!1,Q=1e3,Mt=location.protocol==="https:"?"wss:":"ws:",Tt=n.wsBase||"/ws",St=Mt+"//"+location.host+Tt+"/"+encodeURIComponent(t),Bt="/api/session/"+encodeURIComponent(t)+"/upload",It=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function Dt(){return ge.join("")+V}async function Ht(i,a=0){let d=i?s.viewportY():0,f=oe(Dt());if(s.reset(),!f){i||s.scrollToBottom();return}await s.write(f),i?s.scrollToLine(a+d):s.scrollToBottom()}function j(i){o?.readyState===WebSocket.OPEN&&o.send(JSON.stringify(i))}function At(i){let a;try{a=JSON.parse(i)}catch{C==="live"&&(V+=i,s.write(oe(i)));return}if(a.type==="auth.required"){let d=localStorage.getItem("tmux-web-token");d?j({type:"auth.token",token:d}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(a.type==="attach_failed"&&typeof a.message=="string"){ye=!0,C="dead",s.reset(),s.write(`\r
1
+ var an=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function Ue(e){return e.replace(/\x1b\[\?1049[hl]/g,"")}function oe(e){return e.replace(an,"")}var Ce;function on(){return Ce||(document.querySelector("link[data-tmux-web-xterm-css]")?Ce=Promise.resolve():(Ce=new Promise(e=>{let t=document.createElement("link");t.rel="stylesheet",t.href="/assets/xterm.css",t.setAttribute("data-tmux-web-xterm-css",""),t.addEventListener("load",()=>e(),{once:!0}),t.addEventListener("error",()=>e(),{once:!0}),document.head.appendChild(t)}),Ce))}var ln=class Ht{terminal;fitAddon;container;constructor(t,n,s){this.container=t,this.terminal=n,this.fitAddon=s,this.terminal.loadAddon(this.fitAddon),this.terminal.open(t)}static async create(t,n,s){let[{Terminal:o},{FitAddon:r}]=await Promise.all([import("./xterm-BXEES47M.js"),import("./addon-fit-D5N335P2.js"),on()]),c=new o({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,smoothScrollDuration:120,theme:s});return new Ht(t,c,new r)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(t,n=!1){this.terminal.scrollToLine(Math.max(0,t),n)}scrollLines(t){let n=this.terminal.buffer?.active;this.terminal.scrollLines(t)}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){let t=rn(this.container);if(t.width<=0||t.height<=0)return!1;let n=this.terminal._core?._renderService?.dimensions?.css?.cell;if(!n?.width||!n?.height)return this.fitAddon.fit(),!0;let s=n.width,o=n.height,r=this.terminal.element?.querySelector(".xterm-viewport"),c=r?Math.max(0,r.offsetWidth-r.clientWidth):0,l=Math.max(2,Math.floor((t.width-c)/s)),u=Math.max(2,Math.floor(t.height/o));return(l!==this.terminal.cols||u!==this.terminal.rows)&&this.terminal.resize(l,u),!0}focus(){this.terminal.focus()}pasteText(t){this.terminal.paste(t)}input(t){this.terminal.input(t,!1)}onData(t){this.terminal.onData(t),this.terminal.onBinary(t)}onResize(t){this.terminal.onResize(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){let t=document.activeElement;return t?t===this.container||t===this.terminal.textarea||this.container.contains(t)||!!t.closest?.(".xterm"):!1}getScreenText(){return this.getRowsText(0,this.terminal.rows-1)}getRowsText(t,n){let s=this.terminal.buffer.active,[o,r]=t<=n?[t,n]:[n,t],c=s.viewportY,l=[];for(let u=o;u<=r;u++){let p=s.getLine(c+u);l.push(p?p.translateToString(!0):"")}return l.join(`
2
+ `)}selectRows(t,n){let s=this.terminal.buffer.active,[o,r]=t<=n?[t,n]:[n,t];this.terminal.selectLines(s.viewportY+o,s.viewportY+r)}selectCells(t,n,s,o){let r=this.terminal.buffer.active,c=Math.min(n,o),l=Math.max(n,o);if(c===l){let u=Math.min(t,s),p=Math.max(t,s);this.terminal.select(u,r.viewportY+c,p-u+1)}else this.terminal.selectLines(r.viewportY+c,r.viewportY+l)}getCellsText(t,n,s,o){let r=this.terminal.buffer.active,c=Math.min(n,o),l=Math.max(n,o),u=n<o?t:n>o?s:Math.min(t,s),p=n<o?s:n>o?t:Math.max(t,s),S=r.viewportY,x=[];for(let H=c;H<=l;H++){let ge=r.getLine(S+H);if(!ge){x.push("");continue}let O=H===c?u:0,je=H===l?p:this.terminal.cols-1,K="";for(let C=O;C<=je;C++){let j=ge.getCell(C);j&&(K+=j.getChars())}x.push(K.replace(/\s+$/,""))}return x.join(`
3
+ `)}getSelection(){return this.terminal.getSelection()}clearSelection(){this.terminal.clearSelection()}hasSelectionSupport(){return!0}};function rn(e){let t=e.getBoundingClientRect(),n=window.visualViewport;return n?{width:Math.min(t.width,n.width),height:Math.max(0,Math.min(t.height,n.height-Math.max(0,t.top)))}:t}function cn(e,t,n){return e.innerHTML="",e.classList.add("terminal-pending"),(async()=>{let s=await ln.create(e,n.scrollback,n.theme),o,r=0,c,l=null,u=0,p=0,S=.2,x=10,H=.95,ge=.04;function O(){u&&(cancelAnimationFrame(u),u=0),p=0}function je(i){if(O(),Math.abs(i)<S)return;p=Math.max(-x,Math.min(x,i));let a=()=>{if(N){u=0;return}let d=e.querySelector(".xterm-rows > div"),f=d?d.getBoundingClientRect().height:16,b=f>0?Math.round(p*16/f):p>0?1:-1;b===0&&(b=p>0?1:-1);let B=s.viewportY()-b,F=s.baseY(),ae=Math.max(0,Math.min(B,F));if(s.scrollToLine(ae,!0),p*=H,ae!==B){u=0;return}if(Math.abs(p)<ge){u=0;return}u=requestAnimationFrame(a)};u=requestAnimationFrame(a)}let K=0,C="connecting",j=0,G=!1,ye=[],V="",N=!1,Q=1e3,At=location.protocol==="https:"?"wss:":"ws:",Rt=n.wsBase||"/ws",$t=At+"//"+location.host+Rt+"/"+encodeURIComponent(t),Nt="/api/session/"+encodeURIComponent(t)+"/upload",Pt=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function _t(){return ye.join("")+V}async function jt(i,a=0){let d=i?s.viewportY():0,f=oe(_t());if(s.reset(),!f){i||s.scrollToBottom();return}await s.write(f),i?s.scrollToLine(a+d):s.scrollToBottom()}function z(i){o?.readyState===WebSocket.OPEN&&o.send(JSON.stringify(i))}function zt(i){let a;try{a=JSON.parse(i)}catch{C==="live"&&(V+=i,s.write(oe(i)));return}if(a.type==="auth.required"){let d=localStorage.getItem("tmux-web-token");d?z({type:"auth.token",token:d}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(a.type==="attach_failed"&&typeof a.message=="string"){we=!0,C="dead",s.reset(),s.write(`\r
4
4
  \x1B[31m`+oe(a.message)+`\x1B[0m\r
5
- `).then(()=>{be();try{o?.close()}catch{}});return}if(a.type==="auth.ok"){a.token&&localStorage.setItem("tmux-web-token",a.token),Q=1e3,F(),j({type:"resize",cols:s.cols,rows:s.rows}),M();return}if(a.type==="auth.failed"){if(a.permanentLock||a.retryAfterMs&&a.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(a.message);return}return}if(a.type==="snapshot"&&typeof a.data=="string"){let d=je(oe(a.data));ge=[d],V="",z=typeof a.lines=="number"?a.lines:n.terminal.initialLines,C="live",s.reset(),F(),j({type:"resize",cols:s.cols,rows:s.rows}),s.write(d).then(()=>s.scrollToBottom());return}if(a.type==="window_changed"&&typeof a.activeIndex=="number"){let d=new URL(location.href);d.searchParams.get("window")!==String(a.activeIndex)&&(d.searchParams.set("window",String(a.activeIndex)),history.replaceState(history.state,"",d)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:a.windows}));return}if(a.type==="history"&&typeof a.data=="string"){a.lines>0&&a.data?(ge.unshift(je(a.data)),z+=a.lines,Ht(!0,a.lines).finally(()=>{G=!1})):G=!1;return}if(a.type==="data"&&typeof a.data=="string"){let d=je(oe(a.data));if(C==="connecting"){C="live",V=d,s.write(d);return}C==="live"&&(V+=d,s.write(d))}}let ye=!1;function Ve(){if(ye)return;let i=St,a=localStorage.getItem("tmux-web-token");a&&(i+=(i.includes("?")?"&":"?")+"token="+encodeURIComponent(a)),i+=(i.includes("?")?"&":"?")+"cols="+s.cols+"&rows="+s.rows,o=new WebSocket(i),o.onopen=()=>{C="connecting",z=0,G=!1,ge=[],V="",s.reset(),Q=1e3},o.onmessage=d=>{typeof d.data=="string"&&At(d.data)},o.onclose=()=>{N||(C="connecting",!ye&&setTimeout(()=>{Q=Math.min(Q*2,1e4),!N&&!ye&&Ve()},Q))},o.onerror=()=>o?.close()}function we(i){j({type:"input",data:i})}function F(){return s.fit()}function be(){e.classList.remove("terminal-pending")}function M(){r&&cancelAnimationFrame(r),c&&clearTimeout(c),F(),r=requestAnimationFrame(()=>{r=0,F()&&be()}),c=setTimeout(()=>{F()&&be()},120)}function Rt(){M(),setTimeout(M,50),setTimeout(M,150),setTimeout(M,300)}function Xe(i){return typeof i=="string"&&i.startsWith("image/")}function Je(i){return i?.files?.length?i.files[0]??null:null}function $t(i){if(!i?.items)return null;for(let a=0;a<i.items.length;a++){let d=i.items[a];if(d?.kind==="file"&&Xe(d.type))return d.getAsFile()}return null}async function Nt(i){let a=new FormData,d=i instanceof File&&i.name?i.name:"upload";a.append("file",i,d);let f=localStorage.getItem("tmux-web-token"),b={};f&&(b.Authorization="Bearer "+f);let I=await fetch(Bt,{method:"POST",body:a,headers:b});if(!I.ok){let ae="upload failed";try{let st=await I.json();st.error&&(ae=st.error)}catch{}throw new Error(ae)}let W=await I.json();if(!W.path)throw new Error("no path in response");return W.path}async function Pe(i){try{let a=await Nt(i);we(a)}catch{}}s.onData(i=>we(i)),s.onResize(({cols:i,rows:a})=>j({type:"resize",cols:i,rows:a})),s.onScroll(()=>{C!=="live"||G||!s.isNearScrollbackTop()||(G=!0,j({type:"load_history",before:z}))});let ee=!1,te=!1,w=null,L=null,ne=document.createElement("div");ne.className="terminal-copy-banner",ne.innerHTML='<strong>Copy mode</strong><span class="hint">drag to copy \xB7 Esc exits</span>',ne.hidden=!0,e.appendChild(ne);let R=document.createElement("div");R.className="terminal-copy-banner terminal-copy-toast",R.textContent="Copied",R.hidden=!0,e.appendChild(R);function Ze(i,a){let d=e.querySelector(".xterm-screen, canvas"),f=(d&&d.getBoundingClientRect().width>0?d:e).getBoundingClientRect(),b=f.height>0&&s.rows>0?f.height/s.rows:18,I=f.width>0&&s.cols>0?f.width/s.cols:9;return{row:Math.max(0,Math.min(s.rows-1,Math.floor((a-f.top)/b))),col:Math.max(0,Math.min(s.cols-1,Math.floor((i-f.left)/I)))}}async function Le(i){if(!i)return;try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(i);return}}catch{}let a=document.createElement("textarea");a.value=i,a.style.position="fixed",a.style.opacity="0",a.style.pointerEvents="none",document.body.appendChild(a),a.focus(),a.select();try{document.execCommand("copy")}catch{}a.remove()}let se;function Ee(){R.hidden=!1,se&&clearTimeout(se),se=setTimeout(()=>{R.hidden=!0},1e3)}function X(i){ee=i,ne.hidden=!i,e.classList.toggle("terminal-copy-mode-active",i),i&&s.focus()}function Ge(){X(!ee)}function Pt(i){N||i.button!==0||te||(ee||i.shiftKey)&&(i.preventDefault(),i.stopPropagation(),te=!0,w=Ze(i.clientX,i.clientY),L=w,s.selectCells(w.col,w.row,L.col,L.row),document.addEventListener("mousemove",_e,!0),document.addEventListener("mouseup",ze,!0))}function _e(i){N||!te||!w||(i.preventDefault(),i.stopPropagation(),L=Ze(i.clientX,i.clientY),s.selectCells(w.col,w.row,L.col,L.row))}function ze(i){if(N||!te||!w||!L)return;i.preventDefault(),i.stopPropagation(),document.removeEventListener("mousemove",_e,!0),document.removeEventListener("mouseup",ze,!0),te=!1;let a=s.getCellsText(w.col,w.row,L.col,L.row);w=null,L=null,s.clearSelection(),X(!1),a&&(R.textContent="\u5DF2\u590D\u5236",Le(a).then(Ee)),s.focus()}e.addEventListener("mousedown",Pt,!0),document.addEventListener("keydown",_t,!0);function _t(i){if(!(N||!s.isFocused()||i.defaultPrevented||i.isComposing)){if(ee){if(i.key==="Escape"){i.preventDefault(),i.stopPropagation(),w=null,L=null,s.clearSelection(),X(!1);return}if(i.key==="Enter"||(i.ctrlKey||i.metaKey)&&i.code==="KeyC"){i.preventDefault(),i.stopPropagation(),Le(s.getScreenText()).then(Ee),X(!1);return}}if(i.altKey&&i.code==="KeyC"&&!i.ctrlKey&&!i.metaKey){if(i.preventDefault(),i.stopPropagation(),w&&L){let a=s.getCellsText(w.col,w.row,L.col,L.row);w=null,L=null,s.clearSelection(),X(!1),a&&(R.textContent="Copied",Le(a).then(Ee))}else{let a=s.getSelection();a?(s.clearSelection(),R.textContent="Copied",Le(a).then(Ee)):Ge()}return}if((i.ctrlKey||i.metaKey)&&i.code==="KeyV"){i.preventDefault(),i.stopPropagation(),(async()=>{try{if(navigator.clipboard?.read){let a=await navigator.clipboard.read();for(let d of a)for(let f of d.types)if(Xe(f)){let b=await d.getType(f);await Pe(b);return}}}catch{}try{let a=await navigator.clipboard?.readText();a&&s.pasteText(Qe(a))}catch{}})();return}}}let xe=0;function Qe(i){return i.split(`\r
5
+ `).then(()=>{Le();try{o?.close()}catch{}});return}if(a.type==="auth.ok"){a.token&&localStorage.setItem("tmux-web-token",a.token),Q=1e3,W(),z({type:"resize",cols:s.cols,rows:s.rows}),M();return}if(a.type==="auth.failed"){if(a.permanentLock||a.retryAfterMs&&a.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(a.message);return}return}if(a.type==="snapshot"&&typeof a.data=="string"){let d=Ue(oe(a.data));ye=[d],V="",j=typeof a.lines=="number"?a.lines:n.terminal.initialLines,C="live",s.reset(),W(),z({type:"resize",cols:s.cols,rows:s.rows}),s.write(d).then(()=>s.scrollToBottom());return}if(a.type==="window_changed"&&typeof a.activeIndex=="number"){let d=new URL(location.href);d.searchParams.get("window")!==String(a.activeIndex)&&(d.searchParams.set("window",String(a.activeIndex)),history.replaceState(history.state,"",d)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:a.windows}));return}if(a.type==="history"&&typeof a.data=="string"){a.lines>0&&a.data?(ye.unshift(Ue(a.data)),j+=a.lines,jt(!0,a.lines).finally(()=>{G=!1})):G=!1;return}if(a.type==="data"&&typeof a.data=="string"){let d=Ue(oe(a.data));if(C==="connecting"){C="live",V=d,s.write(d);return}C==="live"&&(V+=d,s.write(d))}}let we=!1;function Ge(){if(we)return;let i=$t,a=localStorage.getItem("tmux-web-token");a&&(i+=(i.includes("?")?"&":"?")+"token="+encodeURIComponent(a)),i+=(i.includes("?")?"&":"?")+"cols="+s.cols+"&rows="+s.rows,o=new WebSocket(i),o.onopen=()=>{C="connecting",j=0,G=!1,ye=[],V="",s.reset(),Q=1e3},o.onmessage=d=>{typeof d.data=="string"&&zt(d.data)},o.onclose=()=>{N||(C="connecting",!we&&setTimeout(()=>{Q=Math.min(Q*2,1e4),!N&&!we&&Ge()},Q))},o.onerror=()=>o?.close()}function be(i){z({type:"input",data:i})}function W(){return s.fit()}function Le(){e.classList.remove("terminal-pending")}function M(){r&&cancelAnimationFrame(r),c&&clearTimeout(c),W(),r=requestAnimationFrame(()=>{r=0,W()&&Le()}),c=setTimeout(()=>{W()&&Le()},120)}function Wt(){M(),setTimeout(M,50),setTimeout(M,150),setTimeout(M,300)}function Qe(i){return typeof i=="string"&&i.startsWith("image/")}function et(i){return i?.files?.length?i.files[0]??null:null}function Ft(i){if(!i?.items)return null;for(let a=0;a<i.items.length;a++){let d=i.items[a];if(d?.kind==="file"&&Qe(d.type))return d.getAsFile()}return null}async function Ut(i){let a=new FormData,d=i instanceof File&&i.name?i.name:"upload";a.append("file",i,d);let f=localStorage.getItem("tmux-web-token"),b={};f&&(b.Authorization="Bearer "+f);let B=await fetch(Nt,{method:"POST",body:a,headers:b});if(!B.ok){let ae="upload failed";try{let lt=await B.json();lt.error&&(ae=lt.error)}catch{}throw new Error(ae)}let F=await B.json();if(!F.path)throw new Error("no path in response");return F.path}async function ze(i){try{let a=await Ut(i);be(a)}catch{}}s.onData(i=>be(i)),s.onResize(({cols:i,rows:a})=>z({type:"resize",cols:i,rows:a})),s.onScroll(()=>{C!=="live"||G||!s.isNearScrollbackTop()||(G=!0,z({type:"load_history",before:j}))});let ee=!1,te=!1,w=null,L=null,ne=document.createElement("div");ne.className="terminal-copy-banner",ne.innerHTML='<strong>Copy mode</strong><span class="hint">drag to copy \xB7 Esc exits</span>',ne.hidden=!0,e.appendChild(ne);let R=document.createElement("div");R.className="terminal-copy-banner terminal-copy-toast",R.textContent="Copied",R.hidden=!0,e.appendChild(R);function tt(i,a){let d=e.querySelector(".xterm-screen, canvas"),f=(d&&d.getBoundingClientRect().width>0?d:e).getBoundingClientRect(),b=f.height>0&&s.rows>0?f.height/s.rows:18,B=f.width>0&&s.cols>0?f.width/s.cols:9;return{row:Math.max(0,Math.min(s.rows-1,Math.floor((a-f.top)/b))),col:Math.max(0,Math.min(s.cols-1,Math.floor((i-f.left)/B)))}}async function Ee(i){if(!i)return;try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(i);return}}catch{}let a=document.createElement("textarea");a.value=i,a.style.position="fixed",a.style.opacity="0",a.style.pointerEvents="none",document.body.appendChild(a),a.focus(),a.select();try{document.execCommand("copy")}catch{}a.remove()}let se;function ke(){R.hidden=!1,se&&clearTimeout(se),se=setTimeout(()=>{R.hidden=!0},1e3)}function J(i){ee=i,ne.hidden=!i,e.classList.toggle("terminal-copy-mode-active",i),i&&s.focus()}function nt(){J(!ee)}function Yt(i){N||i.button!==0||te||(ee||i.shiftKey)&&(i.preventDefault(),i.stopPropagation(),te=!0,w=tt(i.clientX,i.clientY),L=w,s.selectCells(w.col,w.row,L.col,L.row),document.addEventListener("mousemove",We,!0),document.addEventListener("mouseup",Fe,!0))}function We(i){N||!te||!w||(i.preventDefault(),i.stopPropagation(),L=tt(i.clientX,i.clientY),s.selectCells(w.col,w.row,L.col,L.row))}function Fe(i){if(N||!te||!w||!L)return;i.preventDefault(),i.stopPropagation(),document.removeEventListener("mousemove",We,!0),document.removeEventListener("mouseup",Fe,!0),te=!1;let a=s.getCellsText(w.col,w.row,L.col,L.row);w=null,L=null,s.clearSelection(),J(!1),a&&(R.textContent="\u5DF2\u590D\u5236",Ee(a).then(ke)),s.focus()}e.addEventListener("mousedown",Yt,!0),document.addEventListener("keydown",qt,!0);function qt(i){if(!(N||!s.isFocused()||i.defaultPrevented||i.isComposing)){if(ee){if(i.key==="Escape"){i.preventDefault(),i.stopPropagation(),w=null,L=null,s.clearSelection(),J(!1);return}if(i.key==="Enter"||(i.ctrlKey||i.metaKey)&&i.code==="KeyC"){i.preventDefault(),i.stopPropagation(),Ee(s.getScreenText()).then(ke),J(!1);return}}if(i.altKey&&i.code==="KeyC"&&!i.ctrlKey&&!i.metaKey){if(i.preventDefault(),i.stopPropagation(),w&&L){let a=s.getCellsText(w.col,w.row,L.col,L.row);w=null,L=null,s.clearSelection(),J(!1),a&&(R.textContent="Copied",Ee(a).then(ke))}else{let a=s.getSelection();a?(s.clearSelection(),R.textContent="Copied",Ee(a).then(ke)):nt()}return}if((i.ctrlKey||i.metaKey)&&i.code==="KeyV"){i.preventDefault(),i.stopPropagation(),(async()=>{try{if(navigator.clipboard?.read){let a=await navigator.clipboard.read();for(let d of a)for(let f of d.types)if(Qe(f)){let b=await d.getType(f);await ze(b);return}}}catch{}try{let a=await navigator.clipboard?.readText();a&&s.pasteText(st(a))}catch{}})();return}}}let xe=0;function st(i){return i.split(`\r
6
6
  `).join(`
7
7
  `).split(`
8
- `).join("\r")}async function zt(i){let a=$t(i.clipboardData);if(a){i.preventDefault(),i.stopPropagation();let b=Date.now();if(b-xe<50)return;xe=b,await Pe(a);return}let d=i.clipboardData?.getData("text/plain");if(!d)return;i.preventDefault(),i.stopPropagation();let f=Date.now();f-xe<50||(xe=f,s.pasteText(Qe(d)))}let ie=0;function jt(i){i.preventDefault(),ie++,Je(i.dataTransfer)&&e.classList.add("terminal-drag-over")}function Ft(i){i.preventDefault(),i.dataTransfer&&(i.dataTransfer.dropEffect="copy")}function Wt(i){i.preventDefault(),ie--,ie<=0&&(ie=0,e.classList.remove("terminal-drag-over"))}async function Yt(i){i.preventDefault(),ie=0,e.classList.remove("terminal-drag-over");let a=Je(i.dataTransfer);a&&await Pe(a)}function et(i){!It||i.key!=="Escape"||ee||i.defaultPrevented||i.repeat||i.metaKey||i.ctrlKey||i.altKey||s.isFocused()&&(i.preventDefault(),i.stopPropagation(),we("\x1B"))}e.addEventListener("paste",zt),e.addEventListener("dragenter",jt),e.addEventListener("dragover",Ft),e.addEventListener("dragleave",Wt),e.addEventListener("drop",Yt),document.addEventListener("keydown",et,!0);function Ut(i){if(i.touches.length!==1){l=null;return}let a=i.touches[0];a&&(O(),l={startX:a.clientX,startY:a.clientY,lastX:a.clientX,lastY:a.clientY,lastT:performance.now(),velocity:0,scrolling:!1},i.stopPropagation())}function qt(i){if(!l||i.touches.length!==1)return;let a=i.touches[0];if(!a)return;let d=a.clientY-l.startY,f=a.clientX-l.startX;if(!l.scrolling){if(Math.abs(d)<8||Math.abs(d)<Math.abs(f))return;l.scrolling=!0,K=Date.now()+500}i.preventDefault(),i.stopPropagation();let b=performance.now(),I=a.clientY-l.lastY,W=b-l.lastT;W>0&&(l.velocity=I/W),Xt(-I),l.lastX=a.clientX,l.lastY=a.clientY,l.lastT=b}function Ot(i){if(!l)return;let a=l.scrolling,d=l.velocity;if(l=null,i.stopPropagation(),!a){s.focus();return}K=Date.now()+500,i.preventDefault(),d&&Ne(d)}function Kt(i){l=null,O(),i.stopPropagation()}function Vt(i){i.pointerType==="touch"&&Date.now()<K&&(i.preventDefault(),i.stopPropagation())}function Xt(i){let a=e.querySelector(".xterm-rows > div"),d=a?a.getBoundingClientRect().height:16,f=d>0?Math.round(i/d):i>0?1:-1;f===0&&(f=i>0?1:-1),s.scrollLines(f)}e.addEventListener("touchstart",Ut,{passive:!0,capture:!0}),e.addEventListener("touchmove",qt,{passive:!1,capture:!0}),e.addEventListener("touchend",Ot,{passive:!1,capture:!0}),e.addEventListener("touchcancel",Kt,{passive:!0,capture:!0}),e.addEventListener("pointerup",Vt,!0);function tt(){M()}window.addEventListener("resize",tt),window.visualViewport?.addEventListener("resize",M),window.visualViewport?.addEventListener("scroll",M);let nt=new ResizeObserver(()=>M());nt.observe(e),document.fonts?.ready.then(()=>{M(),setTimeout(M,50),setTimeout(M,200)}),e.addEventListener("focusin",Rt,!0);async function Jt(){if(document.fonts?.ready)try{await Promise.race([document.fonts.ready,new Promise(i=>setTimeout(i,1e3))])}catch{}}return requestAnimationFrame(()=>{F()&&be(),(async()=>(await Jt(),F()&&j({type:"resize",cols:s.cols,rows:s.rows}),Ve(),M()))()}),{destroy(){if(N=!0,O(),X(!1),se&&clearTimeout(se),document.removeEventListener("mousemove",_e,!0),document.removeEventListener("mouseup",ze,!0),o){try{o.close(1e3,"session switch")}catch{}o=void 0}e.innerHTML="",e.classList.remove("terminal-pending"),window.removeEventListener("resize",tt),nt.disconnect(),document.removeEventListener("keydown",et,!0)},focus(){s.focus()},sendInput(i){we(i)},toggleCopyMode(){Ge()}}})()}var le=window.__TMUX_WEB_SHELL__,v=null,y=null,it=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null,x=it;window.__tmuxWebScopeAgent=it;var Fe="sessions",P="",Ce="",Me="",at="";function T(e){return x?"/a/"+encodeURIComponent(x)+e:e}var h=document.getElementById("sidebar-content"),Y=document.getElementById("main-placeholder"),B=document.getElementById("terminal-container"),re=document.getElementById("file-editor"),nn=document.getElementById("fe-path"),D=document.getElementById("fe-status"),ce=document.getElementById("fe-content"),U=document.getElementById("fe-save"),sn=document.getElementById("fe-delete"),an=document.getElementById("fe-back"),ot=document.getElementById("fe-new-name"),on=document.getElementById("fe-new-btn"),We=document.getElementById("settings-backdrop"),lt=document.getElementById("settings-popover"),rt=document.querySelector(".sidebar");function ln(){return window.matchMedia("(max-width: 640px)").matches}function Ye(){ln()&&rt.classList.add("collapsed")}function ct(){rt.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{ct(),dt("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{ct(),dt("files")});function dt(e){Fe=e,document.querySelectorAll(".mode-btn").forEach(n=>n.classList.remove("active"));let t=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");t&&t.classList.add("active"),e==="sessions"?(Ue="",J()):Ln()}var Ue="";async function J(){if(Fe!=="sessions")return;try{let t=await(await fetch("/api/sidebar/sessions")).json(),n=JSON.stringify({recent:t.recent,agents:t.agents});if(n===Ue)return;Ue=n,pn(t)}catch{h.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>{let t=window.__openNewSessionModal;typeof t=="function"?t():document.getElementById("new-session-modal")?.classList.add("open")})}var ut="tmux-web-sidebar-group-collapsed";function pt(){try{return new Set(JSON.parse(localStorage.getItem(ut)||"[]"))}catch{return new Set}}function rn(e){try{localStorage.setItem(ut,JSON.stringify([...e]))}catch{}}function cn(e,t,n){if(!e)return"\u2026";let s=n&&(e===n||e.startsWith(n+"/"))?"~"+e.slice(n.length):e,o=s.split("/").filter(Boolean);if(o.length===0)return e;for(let r=1;r<=o.length;r++){let c=o.slice(-r).join("/");if(!t.some(l=>!l||l===e?!1:(n&&(l===n||l.startsWith(n+"/"))?"~"+l.slice(n.length):l).split("/").filter(Boolean).slice(-r).join("/")===c))return c}return s}function dn(e,t){let n=new Map;for(let r of e){let c=r.path||"";n.has(c)||n.set(c,[]),n.get(c).push(r)}let s=[...n.keys()],o=pt();for(let[r,c]of n){let l=cn(r,s,t),u=o.has(r),p=document.createElement("div");p.className="session-group-header"+(u?" collapsed":""),p.title=r||"No working directory",p.innerHTML=`<span class="sg-folder">&#128193;</span><span class="sg-label">${m(l)}</span>`,p.addEventListener("click",()=>{let k=pt(),H=p.nextElementSibling;k.has(r)?(k.delete(r),p.classList.remove("collapsed"),H?.classList.remove("collapsed")):(k.add(r),p.classList.add("collapsed"),H?.classList.add("collapsed")),rn(k)});let S=document.createElement("div");S.className="session-group-body"+(u?" collapsed":"");for(let k of c)S.appendChild(un(k));h.appendChild(p),h.appendChild(S)}}function un(e){let t=document.createElement("div");t.className="session-item"+(e.name===y&&!x?" active":""),t.setAttribute("data-session",e.name),t.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${e.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
9
- <span>${m(e.name)}</span>
10
- <button class="session-edit-btn" data-session="${m(e.name)}" title="Edit session"><svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
11
- `,t.addEventListener("click",s=>{s.target.closest(".session-edit-btn")||de(e.name)});let n=t.querySelector(".session-edit-btn");return n&&n.addEventListener("click",s=>{s.stopPropagation(),Mn(e.name,n)}),t}function pn(e){h.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';let t=e.recent||[],n=document.createElement("div");n.className="sidebar-section-label"+(x?" clickable":""),n.textContent=x?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",x&&(n.title="Back to hub",n.addEventListener("click",()=>{window.location.href="/"})),h.appendChild(n),dn(t,e.home);let s=e.agents||[];for(let o of s){if(!o.sessions||o.sessions.length===0)continue;let r=o.agentId===x,c=document.createElement("div");c.className="sidebar-section-label machine-label"+(o.online?"":" agent-offline-label")+(!r&&o.online?" clickable":""),c.textContent=o.agentName+(o.online?"":" (offline)"),!r&&o.online&&(c.title="\u6253\u5F00 "+o.agentName,c.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(o.agentId)+"/"})),h.appendChild(c);for(let l of o.sessions){let u=document.createElement("div");u.className="session-item"+(l.name===y&&x===o.agentId?" active":"")+(o.online?"":" session-offline"),u.setAttribute("data-session",l.name),u.setAttribute("data-agent",o.agentId),u.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${l.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
12
- <span>${m(l.name)}</span><span class="meta">${o.online?"":"offline"}</span>`,o.online&&u.addEventListener("click",p=>{p.target.closest(".session-edit-btn")||de(l.name,o.agentId)}),h.appendChild(u)}}}function m(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function de(e,t){if(!(y===e&&v&&x===(t||null))){re.style.display="none",Y.style.display="none",B.style.display="",B.classList.add("terminal-pending"),Ye(),v&&(Re(),v.destroy(),v=null),y=e,x=t||null,window.__tmuxWebScopeAgent=x,window.__tmuxWebCurrentSession=y;for(let n of h.querySelectorAll(".session-item"))n.classList.toggle("active",n.getAttribute("data-session")===e&&(n.getAttribute("data-agent")||null)===x);try{v=await tn(B,e,{terminal:le.terminal,scrollback:le.scrollback,theme:le.theme,wsBase:x?"/ws/a/"+encodeURIComponent(x):le.wsBase||"/ws"}),Et()}catch(n){console.error("[shell] terminal init failed:",n);let s=n&&typeof n=="object"&&"message"in n?n.message:String(n);B.textContent="Failed to open terminal for "+e+": "+s,B.classList.remove("terminal-pending")}}}var Te=[],g=null,ft=!1;function fn(e){if(!g||!g.files.length)return null;let t=g.repoRoot;if(!t||!e.startsWith(t))return null;let n=e.slice(t.length+1);for(let s of g.files)if(s.path===n)return s.status;return null}async function mn(e){ft=!0;try{let t=await fetch(T("/api/git/status?path="+encodeURIComponent(e)));t.ok?g=await t.json():g=null}catch{g=null}finally{ft=!1}}function hn(){if(!g||!g.branch)return"";let{branch:e,linesAdded:t,linesRemoved:n}=g,s=`<div class="sidebar-git"><div class="git-branch-badge">
8
+ `).join("\r")}async function Ot(i){let a=Ft(i.clipboardData);if(a){i.preventDefault(),i.stopPropagation();let b=Date.now();if(b-xe<50)return;xe=b,await ze(a);return}let d=i.clipboardData?.getData("text/plain");if(!d)return;i.preventDefault(),i.stopPropagation();let f=Date.now();f-xe<50||(xe=f,s.pasteText(st(d)))}let ie=0;function Kt(i){i.preventDefault(),ie++,et(i.dataTransfer)&&e.classList.add("terminal-drag-over")}function Vt(i){i.preventDefault(),i.dataTransfer&&(i.dataTransfer.dropEffect="copy")}function Jt(i){i.preventDefault(),ie--,ie<=0&&(ie=0,e.classList.remove("terminal-drag-over"))}async function Xt(i){i.preventDefault(),ie=0,e.classList.remove("terminal-drag-over");let a=et(i.dataTransfer);a&&await ze(a)}function it(i){!Pt||i.key!=="Escape"||ee||i.defaultPrevented||i.repeat||i.metaKey||i.ctrlKey||i.altKey||s.isFocused()&&(i.preventDefault(),i.stopPropagation(),be("\x1B"))}e.addEventListener("paste",Ot),e.addEventListener("dragenter",Kt),e.addEventListener("dragover",Vt),e.addEventListener("dragleave",Jt),e.addEventListener("drop",Xt),document.addEventListener("keydown",it,!0);function Zt(i){if(i.touches.length!==1){l=null;return}let a=i.touches[0];a&&(O(),l={startX:a.clientX,startY:a.clientY,lastX:a.clientX,lastY:a.clientY,lastT:performance.now(),velocity:0,scrolling:!1},i.stopPropagation())}function Gt(i){if(!l||i.touches.length!==1)return;let a=i.touches[0];if(!a)return;let d=a.clientY-l.startY,f=a.clientX-l.startX;if(!l.scrolling){if(Math.abs(d)<8||Math.abs(d)<Math.abs(f))return;l.scrolling=!0,K=Date.now()+500}i.preventDefault(),i.stopPropagation();let b=performance.now(),B=a.clientY-l.lastY,F=b-l.lastT;F>0&&(l.velocity=B/F),nn(-B),l.lastX=a.clientX,l.lastY=a.clientY,l.lastT=b}function Qt(i){if(!l)return;let a=l.scrolling,d=l.velocity;if(l=null,i.stopPropagation(),!a){s.focus();return}K=Date.now()+500,i.preventDefault(),d&&je(d)}function en(i){l=null,O(),i.stopPropagation()}function tn(i){i.pointerType==="touch"&&Date.now()<K&&(i.preventDefault(),i.stopPropagation())}function nn(i){let a=e.querySelector(".xterm-rows > div"),d=a?a.getBoundingClientRect().height:16,f=d>0?Math.round(i/d):i>0?1:-1;f===0&&(f=i>0?1:-1),s.scrollLines(f)}e.addEventListener("touchstart",Zt,{passive:!0,capture:!0}),e.addEventListener("touchmove",Gt,{passive:!1,capture:!0}),e.addEventListener("touchend",Qt,{passive:!1,capture:!0}),e.addEventListener("touchcancel",en,{passive:!0,capture:!0}),e.addEventListener("pointerup",tn,!0);function at(){M()}window.addEventListener("resize",at),window.visualViewport?.addEventListener("resize",M),window.visualViewport?.addEventListener("scroll",M);let ot=new ResizeObserver(()=>M());ot.observe(e),document.fonts?.ready.then(()=>{M(),setTimeout(M,50),setTimeout(M,200)}),e.addEventListener("focusin",Wt,!0);async function sn(){if(document.fonts?.ready)try{await Promise.race([document.fonts.ready,new Promise(i=>setTimeout(i,1e3))])}catch{}}return requestAnimationFrame(()=>{W()&&Le(),(async()=>(await sn(),W()&&z({type:"resize",cols:s.cols,rows:s.rows}),Ge(),M()))()}),{destroy(){if(N=!0,O(),J(!1),se&&clearTimeout(se),document.removeEventListener("mousemove",We,!0),document.removeEventListener("mouseup",Fe,!0),o){try{o.close(1e3,"session switch")}catch{}o=void 0}e.innerHTML="",e.classList.remove("terminal-pending"),window.removeEventListener("resize",at),ot.disconnect(),document.removeEventListener("keydown",it,!0)},focus(){s.focus()},sendInput(i){be(i)},toggleCopyMode(){nt()}}})()}var le=window.__TMUX_WEB_SHELL__,v=null,y=null,rt=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null,k=rt;window.__tmuxWebScopeAgent=rt;var Ye="sessions",P="",Me="",Te="",ct="";function T(e){return k?"/a/"+encodeURIComponent(k)+e:e}var m=document.getElementById("sidebar-content"),U=document.getElementById("main-placeholder"),I=document.getElementById("terminal-container"),re=document.getElementById("file-editor"),dn=document.getElementById("fe-path"),D=document.getElementById("fe-status"),ce=document.getElementById("fe-content"),Y=document.getElementById("fe-save"),un=document.getElementById("fe-delete"),pn=document.getElementById("fe-back"),dt=document.getElementById("fe-new-name"),fn=document.getElementById("fe-new-btn"),qe=document.getElementById("settings-backdrop"),ut=document.getElementById("settings-popover"),pt=document.querySelector(".sidebar");function mn(){return window.matchMedia("(max-width: 640px)").matches}function Oe(){mn()&&pt.classList.add("collapsed")}function ft(){pt.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{ft(),mt("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{ft(),mt("files")});function mt(e){Ye=e,document.querySelectorAll(".mode-btn").forEach(n=>n.classList.remove("active"));let t=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");t&&t.classList.add("active"),e==="sessions"?(Ke="",X()):Sn()}var Ke="";async function X(){if(Ye!=="sessions")return;try{let t=await(await fetch("/api/sidebar/sessions")).json(),n=JSON.stringify({recent:t.recent,agents:t.agents});if(n===Ke)return;Ke=n,wn(t)}catch{m.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>{let t=window.__openNewSessionModal;typeof t=="function"?t():document.getElementById("new-session-modal")?.classList.add("open")}),Bt()}var ht="tmux-web-sidebar-group-collapsed";function vt(){try{return new Set(JSON.parse(localStorage.getItem(ht)||"[]"))}catch{return new Set}}function hn(e){try{localStorage.setItem(ht,JSON.stringify([...e]))}catch{}}function vn(e,t,n){if(!e)return"\u2026";let s=n&&(e===n||e.startsWith(n+"/"))?"~"+e.slice(n.length):e,o=s.split("/").filter(Boolean);if(o.length===0)return e;for(let r=1;r<=o.length;r++){let c=o.slice(-r).join("/");if(!t.some(l=>!l||l===e?!1:(n&&(l===n||l.startsWith(n+"/"))?"~"+l.slice(n.length):l).split("/").filter(Boolean).slice(-r).join("/")===c))return c}return s}function gn(e,t){let n=new Map;for(let r of e){let c=r.path||"";n.has(c)||n.set(c,[]),n.get(c).push(r)}let s=[...n.keys()],o=vt();for(let[r,c]of n){let l=vn(r,s,t),u=o.has(r),p=document.createElement("div");p.className="session-group-header"+(u?" collapsed":""),p.title=r||"No working directory",p.innerHTML=`<span class="sg-folder">&#128193;</span><span class="sg-label">${h(l)}</span>`,p.addEventListener("click",()=>{let x=vt(),H=p.nextElementSibling;x.has(r)?(x.delete(r),p.classList.remove("collapsed"),H?.classList.remove("collapsed")):(x.add(r),p.classList.add("collapsed"),H?.classList.add("collapsed")),hn(x)});let S=document.createElement("div");S.className="session-group-body"+(u?" collapsed":"");for(let x of c)S.appendChild(yn(x));m.appendChild(p),m.appendChild(S)}}function yn(e){let t=document.createElement("div");t.className="session-item"+(e.name===y&&!k?" active":""),t.setAttribute("data-session",e.name),t.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${e.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
9
+ <span>${h(e.name)}</span>
10
+ <button class="session-edit-btn" data-session="${h(e.name)}" title="Edit session"><svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
11
+ `,t.addEventListener("click",s=>{s.target.closest(".session-edit-btn")||de(e.name)});let n=t.querySelector(".session-edit-btn");return n&&n.addEventListener("click",s=>{s.stopPropagation(),An(e.name,n)}),t}function wn(e){m.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';let t=e.recent||[],n=document.createElement("div");n.className="sidebar-section-label"+(k?" clickable":""),n.textContent=k?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",k&&(n.title="Back to hub",n.addEventListener("click",()=>{window.location.href="/"})),m.appendChild(n),gn(t,e.home);let s=e.agents||[];for(let o of s){if(!o.sessions||o.sessions.length===0)continue;let r=o.agentId===k,c=document.createElement("div");c.className="sidebar-section-label machine-label"+(o.online?"":" agent-offline-label")+(!r&&o.online?" clickable":""),c.textContent=o.agentName+(o.online?"":" (offline)"),!r&&o.online&&(c.title="\u6253\u5F00 "+o.agentName,c.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(o.agentId)+"/"})),m.appendChild(c);for(let l of o.sessions){let u=document.createElement("div");u.className="session-item"+(l.name===y&&k===o.agentId?" active":"")+(o.online?"":" session-offline"),u.setAttribute("data-session",l.name),u.setAttribute("data-agent",o.agentId),u.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${l.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
12
+ <span>${h(l.name)}</span><span class="meta">${o.online?"":"offline"}</span>`,o.online&&u.addEventListener("click",p=>{p.target.closest(".session-edit-btn")||de(l.name,o.agentId)}),m.appendChild(u)}}}function h(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function de(e,t){if(!(y===e&&v&&k===(t||null))){re.style.display="none",U.style.display="none",I.style.display="",I.classList.add("terminal-pending"),Oe(),v&&($e(),v.destroy(),v=null),y=e,k=t||null,window.__tmuxWebScopeAgent=k,window.__tmuxWebCurrentSession=y;for(let n of m.querySelectorAll(".session-item"))n.classList.toggle("active",n.getAttribute("data-session")===e&&(n.getAttribute("data-agent")||null)===k);try{v=await cn(I,e,{terminal:le.terminal,scrollback:le.scrollback,theme:le.theme,wsBase:k?"/ws/a/"+encodeURIComponent(k):le.wsBase||"/ws"}),Mt()}catch(n){console.error("[shell] terminal init failed:",n);let s=n&&typeof n=="object"&&"message"in n?n.message:String(n);I.textContent="Failed to open terminal for "+e+": "+s,I.classList.remove("terminal-pending")}}}var Se=[],g=null,gt=!1;function bn(e){if(!g||!g.files.length)return null;let t=g.repoRoot;if(!t||!e.startsWith(t))return null;let n=e.slice(t.length+1);for(let s of g.files)if(s.path===n)return s.status;return null}async function Ln(e){gt=!0;try{let t=await fetch(T("/api/git/status?path="+encodeURIComponent(e)));t.ok?g=await t.json():g=null}catch{g=null}finally{gt=!1}}function En(){if(!g||!g.branch)return"";let{branch:e,linesAdded:t,linesRemoved:n}=g,s=`<div class="sidebar-git"><div class="git-branch-badge">
13
13
  <svg viewBox="0 0 16 16" fill="currentColor"><path d="M11.63 1.22a.75.75 0 00-1.06 0L5.99 5.8 4.12 3.93a.75.75 0 10-1.06 1.06L4.93 6.87A3.5 3.5 0 003 10.09v3.16a.75.75 0 001 0v-3.16a2.5 2.5 0 012.5-2.5h.27l-1.06 1.06a.75.75 0 001.06 1.06l2.37-2.38a.75.75 0 000-1.06L7.59 5.27l4.04-4.05a.75.75 0 000-1.06L11.63 1.22zM3.25 13.94a.75.75 0 01-.75-.75v-.69a.75.75 0 011.5 0v.69c0 .414-.336.75-.75.75z"/></svg>
14
- ${m(e)}</div>`;return(t>0||n>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${t}</span> <span class="del">-${n}</span></div>`),s+"</div>"}h.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&Z&&g&&(e.stopPropagation(),yt())});var ue=document.getElementById("header-git"),mt=document.getElementById("git-diff-view"),ht=document.getElementById("gd-path"),Se=document.getElementById("gd-status"),Be=document.getElementById("gd-content"),vn=document.getElementById("gd-back"),Z=null;function pe(e){if(!e||!e.branch){ue.style.display="none",Z=null;return}Z=e.repoRoot;let{branch:t,linesAdded:n,linesRemoved:s}=e,o=`<span class="branch">${m(t)}</span>`;(n>0||s>0)&&(o+=`<span class="sep">\xB7</span>+<span class="diff-add">${n}</span> <span class="diff-del">-${s}</span>`),ue.innerHTML=o,ue.style.display="block"}ue.addEventListener("click",e=>{Z&&g&&(e.stopPropagation(),yt())});var E=document.getElementById("git-popover"),Ie=document.getElementById("git-popover-backdrop");function qe(){let e=ue.getBoundingClientRect();E.style.top=e.bottom+4+"px",E.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function vt(){let e=Z,t=document.getElementById("git-popover-browse");t&&t.addEventListener("click",()=>{if(He(),e){Fe="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));let n=document.getElementById("mode-files");n&&n.classList.add("active"),_(e)}})}var gt="";function yt(){if(!g)return;let e=g.files||[];if(!e.length)E.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
14
+ ${h(e)}</div>`;return(t>0||n>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${t}</span> <span class="del">-${n}</span></div>`),s+"</div>"}m.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&Z&&g&&(e.stopPropagation(),Et())});var ue=document.getElementById("header-git"),yt=document.getElementById("git-diff-view"),wt=document.getElementById("gd-path"),Ie=document.getElementById("gd-status"),Be=document.getElementById("gd-content"),kn=document.getElementById("gd-back"),Z=null;function pe(e){if(!e||!e.branch){ue.style.display="none",Z=null;return}Z=e.repoRoot;let{branch:t,linesAdded:n,linesRemoved:s}=e,o=`<span class="branch">${h(t)}</span>`;(n>0||s>0)&&(o+=`<span class="sep">\xB7</span>+<span class="diff-add">${n}</span> <span class="diff-del">-${s}</span>`),ue.innerHTML=o,ue.style.display="block"}ue.addEventListener("click",e=>{Z&&g&&(e.stopPropagation(),Et())});var E=document.getElementById("git-popover"),De=document.getElementById("git-popover-backdrop");function Ve(){let e=ue.getBoundingClientRect();E.style.top=e.bottom+4+"px",E.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function bt(){let e=Z,t=document.getElementById("git-popover-browse");t&&t.addEventListener("click",()=>{if(Ae(),e){Ye="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));let n=document.getElementById("mode-files");n&&n.classList.add("active"),_(e)}})}var Lt="";function Et(){if(!g)return;let e=g.files||[];if(!e.length)E.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
15
15
  <div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>`;else{let t=`<div class="git-popover-header">${e.length} file${e.length>1?"s":""} changed</div>`;for(let n=0;n<e.length;n++){let s=e[n],o=s.additions||0,r=s.deletions||0,c="";switch(s.status){case"M":c='<span class="git-file-status M">M</span>';break;case"A":c='<span class="git-file-status A">A</span>';break;case"D":c='<span class="git-file-status D">D</span>';break;case"?":c='<span class="git-file-status ?">?</span>';break;case"R":c='<span class="git-file-status R">R</span>';break}t+=`<div class="git-popover-item" data-idx="${n}">
16
- ${c}<span class="file-path">${m(s.path)}</span>
16
+ ${c}<span class="file-path">${h(s.path)}</span>
17
17
  <span class="file-add">${o>0?"+"+o:""}</span>
18
18
  <span class="file-del">${r>0?"-"+r:""}</span>
19
- </div>`}t+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',gt=t,E.innerHTML=t,E.querySelectorAll(".git-popover-item").forEach(n=>{let s=parseInt(n.dataset.idx||"0",10),o=e[s];!o||o.status==="D"||(n.classList.add("clickable"),n.style.cursor="pointer",n.addEventListener("click",()=>bt(o.path)))})}qe(),E.classList.add("open"),Ie.classList.add("open"),vt()}function gn(){return window.innerWidth>=1024}function wt(e,t){let n=(e+(t&&e?`
19
+ </div>`}t+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',Lt=t,E.innerHTML=t,E.querySelectorAll(".git-popover-item").forEach(n=>{let s=parseInt(n.dataset.idx||"0",10),o=e[s];!o||o.status==="D"||(n.classList.add("clickable"),n.style.cursor="pointer",n.addEventListener("click",()=>xt(o.path)))})}Ve(),E.classList.add("open"),De.classList.add("open"),bt()}function xn(){return window.innerWidth>=1024}function kt(e,t){let n=(e+(t&&e?`
20
20
  `:"")+t).split(`
21
- `),s="",o=0,r=0;for(let c of n){let l=c;if(l.startsWith("@@")){let u=l.match(/@@\s+-(\d+)[^+]*\+(\d+)/);u&&(o=parseInt(u[1],10)-1,r=parseInt(u[2],10)-1),s+=`<div class="diff-line hunk"><span class="ln-body">${m(l)}</span></div>`;continue}if(l.startsWith("diff --git")||l.startsWith("index ")||l.startsWith("--- ")||l.startsWith("+++ ")||l.startsWith("new file")||l.startsWith("deleted file")){l.startsWith("diff --git")&&(s+=`<div class="diff-line header"><span class="ln-body">${m(l)}</span></div>`);continue}if(l.startsWith("+"))r++,s+=`<div class="diff-line add"><span class="ln-no">${r}</span><span class="ln-body">${m(l)}</span></div>`;else if(l.startsWith("-"))o++,s+=`<div class="diff-line del"><span class="ln-no">${o}</span><span class="ln-body">${m(l)}</span></div>`;else if(l.startsWith("\\"))s+=`<div class="diff-line"><span class="ln-body">${m(l)}</span></div>`;else{if(!l&&!o&&!r)continue;o++,r++,s+=`<div class="diff-line"><span class="ln-no">${o}</span><span class="ln-no" style="margin-right:12px">${r}</span><span class="ln-body">${m(l)}</span></div>`}}return{html:s,lineCount:n.length}}var De=null;async function bt(e){let t=Z;if(t)if(He(),gn()){B.style.display="none",v&&(Re(),v.destroy(),v=null,De=y,y=null),pe(null),re.style.display="none",Y.style.display="none",mt.style.display="flex",ht.textContent=e,Se.textContent="Loading\u2026",Be.innerHTML="";try{let n=await fetch(T("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!n.ok)throw new Error(await n.text());let s=await n.json();wn(e,s.diff,s.stagedDiff)}catch(n){let s=n instanceof Error?n.message:String(n);Se.textContent="Error",Be.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${m(s)}</div>`}}else{E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
21
+ `),s="",o=0,r=0;for(let c of n){let l=c;if(l.startsWith("@@")){let u=l.match(/@@\s+-(\d+)[^+]*\+(\d+)/);u&&(o=parseInt(u[1],10)-1,r=parseInt(u[2],10)-1),s+=`<div class="diff-line hunk"><span class="ln-body">${h(l)}</span></div>`;continue}if(l.startsWith("diff --git")||l.startsWith("index ")||l.startsWith("--- ")||l.startsWith("+++ ")||l.startsWith("new file")||l.startsWith("deleted file")){l.startsWith("diff --git")&&(s+=`<div class="diff-line header"><span class="ln-body">${h(l)}</span></div>`);continue}if(l.startsWith("+"))r++,s+=`<div class="diff-line add"><span class="ln-no">${r}</span><span class="ln-body">${h(l)}</span></div>`;else if(l.startsWith("-"))o++,s+=`<div class="diff-line del"><span class="ln-no">${o}</span><span class="ln-body">${h(l)}</span></div>`;else if(l.startsWith("\\"))s+=`<div class="diff-line"><span class="ln-body">${h(l)}</span></div>`;else{if(!l&&!o&&!r)continue;o++,r++,s+=`<div class="diff-line"><span class="ln-no">${o}</span><span class="ln-no" style="margin-right:12px">${r}</span><span class="ln-body">${h(l)}</span></div>`}}return{html:s,lineCount:n.length}}var He=null;async function xt(e){let t=Z;if(t)if(Ae(),xn()){I.style.display="none",v&&($e(),v.destroy(),v=null,He=y,y=null),pe(null),re.style.display="none",U.style.display="none",yt.style.display="flex",wt.textContent=e,Ie.textContent="Loading\u2026",Be.innerHTML="";try{let n=await fetch(T("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!n.ok)throw new Error(await n.text());let s=await n.json();Mn(e,s.diff,s.stagedDiff)}catch(n){let s=n instanceof Error?n.message:String(n);Ie.textContent="Error",Be.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${h(s)}</div>`}}else{E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
22
22
  <button class="diff-back">\u2190 Back</button>
23
- <span class="diff-filename">${m(e)}</span>
23
+ <span class="diff-filename">${h(e)}</span>
24
24
  <span class="diff-loading">Loading\u2026</span>
25
- </div></div>`,qe(),E.classList.add("open"),Ie.classList.add("open");try{let n=await fetch(T("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!n.ok)throw new Error(await n.text());let s=await n.json();yn(e,s.diff,s.stagedDiff)}catch(n){let s=n instanceof Error?n.message:String(n);E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
25
+ </div></div>`,Ve(),E.classList.add("open"),De.classList.add("open");try{let n=await fetch(T("/api/git/diff?path="+encodeURIComponent(t)+"&file="+encodeURIComponent(e)));if(!n.ok)throw new Error(await n.text());let s=await n.json();Cn(e,s.diff,s.stagedDiff)}catch(n){let s=n instanceof Error?n.message:String(n);E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
26
26
  <button class="diff-back">\u2190 Back</button>
27
- <span class="diff-filename">${m(e)}</span>
28
- </div><div class="diff-empty">Error loading diff: ${m(s)}</div></div>`}}}function yn(e,t,n){if(!t&&!n){E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
27
+ <span class="diff-filename">${h(e)}</span>
28
+ </div><div class="diff-empty">Error loading diff: ${h(s)}</div></div>`}}}function Cn(e,t,n){if(!t&&!n){E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
29
29
  <button class="diff-back">\u2190 Back</button>
30
- <span class="diff-filename">${m(e)}</span>
31
- </div><div class="diff-empty">No diff content available</div></div>`;return}let{html:s,lineCount:o}=wt(t,n);E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
30
+ <span class="diff-filename">${h(e)}</span>
31
+ </div><div class="diff-empty">No diff content available</div></div>`;return}let{html:s,lineCount:o}=kt(t,n);E.innerHTML=`<div class="git-diff-view"><div class="diff-header">
32
32
  <button class="diff-back">\u2190 Back</button>
33
- <span class="diff-filename">${m(e)}</span>
33
+ <span class="diff-filename">${h(e)}</span>
34
34
  <span class="diff-filesize">${o} lines</span>
35
- </div><div class="diff-content">${s}</div></div>`,E.querySelector(".diff-back")?.addEventListener("click",()=>{E.innerHTML=gt,qe(),g&&E.querySelectorAll(".git-popover-item").forEach(r=>{let c=parseInt(r.dataset.idx||"0",10),l=g.files[c];!l||l.status==="D"||(r.style.cursor="pointer",r.addEventListener("click",()=>bt(l.path)))}),vt()})}function wn(e,t,n){if(!t&&!n){Se.textContent="No diff",Be.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}let{html:s,lineCount:o}=wt(t,n);ht.textContent=e,Se.textContent=o+" lines",Be.innerHTML=s}function bn(){mt.style.display="none",De?(B.style.display="",B.classList.add("terminal-pending"),de(De),De=null):y?(B.style.display="",B.classList.add("terminal-pending"),Et()):Y.style.display="flex"}vn.addEventListener("click",bn);function He(){E.classList.remove("open"),Ie.classList.remove("open")}Ie.addEventListener("click",He),document.addEventListener("keydown",e=>{e.key==="Escape"&&(He(),$e())});async function Lt(e){try{let t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){let n=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(n.ok){let s=await n.json();g=s,pe(s);return}}}catch{}g=null,pe(null)}var Ae=null;function Et(){Re(),y&&(Lt(y),Ae=setInterval(()=>{y&&Lt(y)},3e4))}function Re(){Ae&&(clearInterval(Ae),Ae=null)}function Ln(){if(Te=le.fsRoots||[],Te.length===0){if(y){En(y);return}h.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}h.innerHTML="";for(let e of Te){let t=document.createElement("div");t.className="file-tree-item",t.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${m(e)}`,t.addEventListener("click",()=>_(e)),h.appendChild(t)}}async function En(e){h.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let t=await(await fetch(T("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?_(t.path):h.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{h.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function _(e){P=e,h.innerHTML='<div class="file-tree-empty">Loading...</div>';let t=mn(e);try{let n=await(await fetch(T("/api/fs/list?path="+encodeURIComponent(e)))).json();await t,h.innerHTML="",pe(g),h.innerHTML+=hn();let s=xn(e);if(s&&!Te.some(r=>e===r)){let r=document.createElement("div");r.className="file-tree-item",r.innerHTML="..",r.addEventListener("click",()=>_(s)),h.appendChild(r)}if(n.dirs)for(let r of n.dirs){let c=r.split("/").pop()||r.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item",l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${m(c)}`,l.addEventListener("click",()=>_(r)),h.appendChild(l)}else h.innerHTML="";let o=n.files||[];for(let r of o){let c=r.split("/").pop()||r.split("\\").pop(),l=fn(r),u="",p="";if(l)switch(l){case"M":u="git-mod",p='<span class="git-file-status M">M</span>';break;case"A":u="git-add",p='<span class="git-file-status A">A</span>';break;case"D":u="git-del",p='<span class="git-file-status D">D</span>';break;case"?":u="git-untracked",p='<span class="git-file-status ?">?</span>';break;case"R":u="git-mod",p='<span class="git-file-status R">R</span>';break}let S=document.createElement("div");S.className="file-tree-item file"+(u?" "+u:""),S.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${p}${m(c)}`,S.addEventListener("click",()=>kn(r)),h.appendChild(S)}}catch{h.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function xn(e){let t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function kn(e){Ce=e,B.style.display="none",v&&(Re(),v.destroy(),v=null,y=null),pe(null),Ye(),Y.style.display="none",re.style.display="flex",nn.textContent=e,D.textContent="Loading...",U.style.display="none";try{let t=await fetch(T("/api/file?path="+encodeURIComponent(e)));if(!t.ok){let s=await t.json();D.textContent=s.error||"Failed to load",ce.value="";return}let n=await t.json();ce.value=n.content,Me=n.content,at=n.content,D.textContent=n.size+" bytes",U.style.display=""}catch{D.textContent="Failed to load",ce.value=""}}ce.addEventListener("input",()=>{Me=ce.value}),U.addEventListener("click",async()=>{U.textContent="Saving...",U.disabled=!0;try{let e=await fetch(T("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:Ce,content:Me})});if(!e.ok){let t=await e.json();D.textContent=t.error||"Save failed";return}at=Me,D.textContent="Saved"}catch{D.textContent="Save failed"}finally{U.disabled=!1,U.textContent="Save"}}),sn.addEventListener("click",async()=>{if(confirm("Delete "+Ce+"?"))try{let e=await fetch(T("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:Ce})});if(!e.ok){let t=await e.json();D.textContent=t.error||"Delete failed";return}re.style.display="none",Y.style.display="flex",P&&_(P)}catch{D.textContent="Delete failed"}}),an.addEventListener("click",()=>{re.style.display="none",Y.style.display="flex",P&&_(P)}),on.addEventListener("click",async()=>{let e=ot.value.trim();if(!e)return;let t=P+"/"+e;try{let n=await fetch(T("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok){let s=await n.json();D.textContent=s.error||"Create failed";return}ot.value="",P&&_(P)}catch{D.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let e=lt.classList.toggle("open");We.classList.toggle("open",e)}),We.addEventListener("click",()=>{lt.classList.remove("open"),We.classList.remove("open")});var q="",fe=document.createElement("div");fe.className="session-popover-backdrop",document.body.appendChild(fe);var $=document.createElement("div");$.id="session-popover",$.className="session-popover",$.style.display="none",$.innerHTML=`<div class="sp-field">
35
+ </div><div class="diff-content">${s}</div></div>`,E.querySelector(".diff-back")?.addEventListener("click",()=>{E.innerHTML=Lt,Ve(),g&&E.querySelectorAll(".git-popover-item").forEach(r=>{let c=parseInt(r.dataset.idx||"0",10),l=g.files[c];!l||l.status==="D"||(r.style.cursor="pointer",r.addEventListener("click",()=>xt(l.path)))}),bt()})}function Mn(e,t,n){if(!t&&!n){Ie.textContent="No diff",Be.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}let{html:s,lineCount:o}=kt(t,n);wt.textContent=e,Ie.textContent=o+" lines",Be.innerHTML=s}function Tn(){yt.style.display="none",He?(I.style.display="",I.classList.add("terminal-pending"),de(He),He=null):y?(I.style.display="",I.classList.add("terminal-pending"),Mt()):U.style.display="flex"}kn.addEventListener("click",Tn);function Ae(){E.classList.remove("open"),De.classList.remove("open")}De.addEventListener("click",Ae),document.addEventListener("keydown",e=>{e.key==="Escape"&&(Ae(),Ne())});async function Ct(e){try{let t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){let n=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(n.ok){let s=await n.json();g=s,pe(s);return}}}catch{}g=null,pe(null)}var Re=null;function Mt(){$e(),y&&(Ct(y),Re=setInterval(()=>{y&&Ct(y)},3e4))}function $e(){Re&&(clearInterval(Re),Re=null)}function Sn(){if(Se=le.fsRoots||[],Se.length===0){if(y){In(y);return}m.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}m.innerHTML="";for(let e of Se){let t=document.createElement("div");t.className="file-tree-item",t.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${h(e)}`,t.addEventListener("click",()=>_(e)),m.appendChild(t)}}async function In(e){m.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let t=await(await fetch(T("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?_(t.path):m.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{m.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function _(e){P=e,m.innerHTML='<div class="file-tree-empty">Loading...</div>';let t=Ln(e);try{let n=await(await fetch(T("/api/fs/list?path="+encodeURIComponent(e)))).json();await t,m.innerHTML="",pe(g),m.innerHTML+=En();let s=Bn(e);if(s&&!Se.some(r=>e===r)){let r=document.createElement("div");r.className="file-tree-item",r.innerHTML="..",r.addEventListener("click",()=>_(s)),m.appendChild(r)}if(n.dirs)for(let r of n.dirs){let c=r.split("/").pop()||r.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item",l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${h(c)}`,l.addEventListener("click",()=>_(r)),m.appendChild(l)}else m.innerHTML="";let o=n.files||[];for(let r of o){let c=r.split("/").pop()||r.split("\\").pop(),l=bn(r),u="",p="";if(l)switch(l){case"M":u="git-mod",p='<span class="git-file-status M">M</span>';break;case"A":u="git-add",p='<span class="git-file-status A">A</span>';break;case"D":u="git-del",p='<span class="git-file-status D">D</span>';break;case"?":u="git-untracked",p='<span class="git-file-status ?">?</span>';break;case"R":u="git-mod",p='<span class="git-file-status R">R</span>';break}let S=document.createElement("div");S.className="file-tree-item file"+(u?" "+u:""),S.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${p}${h(c)}`,S.addEventListener("click",()=>Dn(r)),m.appendChild(S)}}catch{m.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function Bn(e){let t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function Dn(e){Me=e,I.style.display="none",v&&($e(),v.destroy(),v=null,y=null),pe(null),Oe(),U.style.display="none",re.style.display="flex",dn.textContent=e,D.textContent="Loading...",Y.style.display="none";try{let t=await fetch(T("/api/file?path="+encodeURIComponent(e)));if(!t.ok){let s=await t.json();D.textContent=s.error||"Failed to load",ce.value="";return}let n=await t.json();ce.value=n.content,Te=n.content,ct=n.content,D.textContent=n.size+" bytes",Y.style.display=""}catch{D.textContent="Failed to load",ce.value=""}}ce.addEventListener("input",()=>{Te=ce.value}),Y.addEventListener("click",async()=>{Y.textContent="Saving...",Y.disabled=!0;try{let e=await fetch(T("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:Me,content:Te})});if(!e.ok){let t=await e.json();D.textContent=t.error||"Save failed";return}ct=Te,D.textContent="Saved"}catch{D.textContent="Save failed"}finally{Y.disabled=!1,Y.textContent="Save"}}),un.addEventListener("click",async()=>{if(confirm("Delete "+Me+"?"))try{let e=await fetch(T("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:Me})});if(!e.ok){let t=await e.json();D.textContent=t.error||"Delete failed";return}re.style.display="none",U.style.display="flex",P&&_(P)}catch{D.textContent="Delete failed"}}),pn.addEventListener("click",()=>{re.style.display="none",U.style.display="flex",P&&_(P)}),fn.addEventListener("click",async()=>{let e=dt.value.trim();if(!e)return;let t=P+"/"+e;try{let n=await fetch(T("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok){let s=await n.json();D.textContent=s.error||"Create failed";return}dt.value="",P&&_(P)}catch{D.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let e=ut.classList.toggle("open");qe.classList.toggle("open",e)}),qe.addEventListener("click",()=>{ut.classList.remove("open"),qe.classList.remove("open")});var q="",fe=document.createElement("div");fe.className="session-popover-backdrop",document.body.appendChild(fe);var $=document.createElement("div");$.id="session-popover",$.className="session-popover",$.style.display="none",$.innerHTML=`<div class="sp-field">
36
36
  <label>Rename session</label>
37
37
  <input type="text" id="sp-name" />
38
38
  </div>
@@ -41,5 +41,5 @@ var Zt=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function
41
41
  <button class="btn" id="sp-cancel">Cancel</button>
42
42
  </div>
43
43
  <hr class="sp-divider" />
44
- <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild($);function Cn(e){let t=e.getBoundingClientRect();$.style.top=t.bottom+4+"px",$.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function Mn(e,t){q=e;let n=document.getElementById("sp-name");n.value=e,fe.classList.add("open"),$.style.display="block",Cn(t),setTimeout(()=>n.focus(),50)}function me(){fe.classList.remove("open"),$.style.display="none"}fe.addEventListener("click",me),document.getElementById("sp-save").addEventListener("click",async()=>{let e=document.getElementById("sp-name").value.trim();if(!e||e===q){me();return}try{if(!(await fetch(T("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:q,newName:e})})).ok)return;me(),J(),y===q&&de(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+q+'"?'))try{if(!(await fetch(T("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:q})})).ok)return;me(),y===q&&(v&&(v.destroy(),v=null),y=null,B.style.display="none",Y.style.display="flex"),J()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",me);var Tn={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r","ctrl-r":"","ctrl-c":"",btw:"/btw ",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{let t=e.target.closest("button[data-key]");if(!t||!v)return;let n=Tn[t.dataset.key];n&&(v.sendInput(n),v.focus())});var A=document.getElementById("mk-input"),xt=document.getElementById("mk-send");function kt(){if(!A||!v)return;let e=A.value;e&&(v.sendInput(e+"\r"),A.value="",A.rows=1,v.focus())}if(A&&(A.addEventListener("input",()=>{A.rows=1;let e=A.value.split(`
45
- `).length;A.rows=Math.min(e,4)}),A.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),kt())})),xt&&xt.addEventListener("click",kt),window.visualViewport){let e=document.querySelector(".app-layout"),t=()=>{e&&(e.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",t),window.addEventListener("orientationchange",()=>setTimeout(t,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}var Oe=document.getElementById("process-panel"),he=document.getElementById("proc-list"),Sn=document.getElementById("proc-close"),Ke=document.getElementById("proc-backdrop"),Bn=document.getElementById("header-ram");function In(){he.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(T("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){he.innerHTML='<div class="proc-empty">No processes</div>';return}let t="";for(let n of e){let s=n.rss,o=s<1048576?(s/1024).toFixed(0)+"K":(s/1048576).toFixed(1)+"M";t+='<div class="proc-row" data-pid="'+n.pid+'"><span class="proc-mem">'+n.mem+'%</span><span class="proc-rss">'+o+'</span><span class="proc-cmd" title="'+m(n.command)+'">'+m(n.command)+'</span><button class="proc-kill" title="Kill PID '+n.pid+'">&times;</button></div>'}he.innerHTML=t;for(let n of he.querySelectorAll(".proc-row")){let s=n.querySelector(".proc-kill");s&&s.addEventListener("click",o=>{let r=o.target.closest(".proc-row");!r||!confirm("Kill PID "+r.dataset.pid+"?")||fetch(T("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(r.dataset.pid,10)})}).then(c=>c.json()).then(c=>{c.ok?r.style.opacity="0.3":alert("Failed: "+(c.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{he.innerHTML='<div class="proc-empty">Failed to load</div>'})}function Dn(){Oe.classList.add("open"),Ke.classList.add("open"),In()}function $e(){Oe.classList.remove("open"),Ke.classList.remove("open")}Bn.addEventListener("click",e=>{e.stopPropagation(),Oe.classList.contains("open")?$e():Dn()}),Sn.addEventListener("click",$e),Ke.addEventListener("click",$e),window.__openSession=de,window.__refreshSidebar=J,J(),Ye(),setInterval(()=>{document.visibilityState==="visible"&&J()},1e4);
44
+ <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild($);function Hn(e){let t=e.getBoundingClientRect();$.style.top=t.bottom+4+"px",$.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function An(e,t){q=e;let n=document.getElementById("sp-name");n.value=e,fe.classList.add("open"),$.style.display="block",Hn(t),setTimeout(()=>n.focus(),50)}function me(){fe.classList.remove("open"),$.style.display="none"}fe.addEventListener("click",me),document.getElementById("sp-save").addEventListener("click",async()=>{let e=document.getElementById("sp-name").value.trim();if(!e||e===q){me();return}try{if(!(await fetch(T("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:q,newName:e})})).ok)return;me(),X(),y===q&&de(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+q+'"?'))try{if(!(await fetch(T("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:q})})).ok)return;me(),y===q&&(v&&(v.destroy(),v=null),y=null,I.style.display="none",U.style.display="flex"),X()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",me);var Rn={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r","ctrl-r":"","ctrl-c":"",btw:"/btw ",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{let t=e.target.closest("button[data-key]");if(!t||!v)return;let n=Rn[t.dataset.key];n&&(v.sendInput(n),v.focus())});var A=document.getElementById("mk-input"),Tt=document.getElementById("mk-send");function St(){if(!A||!v)return;let e=A.value;e&&(v.sendInput(e+"\r"),A.value="",A.rows=1,v.focus())}if(A&&(A.addEventListener("input",()=>{A.rows=1;let e=A.value.split(`
45
+ `).length;A.rows=Math.min(e,4)}),A.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),St())})),Tt&&Tt.addEventListener("click",St),window.visualViewport){let e=document.querySelector(".app-layout"),t=()=>{e&&(e.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",t),window.addEventListener("orientationchange",()=>setTimeout(t,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}var Je=document.getElementById("process-panel"),he=document.getElementById("proc-list"),$n=document.getElementById("proc-close"),Xe=document.getElementById("proc-backdrop"),Nn=document.getElementById("header-ram");function Pn(){he.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(T("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){he.innerHTML='<div class="proc-empty">No processes</div>';return}let t="";for(let n of e){let s=n.rss,o=s<1048576?(s/1024).toFixed(0)+"K":(s/1048576).toFixed(1)+"M";t+='<div class="proc-row" data-pid="'+n.pid+'"><span class="proc-mem">'+n.mem+'%</span><span class="proc-rss">'+o+'</span><span class="proc-cmd" title="'+h(n.command)+'">'+h(n.command)+'</span><button class="proc-kill" title="Kill PID '+n.pid+'">&times;</button></div>'}he.innerHTML=t;for(let n of he.querySelectorAll(".proc-row")){let s=n.querySelector(".proc-kill");s&&s.addEventListener("click",o=>{let r=o.target.closest(".proc-row");!r||!confirm("Kill PID "+r.dataset.pid+"?")||fetch(T("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(r.dataset.pid,10)})}).then(c=>c.json()).then(c=>{c.ok?r.style.opacity="0.3":alert("Failed: "+(c.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{he.innerHTML='<div class="proc-empty">Failed to load</div>'})}function _n(){Je.classList.add("open"),Xe.classList.add("open"),Pn()}function Ne(){Je.classList.remove("open"),Xe.classList.remove("open")}Nn.addEventListener("click",e=>{e.stopPropagation(),Je.classList.contains("open")?Ne():_n()}),$n.addEventListener("click",Ne),Xe.addEventListener("click",Ne);var jn=2e3,ve=null,Ze=null,Pe=new Map,_e=new Map;function zn(e,t){return(t?"a:"+t+":":"l:")+e}function Wn(e,t,n){let s=zn(e,t),o=_e.get(s);_e.set(s,{session:e,agentId:t,state:n,doneUntil:n==="idle"&&o?.state==="working"?Date.now()+jn:void 0});let r=t?'.session-item[data-agent="'+CSS.escape(t)+'"][data-session="'+CSS.escape(e)+'"]':'.session-item[data-session="'+CSS.escape(e)+'"]:not([data-agent])',c=m.querySelector(r);c&&It(c,s,_e.get(s))}function It(e,t,n){let s=Pe.get(t);s&&(clearTimeout(s),Pe.delete(t)),e.classList.remove("working","just-done"),n.state==="working"?e.classList.add("working"):n.doneUntil&&Date.now()<n.doneUntil&&(e.classList.add("just-done"),Pe.set(t,setTimeout(()=>{e.classList.remove("just-done"),Pe.delete(t)},Math.max(0,n.doneUntil-Date.now()))))}function Bt(){for(let[e,t]of _e){let n=t.agentId?'.session-item[data-agent="'+CSS.escape(t.agentId)+'"][data-session="'+CSS.escape(t.session)+'"]':'.session-item[data-session="'+CSS.escape(t.session)+'"]:not([data-agent])',s=m.querySelector(n);s&&It(s,e,t)}}function Dt(){if(ve||Ze)return;let e=localStorage.getItem("tmux-web-token"),t=(location.protocol==="https:"?"wss:":"ws:")+"//"+location.host+"/ws/activity";e&&(t+="?token="+encodeURIComponent(e));try{let n=new WebSocket(t);ve=n,n.onmessage=s=>{try{let o=JSON.parse(String(s.data));o&&o.type==="session.activity"&&typeof o.session=="string"&&Wn(o.session,o.agentId||void 0,o.state==="working"?"working":"idle")}catch{}},n.onclose=()=>{ve===n&&(ve=null,Ze=setTimeout(()=>{Ze=null,Dt()},5e3))},n.onerror=()=>{try{n.close()}catch{}}}catch{ve=null}}window.__openSession=de,window.__refreshSidebar=X,X(),Oe(),Bt(),Dt(),setInterval(()=>{document.visibilityState==="visible"&&X()},1e4);
@@ -1,31 +1,31 @@
1
- import{initTerminal as Be}from"./terminal-core.js";const H=window.__TMUX_WEB_SHELL__;let f=null,m=null;const oe=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null;let g=oe;window.__tmuxWebScopeAgent=oe;let Y="sessions",C="",F="",W="",ae="";function y(e){return g?"/a/"+encodeURIComponent(g)+e:e}const p=document.getElementById("sidebar-content"),B=document.getElementById("main-placeholder"),h=document.getElementById("terminal-container"),$=document.getElementById("file-editor"),xe=document.getElementById("fe-path"),w=document.getElementById("fe-status"),R=document.getElementById("fe-content"),x=document.getElementById("fe-save"),Se=document.getElementById("fe-delete"),Me=document.getElementById("fe-back"),le=document.getElementById("fe-new-name"),Te=document.getElementById("fe-new-btn"),Z=document.getElementById("settings-backdrop"),ce=document.getElementById("settings-popover"),de=document.querySelector(".sidebar");function He(){return window.matchMedia("(max-width: 640px)").matches}function Q(){He()&&de.classList.add("collapsed")}function re(){de.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{re(),pe("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{re(),pe("files")});function pe(e){Y=e,document.querySelectorAll(".mode-btn").forEach(t=>t.classList.remove("active"));const n=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");n&&n.classList.add("active"),e==="sessions"?(ee="",M()):Ue()}let ee="";async function M(){if(Y!=="sessions")return;try{const t=await(await fetch("/api/sidebar/sessions")).json(),s=JSON.stringify({recent:t.recent,agents:t.agents});if(s===ee)return;ee=s,Pe(t)}catch{p.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}const e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>{const n=window.__openNewSessionModal;typeof n=="function"?n():document.getElementById("new-session-modal")?.classList.add("open")})}const fe="tmux-web-sidebar-group-collapsed";function ue(){try{return new Set(JSON.parse(localStorage.getItem(fe)||"[]"))}catch{return new Set}}function $e(e){try{localStorage.setItem(fe,JSON.stringify([...e]))}catch{}}function Re(e,n,t){if(!e)return"\u2026";const s=t&&(e===t||e.startsWith(t+"/"))?"~"+e.slice(t.length):e,i=s.split("/").filter(Boolean);if(i.length===0)return e;for(let l=1;l<=i.length;l++){const o=i.slice(-l).join("/");if(!n.some(a=>!a||a===e?!1:(t&&(a===t||a.startsWith(t+"/"))?"~"+a.slice(t.length):a).split("/").filter(Boolean).slice(-l).join("/")===o))return o}return s}function Ne(e,n){const t=new Map;for(const l of e){const o=l.path||"";t.has(o)||t.set(o,[]),t.get(o).push(l)}const s=[...t.keys()],i=ue();for(const[l,o]of t){const c=Re(l,s,n),a=i.has(l),r=document.createElement("div");r.className="session-group-header"+(a?" collapsed":""),r.title=l||"No working directory",r.innerHTML=`<span class="sg-folder">&#128193;</span><span class="sg-label">${d(c)}</span>`,r.addEventListener("click",()=>{const b=ue(),ie=r.nextElementSibling;b.has(l)?(b.delete(l),r.classList.remove("collapsed"),ie?.classList.remove("collapsed")):(b.add(l),r.classList.add("collapsed"),ie?.classList.add("collapsed")),$e(b)});const L=document.createElement("div");L.className="session-group-body"+(a?" collapsed":"");for(const b of o)L.appendChild(_e(b));p.appendChild(r),p.appendChild(L)}}function _e(e){const n=document.createElement("div");n.className="session-item"+(e.name===m&&!g?" active":""),n.setAttribute("data-session",e.name),n.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${e.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
1
+ import{initTerminal as $e}from"./terminal-core.js";const H=window.__TMUX_WEB_SHELL__;let f=null,m=null;const re=typeof window.__TMUX_WEB_SCOPE__?.agentId=="string"?window.__TMUX_WEB_SCOPE__.agentId:null;let g=re;window.__tmuxWebScopeAgent=re;let ee="sessions",C="",F="",O="",de="";function y(e){return g?"/a/"+encodeURIComponent(g)+e:e}const r=document.getElementById("sidebar-content"),I=document.getElementById("main-placeholder"),h=document.getElementById("terminal-container"),R=document.getElementById("file-editor"),Ne=document.getElementById("fe-path"),b=document.getElementById("fe-status"),$=document.getElementById("fe-content"),x=document.getElementById("fe-save"),_e=document.getElementById("fe-delete"),Ae=document.getElementById("fe-back"),pe=document.getElementById("fe-new-name"),De=document.getElementById("fe-new-btn"),te=document.getElementById("settings-backdrop"),fe=document.getElementById("settings-popover"),ue=document.querySelector(".sidebar");function Pe(){return window.matchMedia("(max-width: 640px)").matches}function ne(){Pe()&&ue.classList.add("collapsed")}function me(){ue.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{me(),ve("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{me(),ve("files")});function ve(e){ee=e,document.querySelectorAll(".mode-btn").forEach(t=>t.classList.remove("active"));const n=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");n&&n.classList.add("active"),e==="sessions"?(se="",M()):Ze()}let se="";async function M(){if(ee!=="sessions")return;try{const t=await(await fetch("/api/sidebar/sessions")).json(),s=JSON.stringify({recent:t.recent,agents:t.agents});if(s===se)return;se=s,Ue(t)}catch{r.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}const e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>{const n=window.__openNewSessionModal;typeof n=="function"?n():document.getElementById("new-session-modal")?.classList.add("open")}),He()}const ge="tmux-web-sidebar-group-collapsed";function ye(){try{return new Set(JSON.parse(localStorage.getItem(ge)||"[]"))}catch{return new Set}}function je(e){try{localStorage.setItem(ge,JSON.stringify([...e]))}catch{}}function We(e,n,t){if(!e)return"\u2026";const s=t&&(e===t||e.startsWith(t+"/"))?"~"+e.slice(t.length):e,i=s.split("/").filter(Boolean);if(i.length===0)return e;for(let a=1;a<=i.length;a++){const o=i.slice(-a).join("/");if(!n.some(l=>!l||l===e?!1:(t&&(l===t||l.startsWith(t+"/"))?"~"+l.slice(t.length):l).split("/").filter(Boolean).slice(-a).join("/")===o))return o}return s}function Fe(e,n){const t=new Map;for(const a of e){const o=a.path||"";t.has(o)||t.set(o,[]),t.get(o).push(a)}const s=[...t.keys()],i=ye();for(const[a,o]of t){const c=We(a,s,n),l=i.has(a),p=document.createElement("div");p.className="session-group-header"+(l?" collapsed":""),p.title=a||"No working directory",p.innerHTML=`<span class="sg-folder">&#128193;</span><span class="sg-label">${d(c)}</span>`,p.addEventListener("click",()=>{const w=ye(),ce=p.nextElementSibling;w.has(a)?(w.delete(a),p.classList.remove("collapsed"),ce?.classList.remove("collapsed")):(w.add(a),p.classList.add("collapsed"),ce?.classList.add("collapsed")),je(w)});const L=document.createElement("div");L.className="session-group-body"+(l?" collapsed":"");for(const w of o)L.appendChild(Oe(w));r.appendChild(p),r.appendChild(L)}}function Oe(e){const n=document.createElement("div");n.className="session-item"+(e.name===m&&!g?" active":""),n.setAttribute("data-session",e.name),n.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${e.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
2
2
  <span>${d(e.name)}</span>
3
3
  <button class="session-edit-btn" data-session="${d(e.name)}" title="Edit session"><svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
4
- `,n.addEventListener("click",s=>{s.target.closest(".session-edit-btn")||N(e.name)});const t=n.querySelector(".session-edit-btn");return t&&t.addEventListener("click",s=>{s.stopPropagation(),Xe(e.name,t)}),n}function Pe(e){p.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';const n=e.recent||[],t=document.createElement("div");t.className="sidebar-section-label"+(g?" clickable":""),t.textContent=g?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",g&&(t.title="Back to hub",t.addEventListener("click",()=>{window.location.href="/"})),p.appendChild(t),Ne(n,e.home);const s=e.agents||[];for(const i of s){if(!i.sessions||i.sessions.length===0)continue;const l=i.agentId===g,o=document.createElement("div");o.className="sidebar-section-label machine-label"+(i.online?"":" agent-offline-label")+(!l&&i.online?" clickable":""),o.textContent=i.agentName+(i.online?"":" (offline)"),!l&&i.online&&(o.title="\u6253\u5F00 "+i.agentName,o.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(i.agentId)+"/"})),p.appendChild(o);for(const c of i.sessions){const a=document.createElement("div");a.className="session-item"+(c.name===m&&g===i.agentId?" active":"")+(i.online?"":" session-offline"),a.setAttribute("data-session",c.name),a.setAttribute("data-agent",i.agentId),a.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${c.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
5
- <span>${d(c.name)}</span><span class="meta">${i.online?"":"offline"}</span>`,i.online&&a.addEventListener("click",r=>{r.target.closest(".session-edit-btn")||N(c.name,i.agentId)}),p.appendChild(a)}}}function d(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function N(e,n){if(!(m===e&&f&&g===(n||null))){$.style.display="none",B.style.display="none",h.style.display="",h.classList.add("terminal-pending"),Q(),f&&(K(),f.destroy(),f=null),m=e,g=n||null,window.__tmuxWebScopeAgent=g,window.__tmuxWebCurrentSession=m;for(const t of p.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e&&(t.getAttribute("data-agent")||null)===g);try{f=await Be(h,e,{terminal:H.terminal,scrollback:H.scrollback,theme:H.theme,wsBase:g?"/ws/a/"+encodeURIComponent(g):H.wsBase||"/ws"}),ke()}catch(t){console.error("[shell] terminal init failed:",t);const s=t&&typeof t=="object"&&"message"in t?t.message:String(t);h.textContent="Failed to open terminal for "+e+": "+s,h.classList.remove("terminal-pending")}}}let G=[],u=null,me=!1;function Ae(e){if(!u||!u.files.length)return null;const n=u.repoRoot;if(!n||!e.startsWith(n))return null;const t=e.slice(n.length+1);for(const s of u.files)if(s.path===t)return s.status;return null}async function De(e){me=!0;try{const n=await fetch(y("/api/git/status?path="+encodeURIComponent(e)));n.ok?u=await n.json():u=null}catch{u=null}finally{me=!1}}function je(){if(!u||!u.branch)return"";const{branch:e,linesAdded:n,linesRemoved:t}=u;let s=`<div class="sidebar-git"><div class="git-branch-badge">
4
+ `,n.addEventListener("click",s=>{s.target.closest(".session-edit-btn")||N(e.name)});const t=n.querySelector(".session-edit-btn");return t&&t.addEventListener("click",s=>{s.stopPropagation(),st(e.name,t)}),n}function Ue(e){r.innerHTML='<button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';const n=e.recent||[],t=document.createElement("div");t.className="sidebar-section-label"+(g?" clickable":""),t.textContent=g?"\u2190 \u8FD4\u56DE\u672C\u673A":"\u672C\u673A",g&&(t.title="Back to hub",t.addEventListener("click",()=>{window.location.href="/"})),r.appendChild(t),Fe(n,e.home);const s=e.agents||[];for(const i of s){if(!i.sessions||i.sessions.length===0)continue;const a=i.agentId===g,o=document.createElement("div");o.className="sidebar-section-label machine-label"+(i.online?"":" agent-offline-label")+(!a&&i.online?" clickable":""),o.textContent=i.agentName+(i.online?"":" (offline)"),!a&&i.online&&(o.title="\u6253\u5F00 "+i.agentName,o.addEventListener("click",()=>{window.location.href="/a/"+encodeURIComponent(i.agentId)+"/"})),r.appendChild(o);for(const c of i.sessions){const l=document.createElement("div");l.className="session-item"+(c.name===m&&g===i.agentId?" active":"")+(i.online?"":" session-offline"),l.setAttribute("data-session",c.name),l.setAttribute("data-agent",i.agentId),l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${c.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
5
+ <span>${d(c.name)}</span><span class="meta">${i.online?"":"offline"}</span>`,i.online&&l.addEventListener("click",p=>{p.target.closest(".session-edit-btn")||N(c.name,i.agentId)}),r.appendChild(l)}}}function d(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function N(e,n){if(!(m===e&&f&&g===(n||null))){R.style.display="none",I.style.display="none",h.style.display="",h.classList.add("terminal-pending"),ne(),f&&(X(),f.destroy(),f=null),m=e,g=n||null,window.__tmuxWebScopeAgent=g,window.__tmuxWebCurrentSession=m;for(const t of r.querySelectorAll(".session-item"))t.classList.toggle("active",t.getAttribute("data-session")===e&&(t.getAttribute("data-agent")||null)===g);try{f=await $e(h,e,{terminal:H.terminal,scrollback:H.scrollback,theme:H.theme,wsBase:g?"/ws/a/"+encodeURIComponent(g):H.wsBase||"/ws"}),xe()}catch(t){console.error("[shell] terminal init failed:",t);const s=t&&typeof t=="object"&&"message"in t?t.message:String(t);h.textContent="Failed to open terminal for "+e+": "+s,h.classList.remove("terminal-pending")}}}let U=[],u=null,he=!1;function Ge(e){if(!u||!u.files.length)return null;const n=u.repoRoot;if(!n||!e.startsWith(n))return null;const t=e.slice(n.length+1);for(const s of u.files)if(s.path===t)return s.status;return null}async function ze(e){he=!0;try{const n=await fetch(y("/api/git/status?path="+encodeURIComponent(e)));n.ok?u=await n.json():u=null}catch{u=null}finally{he=!1}}function qe(){if(!u||!u.branch)return"";const{branch:e,linesAdded:n,linesRemoved:t}=u;let s=`<div class="sidebar-git"><div class="git-branch-badge">
6
6
  <svg viewBox="0 0 16 16" fill="currentColor"><path d="M11.63 1.22a.75.75 0 00-1.06 0L5.99 5.8 4.12 3.93a.75.75 0 10-1.06 1.06L4.93 6.87A3.5 3.5 0 003 10.09v3.16a.75.75 0 001 0v-3.16a2.5 2.5 0 012.5-2.5h.27l-1.06 1.06a.75.75 0 001.06 1.06l2.37-2.38a.75.75 0 000-1.06L7.59 5.27l4.04-4.05a.75.75 0 000-1.06L11.63 1.22zM3.25 13.94a.75.75 0 01-.75-.75v-.69a.75.75 0 011.5 0v.69c0 .414-.336.75-.75.75z"/></svg>
7
- ${d(e)}</div>`;return(n>0||t>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${n}</span> <span class="del">-${t}</span></div>`),s+"</div>"}p.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&T&&u&&(e.stopPropagation(),be())});const _=document.getElementById("header-git"),ve=document.getElementById("git-diff-view"),ge=document.getElementById("gd-path"),O=document.getElementById("gd-status"),z=document.getElementById("gd-content"),Fe=document.getElementById("gd-back");let T=null;function P(e){if(!e||!e.branch){_.style.display="none",T=null;return}T=e.repoRoot;const{branch:n,linesAdded:t,linesRemoved:s}=e;let i=`<span class="branch">${d(n)}</span>`;(t>0||s>0)&&(i+=`<span class="sep">\xB7</span>+<span class="diff-add">${t}</span> <span class="diff-del">-${s}</span>`),_.innerHTML=i,_.style.display="block"}_.addEventListener("click",e=>{T&&u&&(e.stopPropagation(),be())});const v=document.getElementById("git-popover"),U=document.getElementById("git-popover-backdrop");function te(){const e=_.getBoundingClientRect();v.style.top=e.bottom+4+"px",v.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function ye(){const e=T,n=document.getElementById("git-popover-browse");n&&n.addEventListener("click",()=>{if(q(),e){Y="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));const t=document.getElementById("mode-files");t&&t.classList.add("active"),I(e)}})}let he="";function be(){if(!u)return;const e=u.files||[];if(!e.length)v.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
8
- <div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>`;else{let n=`<div class="git-popover-header">${e.length} file${e.length>1?"s":""} changed</div>`;for(let t=0;t<e.length;t++){const s=e[t],i=s.additions||0,l=s.deletions||0;let o="";switch(s.status){case"M":o='<span class="git-file-status M">M</span>';break;case"A":o='<span class="git-file-status A">A</span>';break;case"D":o='<span class="git-file-status D">D</span>';break;case"?":o='<span class="git-file-status ?">?</span>';break;case"R":o='<span class="git-file-status R">R</span>';break}n+=`<div class="git-popover-item" data-idx="${t}">
7
+ ${d(e)}</div>`;return(n>0||t>0)&&(s+=`<div class="git-diff-stats">+<span class="add">${n}</span> <span class="del">-${t}</span></div>`),s+"</div>"}r.addEventListener("click",e=>{e.target.closest(".sidebar-git")&&T&&u&&(e.stopPropagation(),ke())});const _=document.getElementById("header-git"),we=document.getElementById("git-diff-view"),be=document.getElementById("gd-path"),G=document.getElementById("gd-status"),z=document.getElementById("gd-content"),Ve=document.getElementById("gd-back");let T=null;function A(e){if(!e||!e.branch){_.style.display="none",T=null;return}T=e.repoRoot;const{branch:n,linesAdded:t,linesRemoved:s}=e;let i=`<span class="branch">${d(n)}</span>`;(t>0||s>0)&&(i+=`<span class="sep">\xB7</span>+<span class="diff-add">${t}</span> <span class="diff-del">-${s}</span>`),_.innerHTML=i,_.style.display="block"}_.addEventListener("click",e=>{T&&u&&(e.stopPropagation(),ke())});const v=document.getElementById("git-popover"),q=document.getElementById("git-popover-backdrop");function ie(){const e=_.getBoundingClientRect();v.style.top=e.bottom+4+"px",v.style.left=Math.max(8,Math.min(e.left,window.innerWidth-400))+"px"}function Le(){const e=T,n=document.getElementById("git-popover-browse");n&&n.addEventListener("click",()=>{if(J(),e){ee="files",document.querySelectorAll(".mode-btn").forEach(s=>s.classList.remove("active"));const t=document.getElementById("mode-files");t&&t.classList.add("active"),S(e)}})}let Ee="";function ke(){if(!u)return;const e=u.files||[];if(!e.length)v.innerHTML=`<div class="git-popover-header">No changes \u2014 clean working tree</div>
8
+ <div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>`;else{let n=`<div class="git-popover-header">${e.length} file${e.length>1?"s":""} changed</div>`;for(let t=0;t<e.length;t++){const s=e[t],i=s.additions||0,a=s.deletions||0;let o="";switch(s.status){case"M":o='<span class="git-file-status M">M</span>';break;case"A":o='<span class="git-file-status A">A</span>';break;case"D":o='<span class="git-file-status D">D</span>';break;case"?":o='<span class="git-file-status ?">?</span>';break;case"R":o='<span class="git-file-status R">R</span>';break}n+=`<div class="git-popover-item" data-idx="${t}">
9
9
  ${o}<span class="file-path">${d(s.path)}</span>
10
10
  <span class="file-add">${i>0?"+"+i:""}</span>
11
- <span class="file-del">${l>0?"-"+l:""}</span>
12
- </div>`}n+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',he=n,v.innerHTML=n,v.querySelectorAll(".git-popover-item").forEach(t=>{const s=parseInt(t.dataset.idx||"0",10),i=e[s];!i||i.status==="D"||(t.classList.add("clickable"),t.style.cursor="pointer",t.addEventListener("click",()=>Le(i.path)))})}te(),v.classList.add("open"),U.classList.add("open"),ye()}function We(){return window.innerWidth>=1024}function we(e,n){const s=(e+(n&&e?`
11
+ <span class="file-del">${a>0?"-"+a:""}</span>
12
+ </div>`}n+='<div class="git-popover-footer"><button id="git-popover-browse">Browse repository \u2192</button></div>',Ee=n,v.innerHTML=n,v.querySelectorAll(".git-popover-item").forEach(t=>{const s=parseInt(t.dataset.idx||"0",10),i=e[s];!i||i.status==="D"||(t.classList.add("clickable"),t.style.cursor="pointer",t.addEventListener("click",()=>Se(i.path)))})}ie(),v.classList.add("open"),q.classList.add("open"),Le()}function Je(){return window.innerWidth>=1024}function Ce(e,n){const s=(e+(n&&e?`
13
13
  `:"")+n).split(`
14
- `);let i="",l=0,o=0;for(const c of s){const a=c;if(a.startsWith("@@")){const r=a.match(/@@\s+-(\d+)[^+]*\+(\d+)/);r&&(l=parseInt(r[1],10)-1,o=parseInt(r[2],10)-1),i+=`<div class="diff-line hunk"><span class="ln-body">${d(a)}</span></div>`;continue}if(a.startsWith("diff --git")||a.startsWith("index ")||a.startsWith("--- ")||a.startsWith("+++ ")||a.startsWith("new file")||a.startsWith("deleted file")){a.startsWith("diff --git")&&(i+=`<div class="diff-line header"><span class="ln-body">${d(a)}</span></div>`);continue}if(a.startsWith("+"))o++,i+=`<div class="diff-line add"><span class="ln-no">${o}</span><span class="ln-body">${d(a)}</span></div>`;else if(a.startsWith("-"))l++,i+=`<div class="diff-line del"><span class="ln-no">${l}</span><span class="ln-body">${d(a)}</span></div>`;else if(a.startsWith("\\"))i+=`<div class="diff-line"><span class="ln-body">${d(a)}</span></div>`;else{if(!a&&!l&&!o)continue;l++,o++,i+=`<div class="diff-line"><span class="ln-no">${l}</span><span class="ln-no" style="margin-right:12px">${o}</span><span class="ln-body">${d(a)}</span></div>`}}return{html:i,lineCount:s.length}}let V=null;async function Le(e){const n=T;if(n)if(q(),We()){h.style.display="none",f&&(K(),f.destroy(),f=null,V=m,m=null),P(null),$.style.display="none",B.style.display="none",ve.style.display="flex",ge.textContent=e,O.textContent="Loading\u2026",z.innerHTML="";try{const t=await fetch(y("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Oe(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);O.textContent="Error",z.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${d(s)}</div>`}}else{v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
14
+ `);let i="",a=0,o=0;for(const c of s){const l=c;if(l.startsWith("@@")){const p=l.match(/@@\s+-(\d+)[^+]*\+(\d+)/);p&&(a=parseInt(p[1],10)-1,o=parseInt(p[2],10)-1),i+=`<div class="diff-line hunk"><span class="ln-body">${d(l)}</span></div>`;continue}if(l.startsWith("diff --git")||l.startsWith("index ")||l.startsWith("--- ")||l.startsWith("+++ ")||l.startsWith("new file")||l.startsWith("deleted file")){l.startsWith("diff --git")&&(i+=`<div class="diff-line header"><span class="ln-body">${d(l)}</span></div>`);continue}if(l.startsWith("+"))o++,i+=`<div class="diff-line add"><span class="ln-no">${o}</span><span class="ln-body">${d(l)}</span></div>`;else if(l.startsWith("-"))a++,i+=`<div class="diff-line del"><span class="ln-no">${a}</span><span class="ln-body">${d(l)}</span></div>`;else if(l.startsWith("\\"))i+=`<div class="diff-line"><span class="ln-body">${d(l)}</span></div>`;else{if(!l&&!a&&!o)continue;a++,o++,i+=`<div class="diff-line"><span class="ln-no">${a}</span><span class="ln-no" style="margin-right:12px">${o}</span><span class="ln-body">${d(l)}</span></div>`}}return{html:i,lineCount:s.length}}let V=null;async function Se(e){const n=T;if(n)if(J(),Je()){h.style.display="none",f&&(X(),f.destroy(),f=null,V=m,m=null),A(null),R.style.display="none",I.style.display="none",we.style.display="flex",be.textContent=e,G.textContent="Loading\u2026",z.innerHTML="";try{const t=await fetch(y("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Xe(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);G.textContent="Error",z.innerHTML=`<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">Error: ${d(s)}</div>`}}else{v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
15
15
  <button class="diff-back">\u2190 Back</button>
16
16
  <span class="diff-filename">${d(e)}</span>
17
17
  <span class="diff-loading">Loading\u2026</span>
18
- </div></div>`,te(),v.classList.add("open"),U.classList.add("open");try{const t=await fetch(y("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Ge(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
18
+ </div></div>`,ie(),v.classList.add("open"),q.classList.add("open");try{const t=await fetch(y("/api/git/diff?path="+encodeURIComponent(n)+"&file="+encodeURIComponent(e)));if(!t.ok)throw new Error(await t.text());const s=await t.json();Ke(e,s.diff,s.stagedDiff)}catch(t){const s=t instanceof Error?t.message:String(t);v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
19
19
  <button class="diff-back">\u2190 Back</button>
20
20
  <span class="diff-filename">${d(e)}</span>
21
- </div><div class="diff-empty">Error loading diff: ${d(s)}</div></div>`}}}function Ge(e,n,t){if(!n&&!t){v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
21
+ </div><div class="diff-empty">Error loading diff: ${d(s)}</div></div>`}}}function Ke(e,n,t){if(!n&&!t){v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
22
22
  <button class="diff-back">\u2190 Back</button>
23
23
  <span class="diff-filename">${d(e)}</span>
24
- </div><div class="diff-empty">No diff content available</div></div>`;return}const{html:s,lineCount:i}=we(n,t);v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
24
+ </div><div class="diff-empty">No diff content available</div></div>`;return}const{html:s,lineCount:i}=Ce(n,t);v.innerHTML=`<div class="git-diff-view"><div class="diff-header">
25
25
  <button class="diff-back">\u2190 Back</button>
26
26
  <span class="diff-filename">${d(e)}</span>
27
27
  <span class="diff-filesize">${i} lines</span>
28
- </div><div class="diff-content">${s}</div></div>`,v.querySelector(".diff-back")?.addEventListener("click",()=>{v.innerHTML=he,te(),u&&v.querySelectorAll(".git-popover-item").forEach(l=>{const o=parseInt(l.dataset.idx||"0",10),c=u.files[o];!c||c.status==="D"||(l.style.cursor="pointer",l.addEventListener("click",()=>Le(c.path)))}),ye()})}function Oe(e,n,t){if(!n&&!t){O.textContent="No diff",z.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}const{html:s,lineCount:i}=we(n,t);ge.textContent=e,O.textContent=i+" lines",z.innerHTML=s}function ze(){ve.style.display="none",V?(h.style.display="",h.classList.add("terminal-pending"),N(V),V=null):m?(h.style.display="",h.classList.add("terminal-pending"),ke()):B.style.display="flex"}Fe.addEventListener("click",ze);function q(){v.classList.remove("open"),U.classList.remove("open")}U.addEventListener("click",q),document.addEventListener("keydown",e=>{e.key==="Escape"&&(q(),X())});async function Ee(e){try{const t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){const s=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(s.ok){const i=await s.json();u=i,P(i);return}}}catch{}u=null,P(null)}let J=null;function ke(){K(),m&&(Ee(m),J=setInterval(()=>{m&&Ee(m)},3e4))}function K(){J&&(clearInterval(J),J=null)}function Ue(){if(G=H.fsRoots||[],G.length===0){if(m){Ve(m);return}p.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}p.innerHTML="";for(const e of G){const n=document.createElement("div");n.className="file-tree-item",n.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${d(e)}`,n.addEventListener("click",()=>I(e)),p.appendChild(n)}}async function Ve(e){p.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const t=await(await fetch(y("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?I(t.path):p.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{p.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function I(e){C=e,p.innerHTML='<div class="file-tree-empty">Loading...</div>';const n=De(e);try{const s=await(await fetch(y("/api/fs/list?path="+encodeURIComponent(e)))).json();await n,p.innerHTML="",P(u),p.innerHTML+=je();const i=qe(e);if(i&&!G.some(c=>e===c)){const c=document.createElement("div");c.className="file-tree-item",c.innerHTML="..",c.addEventListener("click",()=>I(i)),p.appendChild(c)}if(s.dirs)for(const o of s.dirs){const c=o.split("/").pop()||o.split("\\").pop(),a=document.createElement("div");a.className="file-tree-item",a.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${d(c)}`,a.addEventListener("click",()=>I(o)),p.appendChild(a)}else p.innerHTML="";const l=s.files||[];for(const o of l){const c=o.split("/").pop()||o.split("\\").pop(),a=Ae(o);let r="",L="";if(a)switch(a){case"M":r="git-mod",L='<span class="git-file-status M">M</span>';break;case"A":r="git-add",L='<span class="git-file-status A">A</span>';break;case"D":r="git-del",L='<span class="git-file-status D">D</span>';break;case"?":r="git-untracked",L='<span class="git-file-status ?">?</span>';break;case"R":r="git-mod",L='<span class="git-file-status R">R</span>';break}const b=document.createElement("div");b.className="file-tree-item file"+(r?" "+r:""),b.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${L}${d(c)}`,b.addEventListener("click",()=>Je(o)),p.appendChild(b)}}catch{p.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function qe(e){const n=e.lastIndexOf("/");return n<=0?null:e.substring(0,n)}async function Je(e){F=e,h.style.display="none",f&&(K(),f.destroy(),f=null,m=null),P(null),Q(),B.style.display="none",$.style.display="flex",xe.textContent=e,w.textContent="Loading...",x.style.display="none";try{const n=await fetch(y("/api/file?path="+encodeURIComponent(e)));if(!n.ok){const s=await n.json();w.textContent=s.error||"Failed to load",R.value="";return}const t=await n.json();R.value=t.content,W=t.content,ae=t.content,w.textContent=t.size+" bytes",x.style.display=""}catch{w.textContent="Failed to load",R.value=""}}R.addEventListener("input",()=>{W=R.value}),x.addEventListener("click",async()=>{x.textContent="Saving...",x.disabled=!0;try{const e=await fetch(y("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:F,content:W})});if(!e.ok){const n=await e.json();w.textContent=n.error||"Save failed";return}ae=W,w.textContent="Saved"}catch{w.textContent="Save failed"}finally{x.disabled=!1,x.textContent="Save"}}),Se.addEventListener("click",async()=>{if(confirm("Delete "+F+"?"))try{const e=await fetch(y("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:F})});if(!e.ok){const n=await e.json();w.textContent=n.error||"Delete failed";return}$.style.display="none",B.style.display="flex",C&&I(C)}catch{w.textContent="Delete failed"}}),Me.addEventListener("click",()=>{$.style.display="none",B.style.display="flex",C&&I(C)}),Te.addEventListener("click",async()=>{const e=le.value.trim();if(!e)return;const n=C+"/"+e;try{const t=await fetch(y("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:n})});if(!t.ok){const s=await t.json();w.textContent=s.error||"Create failed";return}le.value="",C&&I(C)}catch{w.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=ce.classList.toggle("open");Z.classList.toggle("open",e)}),Z.addEventListener("click",()=>{ce.classList.remove("open"),Z.classList.remove("open")});let S="";const A=document.createElement("div");A.className="session-popover-backdrop",document.body.appendChild(A);const k=document.createElement("div");k.id="session-popover",k.className="session-popover",k.style.display="none",k.innerHTML=`<div class="sp-field">
28
+ </div><div class="diff-content">${s}</div></div>`,v.querySelector(".diff-back")?.addEventListener("click",()=>{v.innerHTML=Ee,ie(),u&&v.querySelectorAll(".git-popover-item").forEach(a=>{const o=parseInt(a.dataset.idx||"0",10),c=u.files[o];!c||c.status==="D"||(a.style.cursor="pointer",a.addEventListener("click",()=>Se(c.path)))}),Le()})}function Xe(e,n,t){if(!n&&!t){G.textContent="No diff",z.innerHTML='<div class="diff-empty" style="padding:24px;text-align:center;color:var(--panel-muted)">No diff content available</div>';return}const{html:s,lineCount:i}=Ce(n,t);be.textContent=e,G.textContent=i+" lines",z.innerHTML=s}function Ye(){we.style.display="none",V?(h.style.display="",h.classList.add("terminal-pending"),N(V),V=null):m?(h.style.display="",h.classList.add("terminal-pending"),xe()):I.style.display="flex"}Ve.addEventListener("click",Ye);function J(){v.classList.remove("open"),q.classList.remove("open")}q.addEventListener("click",J),document.addEventListener("keydown",e=>{e.key==="Escape"&&(J(),Y())});async function Ie(e){try{const t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();if(t.path){const s=await fetch("/api/git/status?path="+encodeURIComponent(t.path));if(s.ok){const i=await s.json();u=i,A(i);return}}}catch{}u=null,A(null)}let K=null;function xe(){X(),m&&(Ie(m),K=setInterval(()=>{m&&Ie(m)},3e4))}function X(){K&&(clearInterval(K),K=null)}function Ze(){if(U=H.fsRoots||[],U.length===0){if(m){Qe(m);return}r.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}r.innerHTML="";for(const e of U){const n=document.createElement("div");n.className="file-tree-item",n.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${d(e)}`,n.addEventListener("click",()=>S(e)),r.appendChild(n)}}async function Qe(e){r.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const t=await(await fetch(y("/api/fs/session-path?session="+encodeURIComponent(e)))).json();t.path?S(t.path):r.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{r.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function S(e){C=e,r.innerHTML='<div class="file-tree-empty">Loading...</div>';const n=ze(e);try{const s=await(await fetch(y("/api/fs/list?path="+encodeURIComponent(e)))).json();await n,r.innerHTML="",A(u),r.innerHTML+=qe();const i=et(e);if(i&&!U.some(c=>e===c)){const c=document.createElement("div");c.className="file-tree-item",c.innerHTML="..",c.addEventListener("click",()=>S(i)),r.appendChild(c)}if(s.dirs)for(const o of s.dirs){const c=o.split("/").pop()||o.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item",l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${d(c)}`,l.addEventListener("click",()=>S(o)),r.appendChild(l)}else r.innerHTML="";const a=s.files||[];for(const o of a){const c=o.split("/").pop()||o.split("\\").pop(),l=Ge(o);let p="",L="";if(l)switch(l){case"M":p="git-mod",L='<span class="git-file-status M">M</span>';break;case"A":p="git-add",L='<span class="git-file-status A">A</span>';break;case"D":p="git-del",L='<span class="git-file-status D">D</span>';break;case"?":p="git-untracked",L='<span class="git-file-status ?">?</span>';break;case"R":p="git-mod",L='<span class="git-file-status R">R</span>';break}const w=document.createElement("div");w.className="file-tree-item file"+(p?" "+p:""),w.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${L}${d(c)}`,w.addEventListener("click",()=>tt(o)),r.appendChild(w)}}catch{r.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function et(e){const n=e.lastIndexOf("/");return n<=0?null:e.substring(0,n)}async function tt(e){F=e,h.style.display="none",f&&(X(),f.destroy(),f=null,m=null),A(null),ne(),I.style.display="none",R.style.display="flex",Ne.textContent=e,b.textContent="Loading...",x.style.display="none";try{const n=await fetch(y("/api/file?path="+encodeURIComponent(e)));if(!n.ok){const s=await n.json();b.textContent=s.error||"Failed to load",$.value="";return}const t=await n.json();$.value=t.content,O=t.content,de=t.content,b.textContent=t.size+" bytes",x.style.display=""}catch{b.textContent="Failed to load",$.value=""}}$.addEventListener("input",()=>{O=$.value}),x.addEventListener("click",async()=>{x.textContent="Saving...",x.disabled=!0;try{const e=await fetch(y("/api/file"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:F,content:O})});if(!e.ok){const n=await e.json();b.textContent=n.error||"Save failed";return}de=O,b.textContent="Saved"}catch{b.textContent="Save failed"}finally{x.disabled=!1,x.textContent="Save"}}),_e.addEventListener("click",async()=>{if(confirm("Delete "+F+"?"))try{const e=await fetch(y("/api/file/delete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:F})});if(!e.ok){const n=await e.json();b.textContent=n.error||"Delete failed";return}R.style.display="none",I.style.display="flex",C&&S(C)}catch{b.textContent="Delete failed"}}),Ae.addEventListener("click",()=>{R.style.display="none",I.style.display="flex",C&&S(C)}),De.addEventListener("click",async()=>{const e=pe.value.trim();if(!e)return;const n=C+"/"+e;try{const t=await fetch(y("/api/file/touch"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:n})});if(!t.ok){const s=await t.json();b.textContent=s.error||"Create failed";return}pe.value="",C&&S(C)}catch{b.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=fe.classList.toggle("open");te.classList.toggle("open",e)}),te.addEventListener("click",()=>{fe.classList.remove("open"),te.classList.remove("open")});let B="";const D=document.createElement("div");D.className="session-popover-backdrop",document.body.appendChild(D);const k=document.createElement("div");k.id="session-popover",k.className="session-popover",k.style.display="none",k.innerHTML=`<div class="sp-field">
29
29
  <label>Rename session</label>
30
30
  <input type="text" id="sp-name" />
31
31
  </div>
@@ -34,5 +34,5 @@ import{initTerminal as Be}from"./terminal-core.js";const H=window.__TMUX_WEB_SHE
34
34
  <button class="btn" id="sp-cancel">Cancel</button>
35
35
  </div>
36
36
  <hr class="sp-divider" />
37
- <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(k);function Ke(e){const n=e.getBoundingClientRect();k.style.top=n.bottom+4+"px",k.style.left=Math.max(8,Math.min(n.left,window.innerWidth-240))+"px"}function Xe(e,n){S=e;const t=document.getElementById("sp-name");t.value=e,A.classList.add("open"),k.style.display="block",Ke(n),setTimeout(()=>t.focus(),50)}function D(){A.classList.remove("open"),k.style.display="none"}A.addEventListener("click",D),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===S){D();return}try{if(!(await fetch(y("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:S,newName:e})})).ok)return;D(),M(),m===S&&N(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+S+'"?'))try{if(!(await fetch(y("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:S})})).ok)return;D(),m===S&&(f&&(f.destroy(),f=null),m=null,h.style.display="none",B.style.display="flex"),M()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",D);const Ye={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r","ctrl-r":"","ctrl-c":"",btw:"/btw ",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{const n=e.target.closest("button[data-key]");if(!n||!f)return;const t=Ye[n.dataset.key];t&&(f.sendInput(t),f.focus())});const E=document.getElementById("mk-input"),Ce=document.getElementById("mk-send");function Ie(){if(!E||!f)return;const e=E.value;e&&(f.sendInput(e+"\r"),E.value="",E.rows=1,f.focus())}if(E&&(E.addEventListener("input",()=>{E.rows=1;const e=E.value.split(`
38
- `).length;E.rows=Math.min(e,4)}),E.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),Ie())})),Ce&&Ce.addEventListener("click",Ie),window.visualViewport){const e=document.querySelector(".app-layout"),n=()=>{e&&(e.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",n),window.addEventListener("orientationchange",()=>setTimeout(n,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}const ne=document.getElementById("process-panel"),j=document.getElementById("proc-list"),Ze=document.getElementById("proc-close"),se=document.getElementById("proc-backdrop"),Qe=document.getElementById("header-ram");function et(){j.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(y("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){j.innerHTML='<div class="proc-empty">No processes</div>';return}let n="";for(const t of e){const s=t.rss,i=s<1048576?(s/1024).toFixed(0)+"K":(s/1048576).toFixed(1)+"M";n+='<div class="proc-row" data-pid="'+t.pid+'"><span class="proc-mem">'+t.mem+'%</span><span class="proc-rss">'+i+'</span><span class="proc-cmd" title="'+d(t.command)+'">'+d(t.command)+'</span><button class="proc-kill" title="Kill PID '+t.pid+'">&times;</button></div>'}j.innerHTML=n;for(const t of j.querySelectorAll(".proc-row")){const s=t.querySelector(".proc-kill");s&&s.addEventListener("click",i=>{const l=i.target.closest(".proc-row");!l||!confirm("Kill PID "+l.dataset.pid+"?")||fetch(y("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(l.dataset.pid,10)})}).then(o=>o.json()).then(o=>{o.ok?l.style.opacity="0.3":alert("Failed: "+(o.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{j.innerHTML='<div class="proc-empty">Failed to load</div>'})}function tt(){ne.classList.add("open"),se.classList.add("open"),et()}function X(){ne.classList.remove("open"),se.classList.remove("open")}Qe.addEventListener("click",e=>{e.stopPropagation(),ne.classList.contains("open")?X():tt()}),Ze.addEventListener("click",X),se.addEventListener("click",X),window.__openSession=N,window.__refreshSidebar=M,M(),Q(),setInterval(()=>{document.visibilityState==="visible"&&M()},1e4);
37
+ <button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(k);function nt(e){const n=e.getBoundingClientRect();k.style.top=n.bottom+4+"px",k.style.left=Math.max(8,Math.min(n.left,window.innerWidth-240))+"px"}function st(e,n){B=e;const t=document.getElementById("sp-name");t.value=e,D.classList.add("open"),k.style.display="block",nt(n),setTimeout(()=>t.focus(),50)}function P(){D.classList.remove("open"),k.style.display="none"}D.addEventListener("click",P),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===B){P();return}try{if(!(await fetch(y("/api/sessions/rename"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:B,newName:e})})).ok)return;P(),M(),m===B&&N(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+B+'"?'))try{if(!(await fetch(y("/api/sessions/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:B})})).ok)return;P(),m===B&&(f&&(f.destroy(),f=null),m=null,h.style.display="none",I.style.display="flex"),M()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",P);const it={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ",enter:"\r","ctrl-r":"","ctrl-c":"",btw:"/btw ",yes:"yes\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{const n=e.target.closest("button[data-key]");if(!n||!f)return;const t=it[n.dataset.key];t&&(f.sendInput(t),f.focus())});const E=document.getElementById("mk-input"),Be=document.getElementById("mk-send");function Me(){if(!E||!f)return;const e=E.value;e&&(f.sendInput(e+"\r"),E.value="",E.rows=1,f.focus())}if(E&&(E.addEventListener("input",()=>{E.rows=1;const e=E.value.split(`
38
+ `).length;E.rows=Math.min(e,4)}),E.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),Me())})),Be&&Be.addEventListener("click",Me),window.visualViewport){const e=document.querySelector(".app-layout"),n=()=>{e&&(e.style.height=window.visualViewport.height+"px"),window.scrollTo(0,0),document.documentElement.scrollTop=0,document.body.scrollTop=0};window.visualViewport.addEventListener("resize",n),window.addEventListener("orientationchange",()=>setTimeout(n,100)),document.addEventListener("touchstart",()=>{(window.scrollY>0||document.documentElement.scrollTop>0)&&window.scrollTo(0,0)},{passive:!0})}const oe=document.getElementById("process-panel"),j=document.getElementById("proc-list"),ot=document.getElementById("proc-close"),ae=document.getElementById("proc-backdrop"),at=document.getElementById("header-ram");function lt(){j.innerHTML='<div class="proc-empty">Loading\u2026</div>',fetch(y("/api/system/processes")).then(e=>e.json()).then(e=>{if(!Array.isArray(e)||!e.length){j.innerHTML='<div class="proc-empty">No processes</div>';return}let n="";for(const t of e){const s=t.rss,i=s<1048576?(s/1024).toFixed(0)+"K":(s/1048576).toFixed(1)+"M";n+='<div class="proc-row" data-pid="'+t.pid+'"><span class="proc-mem">'+t.mem+'%</span><span class="proc-rss">'+i+'</span><span class="proc-cmd" title="'+d(t.command)+'">'+d(t.command)+'</span><button class="proc-kill" title="Kill PID '+t.pid+'">&times;</button></div>'}j.innerHTML=n;for(const t of j.querySelectorAll(".proc-row")){const s=t.querySelector(".proc-kill");s&&s.addEventListener("click",i=>{const a=i.target.closest(".proc-row");!a||!confirm("Kill PID "+a.dataset.pid+"?")||fetch(y("/api/system/kill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:parseInt(a.dataset.pid,10)})}).then(o=>o.json()).then(o=>{o.ok?a.style.opacity="0.3":alert("Failed: "+(o.error||"unknown"))}).catch(()=>alert("Network error"))})}}).catch(()=>{j.innerHTML='<div class="proc-empty">Failed to load</div>'})}function ct(){oe.classList.add("open"),ae.classList.add("open"),lt()}function Y(){oe.classList.remove("open"),ae.classList.remove("open")}at.addEventListener("click",e=>{e.stopPropagation(),oe.classList.contains("open")?Y():ct()}),ot.addEventListener("click",Y),ae.addEventListener("click",Y);const rt=2e3;let W=null,le=null;const Z=new Map,Q=new Map;function dt(e,n){return(n?"a:"+n+":":"l:")+e}function pt(e,n,t){const s=dt(e,n),i=Q.get(s);Q.set(s,{session:e,agentId:n,state:t,doneUntil:t==="idle"&&i?.state==="working"?Date.now()+rt:void 0});const a=n?'.session-item[data-agent="'+CSS.escape(n)+'"][data-session="'+CSS.escape(e)+'"]':'.session-item[data-session="'+CSS.escape(e)+'"]:not([data-agent])',o=r.querySelector(a);o&&Te(o,s,Q.get(s))}function Te(e,n,t){const s=Z.get(n);s&&(clearTimeout(s),Z.delete(n)),e.classList.remove("working","just-done"),t.state==="working"?e.classList.add("working"):t.doneUntil&&Date.now()<t.doneUntil&&(e.classList.add("just-done"),Z.set(n,setTimeout(()=>{e.classList.remove("just-done"),Z.delete(n)},Math.max(0,t.doneUntil-Date.now()))))}function He(){for(const[e,n]of Q){const t=n.agentId?'.session-item[data-agent="'+CSS.escape(n.agentId)+'"][data-session="'+CSS.escape(n.session)+'"]':'.session-item[data-session="'+CSS.escape(n.session)+'"]:not([data-agent])',s=r.querySelector(t);s&&Te(s,e,n)}}function Re(){if(W||le)return;const e=localStorage.getItem("tmux-web-token");let t=(location.protocol==="https:"?"wss:":"ws:")+"//"+location.host+"/ws/activity";e&&(t+="?token="+encodeURIComponent(e));try{const s=new WebSocket(t);W=s,s.onmessage=i=>{try{const a=JSON.parse(String(i.data));a&&a.type==="session.activity"&&typeof a.session=="string"&&pt(a.session,a.agentId||void 0,a.state==="working"?"working":"idle")}catch{}},s.onclose=()=>{W===s&&(W=null,le=setTimeout(()=>{le=null,Re()},5e3))},s.onerror=()=>{try{s.close()}catch{}}}catch{W=null}}window.__openSession=N,window.__refreshSidebar=M,M(),ne(),He(),Re(),setInterval(()=>{document.visibilityState==="visible"&&M()},1e4);
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import{chmodSync as ue,existsSync as D,readdirSync as de,statSync as me}from"node:fs";import{createRequire as J}from"node:module";import{hostname as U}from"node:os";const fe=J(import.meta.url),{version:pe}=fe("../package.json");import O from"node:path";import{fileURLToPath as he}from"node:url";import{serve as ge}from"@hono/node-server";import{WebSocketServer as ye,WebSocket as H}from"ws";try{const e=O.dirname(J(import.meta.url).resolve("node-pty/package.json")),n=O.join(e,"prebuilds");if(D(n))for(const s of de(n)){const t=O.join(n,s,"spawn-helper");D(t)&&!(me(t).mode&73)&&ue(t,493)}}catch{}import{loadSecurityConfig as ke}from"./lib/security-config.js";import{TokenStore as _e,hashPassword as Te,verifyPassword as we,validatePassword as Se}from"./lib/auth.js";import{RateLimiter as Ae}from"./lib/rateLimiter.js";import{audit as d}from"./lib/auditLog.js";import{db as N}from"./lib/db.js";import{handleClientMessage as z}from"./lib/ws-message.js";import{loadDotEnv as Ce}from"./lib/load-env.js";import{listSessions as ve,resolveTmuxSocketPath as be}from"./sessions.js";import{cmdSetup as xe,cmdTheme as Ie,printUsage as G,printVersion as Re}from"./lib/cli.js";import{readSettings as Pe}from"./lib/settings.js";import{readActiveTheme as Me}from"./lib/theme-store.js";import{buildApp as V}from"./lib/build-app.js";import{attachTerminal as Oe,killAllAttachedPtys as Ne}from"./lib/attach-terminal.js";import{acquireControlClient as X,killAllControlClients as $e}from"./lib/tmux-control.js";import{getSessionPaneTarget as Y,capturePaneTail as K,capturePaneHistoryChunk as Q,toCrlf as Z}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Le}from"./lib/terminal-config.js";import{AgentRegistry as Ue}from"./lib/agent-registry.js";import{AgentChannel as Ee,MAX_RELAY_CONNS_PER_AGENT as Fe}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as He,listAgentTokens as je}from"./lib/agent-tokens.js";import{saveSecurityConfig as Be}from"./lib/security-config.js";import{readFederationConfig as We,writeFederationConfig as De}from"./lib/federation-config.js";import{startAgentClient as Je}from"./lib/agent-client.js";Ce();const E=Le(),h=ke(),k=new _e,A=new Ae;let j=!1;const ze="tmux-web-token";function B(e){if(h.security.trustProxy){const n=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(n)return n}return e.socket.remoteAddress||"unknown"}function W(e){const n=e.headers.authorization;if(typeof n=="string"&&n.startsWith("Bearer "))return n.slice(7);const s=e.headers.cookie;if(typeof s!="string")return null;const t=s.match(new RegExp(`(?:^|;\\s*)${ze}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function Lt(e){const n=W(e);return n?k.validateToken(n):null}const P=new Map;function Ge(e){let n=0;for(const s of P.values())s.ip===e&&n++;return n}function v(e,n,s){try{e.close(n,s)}catch{}}function q(e,n){e.readyState===H.OPEN&&e.send(JSON.stringify(n))}const ee=process.argv.slice(2),$=ee;if($.length>0){const[e,n]=$;switch(e){case"setup":await xe($),process.exit(0);case"theme":await Ie($.slice(1)),process.exit(0);case"agent":{const s=O.join(O.dirname(he(import.meta.url)),"agent.js"),{spawn:t}=await import("node:child_process"),m=t(process.execPath,[s,...ee],{stdio:"inherit"});m.on("error",r=>{console.error(r),process.exit(1)}),m.on("exit",r=>process.exit(r??0)),await new Promise(()=>{})}case"agent-token":await st($.slice(1)),process.exit(0);case"help":case"--help":case"-h":G(),process.exit(0);case"-V":case"--version":case"-v":Re(),process.exit(0);default:console.error(`unknown argument: ${e}`),G(),process.exit(1)}}function I(e,n){e.readyState===H.OPEN&&e.send(JSON.stringify(n))}const te=new Set;await N.read(),N.data.sessionAccess??=[],N.data.pinnedViews??=[],N.data.watchedPanes??=[],N.data.quickCommands??=[];const ne=await Pe();let Ve=await Me();const y=new Ue({onChange:()=>{}}),T=new Map;function oe(e){return y.get(e)?T.get(e)??null:null}const re={activeTheme:Ve,settingUpPassword:!1},Xe=V({mode:"hub",securityConfig:h,tokenStore:k,rateLimiter:A,settings:ne,terminalBufferConfig:E,state:re,federation:{getConfig:()=>x,setConfig:async e=>{x=e,await De(e),ae(e)},status:Ke,hostname:U()},hub:{agents:y,getChannel:oe}}),Ye=V({mode:"agent",securityConfig:h,tokenStore:k,rateLimiter:A,settings:ne,terminalBufferConfig:E,state:re});let x=await We(),M=null;function se(){M&&(M.stop(),M=null)}function ae(e){se(),!(!e.enabled||!e.hub||!e.token)&&(M=Je({hub:e.hub,token:e.token,name:e.name?.trim()||U(),version:pe,app:Ye,listSessions:ve,terminalBufferConfig:E,getSessionPaneTarget:Y,capturePaneTail:K,capturePaneHistoryChunk:Q,toCrlf:Z,handleClientMessage:z,acquireControlClient:X}))}function Ke(){return x.enabled?M?M.status():{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}:{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}}ae(x);const Qe=parseInt(process.env.PORT||"21000",10),Ze=ge({fetch:Xe.fetch,port:Qe,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),L=new ye({noServer:!0});function R(e,n,s){e.write(`HTTP/1.1 ${n} ${s}\r
2
+ import{chmodSync as pe,existsSync as z,readdirSync as he,statSync as ge}from"node:fs";import{createRequire as G}from"node:module";import{hostname as U}from"node:os";const ye=G(import.meta.url),{version:ke}=ye("../package.json");import N from"node:path";import{fileURLToPath as Te}from"node:url";import{serve as _e}from"@hono/node-server";import{WebSocketServer as ve,WebSocket as E}from"ws";try{const e=N.dirname(G(import.meta.url).resolve("node-pty/package.json")),n=N.join(e,"prebuilds");if(z(n))for(const o of he(n)){const t=N.join(n,o,"spawn-helper");z(t)&&!(ge(t).mode&73)&&pe(t,493)}}catch{}import{loadSecurityConfig as we}from"./lib/security-config.js";import{TokenStore as Se,hashPassword as Ae,verifyPassword as Ce,validatePassword as be}from"./lib/auth.js";import{RateLimiter as xe}from"./lib/rateLimiter.js";import{audit as d}from"./lib/auditLog.js";import{db as $}from"./lib/db.js";import{handleClientMessage as V}from"./lib/ws-message.js";import{loadDotEnv as Ie}from"./lib/load-env.js";import{listSessions as Pe,resolveTmuxSocketPath as Re}from"./sessions.js";import{cmdSetup as Me,cmdTheme as Oe,printUsage as X,printVersion as Ne}from"./lib/cli.js";import{readSettings as $e}from"./lib/settings.js";import{readActiveTheme as Le}from"./lib/theme-store.js";import{buildApp as Y}from"./lib/build-app.js";import{attachTerminal as Ue,killAllAttachedPtys as Ee}from"./lib/attach-terminal.js";import{acquireControlClient as K,killAllControlClients as Fe}from"./lib/tmux-control.js";import{getSessionPaneTarget as Q,capturePaneTail as Z,capturePaneHistoryChunk as q,toCrlf as ee}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as He}from"./lib/terminal-config.js";import{AgentRegistry as je}from"./lib/agent-registry.js";import{AgentChannel as Be,MAX_RELAY_CONNS_PER_AGENT as We}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as Je,listAgentTokens as De}from"./lib/agent-tokens.js";import{saveSecurityConfig as ze}from"./lib/security-config.js";import{readFederationConfig as Ge,writeFederationConfig as Ve}from"./lib/federation-config.js";import{startAgentClient as Xe}from"./lib/agent-client.js";import{ActivityProbe as Ye}from"./lib/activity-probe.js";Ie();const F=He(),h=we(),k=new Se,A=new xe;let J=!1;const Ke="tmux-web-token";function H(e){if(h.security.trustProxy){const n=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(n)return n}return e.socket.remoteAddress||"unknown"}function j(e){const n=e.headers.authorization;if(typeof n=="string"&&n.startsWith("Bearer "))return n.slice(7);const o=e.headers.cookie;if(typeof o!="string")return null;const t=o.match(new RegExp(`(?:^|;\\s*)${Ke}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function Bt(e){const n=j(e);return n?k.validateToken(n):null}const R=new Map;function Qe(e){let n=0;for(const o of R.values())o.ip===e&&n++;return n}function b(e,n,o){try{e.close(n,o)}catch{}}function te(e,n){e.readyState===E.OPEN&&e.send(JSON.stringify(n))}const ne=process.argv.slice(2),L=ne;if(L.length>0){const[e,n]=L;switch(e){case"setup":await Me(L),process.exit(0);case"theme":await Oe(L.slice(1)),process.exit(0);case"agent":{const o=N.join(N.dirname(Te(import.meta.url)),"agent.js"),{spawn:t}=await import("node:child_process"),f=t(process.execPath,[o,...ne],{stdio:"inherit"});f.on("error",s=>{console.error(s),process.exit(1)}),f.on("exit",s=>process.exit(s??0)),await new Promise(()=>{})}case"agent-token":await ut(L.slice(1)),process.exit(0);case"help":case"--help":case"-h":X(),process.exit(0);case"-V":case"--version":case"-v":Ne(),process.exit(0);default:console.error(`unknown argument: ${e}`),X(),process.exit(1)}}function P(e,n){e.readyState===E.OPEN&&e.send(JSON.stringify(n))}const oe=new Set;await $.read(),$.data.sessionAccess??=[],$.data.pinnedViews??=[],$.data.watchedPanes??=[],$.data.quickCommands??=[];const re=await $e();let Ze=await Le();const y=new je({onChange:()=>{}}),v=new Map;function se(e){return y.get(e)?v.get(e)??null:null}const ae={activeTheme:Ze,settingUpPassword:!1},qe=Y({mode:"hub",securityConfig:h,tokenStore:k,rateLimiter:A,settings:re,terminalBufferConfig:F,state:ae,federation:{getConfig:()=>x,setConfig:async e=>{x=e,await Ve(e),ce(e)},status:tt,hostname:U()},hub:{agents:y,getChannel:se}}),et=Y({mode:"agent",securityConfig:h,tokenStore:k,rateLimiter:A,settings:re,terminalBufferConfig:F,state:ae});let x=await Ge(),M=null;function ie(){M&&(M.stop(),M=null)}function ce(e){ie(),!(!e.enabled||!e.hub||!e.token)&&(M=Xe({hub:e.hub,token:e.token,name:e.name?.trim()||U(),version:ke,app:et,listSessions:Pe,terminalBufferConfig:F,getSessionPaneTarget:Q,capturePaneTail:Z,capturePaneHistoryChunk:q,toCrlf:ee,handleClientMessage:V,acquireControlClient:K}))}function tt(){return x.enabled?M?M.status():{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}:{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}}ce(x);const le=new Ye,B=new Map;setInterval(()=>{(async()=>{try{const e=await le.scan(),n=[];for(const[o,t]of e)B.get(o)!==t&&n.push({session:o,state:t});for(const o of B.keys())e.has(o)||n.push({session:o,state:"idle"});B.clear();for(const[o,t]of e)B.set(o,t);for(const o of n)ue({type:"session.activity",session:o.session,state:o.state})}catch{}})()},le.scanMs);const nt=parseInt(process.env.PORT||"21000",10),ot=_e({fetch:qe.fetch,port:nt,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),O=new ve({noServer:!0}),D=new Set;function ue(e){const n=JSON.stringify(e);for(const o of D)o.readyState===E.OPEN&&o.send(n)}function I(e,n,o){e.write(`HTTP/1.1 ${n} ${o}\r
3
3
  \r
4
- `),e.destroy()}Ze.on("upgrade",(e,n,s)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),m=t.pathname;if(m==="/agent/ws"){nt(e,n,s);return}const r=m.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(r){qe(e,n,s,decodeURIComponent(r[1]),decodeURIComponent(r[2]));return}const a=m.match(/^\/ws\/(.+)$/);if(!a){n.destroy();return}const c=B(e),l=e.headers.origin,p=h.security.allowedOrigins;if(l&&!(l===`http://${e.headers.host}`||l===`https://${e.headers.host}`)&&(p.length===0||!p.includes(l))){d("ws_rejected_origin",{ip:c,origin:l}),R(n,403,"Origin not allowed");return}const f=Ge(c);if(f>=h.security.maxConnectionsPerIp){d("ws_rejected_per_ip_cap",{ip:c,liveFromIp:f}),R(n,429,"Too many connections");return}const g=W(e)||t.searchParams.get("token")||"",_=g?k.validateToken(g):null;L.handleUpgrade(e,n,s,S=>{L.emit("connection",S,e,decodeURIComponent(a[1]),c,_)})}),L.on("connection",(e,n,s,t,m)=>{const r={ws:e,ip:t,authenticated:!1,authTimeout:null},a=(()=>{try{const i=new URL(n.url||"/",`http://${n.headers.host}`).searchParams,o=Number(i.get("cols")),u=Number(i.get("rows"));if(Number.isInteger(o)&&Number.isInteger(u)&&o>0&&u>0&&o<=1e3&&u<=1e3)return{cols:o,rows:u}}catch{}})();P.set(e,r),d("ws_connected",{ip:t});let c=null,l=null;function p(i,o){r.authenticated=!0,r.authTimeout&&(clearTimeout(r.authTimeout),r.authTimeout=null),d("auth_success",{ip:t,method:i,tokenName:o}),I(e,{type:"auth.ok",setupMode:!h.passwordHash}),g()}function f(i,o={}){I(e,{type:"auth.failed",message:i,...o})}function g(){c||(c=Oe(s,{send:i=>I(e,i),onMessage:i=>{l=i},onClose:i=>{_=i}},{terminalBufferConfig:E,getSessionPaneTarget:Y,capturePaneTail:K,capturePaneHistoryChunk:Q,toCrlf:Z,handleClientMessage:z,acquireControlClient:X,initialCols:a?.cols,initialRows:a?.rows,tmuxSocketPath:be()??void 0,onPtyExit:(i,o)=>v(e,1e3,"pty exited"),onSpawnError:(i,o)=>v(e,1011,"pty spawn failed")}))}let _=null;async function S(i){let o;try{o=JSON.parse(i)}catch{return!1}if(o.type==="auth.token"&&typeof o.token=="string"){const u=k.validateToken(o.token);if(!u){d("token_auth_failed",{ip:t});const w=A.recordFailure(t);return w.permanentLock&&(k.revokeAll(),d("permanent_lock",{ip:t,failures:w.failures})),f("Invalid or expired token",{permanentLock:w.permanentLock,retryAfterMs:w.retryAfterMs}),!0}return k.touch(u.tokenHash),p("token",u.name),!0}if(o.type==="auth"&&typeof o.password=="string"){const u=A.check(t);if(!u.allowed)return d("rate_limited",{ip:t,retryAfterMs:u.retryAfterMs,permanentLock:u.permanentLock}),f("Too many attempts",{permanentLock:u.permanentLock,retryAfterMs:u.retryAfterMs}),!0;if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!rt(t))return d("setup_rejected_remote",{ip:t}),f("First-run setup must be performed from localhost"),!0;if(j)return f("Password setup in progress"),!0;const C=Se(o.password);if(C)return f(C),!0;j=!0;try{h.passwordHash=await Te(o.password),Be(h),d("password_set",{ip:t})}finally{j=!1}const{stored:F,plaintext:le}=k.createAccessToken("setup",h.security.tokenTtlDays);return I(e,{type:"auth.ok",setupMode:!0,token:le}),d("token_created",{ip:t,name:F.name,tokenId:F.id}),d("auth_success",{ip:t,method:"password",tokenName:F.name}),g(),!0}if(!h.passwordHash)return f("Server not configured"),!0;if(!await we(o.password,h.passwordHash)){const C=A.recordFailure(t);return d("auth_failed",{ip:t,method:"password",failures:C.failures,permanentLock:C.permanentLock}),C.permanentLock&&(k.revokeAll(),d("permanent_lock",{ip:t,failures:C.failures})),f("Incorrect password",{permanentLock:C.permanentLock,retryAfterMs:C.retryAfterMs}),!0}A.recordSuccess(t);const b=`ws-${t}`,{plaintext:ce}=k.createAccessToken(b,h.security.tokenTtlDays);return p("password",b),I(e,{type:"auth.ok",setupMode:!1,token:ce}),!0}return!1}m?(k.touch(m.tokenHash),p("token",m.name)):(I(e,{type:"auth.required",setupMode:!h.passwordHash}),r.authTimeout=setTimeout(()=>{r.authenticated||(d("auth_timeout",{ip:t}),f("Authentication timeout"),v(e,4e3,"Auth timeout"))},h.security.authTimeoutMs)),e.on("message",async i=>{const o=typeof i=="string"?i:i.toString("utf-8");if(!r.authenticated){if(o.length>1e6){I(e,{type:"auth.failed",message:"Message too large"});return}await S(o)||f("Authentication required");return}if(l)try{l(o)}catch{}}),e.on("close",()=>{r.authTimeout&&clearTimeout(r.authTimeout),P.delete(e),d("ws_disconnected",{ip:t}),_?.(),c&&(c.dispose(),c=null)}),e.on("error",()=>{r.authTimeout&&clearTimeout(r.authTimeout),P.delete(e),_?.(),c&&(c.dispose(),c=null)})});function qe(e,n,s,t,m){const r=B(e),a=new URL(e.url||"/",`http://${e.headers.host}`),c=W(e)||a.searchParams.get("token")||"";if(!(c?k.validateToken(c):null)){d("ws_rejected_remote_auth",{ip:r,agentId:t}),R(n,401,"Unauthorized");return}const p=oe(t);if(!p){d("ws_rejected_remote_offline",{ip:r,agentId:t}),R(n,503,"Agent offline");return}if(p.relayCount>=Fe){R(n,429,"Too many connections to agent");return}const f=e.headers.origin,g=h.security.allowedOrigins;if(f&&!(f===`http://${e.headers.host}`||f===`https://${e.headers.host}`)&&(g.length===0||!g.includes(f))){R(n,403,"Origin not allowed");return}const _=tt(),S=(()=>{try{const i=new URL(e.url||"/",`http://${e.headers.host}`).searchParams,o=Number(i.get("cols")),u=Number(i.get("rows"));if(Number.isInteger(o)&&Number.isInteger(u)&&o>0&&u>0&&o<=1e3&&u<=1e3)return{cols:o,rows:u}}catch{}})();L.handleUpgrade(e,n,s,i=>{if(p.registerRelay(_,m,i),y.bumpRelayConns(t,1),!p.attach(_,m,S)){i.send(JSON.stringify({type:"data",data:`\r
4
+ `),e.destroy()}ot.on("upgrade",(e,n,o)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),f=t.pathname;if(f==="/ws/activity"){const w=H(e),a=j(e)||t.searchParams.get("token")||"";if(!(a?k.validateToken(a):null)){I(n,401,"Unauthorized");return}O.handleUpgrade(e,n,o,c=>{c.on("close",()=>D.delete(c)),D.add(c)});return}if(f==="/agent/ws"){it(e,n,o);return}const s=f.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(s){rt(e,n,o,decodeURIComponent(s[1]),decodeURIComponent(s[2]));return}const i=f.match(/^\/ws\/(.+)$/);if(!i){n.destroy();return}const l=H(e),u=e.headers.origin,p=h.security.allowedOrigins;if(u&&!(u===`http://${e.headers.host}`||u===`https://${e.headers.host}`)&&(p.length===0||!p.includes(u))){d("ws_rejected_origin",{ip:l,origin:u}),I(n,403,"Origin not allowed");return}const m=Qe(l);if(m>=h.security.maxConnectionsPerIp){d("ws_rejected_per_ip_cap",{ip:l,liveFromIp:m}),I(n,429,"Too many connections");return}const g=j(e)||t.searchParams.get("token")||"",_=g?k.validateToken(g):null;O.handleUpgrade(e,n,o,w=>{O.emit("connection",w,e,decodeURIComponent(i[1]),l,_)})}),O.on("connection",(e,n,o,t,f)=>{const s={ws:e,ip:t,authenticated:!1,authTimeout:null},i=(()=>{try{const a=new URL(n.url||"/",`http://${n.headers.host}`).searchParams,r=Number(a.get("cols")),c=Number(a.get("rows"));if(Number.isInteger(r)&&Number.isInteger(c)&&r>0&&c>0&&r<=1e3&&c<=1e3)return{cols:r,rows:c}}catch{}})();R.set(e,s),d("ws_connected",{ip:t});let l=null,u=null;function p(a,r){s.authenticated=!0,s.authTimeout&&(clearTimeout(s.authTimeout),s.authTimeout=null),d("auth_success",{ip:t,method:a,tokenName:r}),P(e,{type:"auth.ok",setupMode:!h.passwordHash}),g()}function m(a,r={}){P(e,{type:"auth.failed",message:a,...r})}function g(){l||(l=Ue(o,{send:a=>P(e,a),onMessage:a=>{u=a},onClose:a=>{_=a}},{terminalBufferConfig:F,getSessionPaneTarget:Q,capturePaneTail:Z,capturePaneHistoryChunk:q,toCrlf:ee,handleClientMessage:V,acquireControlClient:K,initialCols:i?.cols,initialRows:i?.rows,tmuxSocketPath:Re()??void 0,onPtyExit:(a,r)=>b(e,1e3,"pty exited"),onSpawnError:(a,r)=>b(e,1011,"pty spawn failed")}))}let _=null;async function w(a){let r;try{r=JSON.parse(a)}catch{return!1}if(r.type==="auth.token"&&typeof r.token=="string"){const c=k.validateToken(r.token);if(!c){d("token_auth_failed",{ip:t});const T=A.recordFailure(t);return T.permanentLock&&(k.revokeAll(),d("permanent_lock",{ip:t,failures:T.failures})),m("Invalid or expired token",{permanentLock:T.permanentLock,retryAfterMs:T.retryAfterMs}),!0}return k.touch(c.tokenHash),p("token",c.name),!0}if(r.type==="auth"&&typeof r.password=="string"){const c=A.check(t);if(!c.allowed)return d("rate_limited",{ip:t,retryAfterMs:c.retryAfterMs,permanentLock:c.permanentLock}),m("Too many attempts",{permanentLock:c.permanentLock,retryAfterMs:c.retryAfterMs}),!0;if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!lt(t))return d("setup_rejected_remote",{ip:t}),m("First-run setup must be performed from localhost"),!0;if(J)return m("Password setup in progress"),!0;const C=be(r.password);if(C)return m(C),!0;J=!0;try{h.passwordHash=await Ae(r.password),ze(h),d("password_set",{ip:t})}finally{J=!1}const{stored:W,plaintext:me}=k.createAccessToken("setup",h.security.tokenTtlDays);return P(e,{type:"auth.ok",setupMode:!0,token:me}),d("token_created",{ip:t,name:W.name,tokenId:W.id}),d("auth_success",{ip:t,method:"password",tokenName:W.name}),g(),!0}if(!h.passwordHash)return m("Server not configured"),!0;if(!await Ce(r.password,h.passwordHash)){const C=A.recordFailure(t);return d("auth_failed",{ip:t,method:"password",failures:C.failures,permanentLock:C.permanentLock}),C.permanentLock&&(k.revokeAll(),d("permanent_lock",{ip:t,failures:C.failures})),m("Incorrect password",{permanentLock:C.permanentLock,retryAfterMs:C.retryAfterMs}),!0}A.recordSuccess(t);const S=`ws-${t}`,{plaintext:fe}=k.createAccessToken(S,h.security.tokenTtlDays);return p("password",S),P(e,{type:"auth.ok",setupMode:!1,token:fe}),!0}return!1}f?(k.touch(f.tokenHash),p("token",f.name)):(P(e,{type:"auth.required",setupMode:!h.passwordHash}),s.authTimeout=setTimeout(()=>{s.authenticated||(d("auth_timeout",{ip:t}),m("Authentication timeout"),b(e,4e3,"Auth timeout"))},h.security.authTimeoutMs)),e.on("message",async a=>{const r=typeof a=="string"?a:a.toString("utf-8");if(!s.authenticated){if(r.length>1e6){P(e,{type:"auth.failed",message:"Message too large"});return}await w(r)||m("Authentication required");return}if(u)try{u(r)}catch{}}),e.on("close",()=>{s.authTimeout&&clearTimeout(s.authTimeout),R.delete(e),d("ws_disconnected",{ip:t}),_?.(),l&&(l.dispose(),l=null)}),e.on("error",()=>{s.authTimeout&&clearTimeout(s.authTimeout),R.delete(e),_?.(),l&&(l.dispose(),l=null)})});function rt(e,n,o,t,f){const s=H(e),i=new URL(e.url||"/",`http://${e.headers.host}`),l=j(e)||i.searchParams.get("token")||"";if(!(l?k.validateToken(l):null)){d("ws_rejected_remote_auth",{ip:s,agentId:t}),I(n,401,"Unauthorized");return}const p=se(t);if(!p){d("ws_rejected_remote_offline",{ip:s,agentId:t}),I(n,503,"Agent offline");return}if(p.relayCount>=We){I(n,429,"Too many connections to agent");return}const m=e.headers.origin,g=h.security.allowedOrigins;if(m&&!(m===`http://${e.headers.host}`||m===`https://${e.headers.host}`)&&(g.length===0||!g.includes(m))){I(n,403,"Origin not allowed");return}const _=at(),w=(()=>{try{const a=new URL(e.url||"/",`http://${e.headers.host}`).searchParams,r=Number(a.get("cols")),c=Number(a.get("rows"));if(Number.isInteger(r)&&Number.isInteger(c)&&r>0&&c>0&&r<=1e3&&c<=1e3)return{cols:r,rows:c}}catch{}})();O.handleUpgrade(e,n,o,a=>{if(p.registerRelay(_,f,a),y.bumpRelayConns(t,1),!p.attach(_,f,w)){a.send(JSON.stringify({type:"data",data:`\r
5
5
  \x1B[31magent offline\x1B[0m\r
6
- `})),i.close(1011,"agent offline"),p.relayClosed(_),y.bumpRelayConns(t,-1);return}i.on("message",u=>{const w=typeof u=="string"?u:u.toString("utf-8");let b;try{b=JSON.parse(w)}catch{return}(b.type==="input"||b.type==="resize"||b.type==="load_history")&&p.sendToAgent(_,b)});const o=()=>{p.relayClosed(_),y.bumpRelayConns(t,-1)};i.on("close",o),i.on("error",o)})}let et=1;function tt(){return et++}function nt(e,n,s){const t=B(e),m=e.headers.origin;if(m){d("agent_rejected_origin",{ip:t,origin:m}),R(n,403,"Origin not allowed");return}L.handleUpgrade(e,n,s,r=>{let a=null,c=setTimeout(()=>{d("agent_hello_timeout",{ip:t}),v(r,4e3,"hello timeout")},1e4);r.on("message",(l,p)=>{if(a){y.touch(a.agentId),p?a.handleData(Array.isArray(l)?Buffer.concat(l):Buffer.from(l)):a.handleMessage(typeof l=="string"?l:Buffer.from(l).toString("utf-8"));return}const f=typeof l=="string"?l:l.toString("utf-8");let g;try{g=JSON.parse(f)}catch{v(r,1008,"bad hello");return}if(g.type!=="hello"||typeof g.token!="string"){v(r,1008,"expected hello");return}if(!A.check(t).allowed){d("agent_rate_limited",{ip:t}),v(r,429,"Too many attempts");return}const S=He(g.token);if(!S){A.recordFailure(t),d("agent_bad_token",{ip:t}),q(r,{type:"hello_err",reason:"invalid token"}),v(r,4003,"invalid token");return}const i=typeof g.name=="string"&&g.name.trim()?g.name.trim().slice(0,64):"agent",o=S.id;if(!y.register(o,i,r,S.tokenHash)){d("agent_cap_reached",{ip:t}),q(r,{type:"hello_err",reason:"too many agents"}),v(r,4004,"too many agents");return}clearTimeout(c),A.recordSuccess(t),d("agent_connected",{ip:t,agentId:o,name:i}),a=new Ee(r,o,{onSessions:w=>y.setSessions(o,w),onClose:w=>{T.get(o)===w&&T.delete(o)}}),T.set(o,a),y.touch(o),r.send(JSON.stringify({type:"hello_ok",agentId:o})),a.sendSessionsReq()}),r.on("close",()=>{a&&(a.dispose(),T.delete(a.agentId),y.unregister(a.agentId),d("agent_disconnected",{ip:t,agentId:a.agentId})),clearTimeout(c)}),r.on("error",()=>{a&&(a.dispose(),T.delete(a.agentId),y.unregister(a.agentId)),clearTimeout(c)})})}const ot=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=y.pruneOffline(ot);for(const s of e){const t=y.peek(s);t&&t.ws.readyState===H.CLOSED&&(y.unregister(s),T.get(s)?.dispose(),T.delete(s))}const n=new Set(je().map(s=>s.tokenHash));for(const[s,t]of T){const m=y.peek(s);if(!(!m||n.has(m.tokenHash))){d("agent_token_revoked",{agentId:s}),t.dispose(),T.delete(s),y.unregister(s);try{m.ws.close(4003,"token revoked")}catch{}}}k.purgeExpired()},10*1e3).unref();function ie(){se(),$e(),Ne();for(const e of T.values())e.dispose();T.clear();for(const[e,n]of P){try{e.close(1001,"Server shutting down")}catch{}n.authTimeout&&clearTimeout(n.authTimeout)}P.clear();for(const e of te)try{e.kill()}catch{}te.clear(),A.dispose(),process.exit(0)}process.on("SIGINT",ie),process.on("SIGTERM",ie);function rt(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}async function st(e){const{createAgentToken:n,listAgentTokens:s,removeAgentToken:t}=await import("./lib/agent-tokens.js"),[m,...r]=e;switch(m){case"add":{const a=r.find(p=>!p.startsWith("-"))||"agent",{id:c,token:l}=n(a);console.log("\u2713 agent token created"),console.log(` id: ${c}`),console.log(` name: ${a}`),console.log(` token: ${l}`),console.log(`
7
- Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${l} --name "${a}"`);return}case"list":{const a=s();if(!a.length){console.log("No agent tokens. Create one with: tmux-web agent-token add --name <name>");return}console.log("Agent tokens:");for(const c of a)console.log(` ${c.id} ${c.name} (created ${new Date(c.createdAt).toISOString()})`);return}case"remove":{const a=r.find(l=>!l.startsWith("-"));a||(console.error("usage: tmux-web agent-token remove <id>"),process.exit(1)),t(a)||(console.error(`no token with id ${a}`),process.exit(1)),console.log(`\u2713 token ${a} removed`);return}default:console.error("usage: tmux-web agent-token <add|list|remove>"),process.exit(1)}}
6
+ `})),a.close(1011,"agent offline"),p.relayClosed(_),y.bumpRelayConns(t,-1);return}a.on("message",c=>{const T=typeof c=="string"?c:c.toString("utf-8");let S;try{S=JSON.parse(T)}catch{return}(S.type==="input"||S.type==="resize"||S.type==="load_history")&&p.sendToAgent(_,S)});const r=()=>{p.relayClosed(_),y.bumpRelayConns(t,-1)};a.on("close",r),a.on("error",r)})}let st=1;function at(){return st++}function it(e,n,o){const t=H(e),f=e.headers.origin;if(f){d("agent_rejected_origin",{ip:t,origin:f}),I(n,403,"Origin not allowed");return}O.handleUpgrade(e,n,o,s=>{let i=null,l=setTimeout(()=>{d("agent_hello_timeout",{ip:t}),b(s,4e3,"hello timeout")},1e4);s.on("message",(u,p)=>{if(i){y.touch(i.agentId),p?i.handleData(Array.isArray(u)?Buffer.concat(u):Buffer.from(u)):i.handleMessage(typeof u=="string"?u:Buffer.from(u).toString("utf-8"));return}const m=typeof u=="string"?u:u.toString("utf-8");let g;try{g=JSON.parse(m)}catch{b(s,1008,"bad hello");return}if(g.type!=="hello"||typeof g.token!="string"){b(s,1008,"expected hello");return}if(!A.check(t).allowed){d("agent_rate_limited",{ip:t}),b(s,429,"Too many attempts");return}const w=Je(g.token);if(!w){A.recordFailure(t),d("agent_bad_token",{ip:t}),te(s,{type:"hello_err",reason:"invalid token"}),b(s,4003,"invalid token");return}const a=typeof g.name=="string"&&g.name.trim()?g.name.trim().slice(0,64):"agent",r=w.id;if(!y.register(r,a,s,w.tokenHash)){d("agent_cap_reached",{ip:t}),te(s,{type:"hello_err",reason:"too many agents"}),b(s,4004,"too many agents");return}clearTimeout(l),A.recordSuccess(t),d("agent_connected",{ip:t,agentId:r,name:a}),i=new Be(s,r,{onSessions:T=>y.setSessions(r,T),onActivity:T=>{for(const S of T)ue({type:"session.activity",agentId:r,session:S.session,state:S.state})},onClose:T=>{v.get(r)===T&&v.delete(r)}}),v.set(r,i),y.touch(r),s.send(JSON.stringify({type:"hello_ok",agentId:r})),i.sendSessionsReq()}),s.on("close",()=>{i&&(i.dispose(),v.delete(i.agentId),y.unregister(i.agentId),d("agent_disconnected",{ip:t,agentId:i.agentId})),clearTimeout(l)}),s.on("error",()=>{i&&(i.dispose(),v.delete(i.agentId),y.unregister(i.agentId)),clearTimeout(l)})})}const ct=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=y.pruneOffline(ct);for(const o of e){const t=y.peek(o);t&&t.ws.readyState===E.CLOSED&&(y.unregister(o),v.get(o)?.dispose(),v.delete(o))}const n=new Set(De().map(o=>o.tokenHash));for(const[o,t]of v){const f=y.peek(o);if(!(!f||n.has(f.tokenHash))){d("agent_token_revoked",{agentId:o}),t.dispose(),v.delete(o),y.unregister(o);try{f.ws.close(4003,"token revoked")}catch{}}}k.purgeExpired()},10*1e3).unref();function de(){ie(),Fe(),Ee();for(const e of v.values())e.dispose();v.clear();for(const[e,n]of R){try{e.close(1001,"Server shutting down")}catch{}n.authTimeout&&clearTimeout(n.authTimeout)}R.clear();for(const e of oe)try{e.kill()}catch{}oe.clear(),A.dispose(),process.exit(0)}process.on("SIGINT",de),process.on("SIGTERM",de);function lt(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}async function ut(e){const{createAgentToken:n,listAgentTokens:o,removeAgentToken:t}=await import("./lib/agent-tokens.js"),[f,...s]=e;switch(f){case"add":{const i=s.find(p=>!p.startsWith("-"))||"agent",{id:l,token:u}=n(i);console.log("\u2713 agent token created"),console.log(` id: ${l}`),console.log(` name: ${i}`),console.log(` token: ${u}`),console.log(`
7
+ Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${u} --name "${i}"`);return}case"list":{const i=o();if(!i.length){console.log("No agent tokens. Create one with: tmux-web agent-token add --name <name>");return}console.log("Agent tokens:");for(const l of i)console.log(` ${l.id} ${l.name} (created ${new Date(l.createdAt).toISOString()})`);return}case"remove":{const i=s.find(u=>!u.startsWith("-"));i||(console.error("usage: tmux-web agent-token remove <id>"),process.exit(1)),t(i)||(console.error(`no token with id ${i}`),process.exit(1)),console.log(`\u2713 token ${i} removed`);return}default:console.error("usage: tmux-web agent-token <add|list|remove>"),process.exit(1)}}
@@ -0,0 +1,2 @@
1
+ import{execFile as l}from"node:child_process";import{tmuxEnv as f}from"./tmux-env.js";const r=1e4,h=2e3;function u(c,s,e){return c-s<=e?"working":"idle"}function p(c){return new Promise((s,e)=>{l("tmux",c,{env:f(),maxBuffer:8*1024*1024},(i,n)=>{i?e(i):s(n)})})}class _{idleMs;scanMs;execTmux;hashes=new Map;lastActivity=new Map;constructor(s={}){const e=parseInt(process.env.TMUX_WEB_ACTIVITY_IDLE_MS||String(r),10);this.idleMs=s.idleMs??(e||r);const i=parseInt(process.env.TMUX_WEB_ACTIVITY_SCAN_MS||String(h),10);this.scanMs=s.scanMs??(i||h),this.execTmux=s.execTmux??p}async scan(s=Date.now()){const e=await this.listPanes(),i=new Set;for(const t of e){i.add(t.session);let a;try{a=await this.execTmux(["capture-pane","-p","-e","-t",t.target])}catch{continue}const o=this.hashes.get(t.target);o!==void 0&&a!==o&&this.lastActivity.set(t.session,s),this.hashes.set(t.target,a)}for(const t of this.hashes.keys())e.some(a=>a.target===t)||this.hashes.delete(t);for(const t of this.lastActivity.keys())i.has(t)||this.lastActivity.delete(t);const n=new Map;for(const t of i)this.lastActivity.has(t)||this.lastActivity.set(t,s-this.idleMs-1),n.set(t,u(s,this.lastActivity.get(t),this.idleMs));return n}async listPanes(){const s=await this.execTmux(["list-panes","-a","-F","#{session_name} #{window_index}.#{pane_index}"]),e=[];for(const i of s.split(`
2
+ `)){const[n,t]=i.split(" ");n&&t&&e.push({session:n,target:`${n}:${t}`})}return e}}export{_ as ActivityProbe,r as DEFAULT_ACTIVITY_IDLE_MS,h as DEFAULT_ACTIVITY_SCAN_MS,u as computeAgentState};
@@ -1 +1 @@
1
- import{WebSocket as C}from"ws";import{attachTerminal as D}from"./attach-terminal.js";import{encodeBinaryFrame as J,decodeBinaryFrame as j,BIN_KIND_HTTP_BODY as $}from"./agent-channel.js";const K=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),W=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),F=new Set(["content-type","content-disposition","location","cache-control","etag"]);function Q(o){const T=o.log??(e=>console.log(`[agent] ${e}`)),f=e=>console.error(`[agent] ${e}`);let s=null,g=1e3,B=null,m=!1;const d=new Map,h=new Map,y=new Map,w=new Set;function A(e){return w.has(e)?Promise.resolve():new Promise(t=>{y.set(e,t),setTimeout(()=>{y.delete(e)&&t()},3e4)})}let p=null,_=null,E="",b=null;const S={state:"idle",agentId:null,hub:o.hub,name:o.name};function u(e,t){t!==void 0&&(S.lastError=t),S.state=e,S.agentId=b,o.onStatus?.({...S})}function c(e){s&&s.readyState===C.OPEN&&s.send(JSON.stringify(e))}function k(){let e;try{e=o.listSessions()}catch{return}const t=JSON.stringify(e);t!==E&&(E=t,c({type:"sessions",sessions:e}))}function v(){p||(p=setInterval(k,W)),_||(_=setInterval(()=>c({type:"ping",ts:Date.now()}),K))}function H(){p&&(clearInterval(p),p=null),_&&(clearInterval(_),_=null)}async function O(e){let t;if(e.hasBody){await A(e.id),w.delete(e.id);const r=h.get(e.id);h.delete(e.id),t=r&&r.length?Buffer.concat(r):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let i;try{const r={method:e.method,headers:e.headers};t&&t.length&&(r.body=t),i=await Promise.race([o.app.request(new Request(n,r)),new Promise((G,x)=>setTimeout(()=>x(new Error("app.request timed out")),3e4))])}catch(r){f(`tunnel request failed: ${e.method} ${e.path} ${r.message}`),c({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const I={};i.headers.forEach((r,l)=>{F.has(l.toLowerCase())&&(I[l]=r)});const a=Buffer.from(await i.arrayBuffer());if(c({type:"http_resp",id:e.id,status:i.status,headers:I,hasBody:a.length>0}),a.length>0){for(let l=0;l<a.length;l+=65536){if(!s||s.readyState!==C.OPEN)return;s.send(J($,e.id,a.subarray(l,l+65536)))}c({type:"http_body_end",id:e.id})}}function R(e){const t=e.session;let n=!1;try{n=o.listSessions().some(a=>a.name===t)}catch{n=!1}if(!n){c({type:"attach_err",connId:e.connId,session:t,message:"no such session"});return}let i=null;const I=D(t,{send:a=>c({type:"ws_to_hub",connId:e.connId,msg:a}),onMessage:a=>{i=a},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,initialCols:e.cols,initialRows:e.rows,tmuxSocketPath:o.tmuxSocketPath,onPtyExit:(a,r)=>{c({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${r})`})},onSpawnError:(a,r)=>{f(`attach failed for session "${e.session}": ${r}`),c({type:"attach_err",connId:e.connId,session:t,message:r})},onAttached:a=>{c({type:"attach_ok",connId:e.connId,session:t})}});d.set(e.connId,{onMessage:a=>i?.(a),dispose:()=>{d.delete(e.connId),I.dispose()}})}function L(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":b=t.agentId,g=1e3,T(`connected to hub as "${b}"`),u("connected"),k(),v();break;case"hello_err":if(f(`hub rejected us: ${t.reason}`),u("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":k();break;case"http_req":O(t);break;case"http_body_end":{w.add(t.id);const n=y.get(t.id);n&&(y.delete(t.id),n());break}case"attach":R(t);break;case"ws_to_agent":{const n=d.get(t.connId);if(n)try{n.onMessage(JSON.stringify(t.msg))}catch{}break}case"detach":{const n=d.get(t.connId);n&&n.dispose();break}case"pong":break;default:break}}function q(e){const t=j(e);if(t&&t.kind===$){const n=h.get(t.id)??[];n.push(t.payload),h.set(t.id,n)}}function P(){H();for(const e of d.values())try{e.dispose()}catch{}d.clear(),h.clear(),y.clear(),w.clear(),E=""}function M(){if(s){try{s.removeAllListeners()}catch{}s=null}if(P(),m)return;u("connecting");const e=g+Math.random()*500;T(`reconnecting in ${Math.round(e)}ms\u2026`),B=setTimeout(N,e),g=Math.min(g*2,6e4)}function N(){if(m||s)return;const e=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let t;try{t=new C(e)}catch(n){f(`bad hub URL: ${n.message} (${e})`),u("error",`bad hub URL: ${n.message}`);return}s=t,u("connecting"),t.on("open",()=>{T(`connecting to ${e}\u2026`),c({type:"hello",token:o.token,name:o.name,version:o.version})}),t.on("message",(n,i)=>{i?q(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):L(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),t.on("close",()=>{s===t&&M()}),t.on("error",n=>{f(`connection error: ${n.message}`),s===t&&M()})}function U(){if(!m){if(m=!0,B&&clearTimeout(B),P(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}b=null,u("idle")}}return N(),{stop:U,status:()=>({...S})}}export{Q as startAgentClient};
1
+ import{WebSocket as B}from"ws";import{attachTerminal as W}from"./attach-terminal.js";import{encodeBinaryFrame as F,decodeBinaryFrame as G,BIN_KIND_HTTP_BODY as O}from"./agent-channel.js";import{ActivityProbe as X}from"./activity-probe.js";const Y=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),z=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),Q=new Set(["content-type","content-disposition","location","cache-control","etag"]);function se(o){const P=o.log??(e=>console.log(`[agent] ${e}`)),f=e=>console.error(`[agent] ${e}`);let s=null,w=1e3,v=null,b=!1;const u=new Map,h=new Map,y=new Map,I=new Set;function H(e){return I.has(e)?Promise.resolve():new Promise(t=>{y.set(e,t),setTimeout(()=>{y.delete(e)&&t()},3e4)})}let p=null,_=null,S=null,g=null,M=new Map,k="",T=null;const m={state:"idle",agentId:null,hub:o.hub,name:o.name};function d(e,t){t!==void 0&&(m.lastError=t),m.state=e,m.agentId=T,o.onStatus?.({...m})}function i(e){s&&s.readyState===B.OPEN&&s.send(JSON.stringify(e))}function C(){let e;try{e=o.listSessions()}catch{return}const t=JSON.stringify(e);t!==k&&(k=t,i({type:"sessions",sessions:e}))}function R(){p||(p=setInterval(C,z)),_||(_=setInterval(()=>i({type:"ping",ts:Date.now()}),Y)),S||(g=new X,S=setInterval(()=>void q(),g.scanMs))}function L(){p&&(clearInterval(p),p=null),_&&(clearInterval(_),_=null),S&&(clearInterval(S),S=null),g=null}async function q(){if(!(!s||s.readyState!==B.OPEN||!g))try{const e=await g.scan(),t=[];for(const[n,c]of e)M.get(n)!==c&&t.push({session:n,state:c});M=e,t.length&&i({type:"activity",activities:t})}catch{}}async function U(e){let t;if(e.hasBody){await H(e.id),I.delete(e.id);const r=h.get(e.id);h.delete(e.id),t=r&&r.length?Buffer.concat(r):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let c;try{const r={method:e.method,headers:e.headers};t&&t.length&&(r.body=t),c=await Promise.race([o.app.request(new Request(n,r)),new Promise((V,K)=>setTimeout(()=>K(new Error("app.request timed out")),3e4))])}catch(r){f(`tunnel request failed: ${e.method} ${e.path} ${r.message}`),i({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const E={};c.headers.forEach((r,l)=>{Q.has(l.toLowerCase())&&(E[l]=r)});const a=Buffer.from(await c.arrayBuffer());if(i({type:"http_resp",id:e.id,status:c.status,headers:E,hasBody:a.length>0}),a.length>0){for(let l=0;l<a.length;l+=65536){if(!s||s.readyState!==B.OPEN)return;s.send(F(O,e.id,a.subarray(l,l+65536)))}i({type:"http_body_end",id:e.id})}}function x(e){const t=e.session;let n=!1;try{n=o.listSessions().some(a=>a.name===t)}catch{n=!1}if(!n){i({type:"attach_err",connId:e.connId,session:t,message:"no such session"});return}let c=null;const E=W(t,{send:a=>i({type:"ws_to_hub",connId:e.connId,msg:a}),onMessage:a=>{c=a},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,initialCols:e.cols,initialRows:e.rows,tmuxSocketPath:o.tmuxSocketPath,onPtyExit:(a,r)=>{i({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${r})`})},onSpawnError:(a,r)=>{f(`attach failed for session "${e.session}": ${r}`),i({type:"attach_err",connId:e.connId,session:t,message:r})},onAttached:a=>{i({type:"attach_ok",connId:e.connId,session:t})}});u.set(e.connId,{onMessage:a=>c?.(a),dispose:()=>{u.delete(e.connId),E.dispose()}})}function D(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":T=t.agentId,w=1e3,P(`connected to hub as "${T}"`),d("connected"),C(),R();break;case"hello_err":if(f(`hub rejected us: ${t.reason}`),d("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":C();break;case"http_req":U(t);break;case"http_body_end":{I.add(t.id);const n=y.get(t.id);n&&(y.delete(t.id),n());break}case"attach":x(t);break;case"ws_to_agent":{const n=u.get(t.connId);if(n)try{n.onMessage(JSON.stringify(t.msg))}catch{}break}case"detach":{const n=u.get(t.connId);n&&n.dispose();break}case"pong":break;default:break}}function J(e){const t=G(e);if(t&&t.kind===O){const n=h.get(t.id)??[];n.push(t.payload),h.set(t.id,n)}}function A(){L();for(const e of u.values())try{e.dispose()}catch{}u.clear(),h.clear(),y.clear(),I.clear(),k=""}function N(){if(s){try{s.removeAllListeners()}catch{}s=null}if(A(),b)return;d("connecting");const e=w+Math.random()*500;P(`reconnecting in ${Math.round(e)}ms\u2026`),v=setTimeout($,e),w=Math.min(w*2,6e4)}function $(){if(b||s)return;const e=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let t;try{t=new B(e)}catch(n){f(`bad hub URL: ${n.message} (${e})`),d("error",`bad hub URL: ${n.message}`);return}s=t,d("connecting"),t.on("open",()=>{P(`connecting to ${e}\u2026`),i({type:"hello",token:o.token,name:o.name,version:o.version})}),t.on("message",(n,c)=>{c?J(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):D(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),t.on("close",()=>{s===t&&N()}),t.on("error",n=>{f(`connection error: ${n.message}`),s===t&&N()})}function j(){if(!b){if(b=!0,v&&clearTimeout(v),A(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}T=null,d("idle")}}return $(),{stop:j,status:()=>({...m})}}export{se as startAgentClient};
@@ -1,3 +1,3 @@
1
- import{WebSocket as r}from"ws";import{decodeBinaryFrame as f,encodeBinaryFrame as g,BIN_KIND_TERM as u,BIN_KIND_HTTP_BODY as h}from"./agent-channel.js";const b=parseInt(process.env.TMUX_WEB_MAX_AGENT_CONNS||"64",10),_=parseInt(process.env.TMUX_WEB_TUNNEL_TIMEOUT_MS||"30000",10);let E=1,w=1;class O{ws;agentId;pending=new Map;relays=new Map;onSessions;onClose;disposed=!1;constructor(t,e,s){this.ws=t,this.agentId=e,this.onSessions=s.onSessions,this.onClose=s.onClose}send(t){return this.disposed||this.ws.readyState!==r.OPEN?!1:(this.ws.send(JSON.stringify(t)),!0)}attach(t,e,s){return this.send({type:"attach",connId:t,session:e,cols:s?.cols,rows:s?.rows})}detach(t){this.send({type:"detach",connId:t})}sendToAgent(t,e){this.send({type:"ws_to_agent",connId:t,msg:e})}sendSessionsReq(){this.send({type:"sessions_req"})}async httpRequest(t,e,s,n){const a=w++,o=!!n,c=new Promise((d,i)=>{const l=setTimeout(()=>{this.pending.delete(a),i(new Error("tunnel request timed out"))},_);this.pending.set(a,{id:a,resolve:d,reject:i,status:null,headers:null,body:[],hasBody:o,timer:l})});if(!this.send({type:"http_req",id:a,method:t,path:e,headers:s,hasBody:o}))return this.pending.delete(a),Promise.reject(new Error("agent offline"));if(n){const d=n.getReader();(async()=>{for(;;){const{done:l,value:p}=await d.read();if(l)break;const y=Buffer.from(p);this.ws.send(g(h,a,y))}this.send({type:"http_body_end",id:a})})().catch(()=>{this.send({type:"http_body_end",id:a})})}return c}registerRelay(t,e,s){this.relays.set(t,{connId:t,session:e,ws:s,closed:!1})}relayClosed(t){const e=this.relays.get(t);e&&!e.closed&&(e.closed=!0,this.relays.delete(t),this.detach(t))}get relayCount(){return this.relays.size}handleData(t){const e=Buffer.isBuffer(t)?t:Buffer.from(t),s=f(e);if(s){if(s.kind===u){const n=this.relays.get(s.id);n&&!n.closed&&n.ws.readyState===r.OPEN&&n.ws.send(JSON.stringify({type:"data",data:s.payload.toString("utf-8")}));return}if(s.kind===h){const n=this.pending.get(s.id);n&&n.body.push(s.payload)}}}handleMessage(t){let e;try{e=JSON.parse(t)}catch{return}switch(e.type){case"sessions":this.onSessions?.(e.sessions);break;case"http_resp":{const s=this.pending.get(e.id);if(!s)break;s.status=e.status,s.headers=e.headers,e.hasBody||this.finalizeHttp(s.id);break}case"http_body_end":this.finalizeHttp(e.id);break;case"attach_ok":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify({type:"auth.ok",setupMode:!1}));break}case"attach_err":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify({type:"attach_failed",message:`Agent "${this.agentId}": cannot attach to session "${e.session}": ${e.message}`}));break}case"ws_to_hub":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify(e.msg));break}case"ws_close":{const s=this.relays.get(e.connId);if(s&&!s.closed){s.closed=!0;try{s.ws.close(e.code??1e3,e.reason??"agent closed")}catch{}this.relays.delete(e.connId)}break}case"ping":this.send({type:"pong",ts:e.ts});break;default:break}}finalizeHttp(t){const e=this.pending.get(t);if(!e)return;this.pending.delete(t),clearTimeout(e.timer);const s=e.status??500,n=e.headers??{},a=Buffer.concat(e.body),o={};for(const[c,d]of Object.entries(n)){const i=c.toLowerCase();(i==="content-type"||i==="content-disposition"||i==="cache-control"||i==="etag"||i==="location")&&(o[c]=d)}e.resolve(new Response(a,{status:s,headers:o}))}failAll(t){for(const e of this.pending.values())clearTimeout(e.timer),e.reject(new Error(t));this.pending.clear();for(const e of this.relays.values()){if(!e.closed&&e.ws.readyState===r.OPEN){e.ws.send(JSON.stringify({type:"data",data:`\r
2
- \x1B[31m--- agent disconnected (${t}) ---\x1B[0m\r
1
+ import{WebSocket as r}from"ws";import{decodeBinaryFrame as f,encodeBinaryFrame as g,BIN_KIND_TERM as u,BIN_KIND_HTTP_BODY as h}from"./agent-channel.js";const b=parseInt(process.env.TMUX_WEB_MAX_AGENT_CONNS||"64",10),_=parseInt(process.env.TMUX_WEB_TUNNEL_TIMEOUT_MS||"30000",10);let E=1,w=1;class O{ws;agentId;pending=new Map;relays=new Map;onSessions;onActivity;onClose;disposed=!1;constructor(s,e,t){this.ws=s,this.agentId=e,this.onSessions=t.onSessions,this.onActivity=t.onActivity,this.onClose=t.onClose}send(s){return this.disposed||this.ws.readyState!==r.OPEN?!1:(this.ws.send(JSON.stringify(s)),!0)}attach(s,e,t){return this.send({type:"attach",connId:s,session:e,cols:t?.cols,rows:t?.rows})}detach(s){this.send({type:"detach",connId:s})}sendToAgent(s,e){this.send({type:"ws_to_agent",connId:s,msg:e})}sendSessionsReq(){this.send({type:"sessions_req"})}async httpRequest(s,e,t,n){const i=w++,o=!!n,c=new Promise((d,a)=>{const l=setTimeout(()=>{this.pending.delete(i),a(new Error("tunnel request timed out"))},_);this.pending.set(i,{id:i,resolve:d,reject:a,status:null,headers:null,body:[],hasBody:o,timer:l})});if(!this.send({type:"http_req",id:i,method:s,path:e,headers:t,hasBody:o}))return this.pending.delete(i),Promise.reject(new Error("agent offline"));if(n){const d=n.getReader();(async()=>{for(;;){const{done:l,value:p}=await d.read();if(l)break;const y=Buffer.from(p);this.ws.send(g(h,i,y))}this.send({type:"http_body_end",id:i})})().catch(()=>{this.send({type:"http_body_end",id:i})})}return c}registerRelay(s,e,t){this.relays.set(s,{connId:s,session:e,ws:t,closed:!1})}relayClosed(s){const e=this.relays.get(s);e&&!e.closed&&(e.closed=!0,this.relays.delete(s),this.detach(s))}get relayCount(){return this.relays.size}handleData(s){const e=Buffer.isBuffer(s)?s:Buffer.from(s),t=f(e);if(t){if(t.kind===u){const n=this.relays.get(t.id);n&&!n.closed&&n.ws.readyState===r.OPEN&&n.ws.send(JSON.stringify({type:"data",data:t.payload.toString("utf-8")}));return}if(t.kind===h){const n=this.pending.get(t.id);n&&n.body.push(t.payload)}}}handleMessage(s){let e;try{e=JSON.parse(s)}catch{return}switch(e.type){case"sessions":this.onSessions?.(e.sessions);break;case"activity":this.onActivity?.(e.activities);break;case"http_resp":{const t=this.pending.get(e.id);if(!t)break;t.status=e.status,t.headers=e.headers,e.hasBody||this.finalizeHttp(t.id);break}case"http_body_end":this.finalizeHttp(e.id);break;case"attach_ok":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify({type:"auth.ok",setupMode:!1}));break}case"attach_err":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify({type:"attach_failed",message:`Agent "${this.agentId}": cannot attach to session "${e.session}": ${e.message}`}));break}case"ws_to_hub":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify(e.msg));break}case"ws_close":{const t=this.relays.get(e.connId);if(t&&!t.closed){t.closed=!0;try{t.ws.close(e.code??1e3,e.reason??"agent closed")}catch{}this.relays.delete(e.connId)}break}case"ping":this.send({type:"pong",ts:e.ts});break;default:break}}finalizeHttp(s){const e=this.pending.get(s);if(!e)return;this.pending.delete(s),clearTimeout(e.timer);const t=e.status??500,n=e.headers??{},i=Buffer.concat(e.body),o={};for(const[c,d]of Object.entries(n)){const a=c.toLowerCase();(a==="content-type"||a==="content-disposition"||a==="cache-control"||a==="etag"||a==="location")&&(o[c]=d)}e.resolve(new Response(i,{status:t,headers:o}))}failAll(s){for(const e of this.pending.values())clearTimeout(e.timer),e.reject(new Error(s));this.pending.clear();for(const e of this.relays.values()){if(!e.closed&&e.ws.readyState===r.OPEN){e.ws.send(JSON.stringify({type:"data",data:`\r
2
+ \x1B[31m--- agent disconnected (${s}) ---\x1B[0m\r
3
3
  `}));try{e.ws.close(1011,"agent disconnected")}catch{}}e.closed=!0}this.relays.clear()}dispose(){this.disposed||(this.disposed=!0,this.failAll("agent disconnected"),this.onClose?.(this))}}export{O as AgentChannel,b as MAX_RELAY_CONNS_PER_AGENT,_ as TUNNEL_REQUEST_TIMEOUT_MS};
@@ -138,6 +138,8 @@ import{cssVarsStyle as d}from"../theme.js";import{newSessionModalCSS as c,newSes
138
138
  .session-group-body.collapsed { display: none; }
139
139
  .session-group-body .session-item { padding-left: 30px; }
140
140
  .session-item.session-offline { opacity: 0.5; cursor: default; }
141
+ .session-item.working svg { color: #f5a623; }
142
+ .session-item.just-done svg { color: #3fb950; }
141
143
  .session-item .meta { margin-left: auto; }
142
144
  .new-session-sidebar-btn {
143
145
  display: flex; align-items: center; gap: 8px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "2.0.33",
3
+ "version": "2.2.0",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {