tmux-weblink 2.0.32 → 2.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/build-app.js +3 -3
- package/dist/lib/shared-layout.js +50 -28
- package/package.json +1 -1
package/dist/lib/build-app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync as P,readdirSync as ie,statSync as A,unlinkSync as ae,writeFileSync as de,mkdirSync as ue,readFileSync as $}from"node:fs";import{readFile as ce}from"node:fs/promises";import{execSync as _,execFileSync as G}from"node:child_process";import p from"node:path";import{fileURLToPath as le}from"node:url";import{Hono as me}from"hono";import{listSessions as L}from"../sessions.js";import{renderLoginPage as fe,renderNotesIndex as pe,renderNotesPage as he,renderSettings as ge,renderThemeSettings as ye,renderHistoryIndex as je,renderQuickCommandsPage as we,renderFilesIndex as ke,renderShell as ve}from"../frontend.js";import{renderFederationPage as qe}from"./pages/federation.js";import{createAgentToken as Se,removeAgentToken as Te,listAgentTokens as z}from"./agent-tokens.js";import{hashPassword as Ae,verifyPassword as be,validatePassword as xe}from"./auth.js";import{atomicWriteFileSync as _e}from"./atomicWrite.js";import{resolveFsPath as b,resolveFsRoots as B,MAX_FILE_BYTES as V,walkRecursive as Ce}from"./fs-access.js";import{captureSessionWindowsWithPath as Oe}from"./tmux-windows.js";import{audit as j}from"./auditLog.js";import{db as S}from"./db.js";import{getSessionAccessMap as Ee}from"./session-access.js";import{listWindowHistory as Ie,clearWindowHistory as Ne}from"./window-history.js";import{saveSecurityConfig as Re}from"./security-config.js";import{readSettings as Q,writeSettings as Pe}from"./settings.js";import{setActiveThemeTemplate as K}from"./theme-store.js";import{isThemeTemplateId as X,THEME_TEMPLATE_IDS as Le}from"./themes/index.js";import{pinView as De,unpinView as Fe,listPinnedViews as He}from"./pinned-views.js";import{getStoredWindows as
|
|
1
|
+
import{existsSync as P,readdirSync as ie,statSync as A,unlinkSync as ae,writeFileSync as de,mkdirSync as ue,readFileSync as $}from"node:fs";import{readFile as ce}from"node:fs/promises";import{execSync as _,execFileSync as G}from"node:child_process";import p from"node:path";import{fileURLToPath as le}from"node:url";import{Hono as me}from"hono";import{listSessions as L}from"../sessions.js";import{renderLoginPage as fe,renderNotesIndex as pe,renderNotesPage as he,renderSettings as ge,renderThemeSettings as ye,renderHistoryIndex as je,renderQuickCommandsPage as we,renderFilesIndex as ke,renderShell as ve}from"../frontend.js";import{renderFederationPage as qe}from"./pages/federation.js";import{createAgentToken as Se,removeAgentToken as Te,listAgentTokens as z}from"./agent-tokens.js";import{hashPassword as Ae,verifyPassword as be,validatePassword as xe}from"./auth.js";import{atomicWriteFileSync as _e}from"./atomicWrite.js";import{resolveFsPath as b,resolveFsRoots as B,MAX_FILE_BYTES as V,walkRecursive as Ce}from"./fs-access.js";import{captureSessionWindowsWithPath as Oe}from"./tmux-windows.js";import{audit as j}from"./auditLog.js";import{db as S}from"./db.js";import{getSessionAccessMap as Ee}from"./session-access.js";import{listWindowHistory as Ie,clearWindowHistory as Ne}from"./window-history.js";import{saveSecurityConfig as Re}from"./security-config.js";import{readSettings as Q,writeSettings as Pe}from"./settings.js";import{setActiveThemeTemplate as K}from"./theme-store.js";import{isThemeTemplateId as X,THEME_TEMPLATE_IDS as Le}from"./themes/index.js";import{pinView as De,unpinView as Fe,listPinnedViews as He}from"./pinned-views.js";import{getStoredWindows as Me}from"./session-windows.js";import{buildSidebarSessions as Ue}from"./sessions-sidebar.js";import{createQuickCommand as We,deleteQuickCommand as $e,listQuickCommands as Y,updateQuickCommand as Ge}from"./quick-commands.js";import{getSystemStatus as ze,getTopProcesses as Be,killProcess as Ve}from"./system-monitor.js";import{newTmuxSession as Qe,renameSession as Ke,killSession as Xe,TmuxWindowsError as Ye}from"./tmux-windows.js";import{registerAgentTunnel as Ze}from"./agent-tunnel.js";import{injectScopeScript as Je}from"./scope-script.js";const D="tmux-web-token",et=365;function tt(x){return x==="127.0.0.1"||x==="::1"||x==="::ffff:127.0.0.1"}function Rt(x){const{mode:Z,securityConfig:h,tokenStore:y,rateLimiter:O,settings:rt,terminalBufferConfig:E,getAgentId:F,state:u,federation:w,hub:q}=x,k=Z==="agent";function C(e){if(h.security.trustProxy){const t=(e.req.header("x-forwarded-for")||"").split(",")[0].trim();if(t)return t}return e.env?.incoming?.socket?.remoteAddress||"unknown"}function I(e){const t=e.req.header("authorization");if(t?.startsWith("Bearer "))return t.slice(7);const r=e.req.header("cookie");if(!r)return null;const n=r.match(new RegExp(`(?:^|;\\s*)${D}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function nt(e){const t=I(e);return t?y.validateToken(t):null}function H(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${D}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${et*86400}${r?"; Secure":""}`)}function J(e){e.header("Set-Cookie",`${D}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const o=()=>k?async(e,t)=>t():async(e,t)=>{const r=I(e);return!r||!y.validateToken(r)?(j("http_unauthorized",{ip:C(e)}),e.json({error:"unauthorized"},401)):t()},g=()=>k?async(e,t)=>t():async(e,t)=>{const r=I(e);if(!r||!y.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function N(e){const t=Ue(L(),Ee(),He()),r=[];if(q)for(const n of q.agents.list()){const i=q.agents.get(n.agentId);r.push({agentId:n.agentId,agentName:n.name,online:n.online,sessions:i?i.sessions:[]})}return{...t,currentSession:e??null,agents:r}}function M(e){const t=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!t)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:t};const r=e.windowIndex;return typeof r!="number"||!Number.isInteger(r)||r<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:t,windowIndex:r}}const s=new me;s.use("*",async(e,t)=>{await t(),e.header("X-Content-Type-Options","nosniff"),e.header("X-Frame-Options","DENY"),e.header("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline'","style-src 'self' 'unsafe-inline' https://fonts.googleapis.com","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self' https://fonts.gstatic.com","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),s.use("*",async(e,t)=>{const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS")return t();const n=e.req.header("sec-fetch-site");return n&&n!=="same-origin"&&n!=="none"?e.text("cross-site request blocked",403):t()}),k||s.use("*",async(e,t)=>{if(await t(),e.req.method!=="GET"||e.req.path==="/login"||!(e.res.headers.get("content-type")??"").includes("text/html"))return;const n=await e.res.text();e.res=new Response(Je(n,{prefix:"",agentId:null}),e.res)});const U=p.dirname(le(import.meta.url)),ee=[p.join(U,"..","assets"),p.join(U,"assets"),p.join(process.cwd(),"dist","assets")];s.get("/assets/:file",async e=>{const t=e.req.param("file");if(!/^[a-zA-Z0-9._-]+$/.test(t))return e.notFound();for(const r of ee){const n=p.join(r,t);if(!P(n))continue;const i=await ce(n),a=p.extname(t),c={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(i,200,{"Content-Type":c[a]??"application/octet-stream","Cache-Control":a===".js"||a===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const te=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
2
|
<rect x="2.5" y="5.5" width="27" height="21" rx="4" fill="#0d1117" stroke="#7dd3fc" stroke-width="2"/>
|
|
3
3
|
<path d="M8 13l4.2 3.1L8 19.2" fill="none" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
4
|
<line x1="15.5" y1="19.6" x2="22.5" y2="19.6" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round"/>
|
|
@@ -13,7 +13,7 @@ self.addEventListener("activate", (e) => { e.waitUntil(clients.claim()); });
|
|
|
13
13
|
self.addEventListener("fetch", (e) => {
|
|
14
14
|
e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
|
|
15
15
|
});
|
|
16
|
-
`;s.get("/sw.js",e=>e.body(ne,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!h.passwordHash,n=e.req.query("error");return e.html(fe({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:u.activeTheme}))}),s.get("/",g(),e=>{const t=B();return e.html(ve({theme:u.activeTheme,fsRoots:t,terminalCfg:E,scrollback:E.initialLines+2*E.historyChunk,wsBase:k&&F?.()?`/ws/a/${F()}`:void 0}))}),s.get("/notes",g(),e=>e.html(pe(S.data.notes,u.activeTheme))),s.get("/notes/:session",g(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(he(t,u.activeTheme))}),s.get("/history",g(),e=>{const t=L(),r=new Set(t.map(n=>n.name));return e.html(je(Ie(),u.activeTheme,r))}),s.get("/quick-commands",g(),e=>e.html(we(Y(),u.activeTheme))),s.get("/files",g(),e=>{const t=B();return e.html(ke(u.activeTheme,t))}),s.post("/api/history/clear",o(),async e=>(await Ne(),e.json({ok:!0}))),s.get("/api/quick-commands",o(),e=>e.json(Y())),s.post("/api/quick-commands",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await We(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Ge(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",o(),e=>$e(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(k)return e.json({error:"hub only"},403);const t=C(e),r=O.check(t);if(!r.allowed)return j("rate_limited",{ip:t,retryAfterMs:r.retryAfterMs,permanentLock:r.permanentLock}),r.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:r.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const i=typeof n.password=="string"?n.password:"";if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!tt(t))return j("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(u.settingUpPassword)return e.json({error:"Password setup in progress"},409);const l=xe(i);if(l)return e.json({error:l},400);u.settingUpPassword=!0;try{h.passwordHash=await Ae(i),Re(h),j("password_set",{ip:t})}finally{u.settingUpPassword=!1}const{plaintext:d}=y.createAccessToken("setup",h.security.tokenTtlDays);return H(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await be(i,h.passwordHash)){const l=O.recordFailure(t);return j("auth_failed",{ip:t,method:"password",failures:l.failures,permanentLock:l.permanentLock}),l.permanentLock?(y.revokeAll(),j("permanent_lock",{ip:t,failures:l.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):l.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:l.retryAfterMs},429)}O.recordSuccess(t);const c=`browser-${t}`,{plaintext:v}=y.createAccessToken(c,h.security.tokenTtlDays);return H(e,v),j("auth_success",{ip:t,method:"password",tokenName:c}),e.json({ok:!0,token:v})}),s.post("/api/auth/token",o(),async e=>{if(k)return e.json({error:"hub only"},403);const t=C(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,i=typeof r.ttlDays=="number"?r.ttlDays:h.security.tokenTtlDays,{stored:a,plaintext:c}=y.createAccessToken(n,i);return j("token_created",{ip:t,name:a.name,tokenId:a.id}),e.json({id:a.id,name:a.name,token:c,expiresAt:a.expiresAt})}),s.get("/api/auth/tokens",o(),e=>k?e.json({error:"hub only"},403):e.json(y.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",o(),e=>k?e.json({error:"hub only"},403):y.revoke(e.req.param("id"))?(j("token_revoked",{ip:C(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",o(),e=>(J(e),e.json({ok:!0}))),s.get("/settings",g(),async e=>{const t=await Q();return e.html(ge({settings:t,theme:u.activeTheme,saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Q(),n=t.defaultView==="recent"?"recent":"default";return await Pe({...r,defaultView:n}),e.redirect("/settings?saved=1",303)}),s.get("/settings/theme",g(),e=>e.html(ye({theme:u.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!X(r)?e.redirect("/settings/theme",303):(u.activeTheme=await K(r),e.redirect("/settings/theme?saved=1",303))});const se={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",o(),e=>e.json({active:u.activeTheme.template,templates:Le.map(t=>({id:t,name:se[t]}))})),s.post("/api/theme",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!X(r)?e.json({error:"invalid theme template"},400):(u.activeTheme=await K(r),e.json({ok:!0,active:u.activeTheme.template}))}),s.get("/api/system/status",o(),e=>e.json(ze())),s.get("/api/system/processes",o(),e=>e.json(Be())),w&&(s.get("/settings/federation",g(),e=>e.redirect("/settings/machines",301)),s.get("/settings/machines",g(),async e=>e.html(qe({config:w.getConfig(),status:w.status(),tokens:z().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:w.hostname},u.activeTheme))),s.get("/api/machines",o(),e=>e.json({config:w.getConfig(),status:w.status()})),s.post("/api/machines",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.hub=="string"?t.hub.trim():"",n=typeof t.token=="string"?t.token.trim():"",i=typeof t.name=="string"?t.name.trim().slice(0,64):"",a=t.enabled===!0;if(a){if(!r)return e.json({error:"hub URL is required"},400);if(!/^wss?:\/\//i.test(r))return e.json({error:"hub URL must start with ws:// or wss://"},400);if(!n)return e.json({error:"registration token is required"},400)}const c={hub:r||void 0,token:n||void 0,name:i||void 0,enabled:a};return await w.setConfig(c),e.json({ok:!0,status:w.status()})}),s.get("/api/agent-tokens",o(),e=>e.json(z().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim().slice(0,64):"";if(!r)return e.json({error:"name is required"},400);const{id:n,token:i}=Se(r);return e.json({ok:!0,id:n,name:r,token:i})}),s.delete("/api/agent-tokens/:id",o(),e=>{const t=e.req.param("id");return Te(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",o(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=Ve(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",o(),e=>q?e.json(q.agents.list()):e.json([])),s.post("/api/sessions/new",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(L().some(a=>a.name===r))return e.json({error:"session already exists"},409);try{return Qe(r,n),e.json({ok:!0})}catch(a){const c=a instanceof Ye?a.message:"failed to create session";return e.json({error:c},500)}}),s.post("/api/sessions/rename",o(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(Ke(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",o(),async e=>{try{const{name:t}=await e.req.json();return t?(Xe(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",o(),e=>{const t=e.req.query("session");if(!t)return e.json({
|
|
16
|
+
`;s.get("/sw.js",e=>e.body(ne,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!h.passwordHash,n=e.req.query("error");return e.html(fe({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:u.activeTheme}))}),s.get("/",g(),e=>{const t=B();return e.html(ve({theme:u.activeTheme,fsRoots:t,terminalCfg:E,scrollback:E.initialLines+2*E.historyChunk,wsBase:k&&F?.()?`/ws/a/${F()}`:void 0}))}),s.get("/notes",g(),e=>e.html(pe(S.data.notes,u.activeTheme))),s.get("/notes/:session",g(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(he(t,u.activeTheme))}),s.get("/history",g(),e=>{const t=L(),r=new Set(t.map(n=>n.name));return e.html(je(Ie(),u.activeTheme,r))}),s.get("/quick-commands",g(),e=>e.html(we(Y(),u.activeTheme))),s.get("/files",g(),e=>{const t=B();return e.html(ke(u.activeTheme,t))}),s.post("/api/history/clear",o(),async e=>(await Ne(),e.json({ok:!0}))),s.get("/api/quick-commands",o(),e=>e.json(Y())),s.post("/api/quick-commands",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await We(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Ge(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",o(),e=>$e(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(k)return e.json({error:"hub only"},403);const t=C(e),r=O.check(t);if(!r.allowed)return j("rate_limited",{ip:t,retryAfterMs:r.retryAfterMs,permanentLock:r.permanentLock}),r.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:r.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const i=typeof n.password=="string"?n.password:"";if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!tt(t))return j("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(u.settingUpPassword)return e.json({error:"Password setup in progress"},409);const l=xe(i);if(l)return e.json({error:l},400);u.settingUpPassword=!0;try{h.passwordHash=await Ae(i),Re(h),j("password_set",{ip:t})}finally{u.settingUpPassword=!1}const{plaintext:d}=y.createAccessToken("setup",h.security.tokenTtlDays);return H(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await be(i,h.passwordHash)){const l=O.recordFailure(t);return j("auth_failed",{ip:t,method:"password",failures:l.failures,permanentLock:l.permanentLock}),l.permanentLock?(y.revokeAll(),j("permanent_lock",{ip:t,failures:l.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):l.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:l.retryAfterMs},429)}O.recordSuccess(t);const c=`browser-${t}`,{plaintext:v}=y.createAccessToken(c,h.security.tokenTtlDays);return H(e,v),j("auth_success",{ip:t,method:"password",tokenName:c}),e.json({ok:!0,token:v})}),s.post("/api/auth/token",o(),async e=>{if(k)return e.json({error:"hub only"},403);const t=C(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,i=typeof r.ttlDays=="number"?r.ttlDays:h.security.tokenTtlDays,{stored:a,plaintext:c}=y.createAccessToken(n,i);return j("token_created",{ip:t,name:a.name,tokenId:a.id}),e.json({id:a.id,name:a.name,token:c,expiresAt:a.expiresAt})}),s.get("/api/auth/tokens",o(),e=>k?e.json({error:"hub only"},403):e.json(y.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",o(),e=>k?e.json({error:"hub only"},403):y.revoke(e.req.param("id"))?(j("token_revoked",{ip:C(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",o(),e=>(J(e),e.json({ok:!0}))),s.get("/settings",g(),async e=>{const t=await Q();return e.html(ge({settings:t,theme:u.activeTheme,saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Q(),n=t.defaultView==="recent"?"recent":"default";return await Pe({...r,defaultView:n}),e.redirect("/settings?saved=1",303)}),s.get("/settings/theme",g(),e=>e.html(ye({theme:u.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!X(r)?e.redirect("/settings/theme",303):(u.activeTheme=await K(r),e.redirect("/settings/theme?saved=1",303))});const se={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",o(),e=>e.json({active:u.activeTheme.template,templates:Le.map(t=>({id:t,name:se[t]}))})),s.post("/api/theme",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!X(r)?e.json({error:"invalid theme template"},400):(u.activeTheme=await K(r),e.json({ok:!0,active:u.activeTheme.template}))}),s.get("/api/system/status",o(),e=>e.json(ze())),s.get("/api/system/processes",o(),e=>e.json(Be())),w&&(s.get("/settings/federation",g(),e=>e.redirect("/settings/machines",301)),s.get("/settings/machines",g(),async e=>e.html(qe({config:w.getConfig(),status:w.status(),tokens:z().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:w.hostname},u.activeTheme))),s.get("/api/machines",o(),e=>e.json({config:w.getConfig(),status:w.status()})),s.post("/api/machines",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.hub=="string"?t.hub.trim():"",n=typeof t.token=="string"?t.token.trim():"",i=typeof t.name=="string"?t.name.trim().slice(0,64):"",a=t.enabled===!0;if(a){if(!r)return e.json({error:"hub URL is required"},400);if(!/^wss?:\/\//i.test(r))return e.json({error:"hub URL must start with ws:// or wss://"},400);if(!n)return e.json({error:"registration token is required"},400)}const c={hub:r||void 0,token:n||void 0,name:i||void 0,enabled:a};return await w.setConfig(c),e.json({ok:!0,status:w.status()})}),s.get("/api/agent-tokens",o(),e=>e.json(z().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim().slice(0,64):"";if(!r)return e.json({error:"name is required"},400);const{id:n,token:i}=Se(r);return e.json({ok:!0,id:n,name:r,token:i})}),s.delete("/api/agent-tokens/:id",o(),e=>{const t=e.req.param("id");return Te(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",o(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=Ve(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",o(),e=>q?e.json(q.agents.list()):e.json([])),s.post("/api/sessions/new",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(L().some(a=>a.name===r))return e.json({error:"session already exists"},409);try{return Qe(r,n),e.json({ok:!0})}catch(a){const c=a instanceof Ye?a.message:"failed to create session";return e.json({error:c},500)}}),s.post("/api/sessions/rename",o(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(Ke(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",o(),async e=>{try{const{name:t}=await e.req.json();return t?(Xe(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",o(),e=>{const t=e.req.query("session");if(!t)return e.json({path:process.env.HOME??"/"});try{const r=Oe(t),i=r.find(a=>a.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:i})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",o(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=p.join(t,r));const n=e.req.query("recursive")==="true";let i=r,a="",c=r.endsWith("/");if(!c)try{c=A(r).isDirectory()}catch{}c||(i=p.dirname(r),a=p.basename(r).toLowerCase());try{const v=ie(i),l=[],d=[];for(const m of v)if(!m.startsWith(".")&&!(a&&!m.toLowerCase().startsWith(a))){try{const f=p.join(i,m);A(f).isDirectory()?(l.push(f),n&&Ce(f,l,d,0)):d.push(f)}catch{}if(l.length+d.length>=5e3)break}return e.json({dirs:l,files:d})}catch{return e.json({dirs:[],files:[]})}});function oe(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=_("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=_("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=_("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let i="";try{i=_("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const a=[];for(const d of i.split(`
|
|
17
17
|
`)){if(!d.trim())continue;const m=d[0],f=d[1],T=d.substring(3).trim(),R=f!==" "?f:m;a.push({path:T,status:R,staged:m,unstaged:f,additions:0,deletions:0})}const c=new Map;try{const d=_("git diff --numstat HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3});for(const m of d.split(`
|
|
18
18
|
`)){if(!m.trim())continue;const f=m.split(" ");if(f.length>=3){const T=parseInt(f[0],10)||0,R=parseInt(f[1],10)||0;c.set(f[2],{added:T,deleted:R})}}}catch{}for(const d of a){const m=c.get(d.path);m&&(d.additions=m.added,d.deletions=m.deleted)}let v=0,l=0;for(const d of a)v+=d.additions,l+=d.deletions;for(const d of a)if(d.status==="?"&&d.additions===0)try{const m=p.join(r,d.path),T=$(m,"utf-8").split(`
|
|
19
|
-
`).length;d.additions=T,v+=T}catch{}return{repoRoot:r,branch:n,files:a,linesAdded:v,linesRemoved:l}}catch{return t}}return s.get("/api/git/status",o(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=b(t),n=oe(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",o(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=b(t);if(!P(n)||!A(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const i=G("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),a=G("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:i,stagedDiff:a})}catch(i){throw console.error("[git/diff] execFileSync failed:",i?.code,i.message),i}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",o(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=b(t);if(!A(r).isFile())return e.json({error:"not a file"},400);const n=A(r).size;if(n>V)return e.json({error:"file too large",size:n,maxBytes:V},413);const i=$(r,"utf-8");return e.json({path:r,content:i,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",o(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=b(t.path);return _e(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=b(t.path);return A(r).isFile()?(ae(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=b(t.path);return P(r)?e.json({error:"file already exists"},409):(ue(p.dirname(r),{recursive:!0}),de(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}}),s.get("/api/sidebar/sessions",o(),e=>{const t=e.req.query("currentSession");return e.json(N(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",o(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(
|
|
19
|
+
`).length;d.additions=T,v+=T}catch{}return{repoRoot:r,branch:n,files:a,linesAdded:v,linesRemoved:l}}catch{return t}}return s.get("/api/git/status",o(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=b(t),n=oe(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",o(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=b(t);if(!P(n)||!A(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const i=G("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),a=G("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:i,stagedDiff:a})}catch(i){throw console.error("[git/diff] execFileSync failed:",i?.code,i.message),i}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",o(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=b(t);if(!A(r).isFile())return e.json({error:"not a file"},400);const n=A(r).size;if(n>V)return e.json({error:"file too large",size:n,maxBytes:V},413);const i=$(r,"utf-8");return e.json({path:r,content:i,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",o(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=b(t.path);return _e(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=b(t.path);return A(r).isFile()?(ae(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=b(t.path);return P(r)?e.json({error:"file already exists"},409):(ue(p.dirname(r),{recursive:!0}),de(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}}),s.get("/api/sidebar/sessions",o(),e=>{const t=e.req.query("currentSession");return e.json(N(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",o(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(Me(t))}),s.post("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=M(t);return"error"in r?e.json({error:r.error},400):(await De(r.sessionName,r.windowIndex),e.json(N()))}),s.delete("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=M(t);return"error"in r?e.json({error:r.error},400):(await Fe(r.sessionName,r.windowIndex),e.json(N()))}),s.get("/api/notes",o(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",o(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=S.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",o(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},i=S.data.notes.findIndex(a=>a.scope===t);return i>=0?S.data.notes[i]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),q&&(s.use("/a/:agentId/*",o()),Ze(s,{getChannel:q.getChannel})),s}export{Rt as buildApp};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}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 n(e="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${e}; outline: none;`}function
|
|
1
|
+
import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}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 n(e="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${e}; outline: none;`}function w(e=""){return`
|
|
2
2
|
@media (prefers-reduced-motion: reduce) {
|
|
3
3
|
*, *::before, *::after {
|
|
4
4
|
animation-duration: 0.01ms !important;
|
|
@@ -7,7 +7,7 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
|
|
|
7
7
|
scroll-behavior: auto !important;
|
|
8
8
|
}
|
|
9
9
|
${e}
|
|
10
|
-
}`}function
|
|
10
|
+
}`}function v(e="vscode"){const o=d.find(t=>t.id===e)??d[0],a=d.map(t=>{const s=t.id===e;return`<button type="button" class="theme-option${s?" active":""}" data-theme="${t.id}" role="menuitem">
|
|
11
11
|
<span class="theme-dot" style="background:${t.dot}"></span>
|
|
12
12
|
${c(t.name)}
|
|
13
13
|
${s?i("check",'class="theme-check" aria-hidden="true"'):""}
|
|
@@ -47,7 +47,7 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
|
|
|
47
47
|
document.addEventListener('click', (e) => {
|
|
48
48
|
if (switcher.classList.contains('open') && !switcher.contains(e.target)) close();
|
|
49
49
|
});
|
|
50
|
-
})();`}function
|
|
50
|
+
})();`}function L(e=""){return`
|
|
51
51
|
*, *::before, *::after { box-sizing: border-box; }
|
|
52
52
|
html, body { background: var(--page-bg); color: var(--page-fg); min-height: 100%; font-family: var(--font-sans); margin: 0; padding: 0; }
|
|
53
53
|
|
|
@@ -216,7 +216,7 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
|
|
|
216
216
|
.header-btn, .theme-switcher-btn { padding: 8px; }
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
${
|
|
219
|
+
${w()}
|
|
220
220
|
${e}`}function R(e){const{title:o="TMUX Sessions",themeTemplate:a="vscode"}=e;return`<script>
|
|
221
221
|
(function() {
|
|
222
222
|
const token = localStorage.getItem('tmux-web-token');
|
|
@@ -240,15 +240,15 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
|
|
|
240
240
|
${i("notes")}
|
|
241
241
|
<span>Notes</span>
|
|
242
242
|
</button>
|
|
243
|
-
${
|
|
243
|
+
${v(a)}
|
|
244
244
|
<a class="header-btn" href="/settings" title="Settings">
|
|
245
245
|
${i("settings")}
|
|
246
246
|
<span>Settings</span>
|
|
247
247
|
</a>
|
|
248
248
|
</div>
|
|
249
249
|
</header>
|
|
250
|
-
<script>${y()}</script>`}function A(e){const{activePage:o,refreshHref:a}=e;function t(l,b,
|
|
251
|
-
<svg viewBox="0 0 24 24" fill="currentColor">${
|
|
250
|
+
<script>${y()}</script>`}function A(e){const{activePage:o,refreshHref:a}=e;function t(l,f,b,x,g=""){return`<a href="${f}" class="${l!==null&&l===o?"sidebar-btn current":"sidebar-btn"}"${g}>
|
|
251
|
+
<svg viewBox="0 0 24 24" fill="currentColor">${b}</svg>
|
|
252
252
|
${x}
|
|
253
253
|
</a>`}const s=r("sessions"),p=r("notes"),h=r("history"),m=r("quick-commands"),u=r("file"),k=r("refresh");return`<aside class="action-sidebar">
|
|
254
254
|
<p class="sidebar-label">Actions</p>
|
|
@@ -482,6 +482,7 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
|
|
|
482
482
|
let treeRoot = '/';
|
|
483
483
|
let treeExpanded = new Set(); // paths currently expanded
|
|
484
484
|
let treeSelected = ''; // path currently selected (highlighted)
|
|
485
|
+
let treeUserSelected = ''; // path the user actively clicked (closes on second click)
|
|
485
486
|
let treeRows = []; // visible rows [{path, el}] for keyboard nav
|
|
486
487
|
let treeActiveIdx = -1;
|
|
487
488
|
let treeOpen = false;
|
|
@@ -524,11 +525,24 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
|
|
|
524
525
|
e.stopPropagation();
|
|
525
526
|
void treeToggle(path, depth);
|
|
526
527
|
});
|
|
527
|
-
// Clicking the row selects the directory
|
|
528
|
-
|
|
528
|
+
// Clicking the row selects the directory and expands it so the user can
|
|
529
|
+
// keep browsing without the tree closing on them. Clicking the already
|
|
530
|
+
// selected row confirms and closes.
|
|
531
|
+
row.addEventListener('click', async () => {
|
|
529
532
|
dirInput.value = path;
|
|
530
|
-
|
|
531
|
-
|
|
533
|
+
if (treeUserSelected === path) {
|
|
534
|
+
closeTree();
|
|
535
|
+
dirInput.focus();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
treeUserSelected = path;
|
|
539
|
+
treeSelected = path;
|
|
540
|
+
if (!treeExpanded.has(path)) {
|
|
541
|
+
treeExpanded.add(path);
|
|
542
|
+
await treeRender();
|
|
543
|
+
} else {
|
|
544
|
+
treeRefreshActive();
|
|
545
|
+
}
|
|
532
546
|
});
|
|
533
547
|
return row;
|
|
534
548
|
}
|
|
@@ -608,40 +622,48 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
|
|
|
608
622
|
treeOpen = false;
|
|
609
623
|
treeExpanded = new Set();
|
|
610
624
|
treeSelected = '';
|
|
625
|
+
treeUserSelected = '';
|
|
611
626
|
treeRows = [];
|
|
612
627
|
treeActiveIdx = -1;
|
|
613
628
|
}
|
|
614
629
|
|
|
630
|
+
// Resolve the path the tree should open at: the current session's working
|
|
631
|
+
// directory if one is open, otherwise the home directory (both served by
|
|
632
|
+
// /api/fs/session-path \u2014 it returns HOME when no session is given).
|
|
633
|
+
async function treeOpenTarget() {
|
|
634
|
+
const session = (typeof window.__tmuxWebCurrentSession === 'string' && window.__tmuxWebCurrentSession)
|
|
635
|
+
? window.__tmuxWebCurrentSession : '';
|
|
636
|
+
try {
|
|
637
|
+
const url = session
|
|
638
|
+
? selectedBase() + '/api/fs/session-path?session=' + encodeURIComponent(session)
|
|
639
|
+
: selectedBase() + '/api/fs/session-path';
|
|
640
|
+
const res = await fetch(url);
|
|
641
|
+
const data = await res.json();
|
|
642
|
+
if (data && typeof data.path === 'string' && data.path) {
|
|
643
|
+
if (session) dirInput.value = data.path;
|
|
644
|
+
return data.path.startsWith('/') ? data.path : '/' + data.path;
|
|
645
|
+
}
|
|
646
|
+
} catch {}
|
|
647
|
+
return '/'
|
|
648
|
+
}
|
|
649
|
+
|
|
615
650
|
async function openTree() {
|
|
616
651
|
closeDropdown();
|
|
617
|
-
const v = dirInput.value.trim();
|
|
618
|
-
const target = (!v || v === '~') ? '/' : (v.startsWith('/') ? v : '/' + v);
|
|
619
652
|
treeRoot = '/';
|
|
620
653
|
treePanel.classList.add('open');
|
|
621
654
|
treeOpen = true;
|
|
655
|
+
const target = await treeOpenTarget();
|
|
622
656
|
await treeExpandTo(target);
|
|
623
657
|
}
|
|
624
658
|
|
|
625
659
|
// On modal open, expand the tree directly to the currently selected
|
|
626
660
|
// session's working directory (window.__tmuxWebCurrentSession is set by
|
|
627
|
-
// the shell client; falls back to
|
|
661
|
+
// the shell client; falls back to the home directory otherwise).
|
|
628
662
|
async function openTreeForSession() {
|
|
629
|
-
const session = (typeof window.__tmuxWebCurrentSession === 'string' && window.__tmuxWebCurrentSession)
|
|
630
|
-
? window.__tmuxWebCurrentSession : '';
|
|
631
|
-
let target = '/';
|
|
632
|
-
if (session) {
|
|
633
|
-
try {
|
|
634
|
-
const res = await fetch(selectedBase() + '/api/fs/session-path?session=' + encodeURIComponent(session));
|
|
635
|
-
const data = await res.json();
|
|
636
|
-
if (data && typeof data.path === 'string' && data.path) {
|
|
637
|
-
dirInput.value = data.path;
|
|
638
|
-
target = data.path.startsWith('/') ? data.path : '/' + data.path;
|
|
639
|
-
}
|
|
640
|
-
} catch {}
|
|
641
|
-
}
|
|
642
663
|
treeRoot = '/';
|
|
643
664
|
treePanel.classList.add('open');
|
|
644
665
|
treeOpen = true;
|
|
666
|
+
const target = await treeOpenTarget();
|
|
645
667
|
await treeExpandTo(target);
|
|
646
668
|
}
|
|
647
669
|
|
|
@@ -718,4 +740,4 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
|
|
|
718
740
|
if (e.key === 'Enter') submit();
|
|
719
741
|
});
|
|
720
742
|
window.__openNewSessionModal = openModal;
|
|
721
|
-
})();`}export{T as newSessionModalCSS,B as newSessionModalHTML,z as newSessionModalScript,
|
|
743
|
+
})();`}export{T as newSessionModalCSS,B as newSessionModalHTML,z as newSessionModalScript,w as reducedMotion,R as sharedHeader,L as sharedLayoutCSS,A as sharedSidebar,v as themeSwitcherButtonHTML,y as themeSwitcherScript};
|