handmux 0.5.2

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 (58) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +303 -0
  3. package/README.zh-CN.md +285 -0
  4. package/bin/handmux.js +417 -0
  5. package/hooks/handmux-notify.sh +20 -0
  6. package/hooks/handmux-write.cjs +92 -0
  7. package/package.json +52 -0
  8. package/public/assets/index-CNEiBm9y.js +157 -0
  9. package/public/assets/index-CSX2d9C7.css +32 -0
  10. package/public/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  11. package/public/fonts/TWUnifont.woff2 +0 -0
  12. package/public/icons/apple-touch-icon.png +0 -0
  13. package/public/icons/badge-96.png +0 -0
  14. package/public/icons/icon-192.png +0 -0
  15. package/public/icons/icon-512.png +0 -0
  16. package/public/icons/logo.svg +32 -0
  17. package/public/index.html +105 -0
  18. package/public/manifest.webmanifest +37 -0
  19. package/public/offline.html +50 -0
  20. package/public/sw.js +117 -0
  21. package/src/.gitkeep +0 -0
  22. package/src/appName.js +23 -0
  23. package/src/asr/iflyConfig.js +10 -0
  24. package/src/asr/iflySign.js +16 -0
  25. package/src/auth.js +30 -0
  26. package/src/claudeEvents.js +212 -0
  27. package/src/cli/cfNamed.js +5 -0
  28. package/src/cli/claudeHooks.js +116 -0
  29. package/src/cli/cloudflareUrl.js +9 -0
  30. package/src/cli/cloudflared.js +53 -0
  31. package/src/cli/drivers.js +59 -0
  32. package/src/cli/options.js +169 -0
  33. package/src/cli/probe.js +16 -0
  34. package/src/cli/qr.js +34 -0
  35. package/src/cli/service.js +98 -0
  36. package/src/cli/setupWizard.js +248 -0
  37. package/src/cli/sshTunnel.js +12 -0
  38. package/src/cli/state.js +42 -0
  39. package/src/cli/supervisor.js +172 -0
  40. package/src/cli/tmuxConf.js +90 -0
  41. package/src/cli/tmuxVersion.js +49 -0
  42. package/src/cli/tunlite.js +22 -0
  43. package/src/config.js +6 -0
  44. package/src/docPath.js +46 -0
  45. package/src/docs.js +222 -0
  46. package/src/git.js +185 -0
  47. package/src/httpApi.js +548 -0
  48. package/src/previewServer.js +182 -0
  49. package/src/previews.js +118 -0
  50. package/src/push.js +121 -0
  51. package/src/server.js +97 -0
  52. package/src/staticCache.js +8 -0
  53. package/src/tmux/commands.js +237 -0
  54. package/src/trimCapture.js +28 -0
  55. package/src/uploadTypes.js +28 -0
  56. package/tmux/README.md +77 -0
  57. package/tmux/claude-tab-seed.py +67 -0
  58. package/tmux/claude-tab-seen.sh +14 -0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4
