ttyd-mux 0.3.1 → 0.4.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.
Files changed (188) hide show
  1. package/README.md +52 -1
  2. package/dist/caddy/client.d.ts +3 -78
  3. package/dist/caddy/client.d.ts.map +1 -1
  4. package/dist/caddy/client.js +0 -170
  5. package/dist/caddy/client.js.map +1 -1
  6. package/dist/caddy/route-builder.d.ts +49 -0
  7. package/dist/caddy/route-builder.d.ts.map +1 -0
  8. package/dist/caddy/route-builder.js +175 -0
  9. package/dist/caddy/route-builder.js.map +1 -0
  10. package/dist/caddy/types.d.ts +27 -0
  11. package/dist/caddy/types.d.ts.map +1 -0
  12. package/dist/caddy/types.js +3 -0
  13. package/dist/caddy/types.js.map +1 -0
  14. package/dist/client/api-client.d.ts +26 -0
  15. package/dist/client/api-client.d.ts.map +1 -0
  16. package/dist/client/api-client.js +62 -0
  17. package/dist/client/api-client.js.map +1 -0
  18. package/dist/client/daemon-client.d.ts +48 -0
  19. package/dist/client/daemon-client.d.ts.map +1 -0
  20. package/dist/client/daemon-client.js +205 -0
  21. package/dist/client/daemon-client.js.map +1 -0
  22. package/dist/client/index.d.ts +2 -10
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +4 -149
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/commands/attach.js +3 -4
  27. package/dist/commands/attach.js.map +1 -1
  28. package/dist/commands/caddy.d.ts.map +1 -1
  29. package/dist/commands/caddy.js +73 -108
  30. package/dist/commands/caddy.js.map +1 -1
  31. package/dist/commands/daemon.js.map +1 -1
  32. package/dist/commands/deploy.d.ts.map +1 -1
  33. package/dist/commands/deploy.js +3 -8
  34. package/dist/commands/deploy.js.map +1 -1
  35. package/dist/commands/doctor.d.ts +8 -0
  36. package/dist/commands/doctor.d.ts.map +1 -0
  37. package/dist/commands/doctor.js +180 -0
  38. package/dist/commands/doctor.js.map +1 -0
  39. package/dist/commands/down.d.ts.map +1 -1
  40. package/dist/commands/down.js +11 -0
  41. package/dist/commands/down.js.map +1 -1
  42. package/dist/commands/reload.d.ts +14 -0
  43. package/dist/commands/reload.d.ts.map +1 -0
  44. package/dist/commands/reload.js +50 -0
  45. package/dist/commands/reload.js.map +1 -0
  46. package/dist/commands/shutdown.d.ts +2 -1
  47. package/dist/commands/shutdown.d.ts.map +1 -1
  48. package/dist/commands/shutdown.js +8 -2
  49. package/dist/commands/shutdown.js.map +1 -1
  50. package/dist/commands/start.d.ts.map +1 -1
  51. package/dist/commands/start.js +16 -3
  52. package/dist/commands/start.js.map +1 -1
  53. package/dist/commands/status.js.map +1 -1
  54. package/dist/commands/stop.js.map +1 -1
  55. package/dist/commands/up.js.map +1 -1
  56. package/dist/config/config.d.ts.map +1 -1
  57. package/dist/config/config.js +9 -2
  58. package/dist/config/config.js.map +1 -1
  59. package/dist/config/index.d.ts +3 -3
  60. package/dist/config/index.d.ts.map +1 -1
  61. package/dist/config/index.js +6 -3
  62. package/dist/config/index.js.map +1 -1
  63. package/dist/config/state-store.d.ts +27 -0
  64. package/dist/config/state-store.d.ts.map +1 -0
  65. package/dist/config/state-store.js +55 -0
  66. package/dist/config/state-store.js.map +1 -0
  67. package/dist/config/state.d.ts +6 -0
  68. package/dist/config/state.d.ts.map +1 -1
  69. package/dist/config/state.js +49 -14
  70. package/dist/config/state.js.map +1 -1
  71. package/dist/config/types.d.ts +29 -0
  72. package/dist/config/types.d.ts.map +1 -1
  73. package/dist/config/types.js +20 -1
  74. package/dist/config/types.js.map +1 -1
  75. package/dist/daemon/api-handler.d.ts +5 -0
  76. package/dist/daemon/api-handler.d.ts.map +1 -0
  77. package/dist/daemon/api-handler.js +97 -0
  78. package/dist/daemon/api-handler.js.map +1 -0
  79. package/dist/daemon/config-manager.d.ts +43 -0
  80. package/dist/daemon/config-manager.d.ts.map +1 -0
  81. package/dist/daemon/config-manager.js +154 -0
  82. package/dist/daemon/config-manager.js.map +1 -0
  83. package/dist/daemon/http-proxy.d.ts +27 -0
  84. package/dist/daemon/http-proxy.d.ts.map +1 -0
  85. package/dist/daemon/http-proxy.js +110 -0
  86. package/dist/daemon/http-proxy.js.map +1 -0
  87. package/dist/daemon/ime-helper.d.ts +1 -1
  88. package/dist/daemon/ime-helper.d.ts.map +1 -1
  89. package/dist/daemon/ime-helper.js +24 -53
  90. package/dist/daemon/ime-helper.js.map +1 -1
  91. package/dist/daemon/index.d.ts.map +1 -1
  92. package/dist/daemon/index.js +134 -29
  93. package/dist/daemon/index.js.map +1 -1
  94. package/dist/daemon/portal-utils.d.ts +20 -0
  95. package/dist/daemon/portal-utils.d.ts.map +1 -0
  96. package/dist/daemon/portal-utils.js +109 -0
  97. package/dist/daemon/portal-utils.js.map +1 -0
  98. package/dist/daemon/portal.d.ts.map +1 -1
  99. package/dist/daemon/portal.js +20 -77
  100. package/dist/daemon/portal.js.map +1 -1
  101. package/dist/daemon/pwa.d.ts +52 -0
  102. package/dist/daemon/pwa.d.ts.map +1 -0
  103. package/dist/daemon/pwa.js +229 -0
  104. package/dist/daemon/pwa.js.map +1 -0
  105. package/dist/daemon/router.d.ts +15 -0
  106. package/dist/daemon/router.d.ts.map +1 -0
  107. package/dist/daemon/router.js +164 -0
  108. package/dist/daemon/router.js.map +1 -0
  109. package/dist/daemon/server.d.ts +15 -3
  110. package/dist/daemon/server.d.ts.map +1 -1
  111. package/dist/daemon/server.js +23 -271
  112. package/dist/daemon/server.js.map +1 -1
  113. package/dist/daemon/session-manager.d.ts +44 -10
  114. package/dist/daemon/session-manager.d.ts.map +1 -1
  115. package/dist/daemon/session-manager.js +125 -49
  116. package/dist/daemon/session-manager.js.map +1 -1
  117. package/dist/daemon/session-resolver.d.ts +1 -1
  118. package/dist/daemon/session-resolver.d.ts.map +1 -1
  119. package/dist/daemon/session-resolver.js.map +1 -1
  120. package/dist/daemon/toolbar/config.d.ts +13 -0
  121. package/dist/daemon/toolbar/config.d.ts.map +1 -0
  122. package/dist/daemon/toolbar/config.js +13 -0
  123. package/dist/daemon/toolbar/config.js.map +1 -0
  124. package/dist/daemon/toolbar/index.d.ts +43 -0
  125. package/dist/daemon/toolbar/index.d.ts.map +1 -0
  126. package/dist/daemon/toolbar/index.js +835 -0
  127. package/dist/daemon/toolbar/index.js.map +1 -0
  128. package/dist/daemon/toolbar/styles.d.ts +5 -0
  129. package/dist/daemon/toolbar/styles.d.ts.map +1 -0
  130. package/dist/daemon/toolbar/styles.js +278 -0
  131. package/dist/daemon/toolbar/styles.js.map +1 -0
  132. package/dist/daemon/toolbar/template.d.ts +6 -0
  133. package/dist/daemon/toolbar/template.d.ts.map +1 -0
  134. package/dist/daemon/toolbar/template.js +45 -0
  135. package/dist/daemon/toolbar/template.js.map +1 -0
  136. package/dist/daemon/ws-proxy.d.ts +17 -0
  137. package/dist/daemon/ws-proxy.d.ts.map +1 -0
  138. package/dist/daemon/ws-proxy.js +95 -0
  139. package/dist/daemon/ws-proxy.js.map +1 -0
  140. package/dist/deploy/caddyfile.d.ts.map +1 -1
  141. package/dist/deploy/caddyfile.js.map +1 -1
  142. package/dist/deploy/deploy-script.d.ts.map +1 -1
  143. package/dist/deploy/static-portal.d.ts.map +1 -1
  144. package/dist/deploy/static-portal.js +6 -77
  145. package/dist/deploy/static-portal.js.map +1 -1
  146. package/dist/index.js +16 -3
  147. package/dist/index.js.map +1 -1
  148. package/dist/test-setup.d.ts +19 -0
  149. package/dist/test-setup.d.ts.map +1 -0
  150. package/dist/test-setup.js +33 -0
  151. package/dist/test-setup.js.map +1 -0
  152. package/dist/tmux.d.ts +28 -1
  153. package/dist/tmux.d.ts.map +1 -1
  154. package/dist/tmux.js +37 -32
  155. package/dist/tmux.js.map +1 -1
  156. package/dist/ui.d.ts +2 -1
  157. package/dist/ui.d.ts.map +1 -1
  158. package/dist/ui.js +16 -9
  159. package/dist/ui.js.map +1 -1
  160. package/dist/utils/errors.d.ts +4 -0
  161. package/dist/utils/errors.d.ts.map +1 -1
  162. package/dist/utils/errors.js +9 -1
  163. package/dist/utils/errors.js.map +1 -1
  164. package/dist/utils/logger.d.ts +14 -0
  165. package/dist/utils/logger.d.ts.map +1 -0
  166. package/dist/utils/logger.js +53 -0
  167. package/dist/utils/logger.js.map +1 -0
  168. package/dist/utils/process-runner.d.ts +50 -0
  169. package/dist/utils/process-runner.d.ts.map +1 -0
  170. package/dist/utils/process-runner.js +73 -0
  171. package/dist/utils/process-runner.js.map +1 -0
  172. package/dist/utils/socket-client.d.ts +24 -0
  173. package/dist/utils/socket-client.d.ts.map +1 -0
  174. package/dist/utils/socket-client.js +30 -0
  175. package/dist/utils/socket-client.js.map +1 -0
  176. package/dist/utils/tmux-client.d.ts +57 -0
  177. package/dist/utils/tmux-client.d.ts.map +1 -0
  178. package/dist/utils/tmux-client.js +117 -0
  179. package/dist/utils/tmux-client.js.map +1 -0
  180. package/dist/version.d.ts +3 -0
  181. package/dist/version.d.ts.map +1 -0
  182. package/dist/version.js +4 -0
  183. package/dist/version.js.map +1 -0
  184. package/package.json +6 -2
  185. package/dist/daemon/proxy.d.ts +0 -7
  186. package/dist/daemon/proxy.d.ts.map +0 -1
  187. package/dist/daemon/proxy.js +0 -17
  188. package/dist/daemon/proxy.js.map +0 -1
