handmux 0.14.0 → 0.16.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.
@@ -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}:root{--blue: #0a84ff;--blue-press: #0a6fd8;--blue-rgb: 10,132,255;--green: #34c759;--green-rgb: 52,199,89;--red: #ff453a}@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:6px;padding:4px 8px;background:#2a2b2e}.hamburger{font-size:20px;background:none;border:none;color:#e6e6e6;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;padding:0;margin-left:-4px;touch-action:manipulation}.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:8px;background:#33353a;color:#e6e6e6;font-size:14px;touch-action:manipulation}.fontbtn[aria-pressed=true]{background:#2a4060;color:#7ab4f5}a.settings-linkbtn{display:inline-flex;align-items:center;justify-content:center;text-decoration:none;box-sizing:border-box}.topbar-icon{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;padding:0;border:none;background:none;color:#e6e6e6;touch-action:manipulation}.topbar-icon:active{color:var(--blue)}.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;max-height:calc(100vh - 40px);max-height:calc(100dvh - 40px);display:flex;flex-direction:column;background:#232427;border-radius:12px;padding:16px;box-shadow:0 10px 40px #00000080}.settings-head{flex:none;display:flex;align-items:center;margin-bottom:12px}.settings-body{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;margin:0 -16px;padding:0 16px}.settings-title{font-size:16px;font-weight:600;margin-right:auto}.settings-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:none;border-radius:50%;background:#78788029;color:#ebebf58c;font-size:15px;line-height:1;touch-action:manipulation}.settings-close:active{background:#7878804d;color:#fff}.settings-close svg{width:14px;height:14px;display:block}.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}.settings-toggle{display:flex;align-items:center;justify-content:space-between;gap:12px}.settings-toggle .settings-label{margin-bottom:0}.a2hs-banner{position:fixed;z-index:60;left:8px;right:8px;top:calc(env(safe-area-inset-top,0px) + 8px);display:flex;align-items:center;gap:10px;padding:9px 10px 9px 12px;background:#1e2026eb;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);border:1px solid rgba(255,255,255,.09);border-radius:14px;box-shadow:0 8px 26px #00000061;animation:a2hs-in .28s ease}@keyframes a2hs-in{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:none}}@media (prefers-reduced-motion: reduce){.a2hs-banner{animation:none}}.a2hs-banner-icon{flex:none;font-size:19px;line-height:1;color:#7aa2f0}.a2hs-banner-txt{flex:1;min-width:0;display:grid;gap:2px}.a2hs-banner-title{font-size:13.5px;font-weight:600;color:#f2f3f5}.a2hs-banner-sub{font-size:12px;line-height:1.4;color:#b9bcc4}.a2hs-banner-cta{flex:none;padding:7px 13px;border:none;border-radius:9px;background:#2f6ad9;color:#fff;font-size:13px;font-weight:600;touch-action:manipulation}.a2hs-banner-cta:active{filter:brightness(1.12)}.a2hs-banner-x{flex:none;width:30px;height:30px;padding:0;border:none;background:transparent;color:#9aa0aa;font-size:15px;touch-action:manipulation}.a2hs-banner-x:active{color:#f2f3f5}.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:8px;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-update{margin-top:10px;padding:8px 10px;border-radius:8px;background:rgba(var(--blue-rgb),.12);border:.5px solid rgba(var(--blue-rgb),.35)}.settings-update-title{font-size:14px;color:var(--blue);font-weight:600}.settings-update-new{list-style:none;margin:6px 0 0;padding:0}.settings-update-new li{font-size:12.5px;color:#d6d9de;line-height:1.5;margin:2px 0}.settings-update-new-ver{color:#9db8ff;font-weight:700;font-variant-numeric:tabular-nums;margin-right:6px}.settings-update-how{font-size:12px;color:#9aa0a8;line-height:1.5;margin-top:6px}.settings-update-how code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:#e6e8ec;background:#78788038;padding:1px 5px;border-radius:5px}.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:8px;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:8px;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:var(--blue);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:8px;background:none;color:#e6e6e6;font-size:14px;text-align:left;touch-action:manipulation}.dd-option:active{background:#33353a}.dd-option.is-selected{color:var(--green)}.dd-check{flex:none;color:var(--green);font-size:13px}.bind-error{margin-top:8px;color:var(--red);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:var(--blue);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:8px;background:#33353a;color:#bbb;font-size:13px;touch-action:manipulation}.win-tab.active{background:var(--blue);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-trigger .agent-mark{margin-right:0}.wt-sep{margin:0 -1px;color:#cfe0ff;opacity:.4;font-weight:400}.wt-name,.wt-pane{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wt-name{max-width:9ch}.wt-pane{font-weight:600;max-width:11ch}.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}.wt-menu .dd-option-label{display:inline-flex;align-items:center;gap:8px;min-width:0}.wt-menu .dd-pane-seq{flex:none;opacity:.55;font-variant-numeric:tabular-nums}.wt-menu .dd-pane-cmd{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pane-map{position:fixed;overflow:hidden;z-index:30;background:#181a1ec7;-webkit-backdrop-filter:blur(20px) saturate(1.4);backdrop-filter:blur(20px) saturate(1.4);border:1px solid rgba(255,255,255,.14);border-radius:14px;box-shadow:0 12px 34px #0000008c,inset 0 1px #ffffff0d;transform-origin:top;animation:dd-pop .12s ease}.pane-map-cell{position:absolute;padding:0;border:0;background:none;color:inherit;font:inherit;box-sizing:border-box;-webkit-tap-highlight-color:transparent}.pane-map-cell .pmc-surf{position:absolute;top:3px;right:3px;bottom:3px;left:3px;border-radius:8px;overflow:hidden;display:flex;flex-direction:column;gap:3px;align-items:flex-start;justify-content:flex-start;padding:7px 8px;text-align:left;background:#ffffff0b;border:1px solid rgba(255,255,255,.1);transition:transform .12s ease,background .18s ease,border-color .18s ease,box-shadow .18s ease}.pane-map-cell:active .pmc-surf{transform:scale(.97)}.pane-map-cell .pmc-seq,.pane-map-cell .pmc-cmd{transition:color .18s ease}.pane-map-cell.is-current .pmc-surf{background:var(--blue);border-color:#ffffff40;box-shadow:0 2px 10px #0a84ff66}.pane-map-cell.is-picking .pmc-surf{background:var(--blue);border-color:#fff6;box-shadow:0 0 0 2px #0a84ff80,0 3px 14px #0a84ff8c;animation:pmc-pick .22s ease}.pane-map-cell.is-picking .pmc-seq{color:#fffffff2}.pane-map-cell.is-picking .pmc-cmd{color:#fff}@keyframes pmc-pick{0%{transform:scale(1)}45%{transform:scale(1.09)}to{transform:scale(1)}}.pane-map-cell.is-current.is-releasing .pmc-surf{background:#ffffff0b;border-color:#ffffff1a;box-shadow:none}.pane-map-cell.is-current.is-releasing .pmc-seq{color:#9aa0aa}.pane-map-cell.is-current.is-releasing .pmc-cmd{color:var(--ink, #e6e6e6)}.pane-map-cell .pmc-row{display:flex;align-items:center;gap:5px;max-width:100%}.pane-map-cell .pmc-seq{font-size:11px;font-weight:700;line-height:1;letter-spacing:.02em;color:#9aa0aa;font-variant-numeric:tabular-nums}.pane-map-cell.is-current .pmc-seq{color:#ffffffe6}.pane-map-cell .pmc-cmd{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:11.5px;line-height:1.25;color:var(--ink, #e6e6e6)}.pane-map-cell.is-current .pmc-cmd{color:#fff}.pane-map-cell .pmc-row .agent-mark{width:13px;height:13px;margin:0}.pane-map-cell.is-flat .pmc-surf{flex-direction:row;align-items:center;gap:6px;padding:0 8px}.pane-map-cell.is-narrow .pmc-surf{align-items:center;justify-content:center;padding:4px 2px}.pane-map-cell.is-narrow .pmc-seq{font-size:12px}.pane-map-cell.is-tiny .pmc-surf{align-items:center;justify-content:center;padding:0;gap:0}@media (prefers-reduced-motion: reduce){.pane-map{animation:none}.pane-map-cell .pmc-surf,.pane-map-cell.is-picking .pmc-surf{transition:none;animation:none}}.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-viewport{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.22) transparent}.terminal .xterm-viewport::-webkit-scrollbar{width:7px;height:7px}.terminal .xterm-viewport::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.terminal .xterm-viewport::-webkit-scrollbar-track{background:transparent}.terminal{scrollbar-width:none}.terminal::-webkit-scrollbar{height:0}.terminal .xterm-viewport::-webkit-scrollbar:horizontal{height:0}.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:var(--blue);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:calc(100% - var(--vis-h, 100%));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}.term-banner--sel{background:var(--blue);color:#fff}.sel-handle{position:absolute;z-index:8;width:2px;height:var(--h);background:var(--blue);touch-action:none}.sel-handle:after{content:"";position:absolute;left:50%;width:13px;height:13px;border-radius:50%;background:var(--blue);transform:translate(-50%)}.sel-handle--start:after{top:-11px}.sel-handle--end:after{bottom:-11px}.sel-handle:before{content:"";position:absolute;left:50%;top:50%;width:44px;height:44px;transform:translate(-50%,-50%)}.sel-callout{position:absolute;z-index:9;display:flex;align-items:stretch;width:max-content;border-radius:12px;overflow:hidden;background:#1c1e22eb;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);box-shadow:0 3px 14px #00000080;border:.5px solid rgba(255,255,255,.12)}.sel-callout button{border:none;background:none;color:#fff;font-size:15px;font-weight:500;padding:9px 16px;touch-action:manipulation;white-space:nowrap;flex:none}.sel-callout button+button{border-left:.5px solid rgba(255,255,255,.14)}.term-pager{position:absolute;z-index:6;right:calc(10px + env(safe-area-inset-right));top:calc(100% - var(--vis-h, 100%) / 2);transform:translateY(-50%);display:flex;flex-direction:column;align-items:stretch;gap:10px}.term-pager-grp{display:flex;flex-direction:column;align-items:stretch;border-radius:21px;overflow:hidden;background:#1c1e2285;-webkit-backdrop-filter:blur(16px) saturate(1.4);backdrop-filter:blur(16px) saturate(1.4);border:.5px solid rgba(255,255,255,.14);box-shadow:0 4px 18px #0000004d}.term-pager-btn{width:42px;height:42px;display:flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:#ffffffeb;touch-action:manipulation;-webkit-tap-highlight-color:transparent;transition:background .12s ease}.term-pager-btn:active{background:#ffffff24}.term-pager-btn[aria-pressed=true]{background:#7ab4f54d;color:#cfe4ff}.locate-deco{background:#7ab4f54d;box-shadow:inset 3px 0 #7ab4f5f2}.term-pager-div{height:.5px;background:#ffffff21;margin:0 9px}@media (prefers-reduced-motion: reduce){.term-pager-btn{transition:none}}.input-text{flex:1;min-width:0;padding:7px 6px 7px 11px;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}.input-text::placeholder{color:#6b7280}.token-prompt button{padding:8px 14px;border:none;border-radius:8px;background:var(--blue);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:8px;background:#2e2a2c;color:var(--red);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:8px}.drawer-name{flex:1;min-width:0;padding:8px 4px;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.drawer-session.active{background:var(--blue);color:#fff}.drawer-unbind{flex:none;background:none;border:none;color:#888;font-size:14px;padding:6px 8px;touch-action:manipulation;display:inline-flex;align-items:center;justify-content:center;min-width:40px;min-height:44px}.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:8px;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:8px;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:var(--blue)}.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}.dock-handle{position:relative;display:flex;justify-content:center;align-items:center;width:100%;padding:6px 0 4px;border:none;background:transparent;cursor:grab;-webkit-tap-highlight-color:transparent;touch-action:none;--morph: 0;--drag: 0px;--bar: #6a6f78}.dock-handle:active{cursor:grabbing}.dock-handle.armed{--bar: var(--blue)}.dock-dots{display:flex;align-items:center;justify-content:center;gap:7px;transform:translateY(var(--drag));transition:transform .28s cubic-bezier(.22,.61,.36,1)}.dock-handle.dragging .dock-dots{transition:none}.dock-dot{width:calc(6px + var(--morph) * 15px);height:6px;border-radius:999px;background:color-mix(in srgb,#4a4d54,var(--bar) calc(var(--morph) * 100%));transition:width .15s ease,background .15s ease}.dock-dot.on{background:color-mix(in srgb,var(--blue),var(--bar) calc(var(--morph) * 100%))}.dock-dot[data-page=command]{transform:translate(calc(var(--morph) * 7px))}.dock-dot[data-page=agent]{transform:translate(calc(var(--morph) * -7px))}.dock-handle:active:not(.dragging) .dock-dot{background:#5a5e66}.dock-handle:active:not(.dragging) .dock-dot.on{background:#6a93ff}.dock-mode-label{position:absolute;left:2px;top:50%;transform:translateY(-50%);font-size:10px;line-height:1;color:#8a93a0;white-space:nowrap;pointer-events:none}@media (prefers-reduced-motion: reduce){.dock-dots{transition:none}}.dock-pager{overflow:hidden;min-width:0;contain:paint}.dock-track{display:flex;align-items:flex-start;width:200%;will-change:transform;transform:translateZ(0);transition:transform .3s cubic-bezier(.22,.61,.36,1)}.dock-track.at-chat{transform:translate3d(-50%,0,0)}.dock-track>.dock-page{flex:0 0 50%;width:50%;min-width:0}.dock-page.command{position:relative}.keybar-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;min-width:0}.keybar-key{min-width:0;min-height:40px;padding:0 2px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:8px;white-space:nowrap;background:#33353a;color:#e6e6e6;font:14px/1 ui-monospace,SF Mono,Menlo,monospace;touch-action:manipulation;-webkit-user-select:none;user-select:none}.keybar-key:active{background:var(--blue);color:#fff}.keybar-key svg{width:20px;height:20px;display:block}.keybar-mod.armed{background:var(--blue);color:#fff}.keybar-mod.locked{background:var(--blue);color:#fff;box-shadow:inset 0 -3px #ffd24a}.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:4px;margin-top:6px;padding:5px 6px 5px 5px;border:1px solid #3a3d44;border-radius:20px;background:#202227;box-shadow:0 1px 6px #00000059;transition:border-color .15s,box-shadow .15s}.dock-page.chat{padding-bottom:6px}.input-wrap:focus-within{border-color:var(--green);box-shadow:0 0 0 1px rgba(var(--green-rgb),.4),0 2px 12px #0006}.dock-left.dock-swiping .input-wrap:focus-within{border-color:#3a3d44;box-shadow:0 1px 6px #00000059}.input-wrap.multi>.input-mic,.input-wrap.multi>.input-send{position:absolute;bottom:7px;margin:0;pointer-events:none}.input-wrap.multi>.input-send{right:6px}.input-wrap.multi>.input-mic{right:44px}.input-wrap.multi.crowd>.input-text{padding-bottom:29px}.input-mirror{position:absolute;left:-9999px;top:0;visibility:hidden;pointer-events:none;box-sizing:border-box;padding:7px 6px 7px 11px;border:none;font-family:inherit;font-size:16px;line-height:22px;white-space:pre-wrap;overflow-wrap:break-word}.input-wrap>.input-history,.input-wrap>.input-mic,.input-wrap>.input-send{margin-bottom:2px}.cmd-capture{position:absolute;left:0;bottom:0;width:2px;height:2px;padding:0;margin:0;border:0;opacity:0;background:transparent;color:transparent}.quick-bar{display:flex;align-items:stretch;gap:6px;padding:0 2px 6px}.quick-fixed{flex:none;display:flex;gap:6px;padding-right:7px;border-right:1px solid #33363d}.quick-fix{flex:none;display:inline-flex;align-items:center;gap:5px;border:1px solid #4a4f59;border-radius:9px;background:#383c44;color:#eaedf2;font:inherit;font-size:13.5px;font-weight:600;padding:7px 11px;line-height:1;touch-action:manipulation;-webkit-user-select:none;user-select:none}.quick-fix svg{width:15px;height:15px;display:block;color:#aeb6c2}.quick-fix:active{background:#454a54}.quick-fix:disabled{opacity:.4}.quick-fix-kbd svg{color:#d3dcec}.quick-fix-kbd[aria-pressed=true]{border-color:#3d63a8;background:#2c3849;color:#eaf1ff}.quick-fix-kbd[aria-pressed=true] svg{color:var(--blue)}.quick-scroll{flex:1;min-width:0;display:flex;align-items:stretch;gap:5px;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}.quick-scroll::-webkit-scrollbar{display:none}.quick-cmd{flex:none;display:inline-flex;align-items:center;white-space:nowrap;border:1px solid;border-radius:12px;color:rgb(var(--rgb));border-color:rgba(var(--rgb),.5);background:rgba(var(--rgb),.13);font:inherit;font-size:13.5px;padding:7px 12px;line-height:1;touch-action:manipulation;-webkit-user-select:none;user-select:none}.quick-cmd:active{background:rgba(var(--rgb),.26);border-color:rgb(var(--rgb))}.quick-cmd-add{--rgb: 150,157,167;border-style:dashed;padding:7px 11px}.quick-cmd-add svg{width:15px;height:15px;display:block}.qc-enter{margin-left:5px;opacity:.7;font-size:12px}.qc-esc{--rgb: 150,157,167}.qc-reply{--rgb: 95,208,138}.qc-cmd{--rgb: 110,168,255}.quick-cmd-plain{--rgb: 150,157,167}.quick-cmd-win{--rgb: 95,208,138}.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:var(--red)}.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}.upload-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;display:grid;place-items:center;background:#0000009e;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);padding:24px;padding-bottom:calc(24px + env(safe-area-inset-bottom))}.upload-card{width:min(340px,86vw);background:#141b24;border:1px solid #24303d;border-radius:14px;padding:22px 20px 18px;box-shadow:0 24px 70px #0009;text-align:center}.upload-card-label{color:#e6edf3;font-size:14px;margin-bottom:16px;word-break:break-all;font-variant-numeric:tabular-nums}.upload-bar{height:6px;border-radius:3px;background:#1c2b3a;overflow:hidden}.upload-bar-fill{height:100%;background:var(--green);transition:width .12s linear}.upload-card-sub{color:#9fb6d0;font-size:12px;margin-top:10px;font-variant-numeric:tabular-nums}.upload-spinner{width:34px;height:34px;margin:4px auto 0;border-radius:50%;border:3px solid #24303d;border-top-color:var(--green);animation:upload-spin .8s linear infinite}@keyframes upload-spin{to{transform:rotate(360deg)}}.upload-cancel{margin-top:18px;width:100%;height:40px;border:1px solid #33424f;border-radius:10px;background:#1c2530;color:#e6edf3;font-size:14px}.upload-cancel:active{background:#243040}@media (prefers-reduced-motion: reduce){.upload-spinner{animation-duration:1.6s}}.input-send{flex:none;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:var(--blue);color:#fff;padding:0;box-shadow:0 1px 4px rgba(var(--blue-rgb),.45);touch-action:manipulation;-webkit-user-select:none;user-select:none}.input-send:active{background:var(--blue-press)}.input-send:disabled{background:#00000040;color:#b6c0cc;box-shadow:none}.input-send svg{width:20px;height:20px;display:block}.input-history{flex:none;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:8px;background:transparent;color:#8a93a0;padding:0;touch-action:manipulation;-webkit-user-select:none;user-select:none}.input-history svg{width:20px;height:20px;display:block}.input-history:active{color:var(--blue)}.input-wrap.recording{background:rgba(var(--green-rgb),.14);border:2px solid var(--green);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 rgba(var(--green-rgb),.3);border-color:#2fd07e}50%{box-shadow:0 0 16px 4px rgba(var(--green-rgb),.8);border-color:var(--green)}}.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:var(--green)}.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:16px 16px 0 0;box-shadow:0 -8px 30px #00000080}.cmd-panel.history{min-height:40vh}.cmd-head{display:flex;align-items:center;gap:8px;padding:12px 14px;border-bottom:1px solid #33353a}.cmd-title{font-size:16px;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:8px;padding:1px 6px}.cmd-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:none;border-radius:50%;background:#78788029;color:#ebebf58c;font-size:15px;line-height:1;touch-action:manipulation}.cmd-close:active{background:#7878804d;color:#fff}.cmd-close svg{width:14px;height:14px;display:block}.cmd-add-open{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;border-radius:9px;background:var(--blue);color:#fff;padding:0;touch-action:manipulation;box-shadow:0 1px 4px rgba(var(--blue-rgb),.4)}.cmd-add-open:active{background:var(--blue-press)}.cmd-add-open svg{width:18px;height:18px;display:block}.cmd-list{overflow-y:auto;-webkit-overflow-scrolling:touch;padding:6px 0 10px}.cmd-editor .cmd-list{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden;padding:0}.cmd-editor .cmd-esection{flex:1 1 0;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;display:flex;flex-direction:column}.cmd-editor .cmd-esection .cmd-section{position:sticky;top:0;z-index:1;background:#1c1c1e}.cmd-editor .cmd-empty{flex:1;display:flex;align-items:center;justify-content:center;font-size:15px}.cmd-panel.cmd-editor{background:#1c1c1e}.cmd-editor .cmd-section{padding:12px 16px 6px}.cmd-editor .cmd-section-name{font-size:13px;letter-spacing:.4px}.cmd-editor .cmd-row{padding:0 10px 0 16px}.cmd-editor .cmd-row:not(:last-child){box-shadow:inset 0 -.5px #54545857}.cmd-editor .cmd-text{font-size:14px;padding:12px 0;color:#f2f2f7}.cmd-editor .cmd-enter{font-size:14px}.cmd-editor .cmd-move svg{width:18px;height:18px}.cmd-editor .cmd-del svg{width:16px;height:16px}.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}button.cmd-text{background:none;border:none;text-align:left}.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;justify-content:center;min-width:40px;min-height:44px}.cmd-star svg{width:16px;height:16px;display:block}.cmd-star.on{color:#ffd27a}.cmd-star.on svg{fill:currentColor}.cmd-copy{flex:none;background:none;border:none;color:#8a93a0;padding:6px 8px;touch-action:manipulation;display:inline-flex;align-items:center;justify-content:center;min-width:40px;min-height:44px}.cmd-copy svg{width:15px;height:15px;display:block}.cmd-copy:active{color:#9db8ff}.cmd-del{flex:none;background:none;border:none;color:#888;padding:6px 8px;touch-action:manipulation;display:inline-flex;align-items:center;justify-content:center;min-width:40px;min-height:44px}.cmd-del svg{width:14px;height:14px;display:block}.cmd-empty{padding:20px 14px;color:#888;font-size:13px;text-align:center}.cmd-hint{padding:2px 14px 8px;color:#7f8794;font-size:11px}.fav-chips{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px}.fav-chip{background:#2a3a2e;border:1px solid #3d5a44;color:#9fe0af;border-radius:16px;padding:8px 14px;font-size:14px}.fav-add{display:flex;gap:8px;margin-top:8px}.fav-add-input{flex:1;min-width:0;background:#16171c;border:1px solid #444;border-radius:8px;color:#fff;padding:8px;font-size:16px}.fav-add-btn{border:none;border-radius:8px;background:var(--blue);color:#fff;padding:0 14px}.cmd-panel.cmd-editor{min-height:58vh}.cmd-esection{padding-bottom:6px}.cmd-esection.win{margin-top:4px;border-top:1px solid #2c2e33}.cmd-section.global .cmd-section-name{color:#aab0ba}.cmd-section.win .cmd-section-name{color:#5fd08a}.cmd-fav-text{flex:1}.cmd-enter{margin-left:7px;opacity:.65;font-size:12px}.cmd-move{flex:none;background:none;border:none;color:#8a93a0;padding:6px 5px;touch-action:manipulation;display:inline-flex;align-items:center;justify-content:center;min-width:36px;min-height:44px}.cmd-move svg{width:16px;height:16px;display:block}.cmd-move.up svg{transform:rotate(180deg)}.cmd-move:active{color:#9db8ff}.cmd-move:disabled{opacity:.28}.cmd-add-backdrop{z-index:23;background:#0009}.cmd-addcard{position:fixed;left:50%;top:50%;z-index:24;width:min(360px,calc(100vw - 32px));display:flex;flex-direction:column;gap:16px;background:#1c1c1e;border-radius:20px;padding:18px 18px 20px;box-shadow:0 24px 60px #000000a8;-webkit-font-smoothing:antialiased}.cmd-addcard-head{display:flex;align-items:center}.cmd-addcard-head .cmd-title{margin-right:auto;font-size:17px;font-weight:700;letter-spacing:.2px}.cmd-modetabs{display:flex;gap:24px;border-bottom:1px solid rgba(84,84,88,.5);padding:0 2px}.cmd-modetab{position:relative;border:none;background:none;padding:2px 0 12px;font:inherit;font-size:17px;font-weight:600;color:#ebebf580;touch-action:manipulation;transition:color .15s}.cmd-modetab.on{color:#fff}.cmd-modetab.on:after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2.5px;border-radius:2px;background:var(--blue)}.cmd-seg{display:flex;gap:2px;background:#76768038;border-radius:9px;padding:2px}.cmd-seg-btn{flex:1;border:none;background:transparent;color:#ebebf59e;font:inherit;font-size:14px;font-weight:600;padding:8px 0;border-radius:7px;touch-action:manipulation;transition:color .15s}.cmd-seg-btn.on{background:#48484a;color:#fff;box-shadow:0 1px 3px #00000059,0 0 0 .5px #0003}.cmd-seg-btn.win.on{background:#2f6f4a;color:#d6f5e2}.cmd-seg-btn:active:not(.on){color:#fff}.cmd-seg-btn:disabled{opacity:.38}.cmd-field{display:flex;flex-direction:column;gap:8px}.cmd-field-label{font-size:12px;font-weight:600;color:#ebebf599;padding-left:2px}.cmd-add-input{width:100%;box-sizing:border-box;background:#2c2c2e;border:none;border-radius:10px;color:#f2f2f7;padding:12px 13px;font:16px/1.2 ui-monospace,SF Mono,Menlo,monospace;box-shadow:0 0 0 1px transparent;transition:box-shadow .15s}.cmd-add-input::placeholder{color:#ebebf54d}.cmd-add-input:focus{outline:none;box-shadow:0 0 0 2px var(--blue)}.cmd-chord-preview{align-self:flex-start;color:#64d2ff;font-size:13px;font-weight:700;letter-spacing:.3px}.cmd-toggle-row{display:flex;align-items:center;justify-content:space-between;gap:8px;background:#2c2c2e;border-radius:10px;padding:11px 13px;color:#f2f2f7;font-size:15px;touch-action:manipulation}.cmd-switch{position:relative;flex:none;width:51px;height:31px}.cmd-switch input{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;margin:0;opacity:0;z-index:1}.cmd-switch-track{position:absolute;top:0;right:0;bottom:0;left:0;background:#39393d;border-radius:16px;transition:background .22s ease}.cmd-switch input:checked~.cmd-switch-track{background:var(--green)}.cmd-switch-knob{position:absolute;top:2px;left:2px;width:27px;height:27px;border-radius:50%;background:#fff;box-shadow:0 2px 5px #00000059;transition:transform .22s ease}.cmd-switch input:checked~.cmd-switch-knob{transform:translate(20px)}.cmd-submit{border:none;border-radius:13px;background:var(--blue);color:#fff;font:inherit;font-size:17px;font-weight:600;padding:13px 0;margin-top:2px;touch-action:manipulation;transition:background .12s}.cmd-submit:active{background:var(--blue-press)}.cmd-submit:disabled{background:#2c2c2e;color:#ebebf54d}.cmd-dd{position:relative}.cmd-dd-btn{display:flex;align-items:center;justify-content:space-between;width:100%;box-sizing:border-box;background:#2c2c2e;border:none;border-radius:10px;color:#f2f2f7;font:inherit;font-size:16px;padding:12px 13px;box-shadow:0 0 0 2px transparent;transition:box-shadow .15s;touch-action:manipulation}.cmd-dd-btn.open{box-shadow:0 0 0 2px var(--blue)}.cmd-dd-btn svg{width:17px;height:17px;color:#ebebf580;transition:transform .18s}.cmd-dd-btn.open svg{transform:rotate(180deg)}.cmd-dd-ph{color:#ebebf54d}.cmd-dd-scrim{position:fixed;top:0;right:0;bottom:0;left:0;z-index:24}.cmd-dd-menu{position:absolute;left:0;right:0;top:calc(100% + 6px);z-index:25;background:#2c2c2e;border-radius:12px;padding:5px;max-height:216px;overflow-y:auto;-webkit-overflow-scrolling:touch;box-shadow:0 14px 36px #0009,0 0 0 .5px #0006}.cmd-dd-opt{display:flex;align-items:center;justify-content:space-between;width:100%;border:none;background:transparent;color:#f2f2f7;font:inherit;font-size:16px;text-align:left;padding:11px 12px;border-radius:8px;touch-action:manipulation}.cmd-dd-opt.on{color:var(--blue);font-weight:600}.cmd-dd-opt:active{background:#3a3a3c}.cmd-dd-opt svg{width:16px;height:16px}.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:16px 16px 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:var(--blue)}.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:var(--blue);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:8px;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;display:inline-flex;align-items:center;justify-content:center;min-width:40px;min-height:44px}.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:16px 16px 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{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.rel-ver{font-size:13px;font-weight:700;letter-spacing:.3px;color:#9db8ff;font-variant-numeric:tabular-nums}.rel-day{font-size:11px;font-weight:600;letter-spacing:.5px;color:#6b6f77;font-variant-numeric:tabular-nums}.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:7px;right:7px;width:7px;height:7px;border-radius:50%;background:var(--red);box-shadow:0 0 0 2px #1b1c1f}.idea-badge{position:absolute;top:5px;right:4px;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:var(--red);vertical-align:middle}.sheet-actions{display:flex;flex-direction:column;gap:8px}.sheet-action{width:100%;padding:12px;border:1px solid #33353a;border-radius:8px;background:#2a2c31;color:#ddd;font-size:15px;text-align:center;display:flex;align-items:center;justify-content:center;gap:8px;touch-action:manipulation}.sheet-action-icon{display:inline-flex}.sheet-action-icon svg{width:18px;height:18px}.sheet-action.danger{color:var(--red)}.sheet-action.armed{background:var(--red);color:#fff;border-color:var(--red)}.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 0 var(--blue)}.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:var(--green);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:8px;touch-action:manipulation}.preview-remain:active{color:var(--blue);border-color:var(--blue)}.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:8px;font-size:14px}.preview-pop-item:active{background:#2f3338}.preview-pop-danger{color:var(--red)}.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:var(--blue)}.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:var(--red)}.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:var(--green);font-weight:600;font-size:13px}.live-dot{width:8px;height:8px;border-radius:50%;background:var(--green)}.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;gap:2px;background:#76768038;border-radius:9px;padding:2px}.preview-seg button{background:transparent;border:none;color:#ebebf599;padding:7px 18px;min-width:60px;font-size:14px;font-weight:600;border-radius:7px;touch-action:manipulation;transition:color .15s}.preview-seg button.on{background:#48484a;color:#fff;box-shadow:0 1px 3px #00000059,0 0 0 .5px #0003}.preview-seg button:active:not(.on){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:var(--red)}.preview-live{color:var(--green)}.home-pane{flex:1;display:flex;flex-direction:column;min-width:0}.file-seg{display:flex;gap:2px;margin:10px 12px;padding:2px;background:#76768033;border-radius:9px}.file-seg-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:7px;border:none;border-radius:7px;background:transparent;color:#ebebf599;font-size:14px;font-weight:600;touch-action:manipulation;transition:color .15s}.file-seg-btn.on{background:#48484a;color:#fff;box-shadow:0 1px 3px #00000059,0 0 0 .5px #0003}.file-seg-btn:active:not(.on){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 0 var(--blue)}.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:var(--green);background:rgba(var(--green-rgb),.12);border:1px solid rgba(var(--green-rgb),.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:var(--red);font-size:13px;padding:14px 16px}.git-loading,.git-empty{color:#6b7a89;font-size:13px;padding:16px}.git-notice{color:#8b97a5;font-size:13px;line-height:1.5;padding:14px 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:var(--green)}.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:var(--green);font-size:11px}.git-branch-up{flex:0 0 auto;color:#6b7a89;font-size:11px;font-variant-numeric:tabular-nums}.git-ahead{color:var(--green)}.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:6px;padding:6px 8px;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:44px;height:44px;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:44px;height:44px;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:44px;height:44px;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:44px;height:44px;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:40px;height:44px;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:var(--blue);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:rgba(var(--blue-rgb),.3);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:var(--blue)}.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{background:#6cb4ff33;border-radius:2px;box-sizing:border-box}.cursor-deco{background:#fff;mix-blend-mode:difference;pointer-events:none}.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}.exit-toast{position:fixed;left:50%;bottom:calc(90px + env(safe-area-inset-bottom));transform:translate(-50%);z-index:60;max-width:86vw;white-space:nowrap;background:#1e2024f0;color:#e6e9ee;border:1px solid #3a3d44;padding:9px 16px;border-radius:999px;font-size:13px;box-shadow:0 6px 20px #0006;pointer-events:none;animation:exit-toast-in .18s ease}@keyframes exit-toast-in{0%{opacity:0;transform:translate(-50%,6px)}to{opacity:1;transform:translate(-50%)}}@media (prefers-reduced-motion: reduce){.exit-toast{animation:none}}.inbox-btn{position:relative}.inbox-dot{position:absolute;top:4px;right:4px;width:9px;height:9px;border-radius:50%;border:1.5px solid #2a2b2e}.inbox-dot.needs{background:#e0a020}.inbox-dot.done{background:var(--green)}.inbox-dot.working{background:var(--blue);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}.agent-mark{width:15px;height:15px;flex:none;display:inline-block}.agent-mark svg{display:block;width:100%;height:100%}.win-tab .agent-mark{vertical-align:-2px;margin-right:4px}.inbox-chip{font-size:11px;padding:1px 6px;border-radius:4px;flex:none}.inbox-chip.needs{background:#e0a020;color:#111}.inbox-chip.done{background:var(--green);color:#fff}.inbox-chip.working{background:var(--blue);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:var(--blue);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:var(--blue);color:#fff;font-size:13px;touch-action:manipulation}.inbox-enable-btn:disabled{opacity:.6}.inbox-enable-err{color:var(--red);font-size:12px}.drawer-orphans{border-top:1px solid #34353a;margin-top:10px;padding-top:6px}.drawer-orphans-head{display:flex;align-items:center;width:100%;gap:6px;background:none;border:none;color:#888;font-size:12px;letter-spacing:1px;padding:6px 4px;text-align:left;touch-action:manipulation}.drawer-orphans-caret{margin-left:auto}.drawer-orphans-hint{color:#777;font-size:11px;padding:0 4px 4px}.drawer-orphan-row{padding:6px 4px}.drawer-orphan-row+.drawer-orphan-row{border-top:1px solid #2a2b2f}.drawer-orphan-head{display:flex;align-items:center;gap:6px}.drawer-orphan-cwd{flex:1;min-width:0;color:#cdddff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.drawer-orphan-btn{flex:none;padding:4px 10px;border:none;border-radius:8px;background:#2b3550;color:#cdddff;font-size:13px;touch-action:manipulation}.drawer-orphan-btn:active{background:#344168}.drawer-orphan-btn:disabled{opacity:.45}.drawer-orphan-meta{display:flex;align-items:center;gap:6px;margin-top:2px;min-width:0}.drawer-orphan-agent{flex:none;font-size:10px;padding:0 5px;border-radius:3px;background:#333a45;color:#c7ccd1}.drawer-orphan-state{flex:none;font-size:10px;padding:0 5px;border-radius:3px}.drawer-orphan-state.idle{background:var(--green);color:#fff}.drawer-orphan-state.busy{background:var(--blue);color:#fff}.drawer-orphan-time{flex:none;font-size:11px;color:#888}.drawer-orphan-msg{flex:1;min-width:0;font-size:11px;color:#9aa0a6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bind-mode{display:flex;gap:4px;background:#23252b;border:1px solid #33353a;border-radius:10px;padding:3px}.bind-mode .seg{flex:1;padding:9px 10px;border:none;border-radius:7px;background:transparent;color:#aeb2ba;font-size:14px;touch-action:manipulation}.bind-mode .seg[aria-pressed=true]{background:var(--blue);color:#fff}.bind-mode .seg:disabled{opacity:.4}.orphan-targets{display:flex;flex-wrap:wrap;gap:6px}.orphan-targets .fontbtn[aria-pressed=true]{background:var(--blue);color:#fff;border-color:var(--blue)}.orphan-reco{margin-left:6px;font-size:10px;color:#7ee0a0}.orphan-targets .fontbtn[aria-pressed=true] .orphan-reco{color:#d6f5e3}.orphan-help{font-size:11px;color:#9aa0a6;line-height:1.5;margin-top:6px}.orphan-cmd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:#d7dbe0;background:#ffffff12;padding:1px 6px;border-radius:6px;white-space:nowrap}.usage-card{max-width:420px;max-height:80vh;overflow-y:auto}.usage-agent{padding:12px 0}.usage-agent+.usage-agent{border-top:1px solid #33353a}.usage-agent-head{display:flex;align-items:center;gap:8px;font-weight:600;font-size:14px;margin-bottom:8px}.usage-agent-head .agent-mark{width:18px;height:18px}.usage-row{margin:12px 0}.usage-row-head{display:flex;justify-content:space-between;align-items:baseline;font-size:13px;margin-bottom:5px}.usage-row-label{color:#ddd}.usage-row-sub{color:#888}.usage-row-pct{font-variant-numeric:tabular-nums;color:#ddd;font-weight:600}.usage-bar{position:relative;height:8px;background:#33353a;border-radius:5px;overflow:hidden}.usage-bar-time{position:absolute;top:-1px;bottom:-1px;width:2px;margin-left:-1px;background:#ffffffd9;box-shadow:0 0 2px #0009}.usage-bar-fill{height:100%;border-radius:5px;transition:width .3s ease}.usage-bar-fill.lvl-lo{background:#4ec98a}.usage-bar-fill.lvl-mid{background:#e0b341}.usage-bar-fill.lvl-hi{background:#ff6b6b}.usage-row-reset{margin-top:4px;font-size:11px;color:#888}.usage-empty{font-size:13px;color:#9aa0a6;padding:6px 0;line-height:1.5}.usage-code{display:inline-block;margin-top:6px;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;background:#1a1b1e;color:#b7f0cd;padding:3px 8px;border-radius:8px}.usage-updated{margin-left:auto;font-size:11px;color:#777}.usage-card .settings-title{margin-right:8px}.usage-head-note{margin-right:auto;font-size:11px;color:#777}.push-script-backdrop{z-index:30;background:#0000009e}.push-script-sheet{z-index:31;overflow-y:auto}.push-script-sheet .push-script-intro{margin:0 0 12px;color:var(--muted, #8a8a8e);font-size:13px;line-height:1.5}.push-script-block{margin:0 0 14px;display:flex;flex-direction:column;gap:6px;align-items:flex-start}.push-script-label{font-size:12px;color:var(--muted, #8a8a8e)}.push-script-cmd{margin:0;padding:10px;background:#7878801f;border-radius:8px;overflow-x:auto;font-size:13px;line-height:1.5;align-self:stretch}.push-script-key{font-variant-numeric:tabular-nums;word-break:break-all;padding:8px 10px;background:#7878801f;border-radius:8px;font-size:13px;align-self:stretch}.push-script-fields{margin:0 0 14px;padding-left:18px;color:var(--muted, #8a8a8e);font-size:12px;line-height:1.7}.push-script-hint{margin:6px 0 0;color:var(--muted, #8a8a8e);font-size:13px;line-height:1.4}.push-script-note{margin:4px 0 0;padding:10px 12px;border-radius:8px;background:#7878801f;color:var(--muted, #8a8a8e);font-size:12px;line-height:1.55}
package/public/index.html CHANGED
@@ -48,8 +48,8 @@
48
48
  @keyframes bootDot { 0%,100% { opacity: .22; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
49
49
  @media (prefers-reduced-motion: reduce) { .boot-glow, .boot-dots i { animation: none; opacity: .8; } }
50
50
  </style>
51
- <script type="module" crossorigin src="/assets/index-CL5hFo5W.js"></script>
52
- <link rel="stylesheet" crossorigin href="/assets/index-BIOe84T4.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" crossorigin href="/assets/index-BIOe84T4.css"></noscript>
51
+ <script type="module" crossorigin src="/assets/index-Cf-WEkt5.js"></script>
52
+ <link rel="stylesheet" crossorigin href="/assets/index-D5UQBvph.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" crossorigin href="/assets/index-D5UQBvph.css"></noscript>
53
53
  </head>
54
54
  <body>
55
55
  <div id="boot-splash" aria-hidden="true">
@@ -39,6 +39,55 @@ function pushKey(view, ts) { return view === 'done' ? `done:${ts}` : view; }
39
39
  // its next event, and a new prompt resets the clock.
40
40
  const WORKING_TTL_MS = 2 * 60 * 60 * 1000;
41
41
 
42
+ // A pane latched in `permission` (需要你) has NO hook event to close it when the user resolves the prompt:
43
+ // approving a normal tool (Bash/Edit/…) doesn't hit our PostToolUse matcher (only AskUserQuestion|ExitPlanMode
44
+ // do), and DENYING or ESC-interrupting fires no hook at all (verified across all hook events). So the prompt
45
+ // would read 需要你 until the turn's eventual Stop — or FOREVER after an ESC, which never Stops. Detect the
46
+ // resolution out-of-band: while the prompt is pending Claude is blocked and appends nothing to its session
47
+ // transcript, so the transcript's mtime sits at (or before) the permission event's ts; the instant the user
48
+ // resolves it — approve → tool_result, deny → denial result, ESC → a `[Request interrupted by user]` line —
49
+ // the transcript grows and its mtime jumps past it. mtime beyond the event ts (+ a guard for the near-
50
+ // simultaneous permreq/tool-write case) ⇒ resolved ⇒ drop the stale 需要你 (the pane falls back to neutral
51
+ // process-presence). The guard must clear the gap between the tool_use write and the permreq/notification
52
+ // firing; a real resolution always lands well beyond it (the user takes seconds to answer).
53
+ const PERM_RESOLVED_GUARD_MS = 1500;
54
+
55
+ // Read a transcript file's mtime in ms, or null if it's missing/unreadable (→ can't tell, keep 需要你).
56
+ function defaultStatMtime(p) { try { return fs.statSync(p).mtimeMs; } catch { return null; } }
57
+
58
+ // Read the LAST complete JSON line of a (possibly multi-MB) transcript, or null if unreadable. Reads only a
59
+ // bounded tail so cost is one small read regardless of transcript size; a last line longer than the window
60
+ // won't parse cleanly, but the only line we care to recognise (the interrupt marker) is tiny.
61
+ function defaultReadTail(p) {
62
+ try {
63
+ const fd = fs.openSync(p, 'r');
64
+ try {
65
+ const size = fs.fstatSync(fd).size;
66
+ const len = Math.min(size, 65536);
67
+ const buf = Buffer.alloc(len);
68
+ fs.readSync(fd, buf, 0, len, size - len);
69
+ const lines = buf.toString('utf8').split('\n').filter((l) => l.trim());
70
+ return lines.length ? lines[lines.length - 1] : null;
71
+ } finally { fs.closeSync(fd); }
72
+ } catch { return null; }
73
+ }
74
+
75
+ // Pure: has the user resolved the permission prompt recorded by `rec`, judged by its transcript mtime?
76
+ // True only once the transcript has grown past the event ts by more than the guard. Exported for testing.
77
+ export function permissionResolved(rec, mtimeMs, guard = PERM_RESOLVED_GUARD_MS) {
78
+ return typeof mtimeMs === 'number' && mtimeMs > (rec.ts || 0) + guard;
79
+ }
80
+
81
+ // Pure: once resolved, the transcript's last line tells a RESUME from an INTERRUPT. Approving (yes) or
82
+ // denying-with-feedback appends a tool_result / Claude's next message → Claude is working again → 进行中.
83
+ // Pressing ESC appends a `[Request interrupted by user…]` marker → the turn ended, back to idle → drop the
84
+ // pane to neutral (null). An unparseable/absent tail is treated as a resume (clears the stale 需要你 and
85
+ // shows active — a truncated huge last line is a big tool_result, never the tiny interrupt marker).
86
+ export function resolvedPermissionKind(lastLine) {
87
+ if (lastLine && /Request interrupted by user/.test(lastLine)) return null; // ESC → neutral
88
+ return { kind: 'working', msg: '' }; // approve / deny → 进行中
89
+ }
90
+
42
91
  // Truncate a Claude message to a notification-friendly one-liner.
43
92
  function summarize(msg) {
44
93
  const oneLine = (msg || '').replace(/\s+/g, ' ').trim();
@@ -60,7 +109,7 @@ function readStateFile(file) {
60
109
  // file: the hook-maintained JSON state file (DEFAULT_STATE_FILE).
61
110
  // The hook is the sole writer; the server reads the file fresh on every getStates and on every file
62
111
  // change (the watcher, for push). No persisted state of our own — the file IS the persistence.
63
- export function createClaudeEvents({ commands, push, file = DEFAULT_STATE_FILE, now = () => Date.now() } = {}) {
112
+ export function createClaudeEvents({ commands, push, file = DEFAULT_STATE_FILE, now = () => Date.now(), statMtime = defaultStatMtime, readTail = defaultReadTail } = {}) {
64
113
  const lastPushed = {}; // pane → 'needs' | 'done' | null (in-process push-transition dedup, by display view)
65
114
  // The dedup above is in-process ONLY: a restart (e.g. ./deploy.sh) wipes it while the hook's state
66
115
  // file on disk keeps every pane's latest 需要你/已完成. Without priming, the first read after boot
@@ -98,7 +147,7 @@ export function createClaudeEvents({ commands, push, file = DEFAULT_STATE_FILE,
98
147
  // crash / Ctrl-C-out with no SessionEnd). `allowedSessions` (session NAMES, or null) scopes only the
99
148
  // OUTPUT — push and reconciliation run over every pane. On a tmux failure we degrade: no location, no
100
149
  // reconciliation, no push (we'd have no session name to route to), roster returned best-effort.
101
- async function getStates(allowedSessions = null) {
150
+ async function _getStates(allowedSessions = null) {
102
151
  const allow = allowedSessions == null ? null : new Set(allowedSessions);
103
152
  const recorded = readStateFile(file);
104
153
  let live = null;
@@ -107,7 +156,14 @@ export function createClaudeEvents({ commands, push, file = DEFAULT_STATE_FILE,
107
156
  const out = {};
108
157
  for (const [pane, rec] of Object.entries(recorded)) {
109
158
  const agent = getAgent(rec && rec.agent);
110
- const c = rec && typeof rec.src === 'string' ? agent.classify(rec.src, rec.payload || {}) : null;
159
+ let c = rec && typeof rec.src === 'string' ? agent.classify(rec.src, rec.payload || {}) : null;
160
+ // A 需要你 the user already resolved leaves no closing hook (see PERM_RESOLVED_GUARD_MS). statMtime
161
+ // gates the cheap "still pending" path; only once the transcript has grown past the event do we pay the
162
+ // bounded tail read to tell a RESUME (approve/deny → 进行中) from an INTERRUPT (ESC → neutral present).
163
+ if (c && c.kind === 'permission') {
164
+ const tp = rec.payload && rec.payload.transcript_path;
165
+ if (tp && permissionResolved(rec, statMtime(tp))) c = resolvedPermissionKind(readTail(tp));
166
+ }
111
167
  const lp = live ? live.get(pane) : null;
112
168
  // Dropped when tmux says the pane is gone or no longer running THIS agent (hard kill / crash /
113
169
  // Ctrl-C-out with no clean-exit event). A pane keyed by a legacy entry (no agent field) → Claude.
@@ -162,6 +218,17 @@ export function createClaudeEvents({ commands, push, file = DEFAULT_STATE_FILE,
162
218
  return out;
163
219
  }
164
220
 
221
+ // Run getStates run-to-completion: the /states poll and the file-watcher both call it, and it mutates the
222
+ // shared `lastPushed` dedup and `await sendPush` mid-loop — overlapping runs could interleave on that state.
223
+ // Serialize on a tail promise so calls queue instead of racing; each caller still gets its own filtered
224
+ // roster (allowedSessions differs per call), and the tmux read isn't duplicated concurrently.
225
+ let tail = Promise.resolve();
226
+ function getStates(allowedSessions = null) {
227
+ const run = tail.then(() => _getStates(allowedSessions), () => _getStates(allowedSessions));
228
+ tail = run.catch(() => {});
229
+ return run;
230
+ }
231
+
165
232
  // Watch the state file's directory (the hook replaces the file via rename, which changes the inode, so
166
233
  // watching the file itself would go deaf after the first write — watch the dir and filter by name).
167
234
  // On a change, re-run getStates so 需要你/已完成 push fires even when NO client is polling — that's
@@ -7,6 +7,7 @@
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { homedir } from 'node:os';
10
+ import { writeJsonAtomic, deployHookScripts, removeHookScripts } from './hookScaffold.js';
10
11
 
11
12
  // The six events the inbox reads. src is the arg passed to handmux-notify.sh; only PostToolUse is scoped to
12
13
  // a matcher (the two "需要你" interaction tools) — every other Read/Bash/Edit must NOT wake the hook, or
@@ -73,15 +74,6 @@ export function hooksStatus(home = homedir()) {
73
74
  return Object.keys(hooks).some((ev) => alreadyHas(hooks, ev)) ? 'installed' : 'absent';
74
75
  }
75
76
 
76
- const SCRIPTS = ['handmux-notify.sh', 'handmux-write.cjs'];
77
-
78
- // Atomic JSON write (tmp + rename) so a crash can't leave a half-written settings.json.
79
- function writeJsonAtomic(file, obj) {
80
- const tmp = `${file}.tmp`;
81
- fs.writeFileSync(tmp, JSON.stringify(obj, null, 2));
82
- fs.renameSync(tmp, file);
83
- }
84
-
85
77
  // Install (opt-in): copy the bundled hook scripts to ~/.claude/hooks/, write the env pointing at the state
86
78
  // file, and merge our six hooks into settings.json. Returns { status }. NEVER creates ~/.claude — if it's
87
79
  // absent the user doesn't run Claude Code, so we report 'no-claude' and do nothing.
@@ -90,10 +82,7 @@ function writeJsonAtomic(file, obj) {
90
82
  export function installHooks(home = homedir(), { srcDir, stateFile } = {}) {
91
83
  if (!fs.existsSync(claudeDir(home))) return { status: 'no-claude' };
92
84
  const hooksDir = path.join(claudeDir(home), 'hooks');
93
- fs.mkdirSync(hooksDir, { recursive: true });
94
- for (const f of SCRIPTS) fs.copyFileSync(path.join(srcDir, f), path.join(hooksDir, f));
95
- fs.chmodSync(path.join(hooksDir, 'handmux-notify.sh'), 0o755);
96
- fs.writeFileSync(path.join(hooksDir, 'handmux-notify.env'), `HANDMUX_STATE=${stateFile}\n`, { mode: 0o600 });
85
+ deployHookScripts(hooksDir, srcDir, stateFile);
97
86
 
98
87
  const dest = path.join(hooksDir, 'handmux-notify.sh');
99
88
  let settings = {};
@@ -108,9 +97,6 @@ export function uninstallHooks(home = homedir()) {
108
97
  let settings = {};
109
98
  try { settings = JSON.parse(fs.readFileSync(settingsPath(home), 'utf8')); } catch { /* nothing to strip */ }
110
99
  if (fs.existsSync(settingsPath(home))) writeJsonAtomic(settingsPath(home), stripHooks(settings));
111
- const hooksDir = path.join(claudeDir(home), 'hooks');
112
- for (const f of [...SCRIPTS, 'handmux-notify.env']) {
113
- try { fs.unlinkSync(path.join(hooksDir, f)); } catch { /* already gone */ }
114
- }
100
+ removeHookScripts(path.join(claudeDir(home), 'hooks'));
115
101
  return { status: 'absent' };
116
102
  }
@@ -15,6 +15,7 @@
15
15
  import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
  import { homedir } from 'node:os';
18
+ import { writeFileAtomic, deployHookScripts, removeHookScripts } from './hookScaffold.js';
18
19
 
19
20
  // The events we register → the verb passed to handmux-notify.sh (classified by the shared Claude classifier,
20
21
  // since Codex's payloads match): UserPromptSubmit→working, Stop→done, PermissionRequest→需要你. PostToolUse
@@ -27,8 +28,6 @@ const CODEX_HOOK_EVENTS = [
27
28
  { event: 'PostToolUse', src: 'resume' },
28
29
  ];
29
30
 
30
- // Shared with Claude — the same scripts drive both (stdin payloads are identical).
31
- const SCRIPTS = ['handmux-notify.sh', 'handmux-write.cjs'];
32
31
  const BEGIN = '# >>> handmux codex-hooks >>>';
33
32
  const END = '# <<< handmux codex-hooks <<<';
34
33
 
@@ -96,30 +95,19 @@ export function codexHooksStatus(home = homedir()) {
96
95
  return readConf(home).includes(BEGIN) ? 'installed' : 'absent';
97
96
  }
98
97
 
99
- function writeAtomic(file, text) {
100
- const tmp = `${file}.tmp`;
101
- fs.writeFileSync(tmp, text);
102
- fs.renameSync(tmp, file);
103
- }
104
-
105
98
  // Install (opt-in): copy the shared hook scripts into ~/.codex/hooks/, point their env at the shared state
106
99
  // file, and splice our hook region into config.toml (creating ~/.codex if needed — safe, Codex is on PATH).
107
100
  // Returns { status }: 'no-codex' (nothing to do) | 'installed'.
108
101
  export function installCodexHooks(home = homedir(), { srcDir, stateFile } = {}) {
109
102
  if (!codexOnPath()) return { status: 'no-codex' };
110
- fs.mkdirSync(hooksDir(home), { recursive: true });
111
- for (const f of SCRIPTS) fs.copyFileSync(path.join(srcDir, f), path.join(hooksDir(home), f));
112
- fs.chmodSync(path.join(hooksDir(home), 'handmux-notify.sh'), 0o755);
113
- fs.writeFileSync(path.join(hooksDir(home), 'handmux-notify.env'), `HANDMUX_STATE=${stateFile}\n`, { mode: 0o600 });
114
- writeAtomic(configPath(home), mergeCodexHooks(readConf(home), codexHooksBlock(home)));
103
+ deployHookScripts(hooksDir(home), srcDir, stateFile);
104
+ writeFileAtomic(configPath(home), mergeCodexHooks(readConf(home), codexHooksBlock(home)));
115
105
  return { status: 'installed' };
116
106
  }
117
107
 
118
108
  // Uninstall: strip our config.toml region and remove the copied scripts/env. Best-effort.
119
109
  export function uninstallCodexHooks(home = homedir()) {
120
- if (fs.existsSync(configPath(home))) writeAtomic(configPath(home), stripCodexHooks(readConf(home)));
121
- for (const f of [...SCRIPTS, 'handmux-notify.env']) {
122
- try { fs.unlinkSync(path.join(hooksDir(home), f)); } catch { /* already gone */ }
123
- }
110
+ if (fs.existsSync(configPath(home))) writeFileAtomic(configPath(home), stripCodexHooks(readConf(home)));
111
+ removeHookScripts(hooksDir(home));
124
112
  return { status: 'absent' };
125
113
  }
@@ -0,0 +1,35 @@
1
+ // Shared IO scaffolding for the coding-agent hook installers (claudeHooks.js, codexHooks.js) and the
2
+ // statusLine capturer. The FORMAT each installer merges into differs (settings.json vs config.toml's marked
3
+ // region vs the single statusLine slot) and stays in its own module — but the plumbing underneath is
4
+ // identical: deploy the shared scripts, write the env, remove them on uninstall, and write atomically.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ // The two scripts every agent hook installer deploys. Shared because the stdin payloads are identical across
9
+ // agents (Codex mirrors Claude's hook contract), so the same notify/write pair drives both.
10
+ export const HOOK_SCRIPTS = ['handmux-notify.sh', 'handmux-write.cjs'];
11
+
12
+ // Atomic write (tmp + rename) so a crash can't leave a half-written config file. Text in, text out — callers
13
+ // pass raw TOML for config.toml, or use writeJsonAtomic for pretty-printed settings.json.
14
+ export function writeFileAtomic(file, text) {
15
+ const tmp = `${file}.tmp`;
16
+ fs.writeFileSync(tmp, text);
17
+ fs.renameSync(tmp, file);
18
+ }
19
+ export const writeJsonAtomic = (file, obj) => writeFileAtomic(file, JSON.stringify(obj, null, 2));
20
+
21
+ // Deploy the shared notify/write scripts into `hooksDir` and point their env at the shared state file. Same
22
+ // bytes for every agent — only the registration target (settings.json vs config.toml) differs per installer.
23
+ export function deployHookScripts(hooksDir, srcDir, stateFile) {
24
+ fs.mkdirSync(hooksDir, { recursive: true });
25
+ for (const f of HOOK_SCRIPTS) fs.copyFileSync(path.join(srcDir, f), path.join(hooksDir, f));
26
+ fs.chmodSync(path.join(hooksDir, 'handmux-notify.sh'), 0o755);
27
+ fs.writeFileSync(path.join(hooksDir, 'handmux-notify.env'), `HANDMUX_STATE=${stateFile}\n`, { mode: 0o600 });
28
+ }
29
+
30
+ // Remove the deployed scripts + env (uninstall). Best-effort: a missing file is fine.
31
+ export function removeHookScripts(hooksDir) {
32
+ for (const f of [...HOOK_SCRIPTS, 'handmux-notify.env']) {
33
+ try { fs.unlinkSync(path.join(hooksDir, f)); } catch { /* already gone */ }
34
+ }
35
+ }
@@ -49,6 +49,8 @@ export default {
49
49
  // update
50
50
  'update.available': ' ⬆ handmux {latest} is available (you have {current})',
51
51
  'update.how': ' upgrade: handmux update (or npm i -g handmux@latest)',
52
+ 'update.howBrew': ' upgrade: brew upgrade handmux',
53
+ 'update.brew': 'This handmux was installed with Homebrew — upgrade it with: brew upgrade handmux',
52
54
  'update.running': 'Upgrading handmux (npm i -g handmux@latest)…',
53
55
  'update.done': '✓ handmux updated.',
54
56
  'update.restartHint': ' run `handmux restart` to run the new version.',
@@ -48,6 +48,8 @@ export default {
48
48
  // update
49
49
  'update.available': ' ⬆ handmux {latest} 可升级(当前 {current})',
50
50
  'update.how': ' 升级: handmux update (或 npm i -g handmux@latest)',
51
+ 'update.howBrew': ' 升级: brew upgrade handmux',
52
+ 'update.brew': '这个 handmux 是用 Homebrew 安装的 — 请用: brew upgrade handmux 升级',
51
53
  'update.running': '正在升级 handmux(npm i -g handmux@latest)…',
52
54
  'update.done': '✓ handmux 已更新。',
53
55
  'update.restartHint': ' 运行 `handmux restart` 以启用新版本。',
@@ -0,0 +1,145 @@
1
+ // Pure model for `handmux setup`: the answer↔config mappers, the wizard-owned key sets, the connection
2
+ // summary, the cloudflared provisioning text helpers, and the clack validators. Extracted from the
3
+ // interactive shell (setupWizard.js) so the tested, side-effect-free logic stands on its own. Depends only
4
+ // on i18n (t/getLocale) for user-facing strings — otherwise deterministic given its inputs.
5
+ import { t, getLocale } from './i18n/index.js';
6
+
7
+ // ~/.cloudflared/config.yml for a named tunnel: route the hostname to the local handmux port.
8
+ export function cfConfigYaml({ tunnelName, credentialsFile, hostname, port }) {
9
+ return [
10
+ `tunnel: ${tunnelName}`,
11
+ `credentials-file: ${credentialsFile}`,
12
+ 'ingress:',
13
+ ` - hostname: ${hostname}`,
14
+ ` service: http://localhost:${port}`,
15
+ ' - service: http_status:404',
16
+ '',
17
+ ].join('\n');
18
+ }
19
+
20
+ // Extract the tunnel UUID + credentials path from `cloudflared tunnel create <name>` stdout.
21
+ export function parseTunnelCreate(out) {
22
+ const s = String(out || '');
23
+ const id = (s.match(/Created tunnel \S+ with id ([0-9a-fA-F-]+)/) || [])[1] || null;
24
+ const credentialsFile = (s.match(/credentials written to (\S+\.json)/i) || [])[1]?.replace(/\.$/, '') || null;
25
+ return { id, credentialsFile };
26
+ }
27
+
28
+ // Find an existing named tunnel's UUID in `cloudflared tunnel list --output json`. A named tunnel is a
29
+ // PERSISTENT object in the Cloudflare account, so a second `setup` would hit `cloudflared tunnel create`'s
30
+ // "tunnel with name X already exists" error — which used to force a rename and pile up junk tunnels in the
31
+ // account. Looking it up first lets provisioning REUSE it idempotently. Tolerates non-JSON / errors → null.
32
+ export function findTunnelId(listJsonOut, name) {
33
+ let arr;
34
+ try { arr = JSON.parse(String(listJsonOut || '')); } catch { return null; }
35
+ if (!Array.isArray(arr)) return null;
36
+ const hit = arr.find((tn) => tn && tn.name === name);
37
+ return hit?.id || null;
38
+ }
39
+
40
+ // The config keys the wizard owns: everything it can set. mergeConfig wipes these from the existing config
41
+ // before re-applying the answers, so switching a tunnel (or clearing an optional field) cleanly drops the
42
+ // old value instead of leaving a stale field behind. Anything NOT here (staticDir, previewDomain…) is
43
+ // preserved untouched. `token` IS owned so the Token row can pin one AND clear it back to auto — but it
44
+ // round-trips through answersFromConfig, so a re-run that never touches the row still writes it back.
45
+ const WIZARD_KEYS = [
46
+ 'lang', 'name', 'port', 'tunnel', 'token',
47
+ 'sshHost', 'remotePort', 'sshJump', 'cfHostname', 'cfTunnelName', 'publicUrl',
48
+ 'authtoken', 'cpolarRegion',
49
+ 'vapid', 'xfyun',
50
+ ];
51
+
52
+ // The tunnel-specific keys, cleared whenever the tunnel changes so a switch never carries a stale field.
53
+ export const TUNNEL_KEYS = ['sshHost', 'remotePort', 'sshJump', 'cfHostname', 'cfTunnelName', 'publicUrl', 'authtoken', 'cpolarRegion'];
54
+
55
+ // Wizard answers → the config fragment the user actually set (omit empty optional fields).
56
+ export function configFromAnswers(a) {
57
+ const cfg = { tunnel: a.tunnel, port: a.port };
58
+ if (a.lang) cfg.lang = a.lang;
59
+ if (a.name) cfg.name = a.name;
60
+ if (a.token) cfg.token = a.token; // blank = don't pin one → the server mints a fresh token each start
61
+ if (a.tunnel === 'ssh') {
62
+ cfg.sshHost = a.sshHost;
63
+ cfg.remotePort = a.remotePort;
64
+ if (a.publicUrl) cfg.publicUrl = a.publicUrl;
65
+ if (a.sshJump) cfg.sshJump = a.sshJump;
66
+ }
67
+ if (a.tunnel === 'cloudflare-named') {
68
+ cfg.cfHostname = a.cfHostname;
69
+ cfg.cfTunnelName = a.cfTunnelName;
70
+ }
71
+ if (a.tunnel === 'natapp' || a.tunnel === 'cpolar') {
72
+ if (a.authtoken) cfg.authtoken = a.authtoken;
73
+ if (a.publicUrl) cfg.publicUrl = a.publicUrl; // fixed/reserved domain (blank = temporary random)
74
+ if (a.cpolarRegion) cfg.cpolarRegion = a.cpolarRegion;
75
+ }
76
+ if (a.vapid) cfg.vapid = a.vapid;
77
+ if (a.xfyun) cfg.xfyun = a.xfyun;
78
+ return cfg;
79
+ }
80
+
81
+ // Fold this run's answers into an existing config: preserve every non-wizard field, replace the wizard's
82
+ // own fields wholesale. This is why re-running `setup` to switch tunnels (or edit the name) never drops
83
+ // your token / push keys / static dir, yet also never leaves the previous tunnel's stale keys around.
84
+ export function mergeConfig(existing = {}, answers) {
85
+ const out = { ...existing };
86
+ for (const k of WIZARD_KEYS) delete out[k];
87
+ return { ...out, ...configFromAnswers(answers) };
88
+ }
89
+
90
+ // Seed the working answers from an existing config so the hub shows current values and each edit starts
91
+ // from what's already there. A brand-new config yields safe defaults (none/LAN, port 19999).
92
+ export function answersFromConfig(cfg = {}) {
93
+ const a = {
94
+ lang: cfg.lang || getLocale(),
95
+ name: cfg.name || '',
96
+ token: cfg.token || '', // '' = not pinned (auto each start); seeded so an untouched re-run rewrites it
97
+ tunnel: cfg.tunnel || 'none',
98
+ port: Number(cfg.port) || 19999,
99
+ };
100
+ for (const k of [...TUNNEL_KEYS, 'vapid', 'xfyun']) if (cfg[k] != null) a[k] = cfg[k];
101
+ return a;
102
+ }
103
+
104
+ // The dim one-line summary shown next to the Connection row (and reused nowhere else). Pure.
105
+ export function summarizeConnection(a) {
106
+ const bare = (u) => String(u || '').replace(/^https?:\/\//, '');
107
+ switch (a.tunnel) {
108
+ case 'cloudflare': return `cloudflare · ${t('setup.sumTemp')}`;
109
+ case 'cloudflare-named': return `cloudflare-named · ${a.cfHostname || '?'}`;
110
+ case 'ssh': return `ssh · ${a.sshHost || '?'}`;
111
+ case 'natapp':
112
+ case 'cpolar': return `${a.tunnel} · ${a.publicUrl ? `${t('setup.sumFixed')} ${bare(a.publicUrl)}` : t('setup.sumTemp')}`;
113
+ default: return `${t('setup.tunnelNone')} · ${t('setup.sumNoRelay')}`;
114
+ }
115
+ }
116
+
117
+ // ---- validators (clack: return a string to reject + show inline, undefined to accept). Pure. ----
118
+ export function validatePort(v) {
119
+ const n = Number(v);
120
+ if (!Number.isInteger(n) || n < 1 || n > 65535) return t('setup.valPort');
121
+ return undefined;
122
+ }
123
+ export const validateNonEmpty = (label) => (v) => (String(v || '').trim() ? undefined : t('setup.valRequired', { label }));
124
+ export function validateHost(v) {
125
+ const s = String(v || '').trim().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
126
+ return /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(s) ? undefined : t('setup.valHost');
127
+ }
128
+ // VAPID subject: Apple (APNs) rejects a fake/.local domain with BadJwtToken, so require a real-looking
129
+ // mailto:you@host.tld or an https:// URL and reject the known-bad .local. Keeps push from silently
130
+ // failing on iOS. (Can't fully validate "real" client-side — this just catches the obvious footguns.)
131
+ export function validateContact(v) {
132
+ const s = String(v || '').trim();
133
+ const wellFormed = /^mailto:[^@\s]+@[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(s)
134
+ || /^https:\/\/[a-z0-9-]+(\.[a-z0-9-]+)+/i.test(s);
135
+ if (!wellFormed || /\.local(?:[:/]|$)/i.test(s)) return t('setup.valContact');
136
+ return undefined;
137
+ }
138
+ // Access token: it rides in the phone's URL as ?token=…, so require something and reject whitespace (a space
139
+ // would break the link). Length/charset are otherwise up to the user — a pinned token is used verbatim.
140
+ export function validateToken(v) {
141
+ const s = String(v || '').trim();
142
+ if (!s) return t('setup.valToken');
143
+ if (/\s/.test(s)) return t('setup.valTokenSpace');
144
+ return undefined;
145
+ }