+ * https://github.com/chjj/term.js
5
+ * @license MIT
6
+ *
7
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ * of this software and associated documentation files (the "Software"), to deal
9
+ * in the Software without restriction, including without limitation the rights
10
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ * copies of the Software, and to permit persons to whom the Software is
12
+ * furnished to do so, subject to the following conditions:
13
+ *
14
+ * The above copyright notice and this permission notice shall be included in
15
+ * all copies or substantial portions of the Software.
16
+ *
17
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ * THE SOFTWARE.
24
+ *
25
+ * Originally forked from (with the author's permission):
26
+ * Fabrice Bellard's javascript vt100 for jslinux:
27
+ * http://bellard.org/jslinux/
28
+ * Copyright (c) 2011 Fabrice Bellard
29
+ * The original design remains. The terminal itself
30
+ * has been extended to include xterm CSI codes, among
31
+ * other features.
32
+ */.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}*{box-sizing:border-box}@font-face{font-family:JetBrainsMono Nerd Font;src:url(/fonts/JetBrainsMonoNerdFontMono-Regular.woff2) format("woff2");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:TW Unifont;src:url(/fonts/TWUnifont.woff2) format("woff2");font-weight:400;font-style:normal;font-display:swap;unicode-range:U+2190-21FF,U+2300-23FF,U+2400-243F,U+2500-257F,U+2580-259F,U+25A0-25FF,U+2600-26FF,U+2700-27BF,U+2800-28FF,U+2B00-2BFF}html,body,#root{height:100%;margin:0;overscroll-behavior:none}body{font-family:-apple-system,system-ui,sans-serif;background:#1a1b1e;color:#e6e6e6}.app{display:flex;flex-direction:column;height:100%;overflow:hidden}.topbar{display:flex;align-items:center;gap:12px;padding:8px 12px;background:#2a2b2e}.hamburger{font-size:20px;background:none;border:none;color:#e6e6e6}.session-name{font-weight:600;margin-right:auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fontbtn{min-width:36px;padding:4px 10px;border:none;border-radius:6px;background:#33353a;color:#e6e6e6;font-size:14px;touch-action:manipulation}.fontbtn[aria-pressed=true]{background:#2a4060;color:#7ab4f5}.topbar-icon{display:inline-flex;align-items:center;padding:4px;border:none;background:none;color:#e6e6e6;touch-action:manipulation}.topbar-icon:active{color:#4aa3ff}.topbar-icon svg{width:21px;height:21px;display:block}.settings-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;z-index:19}.settings-card{position:fixed;z-index:20;left:50%;top:50%;transform:translate(-50%,-50%);width:calc(100% - 48px);max-width:360px;background:#232427;border-radius:12px;padding:16px;box-shadow:0 10px 40px #00000080}.settings-head{display:flex;align-items:center;margin-bottom:12px}.settings-title{font-size:16px;font-weight:600;margin-right:auto}.settings-close{background:none;border:none;color:#aaa;font-size:16px;padding:4px 8px;touch-action:manipulation}.settings-section{padding:10px 0;border-top:1px solid #33353a}.settings-label{font-size:12px;color:#888;letter-spacing:1px;margin-bottom:8px}.settings-btns{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.lang-drop{position:relative;display:inline-block}.lang-drop-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1}.lang-trigger{display:flex;align-items:center;gap:8px;padding:5px 10px;background:#33353a;border:none;border-radius:6px;color:#e6e6e6;font-size:14px;touch-action:manipulation;cursor:pointer}.lang-chevron{font-size:11px;color:#888}.lang-menu{position:absolute;top:calc(100% + 4px);left:0;min-width:100%;background:#2a2c30;border-radius:8px;box-shadow:0 4px 20px #00000080;z-index:2;overflow:hidden}.lang-option{display:block;width:100%;padding:9px 14px;border:none;background:none;color:#e6e6e6;font-size:14px;text-align:left;touch-action:manipulation;white-space:nowrap}.lang-option:active{background:#33353a}.lang-option.active{color:#7ab4f5}.settings-value{min-width:56px;text-align:center;font-size:14px;color:#e6e6e6}.cols-btns{flex-wrap:nowrap;gap:5px}.col-step{min-width:40px;padding:4px 6px;letter-spacing:.3px}.col-fine{color:#aaa}.settings-note{font-size:13px;color:#9aa0a6;flex:1 1 auto;min-width:0}.settings-hint{font-size:12px;color:#7c8088;line-height:1.5;margin-top:8px}.settings-hint.preview-dynamic-disabled{color:#6b6f77;border-left:2px solid #34373d;padding-left:8px;margin-top:6px}@keyframes tw-spin{to{transform:rotate(360deg)}}.spinner{display:inline-block;width:14px;height:14px;flex:none;border:2px solid #4a4d52;border-top-color:#e6e6e6;border-radius:50%;animation:tw-spin .7s linear infinite}.bind-input{width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1b1e;color:#fff;font-size:14px}.startup-custom{margin-top:6px}.opt+.opt{margin-top:14px}.field{width:100%;box-sizing:border-box;display:flex;align-items:center;gap:8px;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1b1e;color:#fff;font-size:14px;text-align:left;touch-action:manipulation}.field:active{border-color:#555}.cwd-field{justify-content:space-between}.cwd-path{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;opacity:.9}.cwd-action{flex:none;color:#4aa3ff;font-size:13px}.cwd-reset{margin-top:6px;background:none;border:none;color:#888;font-size:12px;padding:2px 0;touch-action:manipulation}.dd{position:relative}.dd-trigger{justify-content:space-between}.dd-value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dd-caret{flex:none;color:#8a8f98;font-size:12px;transition:transform .15s ease}.dd-caret.open{transform:rotate(180deg)}.dd-menu{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;background:#26282c;border:1px solid #3a3c42;border-radius:8px;padding:4px;box-shadow:0 10px 30px #0000008c;transform-origin:top;animation:dd-pop .12s ease}@keyframes dd-pop{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}.dd-option{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:10px;border:none;border-radius:6px;background:none;color:#e6e6e6;font-size:14px;text-align:left;touch-action:manipulation}.dd-option:active{background:#33353a}.dd-option.is-selected{color:#07c160}.dd-check{flex:none;color:#07c160;font-size:13px}.bind-error{margin-top:8px;color:#ff8585;font-size:13px}.bind-hint{margin-top:8px;color:#888;font-size:13px}.bind-actions{margin-top:12px;justify-content:flex-end}.bind-confirm{background:#4a7cff;color:#fff}.bind-confirm:disabled{opacity:.6}.windowbar{display:flex;gap:8px;align-items:center;padding:6px 8px;background:#222}.windowbar-scroll{flex:1;min-width:0;display:flex;gap:4px;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;touch-action:pan-x;scrollbar-width:none}.windowbar-scroll::-webkit-scrollbar{display:none}.win-tab{white-space:nowrap;padding:4px 10px;border:none;border-radius:6px;background:#33353a;color:#bbb;font-size:13px;touch-action:manipulation}.win-tab.active{background:#4a7cff;color:#fff}.win-new{border:1px dashed #555;color:#8aa;font-weight:700;background:transparent}.win-panes{margin-left:5px;font-size:11px;color:#889}.win-tab.active .win-panes{color:#d6e4ff}.wt-dd{position:relative;flex:none}.wt-trigger{display:inline-flex;align-items:center;gap:4px}.wt-sep{margin:0 -1px;color:#cfe0ff;opacity:.4;font-weight:400}.wt-pane{font-weight:600}.wt-caret{flex:none;font-size:11px;color:#d6e4ff;transition:transform .15s ease}.wt-caret.open{transform:rotate(180deg)}.wt-menu{position:fixed;top:auto;right:auto;min-width:160px;max-width:240px}.terminal-wrap{position:relative;flex:1;min-height:0;overflow:hidden}.terminal{height:100%;overflow-x:auto;overflow-y:hidden;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;touch-action:pan-x pan-y}.terminal--loading{opacity:0}.terminal .xterm{display:flex;flex-direction:column;justify-content:flex-end;height:100%;width:max-content;min-width:100%}.terminal .xterm-screen{flex:none}.terminal .xterm-viewport{touch-action:pan-x pan-y;overscroll-behavior:contain}.terminal .xterm-helper-textarea{pointer-events:none;opacity:0}.new-output{position:absolute;right:12px;bottom:12px;z-index:5;touch-action:manipulation;padding:8px 14px;border:none;border-radius:16px;background:#4a7cff;color:#fff;box-shadow:0 2px 8px #0006}.dbg{position:absolute;left:6px;top:6px;z-index:6;font:12px/1.4 monospace;color:#9fe;background:#0009;padding:2px 6px;border-radius:4px;pointer-events:none}.term-banner{position:absolute;left:0;right:0;top:0;z-index:6;font:12px/1.7 -apple-system,system-ui,sans-serif;text-align:center;padding:2px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}.term-banner--hist{background:#e0a800;color:#1a1400}.term-banner--err{background:#d32f2f;color:#fff}.copy-bubble{position:absolute;z-index:7;padding:7px 16px;border:none;border-radius:16px;background:#4a7cff;color:#fff;font-size:14px;font-weight:600;box-shadow:0 2px 10px #00000080;touch-action:manipulation}.sel-hint{position:absolute;left:50%;top:12px;transform:translate(-50%);z-index:8;max-width:92%;padding:7px 14px;border-radius:16px;background:#4a7cfff5;color:#fff;font-size:13px;font-weight:600;white-space:nowrap;box-shadow:0 2px 10px #00000080;pointer-events:none}.input-text{flex:1;min-width:0;padding:5px 4px;border:none;background:transparent;color:#fff;font-family:inherit;font-size:16px;line-height:22px;max-height:156px;resize:none;overflow-y:auto;display:block}.input-text:focus{outline:none}.token-prompt button{padding:8px 14px;border:none;border-radius:6px;background:#4a7cff;color:#fff}.drawer{position:fixed;top:0;left:0;width:78%;max-width:320px;height:100%;background:#232427;transform:translate(-100%);transition:transform .2s;z-index:10;padding:12px;display:flex;flex-direction:column;overflow:hidden}.drawer.open{transform:translate(0)}.drawer-list{flex:1;min-height:0;overflow-y:auto}.drawer-logout{flex:none;margin-top:8px;padding:10px;border:none;border-radius:6px;background:#2e2a2c;color:#ffb4b4;font-size:14px;touch-action:manipulation}.drawer-logout:active{background:#4a2f33}.drawer-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#0006;z-index:9}.drawer-title{font-size:12px;color:#888;letter-spacing:1px;margin-bottom:8px}.drawer-row{display:flex;align-items:center;gap:6px;border-radius:6px}.drawer-name{flex:1;min-width:0;padding:8px 4px;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.drawer-session.active{background:#4a7cff;color:#fff}.drawer-unbind{flex:none;background:none;border:none;color:#888;font-size:14px;padding:6px 8px;touch-action:manipulation}.drawer-session.active .drawer-unbind{color:#d6e4ff}.drawer-empty{padding:12px 4px;color:#888;font-size:13px}.drawer-bind{width:100%;margin-top:8px;padding:10px;border:none;border-radius:6px;background:#2b3550;color:#cdddff;font-size:14px;touch-action:manipulation}.drawer-bind:active{background:#344168}.token-prompt{max-width:360px;margin:80px auto;display:flex;flex-direction:column;gap:12px;padding:0 16px}.token-prompt input{padding:10px;border-radius:6px;border:1px solid #444;background:#1a1b1e;color:#fff}.loading{padding:40px;text-align:center;color:#888}.empty-cta{margin:auto;display:flex;flex-direction:column;gap:6px;align-items:center;padding:28px 32px;background:none;border:1px dashed #44464c;border-radius:12px;color:#aab;font-size:15px;touch-action:manipulation}.empty-cta span{font-size:13px;color:#6f7bff}.empty-cta:active{background:#232427}.bottom-dock{display:flex;gap:8px;padding:8px;background:#2a2b2e;align-items:stretch}.dock-left{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}.keyrow{display:flex;align-items:stretch;gap:6px}.keyrow .keybar{flex:1;min-width:0}.keyrow-stack{flex:none;width:66px;display:flex;flex-direction:column;gap:4px}.keyrow-del,.keyrow-enter{flex:1;width:100%;min-height:40px;border:none;border-radius:6px;background:#33353a;color:#e6e6e6;touch-action:manipulation;-webkit-user-select:none;user-select:none}.keyrow-del{font:19px/1 ui-monospace,SF Mono,Menlo,monospace}.keyrow-enter{font-size:15px}.keyrow-del:active,.keyrow-enter:active{background:#4a7cff;color:#fff}.keybar{display:flex}.keybar-scroll{flex:1;display:flex;gap:4px;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;touch-action:pan-x;scrollbar-width:none}.keybar-scroll::-webkit-scrollbar{display:none}.keybar-col{display:flex;flex-direction:column;gap:4px}.keybar-key{flex:1;min-width:42px;min-height:34px;padding:0 8px;border:none;border-radius:6px;white-space:nowrap;background:#33353a;color:#e6e6e6;font:13px/1 ui-monospace,SF Mono,Menlo,monospace;touch-action:manipulation;-webkit-user-select:none;user-select:none}.keybar-key:active{background:#4a7cff;color:#fff}.keybar-key[data-key=esc],.keybar-key[data-key=tab],.keybar-key[data-key=up],.keybar-key[data-key=down]{min-width:52px}.dock-input-row{display:flex;gap:6px;align-items:stretch}.input-wrap{position:relative;flex:1;min-width:0;display:flex;align-items:flex-end;gap:2px;padding:4px;border:1px solid #444;border-radius:6px;background:#1a1b1e}.input-wrap:focus-within{border-color:#07c160;box-shadow:0 0 0 1px #07c16073}.input-upload{flex:none;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:#8a93a0;padding:0;touch-action:manipulation;-webkit-user-select:none;user-select:none}.input-upload:active{color:#4a7cff}.input-upload:disabled{opacity:.4}.input-upload svg{width:22px;height:22px;display:block}.dock-upload{padding:6px 4px 2px}.dock-upload-label{display:block;color:#9fb6d0;font-size:12px;margin-bottom:4px;font-variant-numeric:tabular-nums}.dock-upload.error .dock-upload-label{color:#ff8a8a}.dock-upload-track{display:block;height:4px;border-radius:2px;background:#1c2b3a;overflow:hidden}.dock-upload-fill{display:block;height:100%;background:#3a5a7e;transition:width .1s linear}.input-send{flex:none;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:#4a7cff;color:#fff;padding:0;touch-action:manipulation;-webkit-user-select:none;user-select:none}.input-send:active{background:#3a66e0}.input-send:disabled{background:#3a3c42;color:#6b7280}.input-send svg{width:20px;height:20px;display:block}.input-cmd{flex:none;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:#8a93a0;padding:0;touch-action:manipulation;-webkit-user-select:none;user-select:none}.input-cmd svg{width:20px;height:20px;display:block}.input-cmd:active{color:#4a7cff}.input-wrap.recording{background:#07c16024;border:2px solid #07c160;animation:rec-breathe 1.3s ease-in-out infinite}.input-wrap.recording .input-text{color:#eafff3}@keyframes rec-breathe{0%,to{box-shadow:0 0 5px #07c1604d;border-color:#2fd07e}50%{box-shadow:0 0 16px 4px #07c160cc;border-color:#07c160}}.input-mic{flex:none;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:#00000040;color:#b6c0cc;padding:0;touch-action:manipulation;-webkit-user-select:none;user-select:none}.input-mic.on{color:#fff;background:#07c160}.input-mic:disabled{opacity:.5}.input-mic svg{width:21px;height:21px;display:block}.cmd-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;z-index:21}.cmd-panel{position:fixed;left:0;right:0;bottom:0;z-index:22;max-height:72vh;display:flex;flex-direction:column;background:#232427;border-radius:12px 12px 0 0;box-shadow:0 -8px 30px #00000080}.cmd-head{display:flex;align-items:center;gap:8px;padding:12px 14px;border-bottom:1px solid #33353a}.cmd-title{font-size:15px;font-weight:600;margin-right:auto;display:flex;align-items:center;gap:7px}.idea-head-count{font-size:11px;font-weight:600;color:#a8c4e0;background:#3d5068;border-radius:6px;padding:1px 6px}.cmd-close{background:none;border:none;color:#aaa;padding:4px 8px;touch-action:manipulation;display:inline-flex;align-items:center}.cmd-close svg{width:16px;height:16px;display:block}.cmd-list{overflow-y:auto;-webkit-overflow-scrolling:touch;padding:6px 0 10px}.cmd-section{display:flex;align-items:baseline;gap:8px;padding:9px 14px 5px}.cmd-section.recent{margin-top:6px;border-top:1px solid #2c2e33;padding-top:12px}.cmd-section-name{font-size:12px;font-weight:700;letter-spacing:1px}.cmd-section.fav .cmd-section-name{color:#ffd27a}.cmd-section.recent .cmd-section-name{color:#9db8ff}.cmd-scope{margin-left:auto;font-size:10px;color:#9a9a9a;background:#2c2e33;padding:2px 8px;border-radius:10px;white-space:nowrap}.cmd-row{display:flex;align-items:center;gap:6px;padding:0 8px 0 14px}.cmd-text{flex:1;min-width:0;padding:9px 0;font:13px/1.3 ui-monospace,SF Mono,Menlo,monospace;color:#e6e6e6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;touch-action:manipulation}.cmd-text:active{color:#9db8ff}.cmd-star{flex:none;background:none;border:none;color:#667;padding:6px 8px;touch-action:manipulation;display:inline-flex;align-items:center}.cmd-star svg{width:16px;height:16px;display:block}.cmd-star.on{color:#ffd27a}.cmd-star.on svg{fill:currentColor}.cmd-del{flex:none;background:none;border:none;color:#888;padding:6px 8px;touch-action:manipulation;display:inline-flex;align-items:center}.cmd-del svg{width:14px;height:14px;display:block}.cmd-empty{padding:20px 14px;color:#888;font-size:13px;text-align:center}.idea-panel{position:fixed;left:0;right:0;bottom:0;z-index:22;min-height:50vh;max-height:80vh;display:flex;flex-direction:column;background:#232427;border-radius:12px 12px 0 0;box-shadow:0 -8px 30px #00000080}.idea-compose{display:flex;align-items:flex-end;gap:8px;padding:10px 14px;border-bottom:1px solid #33353a}.idea-input{flex:1;min-width:0;resize:none;max-height:96px;padding:8px 10px;border:1px solid #3a3c42;border-radius:8px;background:#1b1c1f;color:#e6e6e6;font:14px/1.4 -apple-system,system-ui,sans-serif}.idea-input:focus{outline:none;border-color:#5b78d6}.idea-compose .input-mic{position:static;right:auto;bottom:auto;flex:none}.idea-act{flex:none;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:#3a6df0;color:#fff;padding:0;touch-action:manipulation}.idea-act:disabled{background:#00000040;color:#b6c0cc;opacity:.55}.idea-act svg{width:20px;height:20px;display:block}.idea-cancel{flex:none;background:none;border:none;color:#aaa;font-size:13px;padding:9px 4px;touch-action:manipulation}.idea-list{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:6px 0 12px}.idea-list:has(.cmd-empty){display:flex;align-items:center;justify-content:center}.idea-row{display:flex;align-items:center;gap:6px;padding:0 8px 0 4px;background:#232427}.idea-row.editing{background:#262a31}.idea-row.dragging{background:#2c2f36;border-radius:8px;box-shadow:0 2px 12px #00000073}.idea-handle{flex:none;background:none;border:none;color:#777;font-size:20px;line-height:1;padding:11px 16px;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.idea-text{flex:1;min-width:0;padding:11px 0;font-size:14px;line-height:1.35;color:#e6e6e6;white-space:pre-wrap;overflow-wrap:anywhere;touch-action:manipulation}.idea-text:active{color:#9db8ff}.idea-send{flex:none;background:#2c2e33;color:#9db8ff;border:none;border-radius:6px;padding:6px 10px;font-size:13px;touch-action:manipulation}.idea-del{flex:none;background:none;border:none;color:#888;font-size:14px;padding:6px 8px;touch-action:manipulation}.changelog-panel{position:fixed;left:0;right:0;bottom:0;z-index:22;max-height:80vh;display:flex;flex-direction:column;background:#232427;border-radius:12px 12px 0 0;box-shadow:0 -8px 30px #00000080}.changelog-list{overflow-y:auto;-webkit-overflow-scrolling:touch;padding:6px 16px 16px}.rel{padding:12px 0;border-bottom:1px solid #2c2e33}.rel:last-child{border-bottom:none}.rel-date{font-size:12px;font-weight:700;letter-spacing:.5px;color:#9db8ff;margin-bottom:6px}.rel-items{margin:0;padding-left:18px}.rel-items li{font-size:13px;line-height:1.5;color:#e6e6e6;margin:3px 0}.topbar-icon{position:relative}.topbar-dot{position:absolute;top:4px;right:4px;width:7px;height:7px;border-radius:50%;background:#ff5a52;box-shadow:0 0 0 2px #1b1c1f}.idea-badge{position:absolute;top:2px;right:1px;min-width:14px;height:14px;border-radius:7px;padding:0 3px;box-sizing:border-box;background:#3d5068;color:#a8c4e0;font-size:9px;font-weight:700;line-height:14px;text-align:center;pointer-events:none}.settings-dot{display:inline-block;width:7px;height:7px;margin-left:6px;border-radius:50%;background:#ff5a52;vertical-align:middle}.sheet-actions{display:flex;flex-direction:column;gap:8px}.sheet-action{width:100%;padding:12px;border:1px solid #33353a;border-radius:6px;background:#2a2c31;color:#ddd;font-size:15px;text-align:center;touch-action:manipulation}.sheet-action.danger{color:#ff8585}.sheet-action.armed{background:#ff8585;color:#fff;border-color:#ff8585}.sheet-action:disabled{opacity:.4}.sheet-row{display:flex;gap:8px}.sheet-row .sheet-action{flex:1}.sheet-cancel{margin-top:4px}.win-tab,.session-name{-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}.file-sheet{position:fixed;top:0;right:0;bottom:0;left:0;z-index:50;display:flex;flex-direction:column;background:#0b0f14;transform:translateY(100%);transition:transform .28s cubic-bezier(.22,.61,.36,1);pointer-events:none}.file-sheet.open{transform:translateY(0);pointer-events:auto}.file-tabs{display:flex;align-items:stretch;border-bottom:1px solid #1d2630;background:#11161d}.file-tabs-scroll{display:flex;overflow-x:auto;flex:1;-webkit-overflow-scrolling:touch}.file-tab{display:inline-flex;align-items:center;white-space:nowrap;border-right:1px solid #1d2630}.file-tab.active{background:#0b0f14;box-shadow:inset 0 -2px #4aa3ff}.file-tab-label{display:inline-flex;align-items:center;gap:5px;padding:10px 8px 10px 12px;background:transparent;border:none;color:#9fb0c0;font-size:13px}.file-tab-label svg{width:15px;height:15px}.file-tab.active .file-tab-label{color:#e6edf3}.file-tab-x{background:transparent;border:none;color:#6b7a89;font-size:11px;padding:0 10px 0 2px}.file-min{flex:0 0 auto;width:44px;display:inline-flex;align-items:center;justify-content:center;background:transparent;color:#9fb0c0;border:none;border-left:1px solid #1d2630}.file-min svg{width:20px;height:20px}.file-body{flex:1;overflow:hidden;display:flex}.preview-frame{flex:1;width:100%;border:0;background:#fff}.preview-body.pc{display:block;position:relative;overflow:auto;-webkit-overflow-scrolling:touch}.preview-scaler{position:relative}.preview-body.pc .preview-frame{position:absolute;top:0;left:0;flex:none}.preview-zoom{position:absolute;bottom:14px;left:50%;transform:translate(-50%);z-index:2;display:flex;align-items:center;gap:2px;padding:4px;background:#1c1e22eb;border:1px solid #3a3c42;border-radius:999px;box-shadow:0 6px 20px #00000073}.preview-zoom-btn{width:34px;height:30px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:#e6edf3;font-size:20px;line-height:1;border-radius:999px;touch-action:manipulation}.preview-zoom-btn:disabled{color:#5a626b}.preview-zoom-val{min-width:46px;text-align:center;color:#9fb0c0;font-size:13px;font-variant-numeric:tabular-nums}.preview-head{position:relative;align-items:center;gap:4px;min-height:44px;padding-left:10px}.preview-status{display:inline-flex;align-items:center;gap:5px;min-width:0;color:#e6edf3;font-size:12px}.preview-status svg{width:15px;height:15px;flex:0 0 auto}.preview-state{color:#3fb950;font-weight:600;flex:0 0 auto}.preview-name{color:#9fb0c0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.preview-detail{color:#6e7d8c;margin-left:6px}.preview-remain{margin-left:auto;flex:0 0 auto;font-variant-numeric:tabular-nums;color:#9fb0c0;font-size:12px;padding:4px 8px;background:transparent;border:1px solid #33353a;border-radius:6px;touch-action:manipulation}.preview-remain:active{color:#4aa3ff;border-color:#4aa3ff}.preview-pop-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:5}.preview-pop{position:absolute;top:46px;right:8px;z-index:6;min-width:168px;display:flex;flex-direction:column;gap:2px;padding:6px;background:#232427;border:1px solid #33353a;border-radius:10px;box-shadow:0 10px 30px #00000080}.preview-pop-item{background:transparent;border:none;color:#e6edf3;text-align:left;padding:10px 12px;border-radius:6px;font-size:14px}.preview-pop-item:active{background:#2f3338}.preview-pop-danger{color:#ffb4b4}.preview-iconbtn{flex:0 0 auto;width:32px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:#9fb0c0;touch-action:manipulation}.preview-iconbtn svg{width:17px;height:17px}.preview-iconbtn:active{color:#4aa3ff}.preview-head .file-min{width:34px}.preview-head .file-min svg{width:18px;height:18px}.preview-confirm-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:flex;align-items:center;justify-content:center;background:#00000080}.preview-confirm{width:min(86vw,320px);background:#232427;border-radius:12px;padding:16px;box-shadow:0 10px 40px #00000080}.preview-confirm-title{font-size:16px;font-weight:600;color:#e6edf3;margin-bottom:8px}.preview-confirm-text{font-size:13px;color:#9aa3ad;line-height:1.5;margin-bottom:14px}.preview-confirm-btns{display:flex;justify-content:flex-end;gap:8px}.preview-confirm-danger{background:#5a1f1f;color:#ffb4b4}.settings-group{margin:16px -16px 0;padding:7px 16px;background:#1b1c1f;color:#9aa3ad;font-size:11px;font-weight:700;letter-spacing:1.2px;border-top:1px solid #33353a;border-bottom:1px solid #33353a}.settings-head+.settings-group{margin-top:0}.settings-group+.settings-section{border-top:none}.preview-running{display:inline-flex;align-items:center;gap:6px;color:#3fb950;font-weight:600;font-size:13px}.live-dot{width:8px;height:8px;border-radius:50%;background:#3fb950}.preview-port{width:9em;padding:6px 10px;font-size:15px;color:#e8e8ea;background:#1b1c1f;border:1px solid #3a3c42;border-radius:8px}.preview-seg{display:inline-flex;align-self:flex-start;border:1px solid #3a3c42;border-radius:8px;overflow:hidden}.preview-seg button{background:transparent;border:none;color:#9aa3ad;padding:7px 18px;min-width:60px;font-size:14px;touch-action:manipulation}.preview-seg button+button{border-left:1px solid #3a3c42}.preview-seg button.on{background:#2f6df6;color:#fff}.preview-start{display:flex;flex-direction:column;gap:12px}.preview-active{display:flex;flex-direction:column;gap:10px}.preview-active-head{display:flex;align-items:center;gap:10px}.preview-remain-s{margin-left:auto;font-variant-numeric:tabular-nums;color:#9aa3ad;font-size:13px}.settings-err{color:#ff9d9d}.preview-live{color:#3fb950}.home-pane{flex:1;display:flex;flex-direction:column;min-width:0}.file-seg{display:flex;gap:8px;padding:10px 12px;border-bottom:1px solid #1d2630;background:#0e131a}.file-seg-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:9px;border-radius:8px;background:#11161d;border:1px solid #1d2630;color:#9fb0c0;font-size:14px}.file-seg-btn.on{background:#15212f;border-color:#2f6fc0;color:#cfe0f2}.file-seg-btn svg{width:16px;height:16px}.git-head{align-items:stretch;min-height:42px}.git-back{flex:0 0 auto;width:40px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:#9fb0c0;font-size:26px;line-height:1}.git-drill-head{flex:1;min-width:0;display:flex;align-items:center;gap:4px;overflow:hidden}.git-drill-title{display:inline-flex;align-items:center;gap:7px;min-width:0;color:#e6edf3;font-size:14px}.git-drill-title svg{width:16px;height:16px;color:#6b86a3;flex:0 0 auto}.git-title-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.git-tabs-scroll{display:flex;align-items:stretch;overflow-x:auto;flex:1;-webkit-overflow-scrolling:touch}.git-tab-add{flex:0 0 auto;width:40px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;border-right:1px solid #1d2630;color:#8fb2dc;font-size:20px;line-height:1}.git-tab{flex:0 0 auto;display:inline-flex;align-items:center;white-space:nowrap;border-right:1px solid #1d2630}.git-tab.active{background:#0b0f14;box-shadow:inset 0 -2px #4aa3ff}.git-tab-label{display:inline-flex;align-items:center;padding:10px 4px 10px 12px;background:transparent;border:none;color:#9fb0c0;font-size:13px}.git-tab.active .git-tab-label{color:#e6edf3}.git-tab-x{background:transparent;border:none;color:#6b7a89;font-size:11px;padding:0 10px 0 4px;align-self:stretch}.git-branch-bar{display:flex;align-items:center;gap:10px;padding:7px 12px 7px 14px;border-bottom:1px solid #1d2630;background:#0b0f14;font-size:13px}.git-branch-bar-name{flex:1;min-width:0;color:#cfe0f2;font-weight:600;display:flex;align-items:center;gap:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.git-branch-glyph{color:#8fb2dc}.git-branch-cur-tag{flex:0 0 auto;font-size:11px;font-weight:600;color:#3fb950;background:#3fb9501f;border:1px solid rgba(63,185,80,.4);border-radius:999px;padding:1px 7px}.git-branch-bar-track{flex:0 0 auto;font-size:12px;font-variant-numeric:tabular-nums;display:flex;gap:5px}.git-branch-dd{position:relative;flex:0 0 auto}.git-branch-trigger{max-width:46vw;display:flex;align-items:center;gap:5px;background:#11161d;border:1px solid #1d2630;border-radius:8px;color:#9fb0c0;font-size:12px;padding:5px 9px}.git-branch-trigger.other{border-color:#2f6fc0;background:#15212f;color:#cfe0f2}.git-branch-trigger-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.git-branch-caret{flex:0 0 auto;display:inline-flex;color:#6b7a89}.git-branch-caret svg{width:13px;height:13px}.git-dd-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:40}.git-dd-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:41;min-width:56vw;max-width:calc(100vw - 24px);max-height:56vh;overflow:auto;-webkit-overflow-scrolling:touch;background:#0e141b;border:1px solid #243040;border-radius:10px;box-shadow:0 10px 28px #00000080;padding:4px}.git-dd-item{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:transparent;border:none;color:#e6edf3;padding:10px;font-size:13px;border-radius:7px}.git-dd-item:active{background:#15212f}.git-dd-item.viewing{background:#15212f;box-shadow:inset 3px 0 #2f6fc0}.git-dd-item.current .git-row-path{color:#cfe0f2;font-weight:600}.git-section{display:flex;flex-direction:column;min-height:0;border-bottom:1px solid #161d26}.git-section:not(.open){flex:0 0 auto}.git-section.open.git-section--top{flex:0 1 auto;min-height:25%;max-height:50%}.git-section.open.git-section--mid{flex:1 1 0;min-height:96px}.git-section-head{flex:0 0 auto;display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:#0b0f14;border:none;color:#9fb0c0;padding:10px 14px;font-size:13px;font-weight:600}.git-section-head:active{background:#11161d}.git-section-caret{flex:0 0 auto;display:inline-flex;color:#6b7a89;transition:transform .15s;transform:rotate(-90deg)}.git-section.open .git-section-caret{transform:rotate(0)}.git-section-caret svg{width:14px;height:14px}.git-section-title{flex:0 0 auto}.git-section-sub{flex:1;min-width:0;color:#6b7a89;font-weight:400;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.git-section-count{flex:0 0 auto;margin-left:auto;color:#6b7a89;font-weight:400;font-size:12px;font-variant-numeric:tabular-nums}.git-section-sub+.git-section-count{margin-left:8px}.git-section-body{flex:1;min-height:0;overflow:auto;-webkit-overflow-scrolling:touch}.git-more{text-align:center;color:#6b7a89;font-size:12px;padding:12px}.git-body{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden;padding-bottom:var(--kb-inset, 0px)}.git-scroll{flex:1;min-height:0;overflow:auto;-webkit-overflow-scrolling:touch}.git-error{color:#ff9d9d;font-size:13px;padding:14px 16px}.git-loading,.git-empty{color:#6b7a89;font-size:13px;padding:16px}.git-list{display:flex;flex-direction:column}.git-row{display:flex;align-items:center;gap:9px;width:100%;text-align:left;background:transparent;border:none;border-bottom:1px solid #161d26;color:#e6edf3;padding:11px 14px;font-size:13px}.git-row:active{background:#11161d}.git-row-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1}.git-badge{flex:0 0 auto;width:20px;text-align:center;font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;color:#9aa3ad}.git-badge-m{color:#d8a13a}.git-badge-a{color:#3fb950}.git-badge-d{color:#f06b6b}.git-badge-r{color:#8fb2dc}.git-badge-q{color:#6b7a89}.git-commit-row{align-items:flex-start}.git-hash{flex:0 0 auto;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#8fb2dc;font-size:12px;padding-top:1px}.git-commit-main{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}.git-commit-meta{color:#6b7a89;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.git-branch{gap:8px}.git-branch.current .git-row-path{color:#cfe0f2;font-weight:600}.git-branch.viewing{background:#11161d;box-shadow:inset 3px 0 #2f6fc0}.git-branch-dot{flex:0 0 auto;width:12px;text-align:center;color:#3fb950;font-size:11px}.git-branch-up{flex:0 0 auto;color:#6b7a89;font-size:11px;font-variant-numeric:tabular-nums}.git-ahead{color:#3fb950}.git-behind{color:#f06b6b}.git-commit-detail{display:flex;flex-direction:column}.git-commit-msg{white-space:pre-wrap;word-break:break-word;margin:0;padding:14px 16px;color:#c9d6e2;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.5;border-bottom:1px solid #161d26;background:#0e131a}.git-diff-wrap{display:flex;flex-direction:column}.git-diff-zoom{position:sticky;top:0;z-index:2;display:flex;gap:6px;justify-content:flex-end;padding:8px 12px}.git-diff-trunc{position:sticky;top:0;z-index:1;background:#3a2a12;color:#f0c674;font-size:12px;padding:8px 14px;border-bottom:1px solid #5a4318}.git-diff-fname{padding:10px 14px 6px;color:#9fb0c0;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.git-diff{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:inherit;line-height:1.5;white-space:pre;overflow-x:auto;-webkit-overflow-scrolling:touch}.git-diff-line{padding:0 14px;min-width:max-content}.git-diff-hunk{color:#6b86a3;background:#0e131a}.git-diff-add{background:#3fb95029;color:#c7f0cf}.git-diff-del{background:#f06b6b29;color:#f3cccc}.git-diff-ctx{color:#c9d6e2}.home-view{flex:1;overflow:auto;-webkit-overflow-scrolling:touch;padding:12px;color:#c9d6e2}.home-empty{color:#6b7a89;font-size:13px;display:flex;flex-direction:column;gap:4px;padding:16px 4px}.home-empty span{color:#4a5663;font-size:12px}.home-recent-row{display:flex;align-items:center;width:100%}.home-recent{flex:1;display:flex;flex-direction:column;align-items:flex-start;background:#11161d;border:1px solid #1d2630;border-radius:8px;padding:8px 10px;margin-bottom:6px;color:#e6edf3;text-align:left}.home-path{font-size:11px;color:#6b7a89;overflow:hidden;text-overflow:ellipsis;max-width:80vw}.home-x{background:transparent;border:none;color:#6b7a89;padding:0 8px}.browse-view{flex:1;display:flex;flex-direction:column;min-height:0;position:relative}.browse-bar{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid #1d2630;background:#0e131a}.browse-root-dd{flex:0 0 auto}.browse-root{display:inline-flex;align-items:center;gap:3px;padding:9px 4px 9px 10px;background:transparent;border:none;color:#6b86a3;font-size:13px}.browse-root .dd-caret{font-size:10px}.browse-root-dd .dd-menu{left:0;right:auto;min-width:120px}.browse-up{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;background:#11161d;border:1px solid #1d2630;border-radius:8px;color:#9fb0c0}.browse-up:disabled{opacity:.4}.browse-up svg{width:18px;height:18px}.browse-cwd{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;background:#11161d;border:1px solid #1d2630;border-radius:8px;color:#9fb0c0}.browse-cwd svg{width:18px;height:18px}.browse-path{flex:1;min-width:0;display:flex;align-items:center;border-radius:8px;border:1px solid #243446;background:#11161d}.browse-path:focus-within{border-color:#3a5a7e}.browse-input:focus{outline:none}.browse-home{flex:0 0 auto;padding:9px 2px 9px 10px;color:#6b86a3;font-size:13px;-webkit-user-select:none;user-select:none}.browse-input{flex:1;min-width:0;padding:9px 10px 9px 0;border:none;border-radius:8px;background:transparent;color:#e6edf3;font-size:13px}.browse-err{padding:8px 12px 0}.browse-notice{position:absolute;left:50%;bottom:16px;transform:translate(-50%);z-index:6;max-width:88%;width:max-content;padding:9px 14px;border-radius:10px;background:#1c1e22f7;color:#d3dbe4;font-size:13px;line-height:1.4;box-shadow:0 6px 24px #00000080;pointer-events:none;word-break:break-all}.browse-saved{display:flex;align-items:center;gap:10px;margin:8px 12px 0;padding:8px 10px;border:1px solid #2a3f56;border-radius:8px;background:#15212e}.browse-saved-text{flex:1;min-width:0;color:#cfe0f0;font-size:13px}.browse-saved-close{flex:0 0 auto;width:32px;height:32px;border:none;border-radius:8px;background:transparent;color:#8aa0b6;font-size:20px;line-height:1}.browse-pending{display:flex;align-items:center;gap:10px;margin:8px 12px 0;padding:8px 10px;border:1px solid #2a3f56;border-radius:8px;background:#15212e}.browse-pending-text{flex:1;min-width:0;color:#cfe0f0;font-size:13px;overflow:hidden;text-overflow:ellipsis}.browse-pending-hint{color:#8aa0b6;font-style:normal;font-size:12px}.browse-pending-btn{flex:0 0 auto;height:32px;padding:0 14px;border:none;border-radius:8px;background:#2f5fa0;color:#fff;font-size:13px}.browse-pending-btn:disabled{opacity:.4}.browse-list{flex:1;overflow:auto;-webkit-overflow-scrolling:touch;padding:4px 12px 16px}.browse-empty{color:#6b7a89;font-size:13px;padding:16px 4px}.browse-overflow{color:#6b7a89;font-size:12px;padding:12px 4px 16px;text-align:center}.browse-entry{display:flex;align-items:center;gap:9px;width:100%;background:transparent;border:none;border-bottom:1px solid #161d26;color:#e6edf3;padding:11px 4px;text-align:left}.browse-entry-icon{flex:0 0 auto;display:inline-flex;color:#6b86a3}.browse-entry-icon svg{width:17px;height:17px}.browse-entry-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.browse-upload{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;background:#11161d;border:1px solid #1d2630;border-radius:8px;color:#9fb0c0}.browse-upload:disabled{opacity:.4}.browse-upload svg{width:18px;height:18px}.browse-file-input{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.browse-mkdir{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;background:#11161d;border:1px solid #1d2630;border-radius:8px;color:#9fb0c0}.browse-mkdir:disabled{opacity:.4}.browse-mkdir svg{width:18px;height:18px}.browse-newfolder{display:flex;align-items:center;gap:8px;padding:8px 12px}.browse-newfolder-input{flex:1;min-width:0;padding:9px 10px;border-radius:8px;border:1px solid #243446;background:#11161d;color:#e6edf3;font-size:13px}.browse-newfolder-input:focus{outline:none;border-color:#3a5a7e}.browse-newfolder-ok{flex:0 0 auto;height:36px;padding:0 14px;border:none;border-radius:8px;background:#2f5fa0;color:#fff;font-size:13px;white-space:nowrap}.browse-newfolder-x{flex:0 0 auto;width:36px;height:36px;border:1px solid #1d2630;border-radius:8px;background:#11161d;color:#9fb0c0;font-size:15px;line-height:1}.browse-progress{padding:8px 12px 0}.browse-progress-label{display:block;color:#9fb6d0;font-size:12px;margin-bottom:4px;font-variant-numeric:tabular-nums}.browse-progress-track{display:block;height:4px;border-radius:2px;background:#1c2b3a;overflow:hidden}.browse-progress-fill{display:block;height:100%;background:#3a5a7e;transition:width .1s linear}.browse-entry-row{display:flex;align-items:center}.browse-entry-row .browse-entry{flex:1;min-width:0}.browse-entry-size{flex:0 0 auto;margin-left:auto;padding-left:8px;color:#6b7a89;font-size:12px;font-variant-numeric:tabular-nums}.browse-dl,.browse-copy{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;background:none;color:#6b86a3;margin-left:4px}.browse-dl svg,.browse-copy svg{width:17px;height:17px}.browse-copy:active{color:#9db8ff}.browse-pick-bar{padding:8px;border-top:1px solid #1d2630}.browse-pick-confirm{width:100%;padding:10px;border:none;border-radius:8px;background:#4a7cff;color:#fff;font-size:14px}.dirpick-backdrop{z-index:30;background:#0000009e}.dirpick-card{width:min(92vw,520px);max-width:none;z-index:31;max-height:min(80vh,calc(100vh - var(--kb-inset, 0px) - 32px));display:flex;flex-direction:column}.dirpick-card .browse-view{flex:1;min-height:0}.dirpick-hint{font-size:12px;color:#9a9a9a;line-height:1.5;margin-bottom:10px;word-break:break-all}.dirpick-hint code{color:#d8c08a;background:#2c2e33;padding:1px 5px;border-radius:4px}.doc-md-wrap{position:relative;flex:1;min-width:0;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch}.doc-zoom{position:sticky;top:0;z-index:1;display:flex;gap:6px;align-items:center;padding:8px 12px 0}.doc-tts{display:flex;gap:6px}.doc-fonts{display:flex;gap:6px;margin-left:auto}.doc-zoom-btn{display:inline-flex;align-items:center;justify-content:center;background:#11161d;border:1px solid #1d2630;color:#9fb0c0;border-radius:8px;padding:4px 12px;font-size:14px;touch-action:manipulation}.doc-zoom-btn:disabled{opacity:.4}.doc-zoom-icon{padding:4px 9px}.doc-zoom-icon svg{width:17px;height:17px;display:block}.tts-active{background:#4a7cff4d;border-radius:3px;box-decoration-break:clone;-webkit-box-decoration-break:clone}.doc-md{padding:16px;color:#e6edf3;line-height:1.6;word-wrap:break-word}.doc-md h1,.doc-md h2,.doc-md h3{border-bottom:1px solid #1d2630;padding-bottom:4px}.doc-md pre{background:#11161d;padding:12px;border-radius:8px;overflow:auto}.doc-md code{background:#11161d;padding:1px 4px;border-radius:4px}.doc-md a{color:#4aa3ff}.doc-md img{max-width:100%}.doc-md table{display:block;max-width:100%;overflow-x:auto;border-collapse:collapse}.doc-text{margin:0;padding:16px;color:#e6edf3;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere}.doc-iframe{width:100%;height:100%;border:0;background:#fff}.doc-image-wrap{flex:1;min-height:0;overflow:hidden;position:relative;display:flex;align-items:center;justify-content:center;padding:8px;background:#1b1d22;touch-action:none;user-select:none;-webkit-user-select:none}.doc-image{max-width:100%;max-height:100%;object-fit:contain;display:block;will-change:transform;-webkit-touch-callout:none}.doc-image-msg{flex:1;display:flex;align-items:center;justify-content:center;color:#9fb0c0;font-size:14px}.doc-image-zoom{position:absolute;bottom:14px;left:50%;transform:translate(-50%);z-index:2;display:flex;align-items:center;gap:2px;padding:4px;background:#1c1e22eb;border:1px solid #3a3c42;border-radius:999px;box-shadow:0 6px 20px #00000073}.doc-image-zoom button{width:34px;height:30px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:#e6edf3;font-size:20px;line-height:1;border-radius:999px;touch-action:manipulation}.doc-image-zoom button:disabled{color:#5a626b}.doc-image-zoom-val{min-width:46px;text-align:center;color:#9fb0c0;font-size:13px;font-variant-numeric:tabular-nums}.doc-image-note{position:absolute;top:14px;left:50%;transform:translate(-50%);z-index:3;max-width:88%;padding:7px 14px;border-radius:999px;white-space:nowrap;background:#1c1e22f0;border:1px solid #3a3c42;color:#e6edf3;font-size:13px;box-shadow:0 6px 20px #00000073}.doclink-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:58}.doclink-pop{position:fixed;z-index:59;width:max-content;max-width:calc(100vw - 16px);background:#1b2330;border:1px solid #2c3a4e;border-radius:12px;padding:12px 14px;box-shadow:0 12px 32px #0000008c;color:#e6edf3}.doclink-name{display:flex;align-items:center;gap:7px;font-weight:600;font-size:14px}.doclink-name svg{width:17px;height:17px;color:#6cb4ff;flex:0 0 auto}.doclink-path{font-size:11px;color:#7d8da0;margin:5px 0 11px;word-break:break-all;max-width:72vw}.doclink-actions{display:flex;gap:8px;justify-content:flex-end}.doclink-actions button{border:1px solid #2c3a4e;background:#11161d;color:#c9d6e2;border-radius:8px;padding:7px 18px;font-size:14px}.doclink-open{background:#2563b0;border-color:#2f6fc0;color:#fff}.doc-deco{border-bottom:2px solid #6cb4ff;background:#4aa3ff38;border-radius:3px;box-sizing:border-box}.doc-toast{position:fixed;left:50%;bottom:24px;transform:translate(-50%);z-index:60;max-width:86vw;background:#2a1518;color:#ffd7d7;border:1px solid #5a2a2a;padding:10px 14px;border-radius:10px;font-size:13px;box-shadow:0 6px 20px #0006}.inbox-btn{position:relative}.inbox-dot{position:absolute;top:1px;right:1px;width:9px;height:9px;border-radius:50%;border:1.5px solid #2a2b2e}.inbox-dot.needs{background:#e0a020}.inbox-dot.done{background:#2e7d46}.inbox-dot.working{background:#2f6fed;animation:tw-blink 1s ease-in-out infinite}@media (prefers-reduced-motion: reduce){.inbox-dot.working{animation:none}}.inbox-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;z-index:19}.inbox-panel{position:fixed;z-index:20;top:48px;right:8px;width:calc(100% - 32px);max-width:360px;max-height:70vh;overflow-y:auto;background:#232427;border-radius:12px;padding:8px;box-shadow:0 10px 40px #00000080}.inbox-group-title{font-size:12px;color:#888;letter-spacing:1px;padding:8px 8px 4px}.inbox-row{display:flex;flex-direction:column;gap:2px;width:100%;text-align:left;background:none;border:none;color:#e6e6e6;padding:8px;border-radius:8px;touch-action:manipulation}.inbox-row:active{background:#2f3034}.inbox-row-head{display:flex;align-items:center;gap:6px;min-width:0}.inbox-chip{font-size:11px;padding:1px 6px;border-radius:4px;flex:none}.inbox-chip.needs{background:#e0a020;color:#111}.inbox-chip.done{background:#2e7d46;color:#fff}.inbox-chip.working{background:#2f6fed;color:#fff;animation:tw-blink 1s ease-in-out infinite}@keyframes tw-blink{50%{opacity:.4}}@media (prefers-reduced-motion: reduce){.inbox-chip.working{animation:none}}.inbox-head{display:flex;align-items:center;gap:8px;padding:2px 8px 8px;font-size:13px;color:#9aa0a6}.inbox-summary{display:flex;gap:4px;margin-left:auto}.inbox-summary .inbox-chip{animation:none;font-size:10px;padding:1px 5px}.inbox-readall{background:none;border:none;color:#4aa3ff;font-size:13px;padding:4px 6px;touch-action:manipulation}.inbox-loc{font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inbox-time{margin-left:auto;font-size:11px;color:#888;flex:none}.inbox-msg{font-size:12px;color:#9aa0a6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inbox-empty{padding:24px 8px;text-align:center;color:#888;font-size:13px}.inbox-enable{display:flex;flex-direction:column;gap:8px;align-items:stretch;text-align:left}.inbox-enable-title{font-weight:600;color:#e8eaed;font-size:14px}.inbox-enable-hint{color:#9aa0a6;font-size:12px;line-height:1.4}.inbox-enable-btn{margin-top:2px;padding:8px 12px;border:none;border-radius:8px;background:#2f6fed;color:#fff;font-size:13px;touch-action:manipulation}.inbox-enable-btn:disabled{opacity:.6}.inbox-enable-err{color:#e06b5e;font-size:12px}
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,32 @@
1
+ <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="0" y2="512" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0" stop-color="#16180f"/><stop offset="1" stop-color="#080907"/></linearGradient>
5
+ <radialGradient id="glow" cx="230" cy="262" r="250" gradientUnits="userSpaceOnUse">
6
+ <stop offset="0" stop-color="#74e6a0" stop-opacity="0.26"/><stop offset="1" stop-color="#74e6a0" stop-opacity="0"/></radialGradient>
7
+ <linearGradient id="body" x1="150" y1="120" x2="300" y2="420" gradientUnits="userSpaceOnUse">
8
+ <stop offset="0" stop-color="#aef9c8"/><stop offset="1" stop-color="#46d6a2"/></linearGradient>
9
+ <linearGradient id="a1" x1="300" y1="190" x2="372" y2="262" gradientUnits="userSpaceOnUse">
10
+ <stop offset="0" stop-color="#7fe9b8" stop-opacity="0.95"/><stop offset="1" stop-color="#7fe9b8" stop-opacity="0.12"/></linearGradient>
11
+ <linearGradient id="a2" x1="296" y1="150" x2="406" y2="262" gradientUnits="userSpaceOnUse">
12
+ <stop offset="0" stop-color="#6fe2ac" stop-opacity="0.7"/><stop offset="1" stop-color="#6fe2ac" stop-opacity="0.05"/></linearGradient>
13
+ <linearGradient id="a3" x1="292" y1="112" x2="446" y2="262" gradientUnits="userSpaceOnUse">
14
+ <stop offset="0" stop-color="#62dca4" stop-opacity="0.45"/><stop offset="1" stop-color="#62dca4" stop-opacity="0"/></linearGradient>
15
+ <filter id="soft" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="7" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
16
+ </defs>
17
+ <rect width="512" height="512" rx="116" fill="url(#bg)"/>
18
+ <rect width="512" height="512" rx="116" fill="url(#glow)"/>
19
+ <g transform="rotate(-7 221 281)">
20
+ <g fill="none" stroke-linecap="round">
21
+ <path d="M300 190 a74 74 0 0 1 70 70" stroke="url(#a1)" stroke-width="15"/>
22
+ <path d="M296 148 a118 118 0 0 1 112 112" stroke="url(#a2)" stroke-width="14"/>
23
+ <path d="M292 108 a160 160 0 0 1 154 154" stroke="url(#a3)" stroke-width="13"/>
24
+ </g>
25
+ <g filter="url(#soft)"><rect x="146" y="150" width="150" height="262" rx="36" fill="url(#body)"/></g>
26
+ <rect x="163" y="184" width="116" height="194" rx="18" fill="#0a0b09"/>
27
+ <rect x="194" y="146" width="30" height="6" rx="3" fill="#0a0b09" opacity="0.45"/>
28
+ <rect x="206" y="396" width="30" height="6" rx="3" fill="#0a0b09" opacity="0.45"/>
29
+ <polyline points="188,236 212,266 188,296" fill="none" stroke="#aef9c8" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
30
+ <rect x="224" y="252" width="15" height="32" rx="3" fill="#aef9c8"/>
31
+ </g>
32
+ </svg>
@@ -0,0 +1,105 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
6
+ <title>handmux</title>
7
+
8
+ <!-- Installable PWA: "Add to Home Screen" + standalone (no browser address bar). -->
9
+ <link rel="manifest" href="/manifest.webmanifest" />
10
+ <meta name="theme-color" content="#1a1b1e" />
11
+ <link rel="icon" type="image/png" href="/icons/icon-192.png" />
12
+ <!-- iOS Safari ignores the manifest's display mode, so these drive standalone + the
13
+ home-screen icon/label. "black" keeps the white status-bar text readable on the dark topbar. -->
14
+ <meta name="apple-mobile-web-app-capable" content="yes" />
15
+ <meta name="mobile-web-app-capable" content="yes" />
16
+ <meta name="apple-mobile-web-app-status-bar-style" content="black" />
17
+ <meta name="apple-mobile-web-app-title" content="handmux" />
18
+ <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
19
+
20
+ <!-- Inline boot splash: paints instantly (before the JS bundle loads) so a cold launch shows the
21
+ brand, not a blank dark screen. main.jsx calls window.__hideBootSplash() once React has painted;
22
+ a safety timer hides it even if the bundle never boots. Self-contained here — no app code. -->
23
+ <style>
24
+ html, body { margin: 0; background: #1a1b1e; } /* opaque dark from frame one — no wallpaper showing through */
25
+ #boot-splash { position: fixed; inset: 0; z-index: 9999; display: flex; flex-direction: column;
26
+ align-items: center; justify-content: center; gap: 22px; background: #1a1b1e;
27
+ transition: opacity .45s ease; }
28
+ #boot-splash.boot-hide { opacity: 0; pointer-events: none; }
29
+ .boot-mark { position: relative; width: 108px; height: 108px; display: flex;
30
+ align-items: center; justify-content: center; }
31
+ .boot-mark svg { width: 108px; height: 108px; border-radius: 24px; position: relative; z-index: 1; }
32
+ .boot-glow { position: absolute; inset: -42%; border-radius: 50%;
33
+ background: radial-gradient(circle, rgba(116,230,160,.42) 0%, rgba(116,230,160,0) 68%);
34
+ animation: bootGlow 2.4s ease-in-out infinite; }
35
+ @keyframes bootGlow { 0%,100% { transform: scale(.9); opacity: .5; } 50% { transform: scale(1.14); opacity: .95; } }
36
+ .boot-word { font: 600 22px/1 -apple-system, system-ui, sans-serif; letter-spacing: .5px; color: #e9efe9; }
37
+ .boot-dots { display: flex; gap: 7px; }
38
+ .boot-dots i { width: 6px; height: 6px; border-radius: 50%; background: #6fe2ac; opacity: .22;
39
+ animation: bootDot 1.3s ease-in-out infinite; }
40
+ .boot-dots i:nth-child(2) { animation-delay: .18s; }
41
+ .boot-dots i:nth-child(3) { animation-delay: .36s; }
42
+ @keyframes bootDot { 0%,100% { opacity: .22; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
43
+ @media (prefers-reduced-motion: reduce) { .boot-glow, .boot-dots i { animation: none; opacity: .8; } }
44
+ </style>
45
+ <script type="module" crossorigin src="/assets/index-CNEiBm9y.js"></script>
46
+ <link rel="stylesheet" crossorigin href="/assets/index-CSX2d9C7.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" crossorigin href="/assets/index-CSX2d9C7.css"></noscript>
47
+ </head>
48
+ <body>
49
+ <div id="boot-splash" aria-hidden="true">
50
+ <div class="boot-mark">
51
+ <span class="boot-glow"></span>
52
+ <svg viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
53
+ <defs>
54
+ <linearGradient id="bsbg" x1="0" y1="0" x2="0" y2="512" gradientUnits="userSpaceOnUse">
55
+ <stop offset="0" stop-color="#16180f"/><stop offset="1" stop-color="#080907"/></linearGradient>
56
+ <radialGradient id="bsglow" cx="230" cy="262" r="250" gradientUnits="userSpaceOnUse">
57
+ <stop offset="0" stop-color="#74e6a0" stop-opacity="0.26"/><stop offset="1" stop-color="#74e6a0" stop-opacity="0"/></radialGradient>
58
+ <linearGradient id="bsbody" x1="150" y1="120" x2="300" y2="420" gradientUnits="userSpaceOnUse">
59
+ <stop offset="0" stop-color="#aef9c8"/><stop offset="1" stop-color="#46d6a2"/></linearGradient>
60
+ <linearGradient id="bsa1" x1="300" y1="190" x2="372" y2="262" gradientUnits="userSpaceOnUse">
61
+ <stop offset="0" stop-color="#7fe9b8" stop-opacity="0.95"/><stop offset="1" stop-color="#7fe9b8" stop-opacity="0.12"/></linearGradient>
62
+ <linearGradient id="bsa2" x1="296" y1="150" x2="406" y2="262" gradientUnits="userSpaceOnUse">
63
+ <stop offset="0" stop-color="#6fe2ac" stop-opacity="0.7"/><stop offset="1" stop-color="#6fe2ac" stop-opacity="0.05"/></linearGradient>
64
+ <linearGradient id="bsa3" x1="292" y1="112" x2="446" y2="262" gradientUnits="userSpaceOnUse">
65
+ <stop offset="0" stop-color="#62dca4" stop-opacity="0.45"/><stop offset="1" stop-color="#62dca4" stop-opacity="0"/></linearGradient>
66
+ <filter id="bssoft" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="7" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
67
+ </defs>
68
+ <rect width="512" height="512" rx="116" fill="url(#bsbg)"/>
69
+ <rect width="512" height="512" rx="116" fill="url(#bsglow)"/>
70
+ <g transform="rotate(-7 221 281)">
71
+ <g fill="none" stroke-linecap="round">
72
+ <path d="M300 190 a74 74 0 0 1 70 70" stroke="url(#bsa1)" stroke-width="15"/>
73
+ <path d="M296 148 a118 118 0 0 1 112 112" stroke="url(#bsa2)" stroke-width="14"/>
74
+ <path d="M292 108 a160 160 0 0 1 154 154" stroke="url(#bsa3)" stroke-width="13"/>
75
+ </g>
76
+ <g filter="url(#bssoft)"><rect x="146" y="150" width="150" height="262" rx="36" fill="url(#bsbody)"/></g>
77
+ <rect x="163" y="184" width="116" height="194" rx="18" fill="#0a0b09"/>
78
+ <rect x="194" y="146" width="30" height="6" rx="3" fill="#0a0b09" opacity="0.45"/>
79
+ <rect x="206" y="396" width="30" height="6" rx="3" fill="#0a0b09" opacity="0.45"/>
80
+ <polyline points="188,236 212,266 188,296" fill="none" stroke="#aef9c8" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
81
+ <rect x="224" y="252" width="15" height="32" rx="3" fill="#aef9c8"/>
82
+ </g>
83
+ </svg>
84
+ </div>
85
+ <div class="boot-word">handmux</div>
86
+ <div class="boot-dots"><i></i><i></i><i></i></div>
87
+ </div>
88
+ <div id="root"></div>
89
+ <script>
90
+ // Hide the splash once the app has painted (main.jsx), but keep it up at least ~650ms so the glow
91
+ // is actually seen, not a flicker. Safety: hide after 8s no matter what, so a failed boot never traps it.
92
+ window.__bootStart = Date.now();
93
+ window.__hideBootSplash = function () {
94
+ var el = document.getElementById('boot-splash');
95
+ if (!el || el.classList.contains('boot-hide')) return;
96
+ var wait = Math.max(0, 650 - (Date.now() - (window.__bootStart || 0)));
97
+ setTimeout(function () {
98
+ el.classList.add('boot-hide');
99
+ setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 500);
100
+ }, wait);
101
+ };
102
+ setTimeout(function () { window.__hideBootSplash(); }, 8000);
103
+ </script>
104
+ </body>
105
+ </html>
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "handmux",
3
+ "short_name": "handmux",
4
+ "description": "Mobile vibe coding — the same live tmux session on your computer and your phone.",
5
+ "lang": "en",
6
+ "start_url": ".",
7
+ "scope": ".",
8
+ "display": "standalone",
9
+ "background_color": "#1a1b1e",
10
+ "theme_color": "#1a1b1e",
11
+ "icons": [
12
+ { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
13
+ { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
14
+ ],
15
+ "share_target": {
16
+ "action": "/share-target",
17
+ "method": "POST",
18
+ "enctype": "multipart/form-data",
19
+ "params": {
20
+ "files": [
21
+ {
22
+ "name": "file",
23
+ "accept": [
24
+ "image/*", "text/*", "application/pdf", "application/json",
25
+ "application/msword",
26
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
27
+ "application/vnd.ms-excel",
28
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
29
+ "application/vnd.ms-powerpoint",
30
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
31
+ ".md", ".markdown", ".csv", ".log"
32
+ ]
33
+ }
34
+ ]
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,50 @@
1
+ <!doctype html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
6
+ <title>tmux web</title>
7
+ <!-- Self-contained: inline CSS + inline JS, NO external resources, so this page is immutable and
8
+ works with no network. The service worker caches it once and serves it when a navigation
9
+ can't reach the network (replacing the browser's own ERR_NETWORK_CHANGED error page). -->
10
+ <style>
11
+ html, body {
12
+ margin: 0; height: 100%; background: #1a1b1e; color: #9aa0a6;
13
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
14
+ display: flex; align-items: center; justify-content: center;
15
+ }
16
+ .box { text-align: center; }
17
+ .dots { display: flex; gap: 6px; justify-content: center; }
18
+ .dot {
19
+ width: 8px; height: 8px; border-radius: 50%; background: #4a7cff;
20
+ opacity: 0.3; animation: blink 1.2s infinite;
21
+ }
22
+ .dot:nth-child(2) { animation-delay: 0.2s; }
23
+ .dot:nth-child(3) { animation-delay: 0.4s; }
24
+ @keyframes blink { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
25
+ p { margin-top: 16px; font-size: 14px; letter-spacing: 0.05em; }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div class="box">
30
+ <div class="dots"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>
31
+ <p>连接中…</p>
32
+ </div>
33
+ <script>
34
+ // We are the SW's offline fallback. Probe connectivity and reload into the real app the moment
35
+ // the network is back. The probe is a normal fetch (mode !== 'navigate'), so the SW won't
36
+ // intercept it — it goes straight to the network. Reloading while still offline just re-shows
37
+ // this page (the browser never gets to show its own error page).
38
+ (function () {
39
+ function probe() {
40
+ fetch('/', { cache: 'no-store' })
41
+ .then(function (r) { if (r && r.ok) location.reload(); })
42
+ .catch(function () { /* still offline — keep waiting */ });
43
+ }
44
+ setInterval(probe, 2000);
45
+ addEventListener('online', probe);
46
+ probe();
47
+ })();
48
+ </script>
49
+ </body>
50
+ </html>
package/public/sw.js ADDED
@@ -0,0 +1,117 @@
1
+ // handmux service worker — single purpose: replace the browser's own cold-launch network-error
2
+ // page (e.g. Chromium's ERR_NETWORK_CHANGED white screen) with our offline fallback, then let the
3
+ // page auto-retry into the real app once the network is ready. It caches EXACTLY one immutable file
4
+ // (offline.html) and NEVER caches app code or /api — every launch fetches the latest app from the
5
+ // network, so deploys always take effect (no stale-shell trap).
6
+ // See docs/superpowers/specs/2026-06-04-cold-launch-offline-shell-design.md
7
+ const CACHE = 'tmw-offline-v1';
8
+ const OFFLINE_URL = '/offline.html';
9
+ // Web Share Target intake: a shared file arrives as a POST navigation to /share-target. We can't hand
10
+ // a File to the page across the redirect, so we stash it in this cache; the page consumes it on boot
11
+ // (see src/shareIntake.js — these two constants MUST match its SHARE_CACHE / SHARE_PREFIX).
12
+ const SHARE_CACHE = 'tmw-share-v1';
13
+ const SHARE_PREFIX = '/__share__/';
14
+ const SHARE_ACTION = '/share-target';
15
+
16
+ self.addEventListener('install', (event) => {
17
+ // skipWaiting is chained AFTER the cache write so we never activate before offline.html is cached.
18
+ event.waitUntil(
19
+ caches.open(CACHE).then((c) => c.add(OFFLINE_URL)).then(() => self.skipWaiting()),
20
+ );
21
+ });
22
+
23
+ self.addEventListener('activate', (event) => {
24
+ // Keep both our caches: the offline shell AND any pending share (a file shared just before an SW
25
+ // update must survive until the page consumes it). Drop everything else.
26
+ const KEEP = new Set([CACHE, SHARE_CACHE]);
27
+ event.waitUntil(
28
+ caches.keys()
29
+ .then((keys) => Promise.all(keys.filter((k) => !KEEP.has(k)).map((k) => caches.delete(k))))
30
+ .then(() => self.clients.claim()),
31
+ );
32
+ });
33
+
34
+ // Stash a shared file, then redirect into the app with ?share so it knows to pick it up. The File
35
+ // can't ride the redirect, so we cache it under SHARE_PREFIX+name (the name rides the key; the type
36
+ // rides the cached Response's Content-Type). Failures still redirect — the page just finds nothing.
37
+ async function handleShareTarget(request) {
38
+ try {
39
+ const form = await request.formData();
40
+ const files = form.getAll('file').filter((f) => f && typeof f.name === 'string');
41
+ if (files.length) {
42
+ const cache = await caches.open(SHARE_CACHE);
43
+ for (const k of await cache.keys()) await cache.delete(k); // keep only the latest share
44
+ const f = files[0];
45
+ await cache.put(
46
+ SHARE_PREFIX + encodeURIComponent(f.name || 'shared'),
47
+ new Response(f, { headers: { 'Content-Type': f.type || 'application/octet-stream' } }),
48
+ );
49
+ }
50
+ } catch { /* fall through to the redirect; the page finds no pending file and carries on */ }
51
+ return Response.redirect('/?share=1', 303);
52
+ }
53
+
54
+ self.addEventListener('fetch', (event) => {
55
+ const req = event.request;
56
+ // Web Share Target POST — intercept before the navigation guard below (a share POST IS a navigate).
57
+ if (req.method === 'POST' && new URL(req.url).pathname === SHARE_ACTION) {
58
+ event.respondWith(handleShareTarget(req));
59
+ return;
60
+ }
61
+ // Only guard top-level navigations. Everything else (JS/CSS/fonts/icons and /api) falls through
62
+ // to the browser's default network handling — the SW neither caches nor intercepts it.
63
+ if (req.mode !== 'navigate') return;
64
+ // Network-first: a normal launch gets the freshest index.html; only when the fetch REJECTS (the
65
+ // radio-wakeup transient that makes the browser show its error page) do we serve the offline page.
66
+ event.respondWith(fetch(req).catch(() => caches.match(OFFLINE_URL)));
67
+ });
68
+
69
+ // --- Web Push ---------------------------------------------------------------------------------
70
+ // Show a notification for each push. `tag` collapses repeats: a second push with the same tag
71
+ // REPLACES the visible notification instead of stacking, so a given pane never shows a pile of
72
+ // stale alerts. iOS requires us to actually show something on every push, which we always do.
73
+ self.addEventListener('push', (event) => {
74
+ let d = {};
75
+ try { d = event.data ? event.data.json() : {}; }
76
+ catch { d = { body: event.data ? event.data.text() : '' }; }
77
+ event.waitUntil(self.registration.showNotification(d.title || 'handmux', {
78
+ body: d.body || '',
79
+ tag: d.tag || 'handmux',
80
+ renotify: false,
81
+ // icon = the large right-side logo. We MUST set it: HyperOS/MIUI always reserves that slot, and an
82
+ // empty `icon` makes it back-fill the slot with the PWA's own icon at a stray size (the `>` reads as
83
+ // a lone "v") plus leaves a gap. An explicit app icon fills the slot cleanly.
84
+ icon: '/icons/icon-192.png',
85
+ // badge = the small monochrome status-bar icon. Android reads only its ALPHA and tints the
86
+ // silhouette; an opaque image (icon-192 has no alpha) can't be used, so it falls back to the
87
+ // browser's own Chrome logo. badge-96.png is a white `>▮` silhouette on transparent → our mark.
88
+ badge: '/icons/badge-96.png',
89
+ data: d.data || {},
90
+ }));
91
+ });
92
+
93
+ // Tapping the notification deep-links to the pane: focus an open client (and tell it where to go),
94
+ // or open a new window at the deep-link hash.
95
+ self.addEventListener('notificationclick', (event) => {
96
+ event.notification.close();
97
+ const d = event.notification.data || {};
98
+ const e = encodeURIComponent;
99
+ const url = d.session
100
+ ? `/#/s/${e(d.session)}/w/${e(d.window || '')}/p/${e(d.pane || '')}`
101
+ : '/';
102
+ event.waitUntil((async () => {
103
+ const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
104
+ const open = all.find((c) => 'focus' in c);
105
+ if (open) {
106
+ await open.focus();
107
+ // Prefer navigate(): it changes the client URL, so the target survives even a backgrounded tab
108
+ // the browser discarded (it reloads into the deep-link hash, which the boot effect reads).
109
+ // postMessage is a fallback for engines without WindowClient.navigate. URL format MUST match
110
+ // hashRoute.readRoute()/buildDeepLink.
111
+ if ('navigate' in open) { try { await open.navigate(url); return; } catch { /* fall back ↓ */ } }
112
+ open.postMessage({ type: 'navigate', session: d.session, window: d.window, pane: d.pane });
113
+ return;
114
+ }
115
+ return self.clients.openWindow(url);
116
+ })());
117
+ });
package/src/.gitkeep ADDED
File without changes
package/src/appName.js ADDED
@@ -0,0 +1,23 @@
1
+ // Runtime app-name injection. The web bundle ships prebuilt, so a user's custom name (set at
2
+ // `handmux start --name`) can't be baked at build time — instead the server rewrites the name into
3
+ // the shell HTML and the PWA manifest as it serves them. Pure string/object transforms so they're
4
+ // unit-testable; the server side-effects (read file / send) live in server.js.
5
+
6
+ const escAttr = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;')
7
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
8
+
9
+ // Replace the browser-tab title and the iOS home-screen label in index.html. Matches by tag/attr,
10
+ // not by the default text, so it survives a changed default. No-op when a tag is absent.
11
+ export function applyAppName(html, name) {
12
+ if (!name) return html;
13
+ const e = escAttr(name);
14
+ return html
15
+ .replace(/<title>[^<]*<\/title>/, `<title>${e}</title>`)
16
+ .replace(/(<meta name="apple-mobile-web-app-title" content=")[^"]*(")/, `$1${e}$2`);
17
+ }
18
+
19
+ // Override the PWA install name (Android uses short_name; both set so home-screen + app list agree).
20
+ export function applyManifestName(manifest, name) {
21
+ if (!name) return manifest;
22
+ return { ...manifest, name, short_name: name };
23
+ }
@@ -0,0 +1,10 @@
1
+ // server/src/asr/iflyConfig.js
2
+ // Read iFlytek IAT credentials lazily from env (default process.env) so it's injectable in tests.
3
+ // app_id is a public identifier (safe to hand the browser); apiKey/apiSecret never leave the server.
4
+ export function asrConfig(env = process.env) {
5
+ return { appId: env.XFYUN_APPID || '', apiKey: env.XFYUN_APIKEY || '', apiSecret: env.XFYUN_APISECRET || '' };
6
+ }
7
+ export function isAsrConfigured(env = process.env) {
8
+ const c = asrConfig(env);
9
+ return !!(c.appId && c.apiKey && c.apiSecret);
10
+ }
@@ -0,0 +1,16 @@
1
+ import { createHmac } from 'node:crypto';
2
+
3
+ // Build a fully-signed iFlytek IAT v2 WebSocket URL. The secret (apiSecret) is used only here on the
4
+ // server — the browser receives only the resulting URL (HMAC output, not the key). `host`/`date` are
5
+ // injectable so this is deterministic and unit-testable; production passes host='iat-api.xfyun.cn'
6
+ // and date=new Date().toUTCString() (RFC1123 GMT, iFlytek allows ±300s skew).
7
+ export function buildIatSignedUrl({ appId, apiKey, apiSecret, host = 'iat-api.xfyun.cn', date }) {
8
+ const requestLine = 'GET /v2/iat HTTP/1.1';
9
+ const signatureOrigin = `host: ${host}\ndate: ${date}\n${requestLine}`;
10
+ const signature = createHmac('sha256', apiSecret).update(signatureOrigin).digest('base64');
11
+ const authorizationOrigin =
12
+ `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
13
+ const authorization = Buffer.from(authorizationOrigin, 'utf8').toString('base64');
14
+ const qs = new URLSearchParams({ authorization, date, host });
15
+ return { url: `wss://${host}/v2/iat?${qs.toString()}`, appId };
16
+ }
package/src/auth.js ADDED
@@ -0,0 +1,30 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ export function loadToken(env = process.env) {
4
+ let token = env.HANDMUX_TOKEN;
5
+ if (!token) {
6
+ token = crypto.randomBytes(24).toString('base64url');
7
+ console.log(`[handmux] no HANDMUX_TOKEN set; generated token: ${token}`);
8
+ }
9
+ return token;
10
+ }
11
+
12
+ const sha = (s) => crypto.createHash('sha256').update(String(s)).digest();
13
+
14
+ export function tokenEquals(a, b) {
15
+ return crypto.timingSafeEqual(sha(a), sha(b));
16
+ }
17
+
18
+ export function bearerFrom(header) {
19
+ if (!header) return null;
20
+ const m = /^Bearer (.+)$/.exec(header);
21
+ return m ? m[1] : null;
22
+ }
23
+
24
+ export function expressAuth(token) {
25
+ return (req, res, next) => {
26
+ const provided = bearerFrom(req.get('authorization'));
27
+ if (provided && tokenEquals(provided, token)) return next();
28
+ res.status(401).json({ error: 'unauthorized' });
29
+ };
30
+ }