@@ -3,6 +3,6 @@
3
3
  * Provides a pseudo copy-paste input field for Japanese IME support
4
4
  * Optimized for mobile devices
5
5
  */
6
- export declare const imeHelperScript = "\n<style>\n#ttyd-ime-container {\n position: fixed;\n bottom: 0;\n left: 0;\n right: 0;\n background: #1e1e1e;\n border-top: 2px solid #007acc;\n padding: 8px;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n box-shadow: 0 -2px 10px rgba(0,0,0,0.3);\n}\n\n#ttyd-ime-container.hidden {\n display: none;\n}\n\n#ttyd-ime-buttons {\n display: flex;\n gap: 6px;\n margin-bottom: 8px;\n flex-wrap: wrap;\n}\n\n#ttyd-ime-buttons button {\n background: #3a3a3a;\n border: 1px solid #555;\n border-radius: 6px;\n color: #fff;\n cursor: pointer;\n font-size: 13px;\n padding: 8px 12px;\n min-height: 40px;\n min-width: 44px;\n touch-action: manipulation;\n flex-shrink: 0;\n}\n\n#ttyd-ime-buttons button:hover, #ttyd-ime-buttons button:active {\n background: #4a4a4a;\n}\n\n#ttyd-ime-buttons button.active {\n background: #007acc;\n border-color: #005a9e;\n}\n\n#ttyd-ime-buttons button.modifier {\n background: #2d2d2d;\n font-weight: bold;\n}\n\n#ttyd-ime-buttons button.modifier.active {\n background: #d9534f;\n border-color: #c9302c;\n}\n\n#ttyd-ime-send {\n background: #007acc !important;\n border-color: #005a9e !important;\n font-weight: bold;\n}\n\n#ttyd-ime-send:hover, #ttyd-ime-send:active {\n background: #005a9e !important;\n}\n\n#ttyd-ime-run {\n background: #28a745 !important;\n border-color: #1e7e34 !important;\n font-weight: bold;\n}\n\n#ttyd-ime-run:hover, #ttyd-ime-run:active {\n background: #1e7e34 !important;\n}\n\n#ttyd-ime-auto.active {\n background: #f0ad4e !important;\n border-color: #eea236 !important;\n color: #000;\n}\n\n#ttyd-ime-input-row {\n display: flex;\n gap: 8px;\n align-items: flex-end;\n}\n\n#ttyd-ime-input {\n flex: 1;\n background: #2d2d2d;\n border: 1px solid #555;\n border-radius: 8px;\n color: #fff;\n font-family: monospace;\n font-size: 16px;\n padding: 12px;\n outline: none;\n resize: none;\n min-height: 44px;\n max-height: 120px;\n line-height: 1.4;\n}\n\n#ttyd-ime-input:focus {\n border-color: #007acc;\n}\n\n#ttyd-ime-input::placeholder {\n color: #888;\n}\n\n#ttyd-ime-toggle {\n position: fixed;\n bottom: 16px;\n right: 16px;\n background: #007acc;\n border: 2px solid #005a9e;\n border-radius: 50%;\n color: #fff;\n cursor: pointer;\n font-size: 20px;\n width: 56px;\n height: 56px;\n z-index: 10001;\n touch-action: manipulation;\n box-shadow: 0 2px 8px rgba(0,0,0,0.3);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n#ttyd-ime-toggle:hover, #ttyd-ime-toggle:active {\n background: #005a9e;\n transform: scale(1.05);\n}\n\n#ttyd-ime-container.hidden ~ #ttyd-ime-toggle {\n bottom: 16px;\n}\n\n/* Adjust terminal height when IME bar is visible */\nbody:has(#ttyd-ime-container:not(.hidden)) .xterm {\n height: calc(100vh - 140px) !important;\n}\n\n/* Mobile optimizations */\n@media (max-width: 768px) {\n #ttyd-ime-container {\n padding: 6px;\n }\n\n #ttyd-ime-buttons {\n gap: 4px;\n margin-bottom: 6px;\n }\n\n #ttyd-ime-buttons button {\n font-size: 12px;\n padding: 6px 10px;\n min-height: 36px;\n min-width: 40px;\n }\n\n #ttyd-ime-input {\n font-size: 16px;\n padding: 10px;\n }\n\n #ttyd-ime-toggle {\n width: 64px;\n height: 64px;\n font-size: 24px;\n }\n\n body:has(#ttyd-ime-container:not(.hidden)) .xterm {\n height: calc(100vh - 130px) !important;\n }\n}\n</style>\n\n<div id=\"ttyd-ime-container\" class=\"hidden\">\n <div id=\"ttyd-ime-buttons\">\n <button id=\"ttyd-ime-ctrl\" class=\"modifier\">Ctrl</button>\n <button id=\"ttyd-ime-alt\" class=\"modifier\">Alt</button>\n <button id=\"ttyd-ime-shift\" class=\"modifier\">Shift</button>\n <button id=\"ttyd-ime-esc\">Esc</button>\n <button id=\"ttyd-ime-tab\">Tab</button>\n <button id=\"ttyd-ime-up\">\u2191</button>\n <button id=\"ttyd-ime-down\">\u2193</button>\n <button id=\"ttyd-ime-enter\">Enter</button>\n <button id=\"ttyd-ime-zoomout\">A-</button>\n <button id=\"ttyd-ime-zoomin\">A+</button>\n <button id=\"ttyd-ime-copy\">Copy</button>\n <button id=\"ttyd-ime-copyall\">All</button>\n <button id=\"ttyd-ime-send\">Send</button>\n <button id=\"ttyd-ime-run\">Run</button>\n <button id=\"ttyd-ime-auto\" class=\"modifier\">Auto</button>\n </div>\n <div id=\"ttyd-ime-input-row\">\n <textarea id=\"ttyd-ime-input\" rows=\"1\" placeholder=\"\u65E5\u672C\u8A9E\u5165\u529B (Enter: \u9001\u4FE1)\"></textarea>\n </div>\n</div>\n<button id=\"ttyd-ime-toggle\">\u2328</button>\n\n<script>\n(function() {\n const container = document.getElementById('ttyd-ime-container');\n const input = document.getElementById('ttyd-ime-input');\n const sendBtn = document.getElementById('ttyd-ime-send');\n const enterBtn = document.getElementById('ttyd-ime-enter');\n const zoomInBtn = document.getElementById('ttyd-ime-zoomin');\n const zoomOutBtn = document.getElementById('ttyd-ime-zoomout');\n const runBtn = document.getElementById('ttyd-ime-run');\n const toggleBtn = document.getElementById('ttyd-ime-toggle');\n const ctrlBtn = document.getElementById('ttyd-ime-ctrl');\n const altBtn = document.getElementById('ttyd-ime-alt');\n const shiftBtn = document.getElementById('ttyd-ime-shift');\n const escBtn = document.getElementById('ttyd-ime-esc');\n const tabBtn = document.getElementById('ttyd-ime-tab');\n const upBtn = document.getElementById('ttyd-ime-up');\n const downBtn = document.getElementById('ttyd-ime-down');\n const copyBtn = document.getElementById('ttyd-ime-copy');\n const copyAllBtn = document.getElementById('ttyd-ime-copyall');\n const autoBtn = document.getElementById('ttyd-ime-auto');\n\n let ws = null;\n let ctrlActive = false;\n let altActive = false;\n let shiftActive = false;\n let autoRunActive = false;\n\n // Detect mobile device\n const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\n\n // Find the WebSocket connection\n function findWebSocket() {\n if (ws && ws.readyState === WebSocket.OPEN) return ws;\n\n if (window.socket && window.socket.readyState === WebSocket.OPEN) {\n ws = window.socket;\n return ws;\n }\n\n return null;\n }\n\n // Intercept WebSocket creation to capture the connection\n const OriginalWebSocket = window.WebSocket;\n window.WebSocket = function(url, protocols) {\n const socket = new OriginalWebSocket(url, protocols);\n if (url.includes('/ws')) {\n ws = socket;\n }\n return socket;\n };\n window.WebSocket.prototype = OriginalWebSocket.prototype;\n window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;\n window.WebSocket.OPEN = OriginalWebSocket.OPEN;\n window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;\n window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;\n\n function sendText(text) {\n const socket = findWebSocket();\n if (!socket) {\n console.error('[IME Helper] WebSocket not found');\n return false;\n }\n\n // ttyd protocol: binary data with '0' (input command) as first byte\n const encoder = new TextEncoder();\n const textBytes = encoder.encode(text);\n const data = new Uint8Array(textBytes.length + 1);\n data[0] = '0'.charCodeAt(0); // Input command\n data.set(textBytes, 1);\n socket.send(data);\n return true;\n }\n\n function sendKey(key) {\n // Apply modifiers\n if (ctrlActive && key.length === 1) {\n // Ctrl+key: send as control character (A=1, B=2, ..., Z=26)\n const code = key.toUpperCase().charCodeAt(0) - 64;\n if (code > 0 && code < 32) {\n sendBytes([code]);\n }\n resetModifiers();\n } else if (altActive && key.length === 1) {\n // Alt+key: send ESC + key\n const keyCode = key.charCodeAt(0);\n sendBytes([0x1B, keyCode]);\n resetModifiers();\n } else {\n sendText(key);\n }\n }\n\n function resetModifiers() {\n ctrlActive = false;\n altActive = false;\n ctrlBtn.classList.remove('active');\n altBtn.classList.remove('active');\n }\n\n // Send raw bytes for special keys\n function sendBytes(bytes) {\n const socket = findWebSocket();\n if (!socket) {\n console.error('[IME Helper] WebSocket not found');\n return false;\n }\n const data = new Uint8Array(bytes.length + 1);\n data[0] = 0x30; // '0' = input command\n data.set(bytes, 1);\n socket.send(data);\n return true;\n }\n\n function sendEnter() {\n sendBytes([0x0D]); // CR\n }\n\n function sendEsc() {\n sendBytes([0x1B]); // ESC\n }\n\n function sendTab() {\n sendBytes([0x09]); // TAB\n }\n\n function sendUp() {\n sendBytes([0x1B, 0x5B, 0x41]); // ESC [ A\n }\n\n function sendDown() {\n sendBytes([0x1B, 0x5B, 0x42]); // ESC [ B\n }\n\n function fitTerminal() {\n if (window.fitAddon && typeof window.fitAddon.fit === 'function') {\n window.fitAddon.fit();\n console.log('[IME Helper] Terminal fitted via fitAddon');\n return;\n }\n\n if (window.term && window.term.fitAddon && typeof window.term.fitAddon.fit === 'function') {\n window.term.fitAddon.fit();\n console.log('[IME Helper] Terminal fitted via term.fitAddon');\n return;\n }\n\n window.dispatchEvent(new Event('resize'));\n console.log('[IME Helper] Dispatched resize event');\n }\n\n function findTerminal() {\n if (window.term) return window.term;\n const termEl = document.querySelector('.xterm');\n if (termEl && termEl._core) return termEl._core;\n return null;\n }\n\n function zoomTerminal(delta) {\n const term = findTerminal();\n\n if (term && term.options) {\n const currentSize = term.options.fontSize || 14;\n const newSize = Math.max(8, Math.min(32, currentSize + delta));\n term.options.fontSize = newSize;\n console.log('[IME Helper] Font size changed to ' + newSize);\n fitTerminal();\n } else {\n console.log('[IME Helper] Terminal not found for zoom');\n }\n }\n\n function copySelection() {\n const term = findTerminal();\n if (!term) {\n console.log('[IME Helper] Terminal not found for copy');\n return;\n }\n const selection = term.getSelection();\n if (selection) {\n navigator.clipboard.writeText(selection).then(function() {\n console.log('[IME Helper] Copied selection to clipboard');\n }).catch(function(err) {\n console.error('[IME Helper] Failed to copy:', err);\n });\n } else {\n console.log('[IME Helper] No text selected');\n }\n }\n\n function copyAll() {\n const term = findTerminal();\n if (!term || !term.buffer || !term.buffer.active) {\n console.log('[IME Helper] Terminal buffer not found');\n return;\n }\n const buffer = term.buffer.active;\n const lines = [];\n for (let i = 0; i < buffer.length; i++) {\n const line = buffer.getLine(i);\n if (line) {\n lines.push(line.translateToString(true));\n }\n }\n const text = lines.join('\\n').trimEnd();\n navigator.clipboard.writeText(text).then(function() {\n console.log('[IME Helper] Copied all text to clipboard');\n }).catch(function(err) {\n console.error('[IME Helper] Failed to copy:', err);\n });\n }\n\n function submitInput() {\n const text = input.value;\n if (!text) return;\n\n if (sendText(text)) {\n input.value = '';\n adjustTextareaHeight();\n // Auto mode: send Enter after 1 second\n if (autoRunActive) {\n setTimeout(function() {\n sendEnter();\n }, 1000);\n }\n }\n }\n\n function runInput() {\n const text = input.value;\n if (!text) return;\n\n if (sendText(text)) {\n input.value = '';\n adjustTextareaHeight();\n // Wait 1 second then send Enter\n setTimeout(function() {\n sendEnter();\n }, 1000);\n }\n }\n\n function toggleIME(show) {\n if (typeof show === 'boolean') {\n container.classList.toggle('hidden', !show);\n } else {\n container.classList.toggle('hidden');\n }\n\n if (!container.classList.contains('hidden')) {\n input.focus();\n // Fit terminal after showing IME bar\n setTimeout(fitTerminal, 100);\n } else {\n const terminal = document.querySelector('.xterm-helper-textarea');\n if (terminal) terminal.focus();\n setTimeout(fitTerminal, 100);\n }\n }\n\n function adjustTextareaHeight() {\n input.style.height = 'auto';\n input.style.height = Math.min(input.scrollHeight, 120) + 'px';\n }\n\n // Event listeners\n sendBtn.addEventListener('click', function(e) {\n e.preventDefault();\n submitInput();\n });\n\n enterBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendEnter();\n });\n\n runBtn.addEventListener('click', function(e) {\n e.preventDefault();\n runInput();\n });\n\n zoomInBtn.addEventListener('click', function(e) {\n e.preventDefault();\n zoomTerminal(2);\n });\n\n zoomOutBtn.addEventListener('click', function(e) {\n e.preventDefault();\n zoomTerminal(-2);\n });\n\n ctrlBtn.addEventListener('click', function(e) {\n e.preventDefault();\n ctrlActive = !ctrlActive;\n ctrlBtn.classList.toggle('active', ctrlActive);\n if (ctrlActive) {\n altActive = false;\n altBtn.classList.remove('active');\n }\n });\n\n altBtn.addEventListener('click', function(e) {\n e.preventDefault();\n altActive = !altActive;\n altBtn.classList.toggle('active', altActive);\n if (altActive) {\n ctrlActive = false;\n ctrlBtn.classList.remove('active');\n }\n });\n\n shiftBtn.addEventListener('click', function(e) {\n e.preventDefault();\n shiftActive = !shiftActive;\n shiftBtn.classList.toggle('active', shiftActive);\n });\n\n autoBtn.addEventListener('click', function(e) {\n e.preventDefault();\n autoRunActive = !autoRunActive;\n autoBtn.classList.toggle('active', autoRunActive);\n });\n\n escBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendEsc();\n });\n\n tabBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendTab();\n });\n\n upBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendUp();\n });\n\n downBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendDown();\n });\n\n copyBtn.addEventListener('click', function(e) {\n e.preventDefault();\n copySelection();\n });\n\n copyAllBtn.addEventListener('click', function(e) {\n e.preventDefault();\n copyAll();\n });\n\n input.addEventListener('input', adjustTextareaHeight);\n\n input.addEventListener('keydown', function(e) {\n if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {\n e.preventDefault();\n submitInput();\n } else if (e.key === 'Escape') {\n e.preventDefault();\n toggleIME(false);\n }\n });\n\n toggleBtn.addEventListener('click', function(e) {\n e.preventDefault();\n toggleIME();\n });\n\n // Keyboard shortcut: Ctrl+J to toggle IME\n document.addEventListener('keydown', function(e) {\n if (e.ctrlKey && e.key === 'j') {\n e.preventDefault();\n toggleIME();\n }\n });\n\n // Inject shiftKey into mouse events when Shift button is active\n // This allows text selection to bypass tmux mouse mode\n ['mousedown', 'mousemove', 'mouseup'].forEach(function(eventType) {\n document.addEventListener(eventType, function(e) {\n // Don't interfere with IME helper buttons\n if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {\n return;\n }\n if (shiftActive && !e.shiftKey) {\n const newEvent = new MouseEvent(e.type, {\n bubbles: e.bubbles,\n cancelable: e.cancelable,\n view: e.view,\n detail: e.detail,\n screenX: e.screenX,\n screenY: e.screenY,\n clientX: e.clientX,\n clientY: e.clientY,\n ctrlKey: e.ctrlKey,\n altKey: e.altKey,\n shiftKey: true,\n metaKey: e.metaKey,\n button: e.button,\n buttons: e.buttons,\n relatedTarget: e.relatedTarget\n });\n e.stopImmediatePropagation();\n e.preventDefault();\n e.target.dispatchEvent(newEvent);\n }\n }, true);\n });\n\n // Track non-Shift drag operations and show hint after 3 consecutive drags\n let nonShiftDragCount = 0;\n let isDragging = false;\n let dragStartPos = null;\n\n document.addEventListener('mousedown', function(e) {\n if (e.button === 0 && !shiftActive && !e.shiftKey) {\n isDragging = true;\n dragStartPos = { x: e.clientX, y: e.clientY };\n }\n });\n\n document.addEventListener('mouseup', function(e) {\n if (isDragging && dragStartPos) {\n const dx = e.clientX - dragStartPos.x;\n const dy = e.clientY - dragStartPos.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n // Consider it a drag if moved more than 10 pixels\n if (distance > 10) {\n if (shiftActive || e.shiftKey) {\n nonShiftDragCount = 0;\n } else {\n nonShiftDragCount++;\n if (nonShiftDragCount >= 3) {\n alert('\u30C6\u30AD\u30B9\u30C8\u9078\u629E\u3059\u308B\u306B\u306F\u3001Shift \u30DC\u30BF\u30F3\u3092 ON \u306B\u3057\u3066\u304B\u3089\u30C9\u30E9\u30C3\u30B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002\\n\\nTo select text, turn ON the Shift button and then drag.');\n nonShiftDragCount = 0;\n }\n }\n }\n }\n isDragging = false;\n dragStartPos = null;\n });\n\n // Convert touch events to mouse events with shiftKey when Shift is active\n // This enables text selection on mobile devices\n let touchStartPos = null;\n\n function dispatchMouseEvent(type, touch, shiftKey) {\n const mouseEvent = new MouseEvent(type, {\n bubbles: true,\n cancelable: true,\n view: window,\n detail: 1,\n screenX: touch.screenX,\n screenY: touch.screenY,\n clientX: touch.clientX,\n clientY: touch.clientY,\n ctrlKey: false,\n altKey: false,\n shiftKey: shiftKey,\n metaKey: false,\n button: 0,\n buttons: type === 'mouseup' ? 0 : 1,\n relatedTarget: null\n });\n touch.target.dispatchEvent(mouseEvent);\n }\n\n let shiftTouchActive = false; // Track if we're in Shift+touch selection mode\n\n document.addEventListener('touchstart', function(e) {\n // Don't interfere with IME helper buttons\n if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {\n return;\n }\n // Single finger touch with Shift active -> convert to mouse event for selection\n if (e.touches.length === 1 && shiftActive) {\n const touch = e.touches[0];\n touchStartPos = { x: touch.clientX, y: touch.clientY };\n shiftTouchActive = true;\n e.preventDefault();\n dispatchMouseEvent('mousedown', touch, true);\n }\n // 2nd finger added -> cancel Shift selection mode, allow pinch\n else if (e.touches.length === 2 && shiftTouchActive) {\n dispatchMouseEvent('mouseup', e.touches[0], true);\n shiftTouchActive = false;\n touchStartPos = null;\n // Don't preventDefault - let pinch handlers take over\n }\n // Track non-Shift single touch for hint\n else if (e.touches.length === 1 && !shiftActive) {\n const touch = e.touches[0];\n touchStartPos = { x: touch.clientX, y: touch.clientY };\n }\n }, { passive: false, capture: true });\n\n document.addEventListener('touchmove', function(e) {\n // Only handle single-finger moves when in Shift selection mode\n if (e.touches.length === 1 && shiftTouchActive) {\n e.preventDefault();\n dispatchMouseEvent('mousemove', e.touches[0], true);\n }\n // Don't interfere with 2-finger gestures (pinch)\n }, { passive: false, capture: true });\n\n document.addEventListener('touchend', function(e) {\n // Shift selection mode ending\n if (shiftTouchActive && e.touches.length === 0) {\n const touch = e.changedTouches[0];\n dispatchMouseEvent('mouseup', touch, true);\n shiftTouchActive = false;\n touchStartPos = null;\n nonShiftDragCount = 0;\n }\n // Non-Shift drag tracking\n else if (touchStartPos && !shiftTouchActive && e.touches.length === 0) {\n const touch = e.changedTouches[0];\n const dx = touch.clientX - touchStartPos.x;\n const dy = touch.clientY - touchStartPos.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n if (distance > 10) {\n nonShiftDragCount++;\n if (nonShiftDragCount >= 3) {\n alert('\u30C6\u30AD\u30B9\u30C8\u9078\u629E\u3059\u308B\u306B\u306F\u3001Shift \u30DC\u30BF\u30F3\u3092 ON \u306B\u3057\u3066\u304B\u3089\u30C9\u30E9\u30C3\u30B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002\\n\\nTo select text, turn ON the Shift button and then drag.');\n nonShiftDragCount = 0;\n }\n }\n touchStartPos = null;\n }\n }, { passive: true, capture: true });\n\n // Pinch-to-zoom for font size (when Ctrl or Shift is active)\n let pinchStartDistance = 0;\n let pinchStartFontSize = 14;\n\n function getTouchDistance(touches) {\n const dx = touches[0].clientX - touches[1].clientX;\n const dy = touches[0].clientY - touches[1].clientY;\n return Math.sqrt(dx * dx + dy * dy);\n }\n\n document.addEventListener('touchstart', function(e) {\n if (e.touches.length === 2 && (ctrlActive || shiftActive)) {\n pinchStartDistance = getTouchDistance(e.touches);\n const term = findTerminal();\n pinchStartFontSize = (term && term.options) ? (term.options.fontSize || 14) : 14;\n }\n }, { passive: true });\n\n document.addEventListener('touchmove', function(e) {\n if (e.touches.length === 2 && (ctrlActive || shiftActive) && pinchStartDistance > 0) {\n e.preventDefault(); // Suppress browser zoom\n const currentDistance = getTouchDistance(e.touches);\n const scale = currentDistance / pinchStartDistance;\n const newSize = Math.round(pinchStartFontSize * scale);\n const clampedSize = Math.max(8, Math.min(32, newSize));\n\n const term = findTerminal();\n if (term && term.options && term.options.fontSize !== clampedSize) {\n term.options.fontSize = clampedSize;\n fitTerminal();\n }\n }\n }, { passive: false });\n\n document.addEventListener('touchend', function(e) {\n if (e.touches.length < 2) {\n pinchStartDistance = 0;\n }\n }, { passive: true });\n\n // Double-tap to send Enter (for reconnecting)\n let lastTapTime = 0;\n const DOUBLE_TAP_DELAY = 300; // 300ms \u4EE5\u5185\u306E2\u56DE\u30BF\u30C3\u30D7\n\n document.addEventListener('touchend', function(e) {\n // IME \u30D8\u30EB\u30D1\u30FC\u8981\u7D20\u306F\u9664\u5916\n if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {\n return;\n }\n // \u30B7\u30F3\u30B0\u30EB\u30BF\u30C3\u30C1\u306E\u307F\n if (e.changedTouches.length !== 1) return;\n\n const now = Date.now();\n if (now - lastTapTime < DOUBLE_TAP_DELAY) {\n // \u30C0\u30D6\u30EB\u30BF\u30C3\u30D7\u691C\u51FA \u2192 Enter \u9001\u4FE1\n sendEnter();\n lastTapTime = 0; // \u30EA\u30BB\u30C3\u30C8\n } else {\n lastTapTime = now;\n }\n }, { passive: true });\n\n // Auto-show on mobile devices\n if (isMobile) {\n setTimeout(function() {\n toggleIME(true);\n }, 1000);\n }\n\n console.log('[IME Helper] Loaded. ' + (isMobile ? 'Mobile mode.' : 'Press Ctrl+J or click keyboard button to toggle.'));\n})();\n</script>\n";
6
+ export declare const imeHelperScript = "\n<style>\n#ttyd-ime-container {\n position: fixed;\n bottom: 0;\n left: 0;\n right: 0;\n background: #1e1e1e;\n border-top: 2px solid #007acc;\n padding: 8px;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n box-shadow: 0 -2px 10px rgba(0,0,0,0.3);\n}\n\n#ttyd-ime-container.hidden {\n display: none;\n}\n\n#ttyd-ime-buttons {\n display: flex;\n gap: 6px;\n margin-bottom: 8px;\n flex-wrap: wrap;\n}\n\n#ttyd-ime-buttons button {\n background: #3a3a3a;\n border: 1px solid #555;\n border-radius: 6px;\n color: #fff;\n cursor: pointer;\n font-size: 13px;\n padding: 8px 12px;\n min-height: 40px;\n min-width: 44px;\n touch-action: manipulation;\n flex-shrink: 0;\n}\n\n#ttyd-ime-buttons button:hover, #ttyd-ime-buttons button:active {\n background: #4a4a4a;\n}\n\n#ttyd-ime-buttons button.active {\n background: #007acc;\n border-color: #005a9e;\n}\n\n#ttyd-ime-buttons button.modifier {\n background: #2d2d2d;\n font-weight: bold;\n}\n\n#ttyd-ime-buttons button.modifier.active {\n background: #d9534f;\n border-color: #c9302c;\n}\n\n#ttyd-ime-send {\n background: #007acc !important;\n border-color: #005a9e !important;\n font-weight: bold;\n}\n\n#ttyd-ime-send:hover, #ttyd-ime-send:active {\n background: #005a9e !important;\n}\n\n#ttyd-ime-run {\n background: #28a745 !important;\n border-color: #1e7e34 !important;\n font-weight: bold;\n}\n\n#ttyd-ime-run:hover, #ttyd-ime-run:active {\n background: #1e7e34 !important;\n}\n\n#ttyd-ime-auto.active {\n background: #f0ad4e !important;\n border-color: #eea236 !important;\n color: #000;\n}\n\n#ttyd-ime-input-row {\n display: flex;\n gap: 8px;\n align-items: flex-end;\n}\n\n#ttyd-ime-input {\n flex: 1;\n background: #2d2d2d;\n border: 1px solid #555;\n border-radius: 8px;\n color: #fff;\n font-family: monospace;\n font-size: 16px;\n padding: 12px;\n outline: none;\n resize: none;\n min-height: 44px;\n max-height: 120px;\n line-height: 1.4;\n}\n\n#ttyd-ime-input:focus {\n border-color: #007acc;\n}\n\n#ttyd-ime-input::placeholder {\n color: #888;\n}\n\n#ttyd-ime-toggle {\n position: fixed;\n bottom: 16px;\n right: 16px;\n background: #007acc;\n border: 2px solid #005a9e;\n border-radius: 50%;\n color: #fff;\n cursor: pointer;\n font-size: 20px;\n width: 56px;\n height: 56px;\n z-index: 10001;\n touch-action: manipulation;\n box-shadow: 0 2px 8px rgba(0,0,0,0.3);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n#ttyd-ime-toggle:hover, #ttyd-ime-toggle:active {\n background: #005a9e;\n transform: scale(1.05);\n}\n\n#ttyd-ime-container.hidden ~ #ttyd-ime-toggle {\n bottom: 16px;\n}\n\n/* Adjust terminal height when IME bar is visible */\nbody:has(#ttyd-ime-container:not(.hidden)) .xterm {\n height: calc(100vh - 140px) !important;\n}\n\n/* Mobile optimizations */\n@media (max-width: 768px) {\n #ttyd-ime-container {\n padding: 6px;\n }\n\n #ttyd-ime-buttons {\n gap: 4px;\n margin-bottom: 6px;\n }\n\n #ttyd-ime-buttons button {\n font-size: 12px;\n padding: 6px 10px;\n min-height: 36px;\n min-width: 40px;\n }\n\n #ttyd-ime-input {\n font-size: 16px;\n padding: 10px;\n }\n\n #ttyd-ime-toggle {\n width: 64px;\n height: 64px;\n font-size: 24px;\n }\n\n body:has(#ttyd-ime-container:not(.hidden)) .xterm {\n height: calc(100vh - 130px) !important;\n }\n}\n</style>\n\n<div id=\"ttyd-ime-container\" class=\"hidden\">\n <div id=\"ttyd-ime-buttons\">\n <button id=\"ttyd-ime-ctrl\" class=\"modifier\">Ctrl</button>\n <button id=\"ttyd-ime-alt\" class=\"modifier\">Alt</button>\n <button id=\"ttyd-ime-shift\" class=\"modifier\">Shift</button>\n <button id=\"ttyd-ime-esc\">Esc</button>\n <button id=\"ttyd-ime-tab\">Tab</button>\n <button id=\"ttyd-ime-up\">\u2191</button>\n <button id=\"ttyd-ime-down\">\u2193</button>\n <button id=\"ttyd-ime-enter\">Enter</button>\n <button id=\"ttyd-ime-zoomout\">A-</button>\n <button id=\"ttyd-ime-zoomin\">A+</button>\n <button id=\"ttyd-ime-copy\">Copy</button>\n <button id=\"ttyd-ime-copyall\">All</button>\n <button id=\"ttyd-ime-send\">Send</button>\n <button id=\"ttyd-ime-run\">Run</button>\n <button id=\"ttyd-ime-auto\" class=\"modifier\">Auto</button>\n </div>\n <div id=\"ttyd-ime-input-row\">\n <textarea id=\"ttyd-ime-input\" rows=\"1\" placeholder=\"\u65E5\u672C\u8A9E\u5165\u529B (Enter: \u9001\u4FE1)\"></textarea>\n </div>\n</div>\n<button id=\"ttyd-ime-toggle\">\u2328</button>\n\n<script>\n(function() {\n const container = document.getElementById('ttyd-ime-container');\n const input = document.getElementById('ttyd-ime-input');\n const sendBtn = document.getElementById('ttyd-ime-send');\n const enterBtn = document.getElementById('ttyd-ime-enter');\n const zoomInBtn = document.getElementById('ttyd-ime-zoomin');\n const zoomOutBtn = document.getElementById('ttyd-ime-zoomout');\n const runBtn = document.getElementById('ttyd-ime-run');\n const toggleBtn = document.getElementById('ttyd-ime-toggle');\n const ctrlBtn = document.getElementById('ttyd-ime-ctrl');\n const altBtn = document.getElementById('ttyd-ime-alt');\n const shiftBtn = document.getElementById('ttyd-ime-shift');\n const escBtn = document.getElementById('ttyd-ime-esc');\n const tabBtn = document.getElementById('ttyd-ime-tab');\n const upBtn = document.getElementById('ttyd-ime-up');\n const downBtn = document.getElementById('ttyd-ime-down');\n const copyBtn = document.getElementById('ttyd-ime-copy');\n const copyAllBtn = document.getElementById('ttyd-ime-copyall');\n const autoBtn = document.getElementById('ttyd-ime-auto');\n\n let ws = null;\n let ctrlActive = false;\n let altActive = false;\n let shiftActive = false;\n let autoRunActive = false;\n\n // Detect mobile device\n const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\n\n // Find the WebSocket connection\n function findWebSocket() {\n if (ws && ws.readyState === WebSocket.OPEN) return ws;\n\n if (window.socket && window.socket.readyState === WebSocket.OPEN) {\n ws = window.socket;\n return ws;\n }\n\n return null;\n }\n\n // Intercept WebSocket creation to capture the connection\n const OriginalWebSocket = window.WebSocket;\n window.WebSocket = function(url, protocols) {\n const socket = new OriginalWebSocket(url, protocols);\n if (url.includes('/ws')) {\n ws = socket;\n }\n return socket;\n };\n window.WebSocket.prototype = OriginalWebSocket.prototype;\n window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;\n window.WebSocket.OPEN = OriginalWebSocket.OPEN;\n window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;\n window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;\n\n function sendText(text) {\n const socket = findWebSocket();\n if (!socket) {\n console.error('[IME Helper] WebSocket not found');\n return false;\n }\n\n // ttyd protocol: binary data with '0' (input command) as first byte\n const encoder = new TextEncoder();\n const textBytes = encoder.encode(text);\n const data = new Uint8Array(textBytes.length + 1);\n data[0] = '0'.charCodeAt(0); // Input command\n data.set(textBytes, 1);\n socket.send(data);\n return true;\n }\n\n function sendKey(key) {\n // Apply modifiers\n if (ctrlActive && key.length === 1) {\n // Ctrl+key: send as control character (A=1, B=2, ..., Z=26)\n const code = key.toUpperCase().charCodeAt(0) - 64;\n if (code > 0 && code < 32) {\n sendBytes([code]);\n }\n resetModifiers();\n } else if (altActive && key.length === 1) {\n // Alt+key: send ESC + key\n const keyCode = key.charCodeAt(0);\n sendBytes([0x1B, keyCode]);\n resetModifiers();\n } else {\n sendText(key);\n }\n }\n\n function resetModifiers() {\n ctrlActive = false;\n altActive = false;\n ctrlBtn.classList.remove('active');\n altBtn.classList.remove('active');\n }\n\n // Send raw bytes for special keys\n function sendBytes(bytes) {\n const socket = findWebSocket();\n if (!socket) {\n console.error('[IME Helper] WebSocket not found');\n return false;\n }\n const data = new Uint8Array(bytes.length + 1);\n data[0] = 0x30; // '0' = input command\n data.set(bytes, 1);\n socket.send(data);\n return true;\n }\n\n function sendEnter() {\n sendBytes([0x0D]); // CR\n }\n\n function sendEsc() {\n sendBytes([0x1B]); // ESC\n }\n\n function sendTab() {\n sendBytes([0x09]); // TAB\n }\n\n function sendUp() {\n sendBytes([0x1B, 0x5B, 0x41]); // ESC [ A\n }\n\n function sendDown() {\n sendBytes([0x1B, 0x5B, 0x42]); // ESC [ B\n }\n\n function fitTerminal() {\n if (window.fitAddon && typeof window.fitAddon.fit === 'function') {\n window.fitAddon.fit();\n console.log('[IME Helper] Terminal fitted via fitAddon');\n return;\n }\n\n if (window.term && window.term.fitAddon && typeof window.term.fitAddon.fit === 'function') {\n window.term.fitAddon.fit();\n console.log('[IME Helper] Terminal fitted via term.fitAddon');\n return;\n }\n\n window.dispatchEvent(new Event('resize'));\n console.log('[IME Helper] Dispatched resize event');\n }\n\n function findTerminal() {\n if (window.term) return window.term;\n const termEl = document.querySelector('.xterm');\n if (termEl && termEl._core) return termEl._core;\n return null;\n }\n\n function zoomTerminal(delta) {\n const term = findTerminal();\n\n if (term && term.options) {\n const currentSize = term.options.fontSize || 14;\n const newSize = Math.max(8, Math.min(32, currentSize + delta));\n term.options.fontSize = newSize;\n console.log('[IME Helper] Font size changed to ' + newSize);\n fitTerminal();\n } else {\n console.log('[IME Helper] Terminal not found for zoom');\n }\n }\n\n function copySelection() {\n const term = findTerminal();\n if (!term) {\n console.log('[IME Helper] Terminal not found for copy');\n return;\n }\n const selection = term.getSelection();\n if (selection) {\n navigator.clipboard.writeText(selection).then(function() {\n console.log('[IME Helper] Copied selection to clipboard');\n }).catch(function(err) {\n console.error('[IME Helper] Failed to copy:', err);\n });\n } else {\n console.log('[IME Helper] No text selected');\n }\n }\n\n function copyAll() {\n const term = findTerminal();\n if (!term || !term.buffer || !term.buffer.active) {\n console.log('[IME Helper] Terminal buffer not found');\n return;\n }\n const buffer = term.buffer.active;\n const lines = [];\n for (let i = 0; i < buffer.length; i++) {\n const line = buffer.getLine(i);\n if (line) {\n lines.push(line.translateToString(true));\n }\n }\n const text = lines.join('\\n').trimEnd();\n navigator.clipboard.writeText(text).then(function() {\n console.log('[IME Helper] Copied all text to clipboard');\n }).catch(function(err) {\n console.error('[IME Helper] Failed to copy:', err);\n });\n }\n\n function submitInput() {\n const text = input.value;\n if (!text) return;\n\n if (sendText(text)) {\n input.value = '';\n adjustTextareaHeight();\n // Auto mode: send Enter after 1 second\n if (autoRunActive) {\n setTimeout(function() {\n sendEnter();\n }, 1000);\n }\n }\n }\n\n function runInput() {\n const text = input.value;\n if (!text) return;\n\n if (sendText(text)) {\n input.value = '';\n adjustTextareaHeight();\n // Wait 1 second then send Enter\n setTimeout(function() {\n sendEnter();\n }, 1000);\n }\n }\n\n function toggleIME(show) {\n if (typeof show === 'boolean') {\n container.classList.toggle('hidden', !show);\n } else {\n container.classList.toggle('hidden');\n }\n\n if (!container.classList.contains('hidden')) {\n input.focus();\n // Fit terminal after showing IME bar\n setTimeout(fitTerminal, 100);\n } else {\n const terminal = document.querySelector('.xterm-helper-textarea');\n if (terminal) terminal.focus();\n setTimeout(fitTerminal, 100);\n }\n }\n\n function adjustTextareaHeight() {\n input.style.height = 'auto';\n input.style.height = Math.min(input.scrollHeight, 120) + 'px';\n }\n\n // Event listeners\n sendBtn.addEventListener('click', function(e) {\n e.preventDefault();\n submitInput();\n });\n\n enterBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendEnter();\n });\n\n runBtn.addEventListener('click', function(e) {\n e.preventDefault();\n runInput();\n });\n\n zoomInBtn.addEventListener('click', function(e) {\n e.preventDefault();\n zoomTerminal(2);\n });\n\n zoomOutBtn.addEventListener('click', function(e) {\n e.preventDefault();\n zoomTerminal(-2);\n });\n\n ctrlBtn.addEventListener('click', function(e) {\n e.preventDefault();\n ctrlActive = !ctrlActive;\n ctrlBtn.classList.toggle('active', ctrlActive);\n if (ctrlActive) {\n altActive = false;\n altBtn.classList.remove('active');\n }\n });\n\n altBtn.addEventListener('click', function(e) {\n e.preventDefault();\n altActive = !altActive;\n altBtn.classList.toggle('active', altActive);\n if (altActive) {\n ctrlActive = false;\n ctrlBtn.classList.remove('active');\n }\n });\n\n shiftBtn.addEventListener('click', function(e) {\n e.preventDefault();\n shiftActive = !shiftActive;\n shiftBtn.classList.toggle('active', shiftActive);\n });\n\n autoBtn.addEventListener('click', function(e) {\n e.preventDefault();\n autoRunActive = !autoRunActive;\n autoBtn.classList.toggle('active', autoRunActive);\n });\n\n escBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendEsc();\n });\n\n tabBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendTab();\n });\n\n upBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendUp();\n });\n\n downBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendDown();\n });\n\n copyBtn.addEventListener('click', function(e) {\n e.preventDefault();\n copySelection();\n });\n\n copyAllBtn.addEventListener('click', function(e) {\n e.preventDefault();\n copyAll();\n });\n\n input.addEventListener('input', adjustTextareaHeight);\n\n input.addEventListener('keydown', function(e) {\n if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {\n e.preventDefault();\n submitInput();\n } else if (e.key === 'Escape') {\n e.preventDefault();\n toggleIME(false);\n }\n });\n\n toggleBtn.addEventListener('click', function(e) {\n e.preventDefault();\n toggleIME();\n });\n\n // Keyboard shortcut: Ctrl+J to toggle IME\n document.addEventListener('keydown', function(e) {\n if (e.ctrlKey && e.key === 'j') {\n e.preventDefault();\n toggleIME();\n }\n });\n\n // Inject shiftKey into mouse events when Shift button is active\n // This allows text selection to bypass tmux mouse mode\n ['mousedown', 'mousemove', 'mouseup'].forEach(function(eventType) {\n document.addEventListener(eventType, function(e) {\n // Don't interfere with IME helper buttons\n if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {\n return;\n }\n if (shiftActive && !e.shiftKey) {\n const newEvent = new MouseEvent(e.type, {\n bubbles: e.bubbles,\n cancelable: e.cancelable,\n view: e.view,\n detail: e.detail,\n screenX: e.screenX,\n screenY: e.screenY,\n clientX: e.clientX,\n clientY: e.clientY,\n ctrlKey: e.ctrlKey,\n altKey: e.altKey,\n shiftKey: true,\n metaKey: e.metaKey,\n button: e.button,\n buttons: e.buttons,\n relatedTarget: e.relatedTarget\n });\n e.stopImmediatePropagation();\n e.preventDefault();\n e.target.dispatchEvent(newEvent);\n }\n }, true);\n });\n\n // Convert touch events to mouse events with shiftKey when Shift is active\n // This enables text selection on mobile devices\n let touchStartPos = null;\n\n function dispatchMouseEvent(type, touch, shiftKey) {\n const mouseEvent = new MouseEvent(type, {\n bubbles: true,\n cancelable: true,\n view: window,\n detail: 1,\n screenX: touch.screenX,\n screenY: touch.screenY,\n clientX: touch.clientX,\n clientY: touch.clientY,\n ctrlKey: false,\n altKey: false,\n shiftKey: shiftKey,\n metaKey: false,\n button: 0,\n buttons: type === 'mouseup' ? 0 : 1,\n relatedTarget: null\n });\n touch.target.dispatchEvent(mouseEvent);\n }\n\n let shiftTouchActive = false; // Track if we're in Shift+touch selection mode\n\n document.addEventListener('touchstart', function(e) {\n // Don't interfere with IME helper buttons\n if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {\n return;\n }\n // Single finger touch with Shift active -> convert to mouse event for selection\n if (e.touches.length === 1 && shiftActive) {\n const touch = e.touches[0];\n touchStartPos = { x: touch.clientX, y: touch.clientY };\n shiftTouchActive = true;\n e.preventDefault();\n dispatchMouseEvent('mousedown', touch, true);\n }\n // 2nd finger added -> cancel Shift selection mode, allow pinch\n else if (e.touches.length === 2 && shiftTouchActive) {\n dispatchMouseEvent('mouseup', e.touches[0], true);\n shiftTouchActive = false;\n touchStartPos = null;\n // Don't preventDefault - let pinch handlers take over\n }\n // Track non-Shift single touch for hint\n else if (e.touches.length === 1 && !shiftActive) {\n const touch = e.touches[0];\n touchStartPos = { x: touch.clientX, y: touch.clientY };\n }\n }, { passive: false, capture: true });\n\n document.addEventListener('touchmove', function(e) {\n // Only handle single-finger moves when in Shift selection mode\n if (e.touches.length === 1 && shiftTouchActive) {\n e.preventDefault();\n dispatchMouseEvent('mousemove', e.touches[0], true);\n }\n // Don't interfere with 2-finger gestures (pinch)\n }, { passive: false, capture: true });\n\n document.addEventListener('touchend', function(e) {\n // Shift selection mode ending\n if (shiftTouchActive && e.touches.length === 0) {\n const touch = e.changedTouches[0];\n dispatchMouseEvent('mouseup', touch, true);\n shiftTouchActive = false;\n touchStartPos = null;\n }\n }, { passive: true, capture: true });\n\n // Pinch-to-zoom for font size (when Ctrl or Shift is active)\n let pinchStartDistance = 0;\n let pinchStartFontSize = 14;\n\n function getTouchDistance(touches) {\n const dx = touches[0].clientX - touches[1].clientX;\n const dy = touches[0].clientY - touches[1].clientY;\n return Math.sqrt(dx * dx + dy * dy);\n }\n\n document.addEventListener('touchstart', function(e) {\n if (e.touches.length === 2 && (ctrlActive || shiftActive)) {\n pinchStartDistance = getTouchDistance(e.touches);\n const term = findTerminal();\n pinchStartFontSize = (term && term.options) ? (term.options.fontSize || 14) : 14;\n }\n }, { passive: true });\n\n document.addEventListener('touchmove', function(e) {\n if (e.touches.length === 2 && (ctrlActive || shiftActive) && pinchStartDistance > 0) {\n e.preventDefault(); // Suppress browser zoom\n const currentDistance = getTouchDistance(e.touches);\n const scale = currentDistance / pinchStartDistance;\n const newSize = Math.round(pinchStartFontSize * scale);\n const clampedSize = Math.max(8, Math.min(32, newSize));\n\n const term = findTerminal();\n if (term && term.options && term.options.fontSize !== clampedSize) {\n term.options.fontSize = clampedSize;\n fitTerminal();\n }\n }\n }, { passive: false });\n\n document.addEventListener('touchend', function(e) {\n if (e.touches.length < 2) {\n pinchStartDistance = 0;\n }\n }, { passive: true });\n\n // ========== PC: Ctrl+Wheel / Trackpad Pinch ==========\n document.addEventListener('wheel', function(e) {\n // ctrlKey = trackpad pinch (Mac) or Ctrl+scroll (PC)\n if (e.ctrlKey) {\n e.preventDefault(); // Suppress browser zoom\n\n // deltaY > 0: zoom out, deltaY < 0: zoom in\n const delta = e.deltaY > 0 ? -2 : 2;\n zoomTerminal(delta);\n }\n }, { passive: false });\n\n // Double-tap to send Enter (for reconnecting)\n let lastTapTime = 0;\n const DOUBLE_TAP_DELAY = 300; // 300ms \u4EE5\u5185\u306E2\u56DE\u30BF\u30C3\u30D7\n\n document.addEventListener('touchend', function(e) {\n // IME \u30D8\u30EB\u30D1\u30FC\u8981\u7D20\u306F\u9664\u5916\n if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {\n return;\n }\n // \u30B7\u30F3\u30B0\u30EB\u30BF\u30C3\u30C1\u306E\u307F\n if (e.changedTouches.length !== 1) return;\n\n const now = Date.now();\n if (now - lastTapTime < DOUBLE_TAP_DELAY) {\n // \u30C0\u30D6\u30EB\u30BF\u30C3\u30D7\u691C\u51FA \u2192 Enter \u9001\u4FE1\n sendEnter();\n lastTapTime = 0; // \u30EA\u30BB\u30C3\u30C8\n } else {\n lastTapTime = now;\n }\n }, { passive: true });\n\n // Auto-show on mobile devices\n if (isMobile) {\n setTimeout(function() {\n toggleIME(true);\n }, 1000);\n }\n\n // Auto-reload when tab becomes visible if WebSocket is disconnected\n document.addEventListener('visibilitychange', function() {\n if (!document.hidden) {\n const socket = findWebSocket();\n if (!socket || socket.readyState !== WebSocket.OPEN) {\n console.log('[IME Helper] Connection lost, reloading...');\n location.reload();\n }\n }\n });\n\n console.log('[IME Helper] Loaded. ' + (isMobile ? 'Mobile mode.' : 'Press Ctrl+J or click keyboard button to toggle.'));\n})();\n</script>\n";
7
7
  export declare function injectImeHelper(html: string): string;
