tmux-weblink 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{WebSocket as E}from"ws";import{attachTerminal as q}from"./attach-terminal.js";import{encodeBinaryFrame as U,decodeBinaryFrame as D,BIN_KIND_HTTP_BODY as N}from"./agent-channel.js";const J=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),j=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),x=new Set(["content-type","content-disposition","location","cache-control","etag"]);function X(o){const I=o.log??(e=>console.log(`[agent] ${e}`)),_=e=>console.error(`[agent] ${e}`);let s=null,g=1e3,T=null,S=!1;const u=new Map,f=new Map;let h=null,p=null,w="",m=null;const y={state:"idle",agentId:null,hub:o.hub,name:o.name};function d(e,t){t!==void 0&&(y.lastError=t),y.state=e,y.agentId=m,o.onStatus?.({...y})}function c(e){s&&s.readyState===E.OPEN&&s.send(JSON.stringify(e))}function B(){let e;try{e=o.listSessions()}catch{return}const t=JSON.stringify(e);t!==w&&(w=t,c({type:"sessions",sessions:e}))}function P(){h||(h=setInterval(B,j)),p||(p=setInterval(()=>c({type:"ping",ts:Date.now()}),J))}function A(){h&&(clearInterval(h),h=null),p&&(clearInterval(p),p=null)}async function $(e){let t;if(e.hasBody){const a=f.get(e.id);f.delete(e.id),t=a&&a.length?Buffer.concat(a):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let i;try{const a={method:e.method,headers:e.headers};t&&t.length&&(a.body=t),i=await Promise.race([o.app.request(new Request(n,a)),new Promise((K,R)=>setTimeout(()=>R(new Error("app.request timed out")),3e4))])}catch(a){_(`tunnel request failed: ${e.method} ${e.path} ${a.message}`),c({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const b={};i.headers.forEach((a,l)=>{x.has(l.toLowerCase())&&(b[l]=a)});const r=Buffer.from(await i.arrayBuffer());if(c({type:"http_resp",id:e.id,status:i.status,headers:b,hasBody:r.length>0}),r.length>0){for(let l=0;l<r.length;l+=65536){if(!s||s.readyState!==E.OPEN)return;s.send(U(N,e.id,r.subarray(l,l+65536)))}c({type:"http_body_end",id:e.id})}}function v(e){const t=e.session;let n=!1;try{n=o.listSessions().some(r=>r.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 b=q(t,{send:r=>c({type:"ws_to_hub",connId:e.connId,msg:r}),onMessage:r=>{i=r},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,onPtyExit:(r,a)=>{c({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${a})`})},onSpawnError:(r,a)=>{c({type:"attach_err",connId:e.connId,session:t,message:a})},onAttached:r=>{c({type:"attach_ok",connId:e.connId,session:t})}});u.set(e.connId,{onMessage:r=>i?.(r),dispose:()=>{u.delete(e.connId),b.dispose()}})}function H(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":m=t.agentId,g=1e3,I(`connected to hub as "${m}"`),d("connected"),B(),P();break;case"hello_err":if(_(`hub rejected us: ${t.reason}`),d("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":B();break;case"http_req":$(t);break;case"http_body_end":break;case"attach":v(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 O(e){const t=D(e);if(t&&t.kind===N){const n=f.get(t.id)??[];n.push(t.payload),f.set(t.id,n)}}function C(){A();for(const e of u.values())try{e.dispose()}catch{}u.clear(),f.clear(),w=""}function k(){if(s){try{s.removeAllListeners()}catch{}s=null}if(C(),S)return;d("connecting");const e=g+Math.random()*500;I(`reconnecting in ${Math.round(e)}ms\u2026`),T=setTimeout(M,e),g=Math.min(g*2,6e4)}function M(){if(S||s)return;const e=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let t;try{t=new E(e)}catch(n){_(`bad hub URL: ${n.message} (${e})`),d("error",`bad hub URL: ${n.message}`);return}s=t,d("connecting"),t.on("open",()=>{I(`connecting to ${e}\u2026`),c({type:"hello",token:o.token,name:o.name,version:o.version})}),t.on("message",(n,i)=>{i?O(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):H(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),t.on("close",()=>{s===t&&k()}),t.on("error",n=>{_(`connection error: ${n.message}`),s===t&&k()})}function L(){if(!S){if(S=!0,T&&clearTimeout(T),C(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}m=null,d("idle")}}return M(),{stop:L,status:()=>({...y})}}export{X as startAgentClient};
1
+ import{WebSocket as k}from"ws";import{attachTerminal as J}from"./attach-terminal.js";import{encodeBinaryFrame as j,decodeBinaryFrame as x,BIN_KIND_HTTP_BODY as A}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}`)),g=e=>console.error(`[agent] ${e}`);let s=null,S=1e3,B=null,m=!1;const d=new Map,f=new Map,h=new Map,b=new Set;function $(e){return b.has(e)?Promise.resolve():new Promise(t=>{h.set(e,t),setTimeout(()=>{h.delete(e)&&t()},3e4)})}let p=null,y=null,E="",w=null;const _={state:"idle",agentId:null,hub:o.hub,name:o.name};function u(e,t){t!==void 0&&(_.lastError=t),_.state=e,_.agentId=w,o.onStatus?.({..._})}function c(e){s&&s.readyState===k.OPEN&&s.send(JSON.stringify(e))}function C(){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(C,W)),y||(y=setInterval(()=>c({type:"ping",ts:Date.now()}),K))}function H(){p&&(clearInterval(p),p=null),y&&(clearInterval(y),y=null)}async function O(e){let t;if(e.hasBody){await $(e.id),b.delete(e.id);const a=f.get(e.id);f.delete(e.id),t=a&&a.length?Buffer.concat(a):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let i;try{const a={method:e.method,headers:e.headers};t&&t.length&&(a.body=t),i=await Promise.race([o.app.request(new Request(n,a)),new Promise((G,D)=>setTimeout(()=>D(new Error("app.request timed out")),3e4))])}catch(a){g(`tunnel request failed: ${e.method} ${e.path} ${a.message}`),c({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const I={};i.headers.forEach((a,l)=>{F.has(l.toLowerCase())&&(I[l]=a)});const r=Buffer.from(await i.arrayBuffer());if(c({type:"http_resp",id:e.id,status:i.status,headers:I,hasBody:r.length>0}),r.length>0){for(let l=0;l<r.length;l+=65536){if(!s||s.readyState!==k.OPEN)return;s.send(j(A,e.id,r.subarray(l,l+65536)))}c({type:"http_body_end",id:e.id})}}function L(e){const t=e.session;let n=!1;try{n=o.listSessions().some(r=>r.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=J(t,{send:r=>c({type:"ws_to_hub",connId:e.connId,msg:r}),onMessage:r=>{i=r},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,onPtyExit:(r,a)=>{c({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${a})`})},onSpawnError:(r,a)=>{c({type:"attach_err",connId:e.connId,session:t,message:a})},onAttached:r=>{c({type:"attach_ok",connId:e.connId,session:t})}});d.set(e.connId,{onMessage:r=>i?.(r),dispose:()=>{d.delete(e.connId),I.dispose()}})}function R(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":w=t.agentId,S=1e3,T(`connected to hub as "${w}"`),u("connected"),C(),v();break;case"hello_err":if(g(`hub rejected us: ${t.reason}`),u("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":C();break;case"http_req":O(t);break;case"http_body_end":{b.add(t.id);const n=h.get(t.id);n&&(h.delete(t.id),n());break}case"attach":L(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=x(e);if(t&&t.kind===A){const n=f.get(t.id)??[];n.push(t.payload),f.set(t.id,n)}}function M(){H();for(const e of d.values())try{e.dispose()}catch{}d.clear(),f.clear(),h.clear(),b.clear(),E=""}function P(){if(s){try{s.removeAllListeners()}catch{}s=null}if(M(),m)return;u("connecting");const e=S+Math.random()*500;T(`reconnecting in ${Math.round(e)}ms\u2026`),B=setTimeout(N,e),S=Math.min(S*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 k(e)}catch(n){g(`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)):R(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),t.on("close",()=>{s===t&&P()}),t.on("error",n=>{g(`connection error: ${n.message}`),s===t&&P()})}function U(){if(!m){if(m=!0,B&&clearTimeout(B),M(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}w=null,u("idle")}}return N(),{stop:U,status:()=>({..._})}}export{Q as startAgentClient};
@@ -722,10 +722,10 @@ import{cssVarsStyle as g}from"../theme.js";import{commandbarCSS as b,commandbarH
722
722
  <button class="theme-option" data-theme="dark-cove" title="Dark Cove"><span class="theme-dot" style="background:#7aa2f7"></span>Dark Cove${e.template==="dark-cove"?'<span class="theme-check">\u2713</span>':""}</button>
723
723
  </div>
724
724
  <hr class="settings-divider" />
725
- <div class="settings-item" style="cursor:pointer" id="set-plugins-link">
726
- <a href="/settings" style="color:inherit;text-decoration:none">Plugins & Settings</a>
725
+ <a href="/settings" class="settings-item" id="set-plugins-link" style="cursor:pointer;color:inherit;text-decoration:none">
726
+ <span>Plugins & Settings</span>
727
727
  <span style="font-size:var(--text-xs);color:var(--panel-muted)">\u2192</span>
728
- </div>
728
+ </a>
729
729
  </div>
730
730
 
731
731
  <!-- Git diff popover -->
@@ -770,9 +770,9 @@ window.__TMUX_WEB_SHELL__ = ${f};
770
770
  await import('/assets/shell-client.js');
771
771
  ${t?m(a,[]):""}
772
772
  // Wrapper: refresh sidebar list, then open session
773
- window.__onSessionCreated = async function(name) {
773
+ window.__onSessionCreated = async function(name, agentId) {
774
774
  await window.__refreshSidebar();
775
- window.__openSession(name);
775
+ window.__openSession(name, agentId || undefined);
776
776
  };
777
777
  ${h("__onSessionCreated")}
778
778
  </script>
@@ -1,4 +1,4 @@
1
- import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}from"./html.js";import{icon as a,iconPath as o}from"./icons.js";const d=[{id:"vscode",name:"VS Code",dot:"#007acc"},{id:"ghostty",name:"Ghostty",dot:"#ff5f00"},{id:"warm-clay",name:"Warm Clay",dot:"#b86b52"},{id:"dark-cove",name:"Dark Cove",dot:"#7aa2f7"}];function r(t="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${t}; outline: none;`}function y(t=""){return`
1
+ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}from"./html.js";import{icon as a,iconPath as n}from"./icons.js";const d=[{id:"vscode",name:"VS Code",dot:"#007acc"},{id:"ghostty",name:"Ghostty",dot:"#ff5f00"},{id:"warm-clay",name:"Warm Clay",dot:"#b86b52"},{id:"dark-cove",name:"Dark Cove",dot:"#7aa2f7"}];function r(t="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${t}; outline: none;`}function y(t=""){return`
2
2
  @media (prefers-reduced-motion: reduce) {
3
3
  *, *::before, *::after {
4
4
  animation-duration: 0.01ms !important;
@@ -7,15 +7,15 @@ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}fr
7
7
  scroll-behavior: auto !important;
8
8
  }
9
9
  ${t}
10
- }`}function k(t="vscode"){const n=d.find(e=>e.id===t)??d[0],i=d.map(e=>{const s=e.id===t;return`<button type="button" class="theme-option${s?" active":""}" data-theme="${e.id}" role="menuitem">
10
+ }`}function k(t="vscode"){const o=d.find(e=>e.id===t)??d[0],i=d.map(e=>{const s=e.id===t;return`<button type="button" class="theme-option${s?" active":""}" data-theme="${e.id}" role="menuitem">
11
11
  <span class="theme-dot" style="background:${e.dot}"></span>
12
12
  ${c(e.name)}
13
13
  ${s?a("check",'class="theme-check" aria-hidden="true"'):""}
14
14
  </button>`}).join(`
15
15
  `);return`<div class="theme-switcher" id="theme-switcher">
16
16
  <button type="button" class="theme-switcher-btn" aria-label="Theme" aria-haspopup="true" aria-expanded="false">
17
- <span class="theme-dot" style="background:${n.dot}"></span>
18
- <span>${c(n.name)}</span>
17
+ <span class="theme-dot" style="background:${o.dot}"></span>
18
+ <span>${c(o.name)}</span>
19
19
  </button>
20
20
  <div class="theme-switcher-popover" role="menu">
21
21
  ${i}
@@ -217,7 +217,7 @@ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}fr
217
217
  }
218
218
 
219
219
  ${y()}
220
- ${t}`}function T(t){const{commandbarEnabled:n,title:i="TMUX Sessions",themeTemplate:e="vscode"}=t;return`<script>
220
+ ${t}`}function A(t){const{commandbarEnabled:o,title:i="TMUX Sessions",themeTemplate:e="vscode"}=t;return`<script>
221
221
  (function() {
222
222
  const token = localStorage.getItem('tmux-web-token');
223
223
  if (token) {
@@ -236,7 +236,7 @@ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}fr
236
236
  <header class="fixed-header">
237
237
  <div class="brand"><a href="/" aria-label="Go to home">tmux<span>-weblink</span></a></div>
238
238
  <div class="header-actions">
239
- ${n?w("Search"):""}
239
+ ${o?w("Search"):""}
240
240
  <button class="header-btn" id="notes-toggle" title="Global notes" aria-label="Global notes">
241
241
  ${a("notes")}
242
242
  <span>Notes</span>
@@ -248,10 +248,10 @@ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}fr
248
248
  </a>
249
249
  </div>
250
250
  </header>
251
- <script>${I()}</script>`}function M(t){const{activePage:n,refreshHref:i}=t;function e(l,x,f,g,v=""){return`<a href="${x}" class="${l!==null&&l===n?"sidebar-btn current":"sidebar-btn"}"${v}>
251
+ <script>${I()}</script>`}function T(t){const{activePage:o,refreshHref:i}=t;function e(l,x,f,g,v=""){return`<a href="${x}" class="${l!==null&&l===o?"sidebar-btn current":"sidebar-btn"}"${v}>
252
252
  <svg viewBox="0 0 24 24" fill="currentColor">${f}</svg>
253
253
  ${g}
254
- </a>`}const s=o("sessions"),p=o("notes"),m=o("schedule"),b=o("history"),u=o("quick-commands"),h=o("file"),E=o("refresh");return`<aside class="action-sidebar">
254
+ </a>`}const s=n("sessions"),p=n("notes"),m=n("schedule"),b=n("history"),u=n("quick-commands"),h=n("file"),E=n("refresh");return`<aside class="action-sidebar">
255
255
  <p class="sidebar-label">Actions</p>
256
256
  <button class="sidebar-btn primary" id="new-session-btn">
257
257
  ${a("add")}
@@ -269,7 +269,7 @@ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}fr
269
269
  ${a("refresh")}
270
270
  Refresh
271
271
  </a>
272
- </aside>`}function A(){return` /* \u2500\u2500 New session modal \u2500\u2500 */
272
+ </aside>`}function M(){return` /* \u2500\u2500 New session modal \u2500\u2500 */
273
273
  .modal-backdrop {
274
274
  display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.3);
275
275
  z-index: 500; align-items: center; justify-content: center;
@@ -349,6 +349,20 @@ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}fr
349
349
  const errorEl = document.getElementById('ns-error');
350
350
  const onCreated = '${t??""}';
351
351
 
352
+ // The new session is created on the machine of the currently selected
353
+ // session (window.__tmuxWebScopeAgent is set by the shell client; agent
354
+ // page spaces also set it via the injected scope). Falls back to this
355
+ // machine when nothing is selected.
356
+ function selectedAgentId() {
357
+ var a = window.__tmuxWebScopeAgent;
358
+ return (typeof a === 'string' && a) ? a : '';
359
+ }
360
+
361
+ function selectedBase() {
362
+ var id = selectedAgentId();
363
+ return id ? '/a/' + encodeURIComponent(id) : '';
364
+ }
365
+
352
366
  function openModal() {
353
367
  modal.classList.add('open');
354
368
  nameInput.value = '';
@@ -412,7 +426,7 @@ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}fr
412
426
  if (!val) { closeDropdown(); return; }
413
427
  debounceTimer = setTimeout(async () => {
414
428
  try {
415
- const res = await fetch('/api/fs/list?path=' + encodeURIComponent(val));
429
+ const res = await fetch(selectedBase() + '/api/fs/list?path=' + encodeURIComponent(val));
416
430
  const data = await res.json();
417
431
  renderDropdown(data.dirs || []);
418
432
  } catch { closeDropdown(); }
@@ -429,7 +443,8 @@ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}fr
429
443
  submitBtn.textContent = 'Creating\u2026';
430
444
  errorEl.style.display = 'none';
431
445
  try {
432
- const res = await fetch('/api/sessions/new', {
446
+ const base = selectedBase();
447
+ const res = await fetch(base + '/api/sessions/new', {
433
448
  method: 'POST',
434
449
  headers: { 'Content-Type': 'application/json' },
435
450
  body: JSON.stringify({ name, dir: dir || undefined }),
@@ -437,8 +452,9 @@ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}fr
437
452
  const data = await res.json();
438
453
  if (!res.ok) { showError(data.error || 'Failed to create session.'); return; }
439
454
  closeModal();
440
- if (onCreated && typeof window[onCreated] === 'function') { window[onCreated](name); return; }
441
- window.location.href = '/s/' + encodeURIComponent(name);
455
+ const agentId = selectedAgentId() || null;
456
+ if (onCreated && typeof window[onCreated] === 'function') { window[onCreated](name, agentId); return; }
457
+ window.location.href = base + '/s/' + encodeURIComponent(name);
442
458
  } catch { showError('Network error. Please try again.'); }
443
459
  finally { submitBtn.disabled = false; submitBtn.textContent = 'Create'; }
444
460
  }
@@ -463,4 +479,4 @@ import{commandbarButtonHTML as w}from"./commandbar.js";import{escapeHtml as c}fr
463
479
  }
464
480
  if (e.key === 'Enter') submit();
465
481
  });
466
- })();`}export{A as newSessionModalCSS,D as newSessionModalHTML,j as newSessionModalScript,y as reducedMotion,T as sharedHeader,B as sharedLayoutCSS,M as sharedSidebar,k as themeSwitcherButtonHTML,I as themeSwitcherScript};
482
+ })();`}export{M as newSessionModalCSS,D as newSessionModalHTML,j as newSessionModalScript,y as reducedMotion,A as sharedHeader,B as sharedLayoutCSS,T as sharedSidebar,k as themeSwitcherButtonHTML,I as themeSwitcherScript};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {