diff --git a/src/webterm/static/js/terminal.js b/src/webterm/static/js/terminal.js
index d0a2370..7e1a096 100644
--- a/src/webterm/static/js/terminal.js
+++ b/src/webterm/static/js/terminal.js
@@ -25,7 +25,7 @@ For tests, pass a Ghostty instance directly:
resize: none;
font-size: 16px;
caret-color: transparent;
- `,this.element.style.position="relative",this.element.appendChild(V),this.mobileInput=V,V.addEventListener("beforeinput",(j)=>{let $=null;switch(j.inputType){case"insertLineBreak":$="\r";break;case"deleteContentBackward":$="";break;case"deleteContentForward":$="\x1B[3~";break}if($)j.preventDefault(),this.send(["stdin",$]),this.deactivateModifiers()}),V.addEventListener("input",()=>{let j=V.value;if(j){let $=j;if(this.shiftActive&&j.length===1)$=j.toUpperCase();if(this.ctrlActive&&j.length===1){let P=$.toUpperCase().charCodeAt(0);if(P>=65&&P<=90)$=String.fromCharCode(P-64)}this.send(["stdin",$]),V.value="",this.deactivateModifiers()}}),V.addEventListener("keydown",(j)=>{let $=j.ctrlKey||this.ctrlActive,P=j.shiftKey||this.shiftActive;if(j.ctrlKey&&j.key.length===1&&!j.altKey&&!j.metaKey){let R=j.key.toUpperCase().charCodeAt(0);if(R>=65&&R<=90){j.preventDefault(),this.send(["stdin",String.fromCharCode(R-64)]);return}}let X=null,K=!1;switch(j.key){case"Escape":X="\x1B",K=!0;break;case"ArrowUp":case"ArrowDown":case"ArrowRight":case"ArrowLeft":{let R=j.key==="ArrowUp"?"A":j.key==="ArrowDown"?"B":j.key==="ArrowRight"?"C":"D";if($&&P)X=`\x1B[1;6${R}`;else if($)X=`\x1B[1;5${R}`;else if(P)X=`\x1B[1;2${R}`;else X=`\x1B[${R}`;K=!0;break}case"Tab":if(P)X="\x1B[Z";else X="\t";j.preventDefault(),K=!0;break}if(X){if(j.preventDefault(),this.send(["stdin",X]),K)this.deactivateModifiers()}});let Z=()=>{this.mobileInput?.focus()};this.element.addEventListener("touchend",Z,{passive:!0}),this.element.addEventListener("click",Z)}setupMobileKeybar(){let V=document.createElement("div");V.className="mobile-keybar",V.innerHTML=`
+ `,this.element.style.position="relative",this.element.appendChild(V),this.mobileInput=V,V.addEventListener("beforeinput",(j)=>{let $=null;switch(j.inputType){case"insertLineBreak":$="\r";break;case"deleteContentBackward":$="";break;case"deleteContentForward":$="\x1B[3~";break}if($)j.preventDefault(),this.send(["stdin",$]),this.deactivateModifiers()}),V.addEventListener("input",()=>{let j=V.value;if(console.log("[webterm:mobile] input event fired, value:",j,"ctrlActive:",this.ctrlActive,"shiftActive:",this.shiftActive),j){let $=j;if(this.shiftActive&&j.length===1)$=j.toUpperCase();if(this.ctrlActive&&j.length===1){let P=$.toUpperCase().charCodeAt(0);if(P>=65&&P<=90)$=String.fromCharCode(P-64)}console.log("[webterm:mobile] sending:",$.charCodeAt(0),"calling deactivateModifiers"),this.send(["stdin",$]),V.value="",this.deactivateModifiers()}}),V.addEventListener("keydown",(j)=>{let $=j.ctrlKey||this.ctrlActive,P=j.shiftKey||this.shiftActive;if(j.ctrlKey&&j.key.length===1&&!j.altKey&&!j.metaKey){let R=j.key.toUpperCase().charCodeAt(0);if(R>=65&&R<=90){j.preventDefault(),this.send(["stdin",String.fromCharCode(R-64)]);return}}let X=null,K=!1;switch(j.key){case"Escape":X="\x1B",K=!0;break;case"ArrowUp":case"ArrowDown":case"ArrowRight":case"ArrowLeft":{let R=j.key==="ArrowUp"?"A":j.key==="ArrowDown"?"B":j.key==="ArrowRight"?"C":"D";if($&&P)X=`\x1B[1;6${R}`;else if($)X=`\x1B[1;5${R}`;else if(P)X=`\x1B[1;2${R}`;else X=`\x1B[${R}`;K=!0;break}case"Tab":if(P)X="\x1B[Z";else X="\t";j.preventDefault(),K=!0;break}if(X){if(j.preventDefault(),this.send(["stdin",X]),K)this.deactivateModifiers()}});let Z=()=>{this.mobileInput?.focus()};this.element.addEventListener("touchend",Z,{passive:!0}),this.element.addEventListener("click",Z)}setupMobileKeybar(){let V=document.createElement("div");V.className="mobile-keybar",V.innerHTML=`
@@ -86,4 +86,4 @@ For tests, pass a Ghostty instance directly:
grid-column: 5;
grid-row: 2;
}
- `,document.head.appendChild(Z),document.body.appendChild(V),this.mobileKeybar=V,V.querySelectorAll("button[data-key]").forEach((j)=>{j.addEventListener("touchstart",($)=>{$.preventDefault();let P=j.dataset.key||"";if(P=P.replace(/\\x([0-9a-fA-F]{2})/g,(X,K)=>String.fromCharCode(parseInt(K,16))),P=P.replace(/\\x1b/g,"\x1B"),this.shiftActive&&P==="\t")P="\x1B[Z";else if(this.shiftActive&&P.startsWith("\x1B[")&&P.length===3)P=`\x1B[1;2${P[2]}`;else if(this.ctrlActive&&P.startsWith("\x1B[")&&P.length===3)P=`\x1B[1;5${P[2]}`;else if(this.ctrlActive&&this.shiftActive&&P.startsWith("\x1B[")&&P.length===3)P=`\x1B[1;6${P[2]}`;else if(this.ctrlActive&&P.length===1){let X=P.toUpperCase().charCodeAt(0);if(X>=65&&X<=90)P=String.fromCharCode(X-64)}this.send(["stdin",P]),this.deactivateModifiers()})}),V.querySelectorAll("button[data-modifier]").forEach((j)=>{j.addEventListener("touchstart",($)=>{$.preventDefault();let P=j.dataset.modifier;if(P==="ctrl")this.ctrlActive=!this.ctrlActive,j.classList.toggle("active",this.ctrlActive);else if(P==="shift")this.shiftActive=!this.shiftActive,j.classList.toggle("active",this.shiftActive)})}),this.setupKeybarDrag(V)}setupKeybarDrag(V){let Z=V.querySelector(".keybar-drag");if(!Z)return;let j=!1,$=0,P=0,X=0,K=0,R=(_)=>{if(_.touches.length!==1)return;j=!0;let z=_.touches[0];$=z.clientX,P=z.clientY;let u=V.getBoundingClientRect();X=window.innerWidth-u.right,K=window.innerHeight-u.bottom,_.preventDefault()},O=(_)=>{if(!j||_.touches.length!==1)return;let z=_.touches[0],u=$-z.clientX,W=P-z.clientY,H=Math.max(0,Math.min(window.innerWidth-100,X+u)),p=Math.max(0,Math.min(window.innerHeight-50,K+W));V.style.right=`${H}px`,V.style.bottom=`${p}px`,_.preventDefault()},Y=()=>{j=!1};Z.addEventListener("touchstart",R,{passive:!1}),document.addEventListener("touchmove",O,{passive:!1}),document.addEventListener("touchend",Y)}deactivateModifiers(){this.ctrlActive=!1,this.shiftActive=!1,this.mobileKeybar?.querySelectorAll("button[data-modifier]").forEach((V)=>{V.classList.remove("active")})}focusMobileInput(){this.mobileInput?.focus()}async waitForFonts(){if(!("fonts"in document))return;try{await document.fonts.ready}catch{}}fit(){let Z=this.terminal.renderer,j,$;if(Z?.getMetrics){let W=Z.getMetrics();if(W&&W.width>0&&W.height>0)j=W.width,$=W.height;else{let H=this.measureCellSize();if(!H){this.fitAddon.fit();return}j=H.width,$=H.height}}else{let W=this.measureCellSize();if(!W){this.fitAddon.fit();return}j=W.width,$=W.height}let P=window.getComputedStyle(this.element),X=parseInt(P.paddingTop)||0,K=parseInt(P.paddingBottom)||0,R=parseInt(P.paddingLeft)||0,O=parseInt(P.paddingRight)||0,Y=this.element.clientWidth-R-O,_=this.element.clientHeight-X-K;if(Y<=0||_<=0)return;let z=Math.max(2,Math.floor(Y/j)),u=Math.max(1,Math.floor(_/$));if(z!==this.terminal.cols||u!==this.terminal.rows)this.terminal.resize(z,u)}measureCellSize(){let V=document.createElement("span");V.style.visibility="hidden",V.style.position="absolute",V.style.fontFamily=this.fontFamily,V.style.fontSize=`${this.fontSize}px`,V.style.lineHeight="normal",V.textContent="W",document.body.appendChild(V);let{offsetWidth:Z,offsetHeight:j}=V;if(document.body.removeChild(V),Z>0&&j>0)return{width:Z,height:j};return null}setupResizeObserver(){new ResizeObserver(()=>{if(this.resizeDebounceTimer)clearTimeout(this.resizeDebounceTimer);this.resizeDebounceTimer=window.setTimeout(()=>{this.fit()},100)}).observe(this.element)}resizeDebounceTimer;isValidSize(V,Z){return V>=2&&V<=500&&Z>=1&&Z<=200}connect(){if(this.socket?.readyState===WebSocket.OPEN)return;this.socket=new WebSocket(this.wsUrl),this.socket.binaryType="arraybuffer",this.socket.addEventListener("open",()=>{this.reconnectAttempts=0,this.element.classList.add("-connected"),this.element.classList.remove("-disconnected"),this.processMessageQueue();let V=this.terminal.cols,Z=this.terminal.rows;if(this.isValidSize(V,Z))this.lastValidSize={cols:V,rows:Z},this.send(["resize",{width:V,height:Z}]);this.terminal.focus()}),this.socket.addEventListener("close",()=>{this.element.classList.remove("-connected"),this.element.classList.add("-disconnected"),this.scheduleReconnect()}),this.socket.addEventListener("error",()=>{}),this.socket.addEventListener("message",(V)=>{this.handleMessage(V.data)})}handleMessage(V){if(V instanceof ArrayBuffer){let Z=new TextDecoder().decode(V);this.terminal.write(Z);return}try{let Z=JSON.parse(V),[j,$]=Z;switch(j){case"stdout":this.terminal.write($);break;case"pong":break;default:console.debug("Unknown message type:",j)}}catch{this.terminal.write(V)}}send(V){this.messageQueue.push(V),this.processMessageQueue()}processMessageQueue(){if(this.socket?.readyState!==WebSocket.OPEN)return;while(this.messageQueue.length>0){let V=this.messageQueue.shift();try{if(V)this.socket.send(JSON.stringify(V))}catch(Z){if(console.error("Failed to send message:",Z,V),V)this.messageQueue.unshift(V);break}}}scheduleReconnect(){if(this.reconnectAttempts>=this.maxReconnectAttempts){console.error("Max reconnection attempts reached");return}this.reconnectAttempts++;let V=this.reconnectDelay*Math.pow(2,this.reconnectAttempts-1);setTimeout(()=>{console.log(`Reconnecting (attempt ${this.reconnectAttempts})...`),this.connect()},V)}dispose(){if(this.socket?.close(),this.mobileInput)this.mobileInput.remove(),this.mobileInput=null;if(this.mobileKeybar)this.mobileKeybar.remove(),this.mobileKeybar=null;this.fitAddon.dispose(),this.terminal.dispose()}setTheme(V){let Z=this.terminal.renderer;if(Z&&typeof Z.setTheme==="function")Z.setTheme(V)}static getTheme(V){return U[V.toLowerCase()]}}var p0=new Map;async function A(){console.log("[webterm:init] initTerminals() called");let V=document.querySelectorAll(".webterm-terminal");console.log(`[webterm:init] Found ${V.length} .webterm-terminal containers`);for(let Z of V){console.log("[webterm:init] Processing container:",Z),console.log("[webterm:init] Dataset:",JSON.stringify(Z.dataset));let j=Z.dataset.sessionWebsocketUrl;if(!j){console.error("Missing data-session-websocket-url on terminal container");continue}let $=z0(Z);console.log("[webterm:init] Parsed config:",JSON.stringify($,null,2));try{console.log("[webterm:init] Calling WebTerminal.create()...");let P=await m.create(Z,j,$);console.log("[webterm:init] WebTerminal created successfully"),p0.set(Z,P)}catch(P){console.error("Failed to create terminal:",P)}}}if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",()=>A());else A();export{p0 as instances,A as initTerminals,m as WebTerminal,U as THEMES};
+ `,document.head.appendChild(Z),document.body.appendChild(V),this.mobileKeybar=V,V.querySelectorAll("button[data-key]").forEach((j)=>{j.addEventListener("touchstart",($)=>{$.preventDefault();let P=j.dataset.key||"";if(P=P.replace(/\\x([0-9a-fA-F]{2})/g,(X,K)=>String.fromCharCode(parseInt(K,16))),P=P.replace(/\\x1b/g,"\x1B"),this.shiftActive&&P==="\t")P="\x1B[Z";else if(this.shiftActive&&P.startsWith("\x1B[")&&P.length===3)P=`\x1B[1;2${P[2]}`;else if(this.ctrlActive&&P.startsWith("\x1B[")&&P.length===3)P=`\x1B[1;5${P[2]}`;else if(this.ctrlActive&&this.shiftActive&&P.startsWith("\x1B[")&&P.length===3)P=`\x1B[1;6${P[2]}`;else if(this.ctrlActive&&P.length===1){let X=P.toUpperCase().charCodeAt(0);if(X>=65&&X<=90)P=String.fromCharCode(X-64)}this.send(["stdin",P]),this.deactivateModifiers()})}),V.querySelectorAll("button[data-modifier]").forEach((j)=>{j.addEventListener("touchstart",($)=>{$.preventDefault();let P=j.dataset.modifier;if(P==="ctrl")this.ctrlActive=!this.ctrlActive,j.classList.toggle("active",this.ctrlActive);else if(P==="shift")this.shiftActive=!this.shiftActive,j.classList.toggle("active",this.shiftActive)})}),this.setupKeybarDrag(V)}setupKeybarDrag(V){let Z=V.querySelector(".keybar-drag");if(!Z)return;let j=!1,$=0,P=0,X=0,K=0,R=(_)=>{if(_.touches.length!==1)return;j=!0;let z=_.touches[0];$=z.clientX,P=z.clientY;let u=V.getBoundingClientRect();X=window.innerWidth-u.right,K=window.innerHeight-u.bottom,_.preventDefault()},O=(_)=>{if(!j||_.touches.length!==1)return;let z=_.touches[0],u=$-z.clientX,W=P-z.clientY,H=Math.max(0,Math.min(window.innerWidth-100,X+u)),p=Math.max(0,Math.min(window.innerHeight-50,K+W));V.style.right=`${H}px`,V.style.bottom=`${p}px`,_.preventDefault()},Y=()=>{j=!1};Z.addEventListener("touchstart",R,{passive:!1}),document.addEventListener("touchmove",O,{passive:!1}),document.addEventListener("touchend",Y)}deactivateModifiers(){console.log("[webterm:mobile] deactivateModifiers called"),this.ctrlActive=!1,this.shiftActive=!1,this.mobileKeybar?.querySelectorAll("button[data-modifier]").forEach((V)=>{V.classList.remove("active")})}focusMobileInput(){this.mobileInput?.focus()}async waitForFonts(){if(!("fonts"in document))return;try{await document.fonts.ready}catch{}}fit(){let Z=this.terminal.renderer,j,$;if(Z?.getMetrics){let W=Z.getMetrics();if(W&&W.width>0&&W.height>0)j=W.width,$=W.height;else{let H=this.measureCellSize();if(!H){this.fitAddon.fit();return}j=H.width,$=H.height}}else{let W=this.measureCellSize();if(!W){this.fitAddon.fit();return}j=W.width,$=W.height}let P=window.getComputedStyle(this.element),X=parseInt(P.paddingTop)||0,K=parseInt(P.paddingBottom)||0,R=parseInt(P.paddingLeft)||0,O=parseInt(P.paddingRight)||0,Y=this.element.clientWidth-R-O,_=this.element.clientHeight-X-K;if(Y<=0||_<=0)return;let z=Math.max(2,Math.floor(Y/j)),u=Math.max(1,Math.floor(_/$));if(z!==this.terminal.cols||u!==this.terminal.rows)this.terminal.resize(z,u)}measureCellSize(){let V=document.createElement("span");V.style.visibility="hidden",V.style.position="absolute",V.style.fontFamily=this.fontFamily,V.style.fontSize=`${this.fontSize}px`,V.style.lineHeight="normal",V.textContent="W",document.body.appendChild(V);let{offsetWidth:Z,offsetHeight:j}=V;if(document.body.removeChild(V),Z>0&&j>0)return{width:Z,height:j};return null}setupResizeObserver(){new ResizeObserver(()=>{if(this.resizeDebounceTimer)clearTimeout(this.resizeDebounceTimer);this.resizeDebounceTimer=window.setTimeout(()=>{this.fit()},100)}).observe(this.element)}resizeDebounceTimer;isValidSize(V,Z){return V>=2&&V<=500&&Z>=1&&Z<=200}connect(){if(this.socket?.readyState===WebSocket.OPEN)return;this.socket=new WebSocket(this.wsUrl),this.socket.binaryType="arraybuffer",this.socket.addEventListener("open",()=>{this.reconnectAttempts=0,this.element.classList.add("-connected"),this.element.classList.remove("-disconnected"),this.processMessageQueue();let V=this.terminal.cols,Z=this.terminal.rows;if(this.isValidSize(V,Z))this.lastValidSize={cols:V,rows:Z},this.send(["resize",{width:V,height:Z}]);this.terminal.focus()}),this.socket.addEventListener("close",()=>{this.element.classList.remove("-connected"),this.element.classList.add("-disconnected"),this.scheduleReconnect()}),this.socket.addEventListener("error",()=>{}),this.socket.addEventListener("message",(V)=>{this.handleMessage(V.data)})}handleMessage(V){if(V instanceof ArrayBuffer){let Z=new TextDecoder().decode(V);this.terminal.write(Z);return}try{let Z=JSON.parse(V),[j,$]=Z;switch(j){case"stdout":this.terminal.write($);break;case"pong":break;default:console.debug("Unknown message type:",j)}}catch{this.terminal.write(V)}}send(V){this.messageQueue.push(V),this.processMessageQueue()}processMessageQueue(){if(this.socket?.readyState!==WebSocket.OPEN)return;while(this.messageQueue.length>0){let V=this.messageQueue.shift();try{if(V)this.socket.send(JSON.stringify(V))}catch(Z){if(console.error("Failed to send message:",Z,V),V)this.messageQueue.unshift(V);break}}}scheduleReconnect(){if(this.reconnectAttempts>=this.maxReconnectAttempts){console.error("Max reconnection attempts reached");return}this.reconnectAttempts++;let V=this.reconnectDelay*Math.pow(2,this.reconnectAttempts-1);setTimeout(()=>{console.log(`Reconnecting (attempt ${this.reconnectAttempts})...`),this.connect()},V)}dispose(){if(this.socket?.close(),this.mobileInput)this.mobileInput.remove(),this.mobileInput=null;if(this.mobileKeybar)this.mobileKeybar.remove(),this.mobileKeybar=null;this.fitAddon.dispose(),this.terminal.dispose()}setTheme(V){let Z=this.terminal.renderer;if(Z&&typeof Z.setTheme==="function")Z.setTheme(V)}static getTheme(V){return U[V.toLowerCase()]}}var p0=new Map;async function A(){console.log("[webterm:init] initTerminals() called");let V=document.querySelectorAll(".webterm-terminal");console.log(`[webterm:init] Found ${V.length} .webterm-terminal containers`);for(let Z of V){console.log("[webterm:init] Processing container:",Z),console.log("[webterm:init] Dataset:",JSON.stringify(Z.dataset));let j=Z.dataset.sessionWebsocketUrl;if(!j){console.error("Missing data-session-websocket-url on terminal container");continue}let $=z0(Z);console.log("[webterm:init] Parsed config:",JSON.stringify($,null,2));try{console.log("[webterm:init] Calling WebTerminal.create()...");let P=await m.create(Z,j,$);console.log("[webterm:init] WebTerminal created successfully"),p0.set(Z,P)}catch(P){console.error("Failed to create terminal:",P)}}}if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",()=>A());else A();export{p0 as instances,A as initTerminals,m as WebTerminal,U as THEMES};
diff --git a/src/webterm/static/js/terminal.ts b/src/webterm/static/js/terminal.ts
index 5552939..7d396cc 100644
--- a/src/webterm/static/js/terminal.ts
+++ b/src/webterm/static/js/terminal.ts
@@ -662,6 +662,7 @@ class WebTerminal {
// Handle input from mobile keyboard (regular text only, special keys handled above)
textarea.addEventListener("input", () => {
const value = textarea.value;
+ console.log("[webterm:mobile] input event fired, value:", value, "ctrlActive:", this.ctrlActive, "shiftActive:", this.shiftActive);
if (value) {
let toSend = value;
// Apply Shift modifier (uppercase letters)
@@ -675,6 +676,7 @@ class WebTerminal {
toSend = String.fromCharCode(code - 64); // Ctrl+A = 0x01, Ctrl+D = 0x04, etc.
}
}
+ console.log("[webterm:mobile] sending:", toSend.charCodeAt(0), "calling deactivateModifiers");
this.send(["stdin", toSend]);
textarea.value = "";
this.deactivateModifiers();
@@ -938,6 +940,7 @@ class WebTerminal {
/** Deactivate all modifiers */
private deactivateModifiers(): void {
+ console.log("[webterm:mobile] deactivateModifiers called");
this.ctrlActive = false;
this.shiftActive = false;
this.mobileKeybar?.querySelectorAll("button[data-modifier]").forEach((btn) => {