8
8
  //# sourceMappingURL=ime-helper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ime-helper.d.ts","sourceRoot":"","sources":["../../src/daemon/ime-helper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,eAAe,6uuBAgzB3B,CAAC;AAEF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGpD"}
1
+ {"version":3,"file":"ime-helper.d.ts","sourceRoot":"","sources":["../../src/daemon/ime-helper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,eAAe,09rBAmxB3B,CAAC;AAEF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGpD"}
@@ -622,41 +622,6 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
622
622
  }, true);
623
623
  });
624
624
 
625
- // Track non-Shift drag operations and show hint after 3 consecutive drags
626
- let nonShiftDragCount = 0;
627
- let isDragging = false;
628
- let dragStartPos = null;
629
-
630
- document.addEventListener('mousedown', function(e) {
631
- if (e.button === 0 && !shiftActive && !e.shiftKey) {
632
- isDragging = true;
633
- dragStartPos = { x: e.clientX, y: e.clientY };
634
- }
635
- });
636
-
637
- document.addEventListener('mouseup', function(e) {
638
- if (isDragging && dragStartPos) {
639
- const dx = e.clientX - dragStartPos.x;
640
- const dy = e.clientY - dragStartPos.y;
641
- const distance = Math.sqrt(dx * dx + dy * dy);
642
-
643
- // Consider it a drag if moved more than 10 pixels
644
- if (distance > 10) {
645
- if (shiftActive || e.shiftKey) {
646
- nonShiftDragCount = 0;
647
- } else {
648
- nonShiftDragCount++;
649
- if (nonShiftDragCount >= 3) {
650
- alert('テキスト選択するには、Shift ボタンを ON にしてからドラッグしてください。\\n\\nTo select text, turn ON the Shift button and then drag.');
651
- nonShiftDragCount = 0;
652
- }
653
- }
654
- }
655
- }
656
- isDragging = false;
657
- dragStartPos = null;
658
- });
659
-
660
625
  // Convert touch events to mouse events with shiftKey when Shift is active
661
626
  // This enables text selection on mobile devices
662
627
  let touchStartPos = null;
@@ -727,23 +692,6 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
727
692
  dispatchMouseEvent('mouseup', touch, true);
728
693
  shiftTouchActive = false;
729
694
  touchStartPos = null;
730
- nonShiftDragCount = 0;
731
- }
732
- // Non-Shift drag tracking
733
- else if (touchStartPos && !shiftTouchActive && e.touches.length === 0) {
734
- const touch = e.changedTouches[0];
735
- const dx = touch.clientX - touchStartPos.x;
736
- const dy = touch.clientY - touchStartPos.y;
737
- const distance = Math.sqrt(dx * dx + dy * dy);
738
-
739
- if (distance > 10) {
740
- nonShiftDragCount++;
741
- if (nonShiftDragCount >= 3) {
742
- alert('テキスト選択するには、Shift ボタンを ON にしてからドラッグしてください。\\n\\nTo select text, turn ON the Shift button and then drag.');
743
- nonShiftDragCount = 0;
744
- }
745
- }
746
- touchStartPos = null;
747
695
  }
748
696
  }, { passive: true, capture: true });
749
697
 
@@ -787,6 +735,18 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
787
735
  }
788
736
  }, { passive: true });
789
737
 
738
+ // ========== PC: Ctrl+Wheel / Trackpad Pinch ==========
739
+ document.addEventListener('wheel', function(e) {
740
+ // ctrlKey = trackpad pinch (Mac) or Ctrl+scroll (PC)
741
+ if (e.ctrlKey) {
742
+ e.preventDefault(); // Suppress browser zoom
743
+
744
+ // deltaY > 0: zoom out, deltaY < 0: zoom in
745
+ const delta = e.deltaY > 0 ? -2 : 2;
746
+ zoomTerminal(delta);
747
+ }
748
+ }, { passive: false });
749
+
790
750
  // Double-tap to send Enter (for reconnecting)
791
751
  let lastTapTime = 0;
792
752
  const DOUBLE_TAP_DELAY = 300; // 300ms 以内の2回タップ
@@ -816,12 +776,23 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
816
776
  }, 1000);
817
777
  }
818
778
 
779
+ // Auto-reload when tab becomes visible if WebSocket is disconnected
780
+ document.addEventListener('visibilitychange', function() {
781
+ if (!document.hidden) {
782
+ const socket = findWebSocket();
783
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
784
+ console.log('[IME Helper] Connection lost, reloading...');
785
+ location.reload();
786
+ }
787
+ }
788
+ });
789
+
819
790
  console.log('[IME Helper] Loaded. ' + (isMobile ? 'Mobile mode.' : 'Press Ctrl+J or click keyboard button to toggle.'));
820
791
  })();
821
792
  </script>
822
793
  `;
823
794
  export function injectImeHelper(html) {
824
795
  // Inject before </body>
825
- return html.replace('</body>', imeHelperScript + '</body>');
796
+ return html.replace('</body>', `${imeHelperScript}</body>`);
826
797
  }
827
798
  //# sourceMappingURL=ime-helper.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ime-helper.js","sourceRoot":"","sources":["../../src/daemon/ime-helper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgzB9B,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,wBAAwB;IACxB,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,eAAe,GAAG,SAAS,CAAC,CAAC;AAC9D,CAAC"}
1
+ {"version":3,"file":"ime-helper.js","sourceRoot":"","sources":["../../src/daemon/ime-helper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmxB9B,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,wBAAwB;IACxB,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,eAAe,SAAS,CAAC,CAAC;AAC9D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/daemon/index.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiG5E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/daemon/index.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA0CD,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+K5E"}
@@ -1,89 +1,194 @@
1
1
  import { existsSync, mkdirSync, unlinkSync } from 'node:fs';
2
2
  import { createServer as createUnixServer } from 'node:net';
3
- import { loadConfig } from '../config/config.js';
4
3
  import { clearDaemonState, getSocketPath, getStateDir, setDaemonState } from '../config/state.js';
5
- import { createDaemonServer } from './server.js';
6
- import { stopAllSessions } from './session-manager.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+ import { getCurrentConfig, initConfigManager, reloadConfig } from './config-manager.js';
6
+ import { createDaemonServer, setConfigGetter } from './server.js';
7
+ import { sessionManager } from './session-manager.js';
8
+ const log = createLogger('daemon');
9
+ /**
10
+ * Clean up a socket file if it exists
11
+ */
12
+ function cleanupSocketFile(socketPath, label = 'socket') {
13
+ if (existsSync(socketPath)) {
14
+ try {
15
+ unlinkSync(socketPath);
16
+ log.info(`Removed old ${label}: ${socketPath}`);
17
+ }
18
+ catch (err) {
19
+ log.warn(`Failed to remove old ${label} ${socketPath}: ${err}`);
20
+ }
21
+ }
22
+ }
23
+ /**
24
+ * Clean up multiple socket files
25
+ */
26
+ function cleanupSocketFiles(socketPaths, label = 'socket') {
27
+ for (const socketPath of socketPaths) {
28
+ cleanupSocketFile(socketPath, label);
29
+ }
30
+ }
31
+ /**
32
+ * Revalidate existing sessions from previous daemon instance
33
+ */
34
+ function revalidateExistingSessions() {
35
+ const { valid, removed } = sessionManager.revalidateSessions();
36
+ if (valid.length === 0 && removed.length === 0) {
37
+ return;
38
+ }
39
+ log.info(`Session revalidation: ${valid.length} active, ${removed.length} removed`);
40
+ if (valid.length > 0) {
41
+ console.log(`Recovered ${valid.length} existing session(s)`);
42
+ }
43
+ if (removed.length > 0) {
44
+ console.log(`Cleaned up ${removed.length} dead session(s)`);
45
+ }
46
+ }
7
47
  export async function startDaemon(options = {}) {
8
- const config = loadConfig(options.configPath);
48
+ log.info('Starting daemon...');
49
+ revalidateExistingSessions();
50
+ // Set up global error handlers
51
+ process.on('uncaughtException', (error) => {
52
+ log.error(`Uncaught exception: ${error.message}`, error.stack);
53
+ process.exit(1);
54
+ });
55
+ process.on('unhandledRejection', (reason, promise) => {
56
+ log.error(`Unhandled rejection at: ${promise}, reason: ${reason}`);
57
+ });
58
+ const configManager = initConfigManager(options.configPath);
59
+ const config = configManager.getConfig();
60
+ log.info(`Config loaded: port=${config.daemon_port}, base_path=${config.base_path}`);
61
+ // Set up config getter for hot-reload support
62
+ setConfigGetter(getCurrentConfig);
9
63
  const socketPath = getSocketPath();
10
64
  // Ensure state directory exists
11
65
  const stateDir = getStateDir();
12
66
  if (!existsSync(stateDir)) {
13
67
  mkdirSync(stateDir, { recursive: true });
68
+ log.info(`Created state directory: ${stateDir}`);
14
69
  }
15
- // Clean up old socket if exists
16
- if (existsSync(socketPath)) {
17
- try {
18
- unlinkSync(socketPath);
19
- }
20
- catch {
21
- // Ignore
22
- }
23
- }
70
+ // Clean up old sockets
71
+ cleanupSocketFile(socketPath, 'CLI socket');
24
72
  // Create HTTP servers for each listen address
25
73
  const listenAddresses = config.listen_addresses ?? ['127.0.0.1', '::1'];
74
+ const listenSockets = config.listen_sockets ?? [];
26
75
  const httpServers = listenAddresses.map(() => createDaemonServer(config));
27
- // Start HTTP servers
76
+ // Create HTTP servers for Unix sockets
77
+ const socketServers = listenSockets.map(() => createDaemonServer(config));
78
+ // Clean up old HTTP socket files
79
+ cleanupSocketFiles(listenSockets, 'HTTP socket');
80
+ // Start HTTP servers on TCP
28
81
  let firstServer = true;
29
82
  for (let i = 0; i < listenAddresses.length; i++) {
30
83
  const address = listenAddresses[i];
31
84
  const server = httpServers[i];
32
- if (!address || !server)
85
+ if (!(address && server)) {
33
86
  continue;
87
+ }
88
+ server.on('error', (err) => {
89
+ log.error(`HTTP server error on ${address}: ${err.message}`, err.stack);
90
+ });
34
91
  server.listen(config.daemon_port, address, () => {
92
+ log.info(`HTTP server listening on ${address}:${config.daemon_port}`);
35
93
  if (firstServer) {
36
94
  firstServer = false;
37
95
  console.log(`ttyd-mux daemon started on http://localhost:${config.daemon_port}${config.base_path}/`);
38
96
  console.log(` Listening on: ${listenAddresses.join(', ')}`);
97
+ if (listenSockets.length > 0) {
98
+ console.log(` Unix sockets: ${listenSockets.join(', ')}`);
99
+ }
39
100
  // Save daemon state
40
101
  setDaemonState({
41
102
  pid: process.pid,
42
103
  port: config.daemon_port,
43
104
  started_at: new Date().toISOString()
44
105
  });
106
+ log.info(`Daemon state saved: pid=${process.pid}`);
45
107
  }
46
108
  });
47
109
  }
110
+ // Start HTTP servers on Unix sockets
111
+ for (let i = 0; i < listenSockets.length; i++) {
112
+ const sockPath = listenSockets[i];
113
+ const server = socketServers[i];
114
+ if (!(sockPath && server)) {
115
+ continue;
116
+ }
117
+ server.on('error', (err) => {
118
+ log.error(`HTTP socket server error on ${sockPath}: ${err.message}`, err.stack);
119
+ });
120
+ server.listen(sockPath, () => {
121
+ log.info(`HTTP server listening on unix:${sockPath}`);
122
+ });
123
+ }
48
124
  // Create Unix socket for CLI communication
49
125
  const unixServer = createUnixServer((socket) => {
50
126
  socket.on('data', (data) => {
51
127
  const command = data.toString().trim();
128
+ log.debug(`Unix socket received command: ${command}`);
52
129
  if (command === 'ping') {
53
130
  socket.write('pong');
54
131
  }
55
132
  else if (command === 'shutdown') {
56
133
  socket.write('ok');
57
- shutdown();
134
+ shutdown(false);
135
+ }
136
+ else if (command === 'shutdown-with-sessions') {
137
+ socket.write('ok');
138
+ shutdown(true);
139
+ }
140
+ else if (command === 'reload') {
141
+ const result = reloadConfig();
142
+ socket.write(JSON.stringify(result));
58
143
  }
59
144
  socket.end();
60
145
  });
146
+ socket.on('error', (err) => {
147
+ log.error(`Unix socket connection error: ${err.message}`);
148
+ });
149
+ });
150
+ unixServer.on('error', (err) => {
151
+ log.error(`Unix server error: ${err.message}`, err.stack);
61
152
  });
62
153
  unixServer.listen(socketPath, () => {
154
+ log.info(`Unix socket listening: ${socketPath}`);
63
155
  console.log(`Unix socket: ${socketPath}`);
64
156
  });
65
157
  // Handle shutdown signals
66
- const shutdown = () => {
158
+ const shutdown = (stopSessions = false) => {
159
+ log.info(`Shutdown requested (stopSessions=${stopSessions})`);
67
160
  console.log('\nShutting down...');
68
- stopAllSessions();
161
+ if (stopSessions) {
162
+ sessionManager.stopAllSessions();
163
+ log.info('All sessions stopped');
164
+ }
165
+ else {
166
+ log.info('Sessions preserved (daemon-only shutdown)');
167
+ }
69
168
  clearDaemonState();
169
+ log.info('Daemon state cleared');
70
170
  for (const server of httpServers) {
71
171
  server.close();
72
172
  }
73
- unixServer.close();
74
- // Clean up socket file
75
- if (existsSync(socketPath)) {
76
- try {
77
- unlinkSync(socketPath);
78
- }
79
- catch {
80
- // Ignore
81
- }
173
+ for (const server of socketServers) {
174
+ server.close();
82
175
  }
176
+ unixServer.close();
177
+ log.info('Servers closed');
178
+ // Clean up socket files
179
+ cleanupSocketFile(socketPath, 'CLI socket');
180
+ cleanupSocketFiles(listenSockets, 'HTTP socket');
181
+ log.info('Daemon shutdown complete');
83
182
  process.exit(0);
84
183
  };
85
- process.on('SIGINT', shutdown);
86
- process.on('SIGTERM', shutdown);
184
+ process.on('SIGINT', () => {
185
+ log.info('Received SIGINT');
186
+ shutdown();
187
+ });
188
+ process.on('SIGTERM', () => {
189
+ log.info('Received SIGTERM');
190
+ shutdown();
191
+ });
87
192
  // Keep process running
88
193
  if (!options.foreground) {
89
194
  // When not in foreground, detach stdio
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/daemon/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/daemon/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,YAAY,IAAI,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACjG,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;AAOnC;;GAEG;AACH,SAAS,iBAAiB,CAAC,UAAkB,EAAE,KAAK,GAAG,QAAQ;IAC7D,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,UAAU,CAAC,UAAU,CAAC,CAAC;YACvB,GAAG,CAAC,IAAI,CAAC,eAAe,KAAK,KAAK,UAAU,EAAE,CAAC,CAAC;QAClD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,IAAI,CAAC,wBAAwB,KAAK,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,WAAqB,EAAE,KAAK,GAAG,QAAQ;IACjE,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACvC,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,0BAA0B;IACjC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC,kBAAkB,EAAE,CAAC;IAC/D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/C,OAAO;IACT,CAAC;IACD,GAAG,CAAC,IAAI,CAAC,yBAAyB,KAAK,CAAC,MAAM,YAAY,OAAO,CAAC,MAAM,UAAU,CAAC,CAAC;IACpF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,CAAC,MAAM,sBAAsB,CAAC,CAAC;IAC/D,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,cAAc,OAAO,CAAC,MAAM,kBAAkB,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,UAAyB,EAAE;IAC3D,GAAG,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IAE/B,0BAA0B,EAAE,CAAC;IAE7B,+BAA+B;IAC/B,OAAO,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,KAAK,EAAE,EAAE;QACxC,GAAG,CAAC,KAAK,CAAC,uBAAuB,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE;QACnD,GAAG,CAAC,KAAK,CAAC,2BAA2B,OAAO,aAAa,MAAM,EAAE,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,MAAM,aAAa,GAAG,iBAAiB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,EAAE,CAAC;IACzC,GAAG,CAAC,IAAI,CAAC,uBAAuB,MAAM,CAAC,WAAW,eAAe,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;IAErF,8CAA8C;IAC9C,eAAe,CAAC,gBAAgB,CAAC,CAAC;IAElC,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;IAEnC,gCAAgC;IAChC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;IAC/B,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,GAAG,CAAC,IAAI,CAAC,4BAA4B,QAAQ,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,uBAAuB;IACvB,iBAAiB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAE5C,8CAA8C;IAC9C,MAAM,eAAe,GAAG,MAAM,CAAC,gBAAgB,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IACxE,MAAM,aAAa,GAAG,MAAM,CAAC,cAAc,IAAI,EAAE,CAAC;IAClD,MAAM,WAAW,GAAG,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC;IAE1E,uCAAuC;IACvC,MAAM,aAAa,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC;IAE1E,iCAAiC;IACjC,kBAAkB,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;IAEjD,4BAA4B;IAC5B,IAAI,WAAW,GAAG,IAAI,CAAC;IACvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChD,MAAM,OAAO,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;QACnC,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;QAC9B,IAAI,CAAC,CAAC,OAAO,IAAI,MAAM,CAAC,EAAE,CAAC;YACzB,SAAS;QACX,CAAC;QAED,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACzB,GAAG,CAAC,KAAK,CAAC,wBAAwB,OAAO,KAAK,GAAG,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE;YAC9C,GAAG,CAAC,IAAI,CAAC,4BAA4B,OAAO,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;YACtE,IAAI,WAAW,EAAE,CAAC;gBAChB,WAAW,GAAG,KAAK,CAAC;gBACpB,OAAO,CAAC,GAAG,CACT,+CAA+C,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,SAAS,GAAG,CACxF,CAAC;gBACF,OAAO,CAAC,GAAG,CAAC,mBAAmB,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAC7D,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC7B,OAAO,CAAC,GAAG,CAAC,mBAAmB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAC7D,CAAC;gBAED,oBAAoB;gBACpB,cAAc,CAAC;oBACb,GAAG,EAAE,OAAO,CAAC,GAAG;oBAChB,IAAI,EAAE,MAAM,CAAC,WAAW;oBACxB,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;iBACrC,CAAC,CAAC;gBACH,GAAG,CAAC,IAAI,CAAC,2BAA2B,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;YACrD,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,qCAAqC;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9C,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,CAAC,CAAC,QAAQ,IAAI,MAAM,CAAC,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACzB,GAAG,CAAC,KAAK,CAAC,+BAA+B,QAAQ,KAAK,GAAG,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QAClF,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE;YAC3B,GAAG,CAAC,IAAI,CAAC,iCAAiC,QAAQ,EAAE,CAAC,CAAC;QACxD,CAAC,CAAC,CAAC;IACL,CAAC;IAED,2CAA2C;IAC3C,MAAM,UAAU,GAAG,gBAAgB,CAAC,CAAC,MAAM,EAAE,EAAE;QAC7C,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACzB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;YACvC,GAAG,CAAC,KAAK,CAAC,iCAAiC,OAAO,EAAE,CAAC,CAAC;YACtD,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;gBACvB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACvB,CAAC;iBAAM,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;gBAClC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACnB,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;iBAAM,IAAI,OAAO,KAAK,wBAAwB,EAAE,CAAC;gBAChD,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACnB,QAAQ,CAAC,IAAI,CAAC,CAAC;YACjB,CAAC;iBAAM,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAChC,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;gBAC9B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;YACvC,CAAC;YACD,MAAM,CAAC,GAAG,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACzB,GAAG,CAAC,KAAK,CAAC,iCAAiC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;QAC7B,GAAG,CAAC,KAAK,CAAC,sBAAsB,GAAG,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,UAAU,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,EAAE;QACjC,GAAG,CAAC,IAAI,CAAC,0BAA0B,UAAU,EAAE,CAAC,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,gBAAgB,UAAU,EAAE,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,0BAA0B;IAC1B,MAAM,QAAQ,GAAG,CAAC,YAAY,GAAG,KAAK,EAAE,EAAE;QACxC,GAAG,CAAC,IAAI,CAAC,oCAAoC,YAAY,GAAG,CAAC,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;QAClC,IAAI,YAAY,EAAE,CAAC;YACjB,cAAc,CAAC,eAAe,EAAE,CAAC;YACjC,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QACnC,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,2CAA2C,CAAC,CAAC;QACxD,CAAC;QACD,gBAAgB,EAAE,CAAC;QACnB,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAEjC,KAAK,MAAM,MAAM,IAAI,WAAW,EAAE,CAAC;YACjC,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,aAAa,EAAE,CAAC;YACnC,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,CAAC;QACD,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAE3B,wBAAwB;QACxB,iBAAiB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QAC5C,kBAAkB,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QAEjD,GAAG,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC5B,QAAQ,EAAE,CAAC;IACb,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,GAAG,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAC7B,QAAQ,EAAE,CAAC;IACb,CAAC,CAAC,CAAC;IAEH,uBAAuB;IACvB,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;QACxB,uCAAuC;QACvC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,oEAAoE"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shared utilities for portal HTML generation
3
+ */
4
+ /**
5
+ * Escape HTML special characters to prevent XSS
6
+ */
7
+ export declare function escapeHtml(str: string): string;
8
+ /**
9
+ * Generate PWA meta tags and links
10
+ */
11
+ export declare function generatePwaHead(basePath: string): string;
12
+ /**
13
+ * Generate Service Worker registration script
14
+ */
15
+ export declare function generateSwRegistration(basePath: string): string;
16
+ /**
17
+ * Common CSS styles for portal pages
18
+ */
19
+ export declare const portalStyles = "\n * {\n box-sizing: border-box;\n }\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n max-width: 800px;\n margin: 0 auto;\n padding: 2rem;\n background: #1a1a2e;\n color: #eee;\n min-height: 100vh;\n }\n h1 {\n color: #00d9ff;\n margin-bottom: 0.5rem;\n }\n .subtitle {\n color: #888;\n margin-bottom: 2rem;\n }\n ul {\n list-style: none;\n padding: 0;\n margin: 0;\n }\n .session {\n margin: 0.5rem 0;\n }\n .session a {\n display: block;\n padding: 1rem;\n background: #16213e;\n border-radius: 8px;\n text-decoration: none;\n color: #eee;\n transition: background 0.2s, transform 0.1s;\n }\n .session a:hover {\n background: #1f3460;\n transform: translateX(4px);\n }\n .name {\n font-weight: 600;\n font-size: 1.1rem;\n color: #00d9ff;\n }\n .info {\n display: block;\n font-size: 0.85rem;\n color: #888;\n margin-top: 0.25rem;\n }\n .no-sessions {\n color: #888;\n padding: 2rem;\n text-align: center;\n background: #16213e;\n border-radius: 8px;\n }\n code {\n background: #0f0f23;\n padding: 0.2rem 0.4rem;\n border-radius: 4px;\n font-family: \"SF Mono\", Monaco, \"Cascadia Code\", monospace;\n }";
20
+ //# sourceMappingURL=portal-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-utils.d.ts","sourceRoot":"","sources":["../../src/daemon/portal-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CASxD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,o7CAiEnB,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Shared utilities for portal HTML generation
3
+ */
4
+ /**
5
+ * Escape HTML special characters to prevent XSS
6
+ */
7
+ export function escapeHtml(str) {
8
+ return str
9
+ .replace(/&/g, '&amp;')
10
+ .replace(/</g, '&lt;')
11
+ .replace(/>/g, '&gt;')
12
+ .replace(/"/g, '&quot;')
13
+ .replace(/'/g, '&#039;');
14
+ }
15
+ /**
16
+ * Generate PWA meta tags and links
17
+ */
18
+ export function generatePwaHead(basePath) {
19
+ return `
20
+ <meta name="theme-color" content="#00d9ff">
21
+ <meta name="apple-mobile-web-app-capable" content="yes">
22
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
23
+ <meta name="apple-mobile-web-app-title" content="ttyd-mux">
24
+ <link rel="manifest" href="${basePath}/manifest.json">
25
+ <link rel="apple-touch-icon" href="${basePath}/icon-192.png">
26
+ <link rel="icon" type="image/svg+xml" href="${basePath}/icon.svg">`;
27
+ }
28
+ /**
29
+ * Generate Service Worker registration script
30
+ */
31
+ export function generateSwRegistration(basePath) {
32
+ return `
33
+ <script>
34
+ if ('serviceWorker' in navigator) {
35
+ navigator.serviceWorker.register('${basePath}/sw.js')
36
+ .catch(err => console.warn('SW registration failed:', err));
37
+ }
38
+ </script>`;
39
+ }
40
+ /**
41
+ * Common CSS styles for portal pages
42
+ */
43
+ export const portalStyles = `
44
+ * {
45
+ box-sizing: border-box;
46
+ }
47
+ body {
48
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
49
+ max-width: 800px;
50
+ margin: 0 auto;
51
+ padding: 2rem;
52
+ background: #1a1a2e;
53
+ color: #eee;
54
+ min-height: 100vh;
55
+ }
56
+ h1 {
57
+ color: #00d9ff;
58
+ margin-bottom: 0.5rem;
59
+ }
60
+ .subtitle {
61
+ color: #888;
62
+ margin-bottom: 2rem;
63
+ }
64
+ ul {
65
+ list-style: none;
66
+ padding: 0;
67
+ margin: 0;
68
+ }
69
+ .session {
70
+ margin: 0.5rem 0;
71
+ }
72
+ .session a {
73
+ display: block;
74
+ padding: 1rem;
75
+ background: #16213e;
76
+ border-radius: 8px;
77
+ text-decoration: none;
78
+ color: #eee;
79
+ transition: background 0.2s, transform 0.1s;
80
+ }
81
+ .session a:hover {
82
+ background: #1f3460;
83
+ transform: translateX(4px);
84
+ }
85
+ .name {
86
+ font-weight: 600;
87
+ font-size: 1.1rem;
88
+ color: #00d9ff;
89
+ }
90
+ .info {
91
+ display: block;
92
+ font-size: 0.85rem;
93
+ color: #888;
94
+ margin-top: 0.25rem;
95
+ }
96
+ .no-sessions {
97
+ color: #888;
98
+ padding: 2rem;
99
+ text-align: center;
100
+ background: #16213e;
101
+ border-radius: 8px;
102
+ }
103
+ code {
104
+ background: #0f0f23;
105
+ padding: 0.2rem 0.4rem;
106
+ border-radius: 4px;
107
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
108
+ }`;
109
+ //# sourceMappingURL=portal-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-utils.js","sourceRoot":"","sources":["../../src/daemon/portal-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO;;;;;+BAKsB,QAAQ;uCACA,QAAQ;gDACC,QAAQ,aAAa,CAAC;AACtE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,OAAO;;;0CAGiC,QAAQ;;;YAGtC,CAAC;AACb,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAiEtB,CAAC"}