| 1 | <!doctype html> |
| 2 | <html lang="en"> |
| 3 | <head> |
| 4 | <meta charset="utf-8" /> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| 6 | <title>Reasonix</title> |
| 7 | <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| 8 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> |
| 9 | <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" /> |
| 10 | <style> |
| 11 | *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} |
| 12 | :root{ |
| 13 | --bg:oklch(17% .005 280);--bg-2:oklch(20% .006 275); |
| 14 | --panel:oklch(21.5% .007 275);--panel-2:oklch(24.5% .008 275); |
| 15 | --card:oklch(23.5% .007 275);--card-hover:oklch(26.5% .008 275); |
| 16 | --border:oklch(30% .008 275);--border-strong:oklch(38% .01 275); |
| 17 | --fg:oklch(96% .004 280);--fg-2:oklch(81% .005 280); |
| 18 | --muted:oklch(63% .006 280);--muted-2:oklch(49% .006 280); |
| 19 | --accent:oklch(68% .16 38);--accent-soft:oklch(68% .16 38/.15);--accent-strong:oklch(72% .18 36); |
| 20 | --success:oklch(70% .14 145);--success-soft:oklch(70% .14 145/.14); |
| 21 | --warning:oklch(78% .14 85);--warning-soft:oklch(78% .14 85/.16); |
| 22 | --danger:oklch(66% .18 25);--danger-soft:oklch(66% .18 25/.16); |
| 23 | --violet:oklch(72% .14 295);--violet-soft:oklch(72% .14 295/.16); |
| 24 | --shadow-sm:0 1px 0 oklch(0% 0 0/.35); |
| 25 | --shadow-md:0 8px 24px -8px oklch(0% 0 0/.5); |
| 26 | --shadow-lg:0 24px 60px -16px oklch(0% 0 0/.65); |
| 27 | --radius:8px;--radius-lg:12px; |
| 28 | --sans:'DM Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; |
| 29 | --heading:'Space Grotesk',var(--sans); |
| 30 | --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; |
| 31 | color-scheme:dark; |
| 32 | } |
| 33 | html,body{height:100%;overflow:hidden} |
| 34 | body{font:14px/1.6 var(--sans);color:var(--fg);background:var(--bg);-webkit-font-smoothing:antialiased} |
| 35 | ::-webkit-scrollbar{width:6px;height:6px} |
| 36 | ::-webkit-scrollbar-thumb{background:var(--border);border-radius:99px} |
| 37 | ::-webkit-scrollbar-track{background:transparent} |
| 38 | :focus-visible{outline:2px solid var(--accent);outline-offset:2px} |
| 39 | button{cursor:pointer;font:inherit;border:none;background:none;color:inherit} |
| 40 | input,textarea{font:inherit;color:inherit} |
| 41 | |
| 42 | .app{display:grid;grid-template-columns:220px 1fr;grid-template-rows:1fr auto;height:100vh} |
| 43 | .sidebar{grid-row:1/3;background:var(--bg-2);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden} |
| 44 | .transcript{overflow-y:auto;padding:24px 28px} |
| 45 | .footer{border-top:1px solid var(--border);background:var(--bg);padding:12px 28px 16px;display:flex;flex-direction:column;gap:8px;position:relative} |
| 46 | .footer::before{content:'';position:absolute;inset:-20px 0 auto 0;height:20px;background:linear-gradient(to top,var(--bg),transparent);pointer-events:none} |
| 47 | |
| 48 | .sidebar__brand{display:flex;align-items:center;gap:9px;padding:16px 14px 12px;border-bottom:1px solid var(--border)} |
| 49 | .sidebar__logo{width:26px;height:26px;border-radius:7px;background:linear-gradient(135deg,var(--accent),var(--violet));display:flex;align-items:center;justify-content:center;font-family:var(--heading);font-weight:700;font-size:13px;color:oklch(99% 0 0)} |
| 50 | .sidebar__name{font-family:var(--heading);font-size:14px;font-weight:600} |
| 51 | .brand-wordmark{display:block;object-fit:contain;filter:brightness(0) invert(1);opacity:.94} |
| 52 | .brand-wordmark--sidebar{width:136px;height:32px;object-position:left center} |
| 53 | .brand-wordmark--welcome{width:240px;height:56px} |
| 54 | .sidebar__nav{flex:1;padding:8px;display:flex;flex-direction:column;gap:1px;overflow-y:auto} |
| 55 | .sidebar__item{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:6px;color:var(--fg-2);font-size:13px;transition:background .18s ease,color .18s ease;cursor:pointer} |
| 56 | .sidebar__item:hover{background:var(--panel);color:var(--fg)} |
| 57 | .sidebar__item--active{background:var(--accent-soft);color:var(--accent)} |
| 58 | .sidebar__item--accent{background:var(--accent);color:oklch(99% 0 0);font-weight:500;box-shadow:var(--shadow-sm)} |
| 59 | .sidebar__item--accent:hover{background:var(--accent-strong);color:oklch(99% 0 0)} |
| 60 | .sidebar__item--disabled{opacity:.45;cursor:not-allowed} |
| 61 | .sidebar__item--disabled:hover{background:transparent;color:var(--fg-2)} |
| 62 | .sidebar__item svg{width:15px;height:15px;flex-shrink:0;opacity:.7} |
| 63 | .sidebar__item--active svg,.sidebar__item--accent svg{opacity:1} |
| 64 | .sidebar__section{padding:8px;border-top:1px solid var(--border)} |
| 65 | .sidebar__sep{height:1px;background:var(--border);margin:4px 8px} |
| 66 | .sidebar__label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;padding:4px 10px} |
| 67 | .sidebar__label-row{display:flex;align-items:center;justify-content:space-between;padding:8px 10px 4px} |
| 68 | .sidebar__ctx{padding:6px 10px;display:flex;flex-direction:column;gap:5px} |
| 69 | .ctx-bar{height:3px;border-radius:99px;background:var(--panel-2);overflow:hidden} |
| 70 | .ctx-bar__fill{height:100%;border-radius:99px;background:var(--accent);transition:width .3s ease} |
| 71 | .ctx-label{display:flex;justify-content:space-between;font-family:var(--mono);font-size:11px;color:var(--muted-2)} |
| 72 | |
| 73 | /* status metrics */ |
| 74 | .status-metrics{display:flex;gap:0;padding:5px 8px;border-top:1px dashed var(--border);font-family:var(--mono);font-size:10.5px;color:var(--muted-2)} |
| 75 | .status-metrics .sm-item{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px} |
| 76 | .status-metrics .sm-val{color:var(--fg-2);font-weight:500;font-size:11.5px} |
| 77 | .status-metrics .sm-val.acc{color:var(--accent)} |
| 78 | |
| 79 | .msg{max-width:760px;margin:0 auto 14px;animation:msg-in .25s ease} |
| 80 | @keyframes msg-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}} |
| 81 | .msg--user{display:flex;gap:9px;align-items:baseline} |
| 82 | .msg__caret{color:var(--accent);font-family:var(--mono);font-weight:600;flex-shrink:0;font-size:15px} |
| 83 | .msg__text{white-space:pre-wrap;word-break:break-word;font-weight:500;line-height:1.65} |
| 84 | .msg--assistant{color:var(--fg);line-height:1.7} |
| 85 | .msg--assistant .msg__text{font-weight:400} |
| 86 | .msg--system{font-size:13px;color:var(--fg-2);border-left:2px solid var(--border);padding:4px 12px;margin:8px auto} |
| 87 | .msg--error{font-size:13px;color:var(--danger);border-left:2px solid var(--danger);padding:6px 12px;margin:8px auto;background:var(--danger-soft);border-radius:0 var(--radius) var(--radius) 0} |
| 88 | .reasoning{margin-bottom:6px} |
| 89 | .reasoning__toggle{display:inline-flex;align-items:center;gap:4px;background:none;border:none;color:var(--muted);font-size:12px;cursor:pointer;padding:2px 0;transition:color .18s ease} |
| 90 | .reasoning__toggle:hover{color:var(--fg-2)} |
| 91 | .reasoning__chevron{transition:transform .15s;display:inline-block} |
| 92 | .reasoning__chevron--open{transform:rotate(90deg)} |
| 93 | .reasoning__body{margin-top:5px;padding:8px 12px;border-left:2px solid var(--border);color:var(--fg-2);font-size:13px;white-space:pre-wrap;line-height:1.6;background:var(--bg-2);border-radius:0 var(--radius) var(--radius) 0;max-height:280px;overflow-y:auto} |
| 94 | .cursor{display:inline-block;width:2px;height:1.1em;background:var(--accent);vertical-align:text-bottom;margin-left:2px;animation:blink 1s steps(2,start) infinite} |
| 95 | @keyframes blink{to{visibility:hidden}} |
| 96 | |
| 97 | /* card (v1 pattern) */ |
| 98 | .card{background:var(--card);border-radius:var(--radius-lg);overflow:hidden;font-size:14px;box-shadow:var(--shadow-sm);margin:8px auto;transition:border-color .18s ease} |
| 99 | .card-head{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;gap:8px;padding:7px 12px;font-size:13px;color:var(--fg-2);cursor:pointer;user-select:none;background:var(--card);transition:background .18s ease} |
| 100 | .card-head:hover{background:var(--card-hover)} |
| 101 | .card-head .ico{width:18px;height:18px;border-radius:5px;display:inline-flex;align-items:center;justify-content:center;background:var(--panel-2);color:var(--fg-2);flex-shrink:0;font-size:10px} |
| 102 | .card-main{display:grid;gap:2px;min-width:0} |
| 103 | .card-title{display:flex;align-items:center;gap:7px;min-width:0} |
| 104 | .card-head .name{color:var(--fg);font-weight:500;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 105 | .card-head .subject{font-family:var(--mono);font-size:11.5px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0} |
| 106 | .card-meta{display:flex;align-items:center;gap:8px;min-width:0;font-family:var(--mono);font-size:10.5px;color:var(--muted-2)} |
| 107 | .card-badge{display:inline-flex;align-items:center;height:20px;padding:0 7px;border-radius:999px;border:1px solid var(--border);font-size:10.5px;color:var(--muted);white-space:nowrap} |
| 108 | .card-actions{display:flex;align-items:center;gap:4px} |
| 109 | .card-action{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid var(--border);border-radius:6px;background:var(--bg-2);color:var(--muted);transition:background .15s ease,color .15s ease,border-color .15s ease} |
| 110 | .card-action:hover{background:var(--panel);color:var(--fg);border-color:var(--border-strong)} |
| 111 | .card-head .chev{color:var(--muted-2);transition:transform .15s;display:inline-flex} |
| 112 | .card[data-open="false"] .card-head .chev{transform:rotate(-90deg)} |
| 113 | .card-body{border-top:1px solid var(--border);background:var(--bg-2);padding:8px 12px;font-family:var(--mono);font-size:12px;color:var(--fg-2);white-space:pre-wrap;word-break:break-word;max-height:240px;overflow-y:auto;line-height:1.55} |
| 114 | .card-body:empty{display:none} |
| 115 | .card[data-tone="success"] .card-head .ico{background:var(--success-soft);color:var(--success)} |
| 116 | .card[data-tone="danger"] .card-head .ico{background:var(--danger-soft);color:var(--danger)} |
| 117 | .card[data-tone="accent"] .card-head .ico{background:var(--accent-soft);color:var(--accent)} |
| 118 | .card[data-tone="success"] .card-badge{background:var(--success-soft);border-color:transparent;color:var(--success)} |
| 119 | .card[data-tone="danger"] .card-badge{background:var(--danger-soft);border-color:transparent;color:var(--danger)} |
| 120 | .card[data-tone="accent"] .card-badge{background:var(--accent-soft);border-color:transparent;color:var(--accent)} |
| 121 | .card .err-body{padding:6px 12px;color:var(--danger);background:var(--danger-soft);font-family:var(--mono);font-size:12px;white-space:pre-wrap;border-top:1px solid var(--border)} |
| 122 | .spin{animation:spin .9s linear infinite} |
| 123 | @keyframes spin{to{transform:rotate(360deg)}} |
| 124 | |
| 125 | /* metric strip (v1) */ |
| 126 | .metric-strip{display:flex;gap:0;padding:5px 0;border-top:1px dashed var(--border);border-bottom:1px dashed var(--border);margin:8px auto;max-width:760px;font-family:var(--mono);font-size:11px;color:var(--muted)} |
| 127 | .metric-strip .item{padding:0 10px;display:flex;align-items:center;gap:5px;border-right:1px solid var(--border)} |
| 128 | .metric-strip .item:last-child{border-right:none} |
| 129 | .metric-strip .v{color:var(--fg)} |
| 130 | .metric-strip .v.acc{color:var(--accent)} |
| 131 | .metric-strip .v.ok{color:var(--success)} |
| 132 | |
| 133 | .notice{max-width:760px;margin:5px auto;font-size:13px;color:var(--fg-2);padding:5px 12px;border-left:2px solid var(--border);border-radius:0 var(--radius) var(--radius) 0;white-space:pre-wrap} |
| 134 | .notice--warn{color:var(--warning);border-left-color:var(--warning);background:var(--warning-soft)} |
| 135 | .phase{max-width:760px;margin:10px auto;text-align:center;font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted-2);font-family:var(--mono)} |
| 136 | .compaction{max-width:760px;margin:8px auto;border:1px dashed var(--border);border-radius:var(--radius);padding:8px 12px;font-size:13px;color:var(--fg-2);background:var(--bg-2)} |
| 137 | .compaction__head{display:flex;align-items:center;gap:7px;cursor:pointer} |
| 138 | .compaction__title{font-weight:600;color:var(--fg)} |
| 139 | .compaction__body{margin-top:6px;padding-top:6px;border-top:1px solid var(--border);white-space:pre-wrap;font-family:var(--mono);font-size:11.5px;line-height:1.5;max-height:180px;overflow-y:auto;color:var(--fg-2);display:none} |
| 140 | .compaction__body--open{display:block} |
| 141 | |
| 142 | /* approval */ |
| 143 | .approval{background:var(--panel);border:1px solid var(--accent);border-radius:var(--radius-lg);padding:12px 14px;margin:8px 0;box-shadow:var(--shadow-md);animation:msg-in .2s ease} |
| 144 | .approval:focus{outline:none} |
| 145 | .approval__header{display:flex;align-items:center;gap:7px;margin-bottom:8px} |
| 146 | .approval__icon{color:var(--warning);width:16px;height:16px} |
| 147 | .approval__title{font-weight:600;font-size:13px} |
| 148 | .approval__subject{font-family:var(--mono);font-size:11.5px;color:var(--fg-2);padding:7px 10px;background:var(--bg-2);border:1px solid var(--border);border-radius:6px;margin-bottom:10px;white-space:pre-wrap;word-break:break-word;max-height:100px;overflow-y:auto} |
| 149 | .approval__actions{display:flex;gap:6px;flex-wrap:wrap} |
| 150 | .approval__btn{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;border-radius:6px;font-size:12px;font-weight:500;transition:all .18s ease;border:1px solid var(--border)} |
| 151 | .approval__btn--allow{background:var(--accent-soft);color:var(--accent);border-color:var(--accent)} |
| 152 | .approval__btn--allow:hover{background:oklch(68% .16 38/.25)} |
| 153 | .approval__btn--deny{background:var(--bg-2);color:var(--fg-2)} |
| 154 | .approval__btn--deny:hover{background:var(--panel);color:var(--fg)} |
| 155 | .approval__key{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border:1px solid var(--border);border-radius:4px;font-family:var(--mono);font-size:9px;font-weight:700;color:var(--muted);background:var(--bg)} |
| 156 | |
| 157 | /* ask card */ |
| 158 | .ask{background:var(--panel);border:1px solid var(--border-strong);border-radius:var(--radius-lg);padding:14px;margin:8px 0;box-shadow:var(--shadow-md);animation:msg-in .2s ease} |
| 159 | .ask__prompt{font-weight:600;margin-bottom:10px;font-size:13px;line-height:1.5} |
| 160 | .ask__options{display:flex;flex-direction:column;gap:5px} |
| 161 | .ask__opt{display:flex;align-items:flex-start;gap:8px;width:100%;text-align:left;padding:8px 10px;background:var(--bg-2);border:1px solid var(--border);border-radius:6px;color:var(--fg);cursor:pointer;transition:all .18s ease} |
| 162 | .ask__opt:hover{border-color:var(--border-strong);background:var(--panel)} |
| 163 | .ask__opt--selected{border-color:var(--accent);background:var(--accent-soft)} |
| 164 | .ask__opt-label{font-size:12.5px;font-weight:500} |
| 165 | .ask__opt-desc{font-size:11.5px;color:var(--fg-2);margin-top:1px} |
| 166 | .ask__submit{margin-top:8px;width:100%;padding:7px;background:var(--accent);color:oklch(99% 0 0);border-radius:6px;font-weight:600;font-size:12px;transition:background .18s ease} |
| 167 | .ask__submit:hover{background:var(--accent-strong)} |
| 168 | .ask__submit:disabled{background:var(--panel-2);color:var(--muted)} |
| 169 | |
| 170 | /* rewind picker */ |
| 171 | .rewind-overlay{position:fixed;inset:0;background:oklch(0% 0 0/.55);display:flex;align-items:center;justify-content:center;z-index:100;animation:msg-in .15s ease} |
| 172 | .rewind-picker{background:var(--panel);border:1px solid var(--border-strong);border-radius:var(--radius-lg);width:min(480px,90vw);max-height:70vh;display:flex;flex-direction:column;box-shadow:var(--shadow-lg);overflow:hidden} |
| 173 | .rewind-picker__head{padding:12px 14px;border-bottom:1px solid var(--border);font-family:var(--heading);font-weight:600;font-size:14px;display:flex;align-items:center;gap:8px} |
| 174 | .rewind-picker__list{flex:1;overflow-y:auto;padding:6px} |
| 175 | .rewind-picker__item{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:6px;cursor:pointer;font-size:13px;color:var(--fg-2);transition:background .15s ease} |
| 176 | .rewind-picker__item:hover,.rewind-picker__item--active{background:var(--card-hover);color:var(--fg)} |
| 177 | .rewind-picker__item--active{background:var(--accent-soft);color:var(--accent)} |
| 178 | .rewind-picker__turn{font-family:var(--mono);font-size:11px;color:var(--muted);flex-shrink:0;min-width:24px} |
| 179 | .rewind-picker__prompt{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 180 | .rewind-picker__files{font-family:var(--mono);font-size:11px;color:var(--muted-2);flex-shrink:0} |
| 181 | .rewind-picker__scopes{padding:8px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:3px} |
| 182 | .rewind-picker__scope{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;cursor:pointer;font-size:12.5px;color:var(--fg-2);transition:background .15s ease} |
| 183 | .rewind-picker__scope:hover,.rewind-picker__scope--active{background:var(--card-hover);color:var(--fg)} |
| 184 | .rewind-picker__scope--active{background:var(--accent-soft);color:var(--accent)} |
| 185 | .rewind-picker__scope-key{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--panel-2);border:1px solid var(--border);border-radius:3px;padding:1px 5px;flex-shrink:0} |
| 186 | .rewind-picker__foot{padding:8px 14px;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-size:11px;color:var(--muted-2)} |
| 187 | |
| 188 | /* slash menu */ |
| 189 | .slash-menu{position:absolute;bottom:calc(100% + 6px);left:0;right:0;max-height:min(360px,56vh);overflow:hidden;background:var(--panel);border:1px solid var(--border-strong);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);z-index:50;display:flex;flex-direction:column} |
| 190 | .slash-menu__head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border-bottom:1px solid var(--border);font-size:11.5px;color:var(--muted)} |
| 191 | .slash-menu__query{font-family:var(--mono);color:var(--fg-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 192 | .slash-menu__list{overflow-y:auto;padding:4px;min-height:0} |
| 193 | .slash-menu__group{padding:8px 8px 3px;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted-2);font-weight:700} |
| 194 | .slash-menu__item{display:grid;grid-template-columns:minmax(92px,auto) minmax(0,1fr) auto minmax(90px,auto);align-items:center;gap:8px;width:100%;padding:7px 9px;background:none;border:none;border-radius:6px;color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background .12s ease} |
| 195 | .slash-menu__item:hover,.slash-menu__item--active{background:var(--accent-soft)} |
| 196 | .slash-menu__name{font-family:var(--mono);font-size:12.5px;color:var(--accent);flex-shrink:0;font-weight:500} |
| 197 | .slash-menu__desc{font-size:12px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0} |
| 198 | .slash-menu__sig{font-family:var(--mono);font-size:10.5px;color:var(--muted-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0} |
| 199 | .slash-menu__pill{display:inline-flex;align-items:center;height:18px;padding:0 6px;border-radius:999px;background:var(--bg-2);border:1px solid var(--border);font-size:10px;color:var(--muted);white-space:nowrap} |
| 200 | .slash-menu__pill--danger{background:var(--danger-soft);border-color:transparent;color:var(--danger)} |
| 201 | .slash-menu__foot{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;border-top:1px solid var(--border);font-size:10.5px;color:var(--muted-2)} |
| 202 | |
| 203 | /* composer */ |
| 204 | .composer{display:flex;gap:7px;align-items:center;background:var(--card);border:1px solid var(--border-strong);border-radius:14px;padding:4px 4px 4px 12px;box-shadow:var(--shadow-md);transition:border-color .18s ease,box-shadow .18s ease;overflow:hidden} |
| 205 | .composer:focus-within{border-color:var(--accent);box-shadow:var(--shadow-md),0 0 0 3px var(--accent-soft)} |
| 206 | .composer__caret{color:var(--accent);font-family:var(--mono);font-weight:600;font-size:16px;flex-shrink:0} |
| 207 | .composer__input{flex:1;border:none;background:none;color:var(--fg);font-size:14px;line-height:1.5;padding:9px 0;outline:none;resize:none;min-height:22px;max-height:140px;field-sizing:content} |
| 208 | .composer__input::placeholder{color:var(--muted-2)} |
| 209 | .composer__btn{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:9px;flex-shrink:0;transition:background .18s ease,transform .15s ease} |
| 210 | .composer__btn--send{background:var(--accent);color:oklch(99% 0 0)} |
| 211 | .composer__btn--send:hover{transform:scale(1.05)} |
| 212 | .composer__btn--send:disabled{background:var(--panel-2);color:var(--muted);cursor:default;transform:none} |
| 213 | .composer__btn--stop{background:var(--panel-2);color:var(--danger)} |
| 214 | .composer__btn--stop:hover{background:var(--danger-soft)} |
| 215 | /* goal chip */ |
| 216 | .goal-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:99px;background:var(--accent-soft);border:1px solid var(--accent);font-size:12px;color:var(--accent);font-weight:500;cursor:pointer;transition:all .18s ease;max-width:320px;overflow:hidden} |
| 217 | .goal-chip:hover{background:oklch(68% .16 38/.25)} |
| 218 | .goal-chip__icon{flex-shrink:0;width:12px;height:12px} |
| 219 | .goal-chip__text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 220 | .goal-chip__close{flex-shrink:0;margin-left:2px;opacity:.6;font-size:14px;line-height:1} |
| 221 | .goal-chip__close:hover{opacity:1} |
| 222 | /* todo panel */ |
| 223 | .todos{max-width:760px;margin:0 auto 8px;border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--panel);overflow:hidden;animation:msg-in .2s ease;display:none} |
| 224 | .todos--visible{display:block} |
| 225 | .todos__head{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;user-select:none;font-size:13px;color:var(--fg-2);transition:background .15s ease} |
| 226 | .todos__head:hover{background:var(--card-hover)} |
| 227 | .todos__title{font-weight:600;color:var(--fg);font-size:12.5px} |
| 228 | .todos__badge{font-family:var(--mono);font-size:11px;color:var(--accent);background:var(--accent-soft);padding:1px 7px;border-radius:99px} |
| 229 | .todos__summary{flex:1;font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0} |
| 230 | .todos__chev{color:var(--muted-2);transition:transform .15s;display:inline-flex} |
| 231 | .todos--collapsed .todos__chev{transform:rotate(-90deg)} |
| 232 | .todos--collapsed .todos__list{display:none} |
| 233 | .todos__list{list-style:none;margin:0;padding:0 12px 8px;display:grid;gap:5px;max-height:220px;overflow-y:auto;border-top:1px solid var(--border);padding-top:8px} |
| 234 | .todos__item{display:grid;grid-template-columns:auto 1fr;align-items:flex-start;gap:8px;font-size:12.5px;line-height:1.5;padding:3px 0} |
| 235 | .todos__item--sub{padding-left:16px} |
| 236 | .todos__status{display:inline-flex;align-items:center;justify-content:center;padding:1px 7px;border:1px solid var(--border);border-radius:99px;background:var(--bg-2);color:var(--muted);font-size:10.5px;line-height:1.35;white-space:nowrap;min-width:0} |
| 237 | .todos__status--completed{border-color:var(--success-soft);color:var(--success);background:var(--success-soft)} |
| 238 | .todos__status--in_progress{border-color:var(--accent-soft);color:var(--accent);background:var(--accent-soft)} |
| 239 | .todos__text{min-width:0;color:var(--fg-2)} |
| 240 | .todos__item--completed .todos__text{color:var(--muted-2);text-decoration:line-through} |
| 241 | .todos__item--in_progress .todos__text{color:var(--fg);font-weight:500} |
| 242 | .todos__dismiss{flex-shrink:0;padding:2px 6px;color:var(--muted-2);font-size:14px;line-height:1;cursor:pointer;border-radius:4px} |
| 243 | .todos__dismiss:hover{color:var(--danger);background:var(--danger-soft)} |
| 244 | .composer__btn svg{width:17px;height:17px} |
| 245 | |
| 246 | /* toolbar */ |
| 247 | .toolbar{display:flex;align-items:center;gap:5px;flex-wrap:wrap} |
| 248 | .toolbar__btn{display:inline-flex;align-items:center;gap:4px;height:26px;padding:0 9px;border:1px solid var(--border);border-radius:6px;background:var(--bg-2);color:var(--muted);font-size:11.5px;transition:all .18s ease} |
| 249 | .toolbar__btn:hover{background:var(--panel);color:var(--fg);border-color:var(--border-strong)} |
| 250 | .toolbar__btn--active{background:var(--accent-soft);color:var(--accent);border-color:transparent} |
| 251 | .toolbar__btn--ok{background:var(--success-soft);color:var(--success);border-color:transparent} |
| 252 | .toolbar__btn--danger{background:var(--danger-soft);color:var(--danger);border-color:transparent} |
| 253 | .toolbar__btn--goal{background:var(--violet-soft);color:var(--violet);border-color:transparent} |
| 254 | .toolbar__btn--goal-active{background:var(--accent-soft);color:var(--accent);border-color:var(--accent)} |
| 255 | .toolbar__sep{width:1px;height:16px;background:var(--border)} |
| 256 | .toolbar__spacer{flex:1} |
| 257 | .status{font-family:var(--mono);font-size:11px;color:var(--muted);display:flex;align-items:center;gap:6px} |
| 258 | .status__dot{width:5px;height:5px;border-radius:50%;background:var(--muted-2);flex-shrink:0} |
| 259 | .status__dot--busy{background:var(--accent);animation:pulse 1.2s ease-in-out infinite} |
| 260 | @keyframes pulse{50%{opacity:.35}} |
| 261 | |
| 262 | /* welcome */ |
| 263 | .welcome{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;position:relative} |
| 264 | .welcome::before{content:'';position:absolute;top:40%;left:50%;transform:translate(-50%,-50%);width:360px;height:360px;background:radial-gradient(circle,oklch(68% .16 38/.08) 0%,transparent 70%);pointer-events:none} |
| 265 | .welcome__brand{display:inline-flex;align-items:center;justify-content:center;margin-bottom:16px;position:relative} |
| 266 | .welcome__logo{width:52px;height:52px;border-radius:13px;background:linear-gradient(135deg,var(--accent),var(--violet));display:flex;align-items:center;justify-content:center;font-family:var(--heading);font-weight:700;font-size:20px;color:oklch(99% 0 0);margin-bottom:16px;box-shadow:0 4px 18px oklch(68% .16 38/.2);position:relative} |
| 267 | .welcome__title{font-family:var(--heading);font-size:24px;font-weight:700;letter-spacing:-.03em;position:relative} |
| 268 | .welcome__tag{margin-top:6px;font-size:13.5px;color:var(--fg-2);position:relative} |
| 269 | .welcome__meta{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:14px;max-width:680px;position:relative} |
| 270 | .welcome__pill{display:inline-flex;align-items:center;gap:6px;max-width:320px;height:28px;padding:0 10px;border:1px solid var(--border);border-radius:999px;background:var(--bg-2);color:var(--fg-2);font-size:12px} |
| 271 | .welcome__pill strong{font-weight:600;color:var(--fg)} |
| 272 | .welcome__pill span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 273 | .welcome__hints{display:flex;gap:18px;margin-top:20px;font-size:12px;color:var(--muted);position:relative} |
| 274 | .welcome__hints span{display:inline-flex;align-items:center;gap:5px} |
| 275 | .welcome__hints kbd{font-family:var(--mono);font-size:10px;background:var(--panel-2);border:1px solid var(--border);border-radius:4px;padding:1px 6px;color:var(--fg-2)} |
| 276 | .welcome__examples{display:flex;flex-direction:column;gap:7px;margin-top:24px;width:100%;max-width:400px;position:relative} |
| 277 | .welcome__ex{text-align:left;background:var(--bg-2);border:1px solid var(--border);border-left:3px solid var(--accent);padding:9px 12px;border-radius:6px;color:var(--fg-2);font-size:12.5px;cursor:pointer;transition:all .18s ease} |
| 278 | .welcome__ex:hover{color:var(--fg);border-color:var(--accent);background:var(--panel);transform:translateY(-1px)} |
| 279 | |
| 280 | .loading{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px} |
| 281 | .spinner{width:22px;height:22px;border:2.5px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite} |
| 282 | .loading__text{font-size:12.5px;color:var(--muted)} |
| 283 | |
| 284 | /* session list */ |
| 285 | .session-list{flex:1;overflow-y:auto;padding:4px 6px;display:flex;flex-direction:column;gap:1px} |
| 286 | .session-search{padding:0 8px 4px} |
| 287 | .session-search__input{width:100%;height:28px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--fg);padding:0 9px;font-size:12px;outline:none} |
| 288 | .session-search__input::placeholder{color:var(--muted-2)} |
| 289 | .session-search__input:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)} |
| 290 | .session-item{display:flex;align-items:center;gap:7px;padding:7px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--fg-2);transition:background .15s ease,color .15s ease;overflow:hidden} |
| 291 | .session-item:hover{background:var(--panel);color:var(--fg)} |
| 292 | .session-item--active{background:var(--accent-soft);color:var(--accent)} |
| 293 | .session-item__icon{flex-shrink:0;opacity:.5} |
| 294 | .session-item__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} |
| 295 | .session-item__title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;font-size:12.5px;line-height:1.3} |
| 296 | .session-item__meta{font-family:var(--mono);font-size:10px;color:var(--muted-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 297 | .session-item__path{font-family:var(--mono);font-size:10px;color:var(--muted-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 298 | .session-del{flex-shrink:0;padding:2px 6px;color:var(--muted-2);font-size:14px;line-height:1;display:none} |
| 299 | .session-item:hover .session-del,.session-del:focus-visible{display:inline-flex} |
| 300 | .session-del:hover{color:var(--danger)} |
| 301 | |
| 302 | @media(max-width:768px){ |
| 303 | .app{grid-template-columns:1fr} |
| 304 | .sidebar{position:fixed;inset:0 auto 0 0;width:260px;z-index:100;transform:translateX(-100%);transition:transform .25s ease;box-shadow:var(--shadow-lg)} |
| 305 | .sidebar--open{transform:translateX(0)} |
| 306 | .sidebar-overlay{position:fixed;inset:0;background:oklch(0% 0 0/.45);z-index:99;opacity:0;pointer-events:none;transition:opacity .25s ease} |
| 307 | .sidebar-overlay--visible{opacity:1;pointer-events:auto} |
| 308 | .menu-btn{display:flex;align-items:center;justify-content:center;position:fixed;top:8px;left:8px;z-index:98;width:34px;height:34px;border-radius:var(--radius);background:var(--panel);border:1px solid var(--border);color:var(--fg-2);transition:background .15s} |
| 309 | .menu-btn:hover{background:var(--card-hover);color:var(--fg)} |
| 310 | .menu-btn svg{width:16px;height:16px} |
| 311 | .footer{padding:10px 16px 12px} |
| 312 | .transcript{padding:16px} |
| 313 | .slash-menu__item{grid-template-columns:minmax(78px,auto) 1fr auto} |
| 314 | .slash-menu__sig{grid-column:2 / -1} |
| 315 | .slash-menu__foot{display:none} |
| 316 | .card-head{grid-template-columns:auto minmax(0,1fr) auto} |
| 317 | .card-actions{grid-column:3} |
| 318 | .card-badge{display:none} |
| 319 | } |
| 320 | @media(min-width:769px){ |
| 321 | .menu-btn,.sidebar-overlay{display:none} |
| 322 | } |
| 323 | |
| 324 | /* stats modal */ |
| 325 | .modal-overlay{position:fixed;inset:0;background:oklch(0% 0 0/.55);display:flex;align-items:center;justify-content:center;z-index:200;animation:msg-in .15s ease} |
| 326 | .modal{background:var(--panel);border:1px solid var(--border-strong);border-radius:var(--radius-lg);width:min(520px,90vw);max-height:80vh;overflow-y:auto;box-shadow:var(--shadow-lg)} |
| 327 | .modal__head{padding:14px 16px;border-bottom:1px solid var(--border);font-family:var(--heading);font-weight:600;font-size:15px;display:flex;align-items:center;gap:8px} |
| 328 | .modal__body{padding:14px 16px} |
| 329 | .modal__close{margin-left:auto;cursor:pointer;color:var(--muted-2);width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:background .15s} |
| 330 | .modal__close:hover{background:var(--card-hover);color:var(--fg)} |
| 331 | .stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px} |
| 332 | .stat-card{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);padding:12px} |
| 333 | .stat-card__label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600} |
| 334 | .stat-card__value{font-family:var(--mono);font-size:18px;font-weight:600;color:var(--fg);margin-top:4px} |
| 335 | .stat-card__value.acc{color:var(--accent)} |
| 336 | .stat-card__value.ok{color:var(--success)} |
| 337 | .stat-card--wide{grid-column:1/-1} |
| 338 | .sidebar__item--danger{color:var(--danger)} |
| 339 | .branch-tree{font-family:var(--mono);font-size:12px;line-height:1.6;color:var(--fg-2);white-space:pre-wrap;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);padding:10px;max-height:220px;overflow:auto} |
| 340 | .branch-list{display:grid;gap:6px;margin-top:10px} |
| 341 | .branch-item{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;padding:8px 10px;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-2)} |
| 342 | .branch-item__title{font-weight:600;font-size:12.5px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 343 | .branch-item__meta{font-family:var(--mono);font-size:10.5px;color:var(--muted-2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 344 | .branch-item__btn{height:26px;padding:0 9px;border:1px solid var(--border);border-radius:6px;background:var(--panel);color:var(--fg-2);font-size:11.5px} |
| 345 | .branch-item__btn:hover{border-color:var(--border-strong);color:var(--fg)} |
| 346 | .empty-note{font-size:12.5px;color:var(--muted);padding:10px;border:1px dashed var(--border);border-radius:var(--radius);background:var(--bg-2)} |
| 347 | .model-list{display:grid;gap:6px} |
| 348 | .model-item{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;padding:9px 10px;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-2)} |
| 349 | .model-item--active{border-color:var(--accent);background:var(--accent-soft)} |
| 350 | .model-item__title{font-family:var(--mono);font-size:12.5px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 351 | .model-item__meta{font-size:11px;color:var(--muted);margin-top:2px} |
| 352 | .model-item__status{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:999px;border:1px solid var(--border);color:var(--muted);font-size:11px;white-space:nowrap} |
| 353 | .model-item__status--active{border-color:transparent;background:var(--success-soft);color:var(--success)} |
| 354 | .dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px} |
| 355 | .dialog-btn{height:30px;padding:0 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-2);color:var(--fg-2);font-size:12px} |
| 356 | .dialog-btn:hover{border-color:var(--border-strong);color:var(--fg)} |
| 357 | .dialog-btn--danger{background:var(--danger-soft);border-color:transparent;color:var(--danger)} |
| 358 | </style> |
| 359 | </head> |
| 360 | <body> |
| 361 | <button class="menu-btn" id="menu-btn" aria-label="Toggle sidebar"> |
| 362 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg> |
| 363 | </button> |
| 364 | <div class="sidebar-overlay" id="sidebar-overlay"></div> |
| 365 | <div class="app"> |
| 366 | <aside class="sidebar"> |
| 367 | <div class="sidebar__brand"> |
| 368 | <img src="/assets/logo-wordmark.svg" alt="Reasonix" class="brand-wordmark brand-wordmark--sidebar" draggable="false"> |
| 369 | </div> |
| 370 | <nav class="sidebar__nav" style="border-top:none;padding-top:0"> |
| 371 | <div class="sidebar__item sidebar__item--accent" id="btn-new"> |
| 372 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> |
| 373 | <span>__('new_session')</span> |
| 374 | </div> |
| 375 | <div class="sidebar__item" id="btn-compact"> |
| 376 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg> |
| 377 | <span>__('compact')</span> |
| 378 | </div> |
| 379 | <div class="sidebar__item" id="btn-rewind"> |
| 380 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg> |
| 381 | <span>__('rewind')</span> |
| 382 | </div> |
| 383 | <div class="sidebar__item" id="btn-tree"> |
| 384 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg> |
| 385 | <span>__('branches')</span> |
| 386 | </div> |
| 387 | <div class="sidebar__item" id="btn-models"> |
| 388 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v3"/><path d="M15 1v3"/><path d="M9 20v3"/><path d="M15 20v3"/><path d="M20 9h3"/><path d="M20 14h3"/><path d="M1 9h3"/><path d="M1 14h3"/></svg> |
| 389 | <span>__('models')</span> |
| 390 | </div> |
| 391 | <div class="sidebar__sep"></div> |
| 392 | <div class="sidebar__item" id="btn-stats"> |
| 393 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg> |
| 394 | <span>__('stats')</span> |
| 395 | </div> |
| 396 | </nav> |
| 397 | <div class="sidebar__label-row"><span class="sidebar__label" style="padding:0">__('sessions')</span><span id="session-count" class="session-item__meta"></span></div> |
| 398 | <div class="session-search"> |
| 399 | <input class="session-search__input" id="session-search" type="search" placeholder="__('search_sessions')" aria-label="__('search_sessions')"> |
| 400 | </div> |
| 401 | <div class="session-list" id="session-list"> |
| 402 | <div style="padding:10px;color:var(--muted-2);font-size:12px">__('loading')</div> |
| 403 | </div> |
| 404 | <div class="sidebar__section"> |
| 405 | <div class="sidebar__label">__('status')</div> |
| 406 | <div class="sidebar__ctx"> |
| 407 | <div class="ctx-bar"><div class="ctx-bar__fill" id="ctx-fill" style="width:0%"></div></div> |
| 408 | <div class="ctx-label"><span id="ctx-used">0 tok</span><span id="ctx-window">0 tok</span></div> |
| 409 | </div> |
| 410 | <div class="status-metrics" id="status-metrics"> |
| 411 | <div class="sm-item"><span class="sm-val" id="sm-cache">—</span><span>__('cache')</span></div> |
| 412 | <div class="sm-item"><span class="sm-val" id="sm-cost">—</span><span>__('cost')</span></div> |
| 413 | <div class="sm-item"><span class="sm-val acc" id="sm-balance">—</span><span>__('balance')</span></div> |
| 414 | </div> |
| 415 | <div style="padding:4px 10px"> |
| 416 | <div class="status" id="sidebar-status"><span class="status__dot" id="status-dot"></span><span id="status-model">-</span></div> |
| 417 | </div> |
| 418 | </div> |
| 419 | </aside> |
| 420 | |
| 421 | <section class="transcript" id="log"> |
| 422 | <div class="welcome" id="welcome"> |
| 423 | <div class="welcome__brand"><img src="/assets/logo-wordmark.svg" alt="Reasonix" class="brand-wordmark brand-wordmark--welcome" draggable="false"></div> |
| 424 | <div class="welcome__tag">__('welcome_tag')</div> |
| 425 | <div class="welcome__meta"> |
| 426 | <div class="welcome__pill"><strong>__('model')</strong><span id="welcome-model">-</span></div> |
| 427 | <div class="welcome__pill"><strong>__('workspace')</strong><span id="welcome-cwd">-</span></div> |
| 428 | </div> |
| 429 | <div class="welcome__hints"> |
| 430 | <span><kbd>/</kbd> __('hint_commands')</span> |
| 431 | <span><kbd>Shift+Tab</kbd> __('hint_mode')</span> |
| 432 | <span><kbd>Ctrl+Y</kbd> __('hint_yolo')</span> |
| 433 | <span><kbd>Esc×2</kbd> __('hint_rewind')</span> |
| 434 | </div> |
| 435 | <div class="welcome__examples"> |
| 436 | <button class="welcome__ex" data-prompt="">__('example_explain')</button> |
| 437 | <button class="welcome__ex" data-prompt="">__('example_fix')</button> |
| 438 | <button class="welcome__ex" data-prompt="">__('example_test')</button> |
| 439 | </div> |
| 440 | </div> |
| 441 | </section> |
| 442 | |
| 443 | <footer class="footer"> |
| 444 | <div id="slash-menu-anchor"></div> |
| 445 | <div id="goal-active-bar" style="margin-bottom:6px;display:none"> |
| 446 | <span class="goal-chip" id="goal-chip" title="__('goal_exit')"> |
| 447 | <svg class="goal-chip__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/></svg> |
| 448 | <span class="goal-chip__text" id="goal-chip-text"></span> |
| 449 | <span class="goal-chip__close">×</span> |
| 450 | </span> |
| 451 | </div> |
| 452 | <div id="todo-panel" class="todos"> |
| 453 | <div class="todos__head" id="todos-head"> |
| 454 | <span class="todos__title" id="todos-title">To-dos</span> |
| 455 | <span class="todos__badge" id="todos-badge"></span> |
| 456 | <span class="todos__summary" id="todos-summary"></span> |
| 457 | <span class="todos__dismiss" id="todos-dismiss" title="Close" style="display:none">×</span> |
| 458 | <span class="todos__chev"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></span> |
| 459 | </div> |
| 460 | <ul class="todos__list" id="todos-list"></ul> |
| 461 | </div> |
| 462 | <div class="toolbar"> |
| 463 | <button class="toolbar__btn toolbar__btn--ok" id="btn-auto" title="__('auto_mode')"> |
| 464 | <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 12l2 2 4-4"/></svg> |
| 465 | __('auto') |
| 466 | </button> |
| 467 | <button class="toolbar__btn" id="btn-plan" title="__('plan_mode')"> |
| 468 | <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg> |
| 469 | __('plan') |
| 470 | </button> |
| 471 | <button class="toolbar__btn" id="btn-bypass" title="__('yolo_mode')"> |
| 472 | <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg> |
| 473 | __('yolo') |
| 474 | </button> |
| 475 | <button class="toolbar__btn" id="btn-goal" title="__('goal_mode_desc')"> |
| 476 | <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><line x1="12" y1="2" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22"/><line x1="2" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/></svg> |
| 477 | __('goal_btn') |
| 478 | </button> |
| 479 | <div class="toolbar__sep"></div> |
| 480 | <div class="status"> |
| 481 | <span class="status__dot" id="status-dot-footer"></span> |
| 482 | <span id="status-text">__('ready')</span> |
| 483 | </div> |
| 484 | <div class="toolbar__spacer"></div> |
| 485 | <div class="status" id="turn-info"></div> |
| 486 | <div class="status" id="balance-info"></div> |
| 487 | </div> |
| 488 | <div class="composer"> |
| 489 | <span class="composer__caret">›</span> |
| 490 | <textarea class="composer__input" id="in" placeholder="__('placeholder')" rows="1" autofocus></textarea> |
| 491 | <button class="composer__btn composer__btn--send" id="btn-send" title="__('send')"> |
| 492 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg> |
| 493 | </button> |
| 494 | <button class="composer__btn composer__btn--stop" id="btn-stop" title="__('cancel')" style="display:none"> |
| 495 | <svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg> |
| 496 | </button> |
| 497 | </div> |
| 498 | </footer> |
| 499 | </div> |
| 500 | |
| 501 | <!-- stats modal --> |
| 502 | <div class="modal-overlay" id="stats-modal" style="display:none"> |
| 503 | <div class="modal"> |
| 504 | <div class="modal__head"> |
| 505 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg> |
| 506 | __('statistics') |
| 507 | <span class="modal__close" id="stats-modal-close">×</span> |
| 508 | </div> |
| 509 | <div class="modal__body"> |
| 510 | <div class="stat-grid"> |
| 511 | <div class="stat-card"> |
| 512 | <div class="stat-card__label">__('model')</div> |
| 513 | <div class="stat-card__value" id="stats-model">-</div> |
| 514 | </div> |
| 515 | <div class="stat-card"> |
| 516 | <div class="stat-card__label">__('sessions')</div> |
| 517 | <div class="stat-card__value" id="stats-sessions">0</div> |
| 518 | </div> |
| 519 | <div class="stat-card"> |
| 520 | <div class="stat-card__label">__('total_tokens')</div> |
| 521 | <div class="stat-card__value acc" id="stats-total-tokens">0</div> |
| 522 | </div> |
| 523 | <div class="stat-card"> |
| 524 | <div class="stat-card__label">__('cache_hit_rate')</div> |
| 525 | <div class="stat-card__value ok" id="stats-cache-rate">0%</div> |
| 526 | </div> |
| 527 | <div class="stat-card"> |
| 528 | <div class="stat-card__label">__('total_cost')</div> |
| 529 | <div class="stat-card__value" id="stats-total-cost">-</div> |
| 530 | </div> |
| 531 | <div class="stat-card"> |
| 532 | <div class="stat-card__label">__('balance')</div> |
| 533 | <div class="stat-card__value" id="stats-balance">-</div> |
| 534 | </div> |
| 535 | <div class="stat-card stat-card--wide"> |
| 536 | <div class="stat-card__label">__('context_usage')</div> |
| 537 | <div class="ctx-bar" style="margin-top:8px"><div class="ctx-bar__fill" id="stats-ctx-fill" style="width:0%"></div></div> |
| 538 | <div class="ctx-label" style="margin-top:4px"><span id="stats-ctx-used">0 tok</span><span id="stats-ctx-window">0 tok</span></div> |
| 539 | </div> |
| 540 | </div> |
| 541 | </div> |
| 542 | </div> |
| 543 | </div> |
| 544 | |
| 545 | <!-- branches modal --> |
| 546 | <div class="modal-overlay" id="branches-modal" style="display:none"> |
| 547 | <div class="modal"> |
| 548 | <div class="modal__head"> |
| 549 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg> |
| 550 | __('branches') |
| 551 | <span class="modal__close" id="branches-modal-close">×</span> |
| 552 | </div> |
| 553 | <div class="modal__body"> |
| 554 | <div class="branch-tree" id="branches-tree">__('loading')</div> |
| 555 | <div class="branch-list" id="branches-list"></div> |
| 556 | </div> |
| 557 | </div> |
| 558 | </div> |
| 559 | |
| 560 | <!-- models modal --> |
| 561 | <div class="modal-overlay" id="models-modal" style="display:none"> |
| 562 | <div class="modal"> |
| 563 | <div class="modal__head"> |
| 564 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/></svg> |
| 565 | __('models') |
| 566 | <span class="modal__close" id="models-modal-close">×</span> |
| 567 | </div> |
| 568 | <div class="modal__body"> |
| 569 | <div class="model-list" id="models-list"></div> |
| 570 | </div> |
| 571 | </div> |
| 572 | </div> |
| 573 | |
| 574 | <!-- delete confirmation modal --> |
| 575 | <div class="modal-overlay" id="delete-modal" style="display:none"> |
| 576 | <div class="modal"> |
| 577 | <div class="modal__head"> |
| 578 | __('delete_session') |
| 579 | <span class="modal__close" id="delete-modal-close">×</span> |
| 580 | </div> |
| 581 | <div class="modal__body"> |
| 582 | <div class="empty-note" id="delete-session-title"></div> |
| 583 | <div class="dialog-actions"> |
| 584 | <button class="dialog-btn" id="delete-cancel" type="button">__('cancel_plain')</button> |
| 585 | <button class="dialog-btn dialog-btn--danger" id="delete-confirm" type="button">__('delete')</button> |
| 586 | </div> |
| 587 | </div> |
| 588 | </div> |
| 589 | </div> |
| 590 | <script> |
| 591 | // ── i18n ── |
| 592 | const __LANG_PREF = '__LANG__'; |
| 593 | const __LANG = (__LANG_PREF === 'zh' || __LANG_PREF === 'en') ? __LANG_PREF : ((navigator.language || '').startsWith('zh') ? 'zh' : 'en'); |
| 594 | document.documentElement.lang = __LANG; |
| 595 | const __T = { |
| 596 | en: { |
| 597 | 'new_session': 'New Session', |
| 598 | 'compact': 'Compact', |
| 599 | 'rewind': 'Rewind', |
| 600 | 'branches': 'Branches', |
| 601 | 'models': 'Models', |
| 602 | 'sessions': 'Sessions', |
| 603 | 'status': 'Status', |
| 604 | 'loading': 'Loading...', |
| 605 | 'cache': 'Cache', |
| 606 | 'cost': 'Cost', |
| 607 | 'balance': 'Balance', |
| 608 | 'ready': 'Ready', |
| 609 | 'thinking': 'Thinking...', |
| 610 | 'retrying_status': 'Retrying ({attempt}/{max})...', |
| 611 | 'recovery_paused': 'Automatic retries paused. Reasonix stopped repeated attempts and kept completed work. Send “Continue” to start a fresh attempt, or add instructions to change direction.', |
| 612 | 'stats': 'Stats', |
| 613 | 'statistics': 'Statistics', |
| 614 | 'model': 'Model', |
| 615 | 'workspace': 'Workspace', |
| 616 | 'total_tokens': 'Total Tokens', |
| 617 | 'cache_hit_rate': 'Cache Hit Rate', |
| 618 | 'total_cost': 'Session Cost', |
| 619 | 'context_usage': 'Context Usage', |
| 620 | |
| 621 | 'connected': 'Connected', |
| 622 | 'reconnecting': 'Reconnecting...', |
| 623 | 'disconnected': 'Disconnected', |
| 624 | 'placeholder': 'Message Reasonix... / for commands', |
| 625 | 'cmd_compact': 'Compact conversation', |
| 626 | 'cmd_new': 'New session', |
| 627 | 'cmd_resume': 'Resume session', |
| 628 | 'cmd_rewind': 'Rewind to checkpoint', |
| 629 | 'cmd_tree': 'Show branch tree', |
| 630 | 'cmd_branch': 'Create branch', |
| 631 | 'cmd_switch': 'Switch branch', |
| 632 | 'cmd_model': 'List/switch models', |
| 633 | 'cmd_effort': 'Reasoning effort level', |
| 634 | 'cmd_mcp': 'MCP servers', |
| 635 | 'cmd_skill': 'Skills', |
| 636 | 'cmd_hooks': 'Hooks', |
| 637 | 'cmd_reload': 'Reload extensions', |
| 638 | 'cmd_memory': 'Show memory', |
| 639 | 'cmd_forget': 'Forget memory', |
| 640 | 'cmd_goal': 'Set a goal for the agent to pursue autonomously', |
| 641 | 'cmd_thinking': 'Thinking effort', |
| 642 | 'cmd_verbose': 'Toggle reasoning', |
| 643 | 'cmd_help': 'Help', |
| 644 | 'cmd_group_session': 'Session', |
| 645 | 'cmd_group_branch': 'Branches', |
| 646 | 'cmd_group_model': 'Model', |
| 647 | 'cmd_group_system': 'System', |
| 648 | 'cmd_group_memory': 'Memory', |
| 649 | 'cmd_group_agent': 'Agent', |
| 650 | 'cmd_group_help': 'Help', |
| 651 | 'extensions_reloading': 'Reloading extensions...', |
| 652 | 'extensions_reloaded': 'Extensions reloaded', |
| 653 | 'extensions_reload_failed': 'Could not reload extensions', |
| 654 | 'command_palette': 'Command palette', |
| 655 | 'command_empty': 'No commands match', |
| 656 | 'command_nav': '↑↓ navigate · Enter insert · Esc close', |
| 657 | 'danger': 'Danger', |
| 658 | 'tool_running': 'Running', |
| 659 | 'tool_done': 'Done', |
| 660 | 'tool_failed': 'Failed', |
| 661 | 'tool_copy': 'Copy output', |
| 662 | 'tool_copied': 'Copied', |
| 663 | 'tool_no_output': 'No output', |
| 664 | 'tool_args': 'Args', |
| 665 | 'tool_output': 'Output', |
| 666 | 'tool_lines': 'lines', |
| 667 | 'submit': 'Submit', |
| 668 | 'approval_title': 'Approval Required', |
| 669 | 'allow': 'Allow', |
| 670 | 'session': 'Allow for session', |
| 671 | 'session_command': 'Command for session', |
| 672 | 'session_prefix': 'Command prefix for session', |
| 673 | 'persist_command': 'Always allow command (save)', |
| 674 | 'persist_prefix': 'Always allow command prefix (save)', |
| 675 | 'persist_edit': 'Always allow edits (save)', |
| 676 | 'persist_tool': 'Always allow (save)', |
| 677 | 'session_edit': 'Edits for session', |
| 678 | 'session_tool': 'Tool for session', |
| 679 | 'deny': 'Deny', |
| 680 | 'auto_mode': 'Auto mode (auto-approve fallback approvals)', |
| 681 | 'plan_mode': 'Plan first; permissions and sandbox still apply', |
| 682 | 'yolo_mode': 'YOLO mode (auto-approve tool approvals; ask and plan still wait)', |
| 683 | 'goal_mode_desc': 'Goal mode — type a task for the agent to pursue autonomously', |
| 684 | 'goal_mode': 'Goal mode', |
| 685 | 'goal_placeholder': 'Describe your goal...', |
| 686 | 'goal_active': 'Active goal', |
| 687 | 'goal_exit': 'Exit goal mode', |
| 688 | 'auto': 'Auto', |
| 689 | 'plan': 'Plan', |
| 690 | 'goal_btn': 'Goal', |
| 691 | 'yolo': 'YOLO', |
| 692 | 'send': 'Send (Enter)', |
| 693 | 'cancel': 'Cancel (Esc)', |
| 694 | 'cancel_plain': 'Cancel', |
| 695 | 'delete': 'Delete', |
| 696 | 'delete_session': 'Delete Session', |
| 697 | 'cannot_delete_active': 'Cannot delete the active session', |
| 698 | 'delete_failed': 'Could not delete the session. Check your connection and try again.', |
| 699 | 'search_sessions': 'Search sessions', |
| 700 | 'active': 'Active', |
| 701 | 'use_model': 'Use', |
| 702 | 'no_sessions': 'No sessions', |
| 703 | 'error_loading': 'Error loading', |
| 704 | 'no_checkpoints': 'No checkpoints available.', |
| 705 | 'no_conversation': 'Start a conversation before using this action.', |
| 706 | 'no_branches': 'No conversation branches yet.', |
| 707 | 'switch': 'Switch', |
| 708 | 'compacted': 'Compacted', |
| 709 | 'messages': 'messages', |
| 710 | 'compacting': 'Compacting...', |
| 711 | 'scope_both': 'Code + conversation', |
| 712 | 'scope_conversation': 'Conversation only', |
| 713 | 'scope_code': 'Code only', |
| 714 | 'scope_fork': 'Fork (new branch)', |
| 715 | 'scope_sumfrom': 'Summarize from here', |
| 716 | 'scope_sumupto': 'Summarize up to here', |
| 717 | 'delete_confirm': 'Delete this session?', |
| 718 | 'welcome_tag': 'AI coding agent', |
| 719 | 'hint_commands': '/ commands', |
| 720 | 'hint_mode': 'Plan', |
| 721 | 'hint_yolo': 'YOLO', |
| 722 | 'hint_rewind': 'Esc×2 rewind', |
| 723 | 'example_explain': 'Explain the project structure', |
| 724 | 'example_fix': 'Find and fix any bugs', |
| 725 | 'example_test': 'Write tests for the main module', |
| 726 | 'total': 'Total', |
| 727 | 'in': 'In', |
| 728 | 'out': 'Out', |
| 729 | 'nav_jk': 'j/k or ↑↓ to navigate', |
| 730 | 'nav_enter_esc': 'Enter to select · Esc to close', |
| 731 | 'nav_keys': 'b/c/d/f/s/u quick keys', |
| 732 | 'nav_apply_esc': 'Enter to apply · Esc to go back', |
| 733 | 'rewind_title': 'Rewind — Select Turn', |
| 734 | 'action_title': 'Turn #{turn} — Select Action', |
| 735 | 'files': 'files', |
| 736 | }, |
| 737 | zh: { |
| 738 | 'new_session': '新会话', |
| 739 | 'compact': '压缩', |
| 740 | 'rewind': '回退', |
| 741 | 'branches': '分支', |
| 742 | 'models': '模型', |
| 743 | 'sessions': '会话', |
| 744 | 'status': '状态', |
| 745 | 'loading': '加载中...', |
| 746 | 'cache': '缓存', |
| 747 | 'cost': '费用', |
| 748 | 'balance': '余额', |
| 749 | 'ready': '就绪', |
| 750 | 'thinking': '思考中...', |
| 751 | 'retrying_status': '正在重试 ({attempt}/{max})...', |
| 752 | 'recovery_paused': '已暂停自动重试。Reasonix 已停止重复尝试,并保留已完成的工作。发送“继续”即可开始新一轮,也可以补充要求来调整方向。', |
| 753 | 'stats': '统计', |
| 754 | 'statistics': '统计', |
| 755 | 'model': '模型', |
| 756 | 'workspace': '工作区', |
| 757 | 'total_tokens': '总 Token', |
| 758 | 'cache_hit_rate': '缓存命中率', |
| 759 | 'total_cost': '会话费用', |
| 760 | 'context_usage': '上下文用量', |
| 761 | |
| 762 | 'connected': '已连接', |
| 763 | 'reconnecting': '重新连接...', |
| 764 | 'disconnected': '已断开', |
| 765 | 'placeholder': '给 Reasonix 发消息... / 查看命令', |
| 766 | 'cmd_compact': '压缩对话', |
| 767 | 'cmd_new': '新建会话', |
| 768 | 'cmd_resume': '恢复会话', |
| 769 | 'cmd_rewind': '回退到检查点', |
| 770 | 'cmd_tree': '显示分支树', |
| 771 | 'cmd_branch': '创建分支', |
| 772 | 'cmd_switch': '切换分支', |
| 773 | 'cmd_model': '列出/切换模型', |
| 774 | 'cmd_effort': '推理努力级别', |
| 775 | 'cmd_mcp': 'MCP 服务器', |
| 776 | 'cmd_skill': '技能', |
| 777 | 'cmd_hooks': '钩子', |
| 778 | 'cmd_reload': '重新加载扩展', |
| 779 | 'cmd_memory': '显示记忆', |
| 780 | 'cmd_forget': '忘记记忆', |
| 781 | 'cmd_goal': '设置目标让代理自主执行', |
| 782 | 'cmd_thinking': '思考努力', |
| 783 | 'cmd_verbose': '切换推理显示', |
| 784 | 'cmd_help': '帮助', |
| 785 | 'cmd_group_session': '会话', |
| 786 | 'cmd_group_branch': '分支', |
| 787 | 'cmd_group_model': '模型', |
| 788 | 'cmd_group_system': '系统', |
| 789 | 'cmd_group_memory': '记忆', |
| 790 | 'cmd_group_agent': '代理', |
| 791 | 'cmd_group_help': '帮助', |
| 792 | 'extensions_reloading': '正在重新加载扩展…', |
| 793 | 'extensions_reloaded': '扩展已重新加载', |
| 794 | 'extensions_reload_failed': '无法重新加载扩展', |
| 795 | 'command_palette': '命令面板', |
| 796 | 'command_empty': '没有匹配的命令', |
| 797 | 'command_nav': '↑↓ 选择 · Enter 插入 · Esc 关闭', |
| 798 | 'danger': '危险', |
| 799 | 'tool_running': '运行中', |
| 800 | 'tool_done': '完成', |
| 801 | 'tool_failed': '失败', |
| 802 | 'tool_copy': '复制输出', |
| 803 | 'tool_copied': '已复制', |
| 804 | 'tool_no_output': '无输出', |
| 805 | 'tool_args': '参数', |
| 806 | 'tool_output': '输出', |
| 807 | 'tool_lines': '行', |
| 808 | 'submit': '提交', |
| 809 | 'approval_title': '需要批准', |
| 810 | 'allow': '允许', |
| 811 | 'session': '本会话允许', |
| 812 | 'session_command': '本会话允许此命令', |
| 813 | 'session_prefix': '本会话允许命令前缀', |
| 814 | 'persist_command': '总是允许此命令(保存)', |
| 815 | 'persist_prefix': '总是允许命令前缀(保存)', |
| 816 | 'persist_edit': '总是允许编辑(保存)', |
| 817 | 'persist_tool': '总是允许(保存)', |
| 818 | 'session_edit': '本会话允许编辑', |
| 819 | 'session_tool': '本会话允许此工具', |
| 820 | 'deny': '拒绝', |
| 821 | 'auto_mode': '自动模式(自动批准兜底审批)', |
| 822 | 'plan_mode': '先规划;权限与沙箱仍然生效', |
| 823 | 'yolo_mode': 'YOLO 模式(自动批准工具权限;ask 和计划仍会等待)', |
| 824 | 'goal_mode_desc': '目标模式 — 输入任务让代理自主执行', |
| 825 | 'goal_mode': '目标模式', |
| 826 | 'goal_placeholder': '描述你的目标...', |
| 827 | 'goal_active': '活跃目标', |
| 828 | 'goal_exit': '退出目标模式', |
| 829 | 'auto': '自动', |
| 830 | 'plan': '计划', |
| 831 | 'goal_btn': '目标', |
| 832 | 'yolo': 'YOLO', |
| 833 | 'send': '发送 (Enter)', |
| 834 | 'cancel': '取消 (Esc)', |
| 835 | 'cancel_plain': '取消', |
| 836 | 'delete': '删除', |
| 837 | 'delete_session': '删除会话', |
| 838 | 'cannot_delete_active': '无法删除当前会话', |
| 839 | 'delete_failed': '无法删除会话,请检查连接后重试', |
| 840 | 'search_sessions': '搜索会话', |
| 841 | 'active': '当前', |
| 842 | 'use_model': '使用', |
| 843 | 'no_sessions': '暂无会话', |
| 844 | 'error_loading': '加载失败', |
| 845 | 'no_checkpoints': '暂无可用检查点', |
| 846 | 'no_conversation': '先开始一段对话,再使用此操作。', |
| 847 | 'no_branches': '暂无会话分支', |
| 848 | 'switch': '切换', |
| 849 | 'compacted': '已压缩', |
| 850 | 'messages': '条消息', |
| 851 | 'compacting': '压缩中...', |
| 852 | 'scope_both': '代码 + 对话', |
| 853 | 'scope_conversation': '仅对话', |
| 854 | 'scope_code': '仅代码', |
| 855 | 'scope_fork': '分叉(新分支)', |
| 856 | 'scope_sumfrom': '从此处总结', |
| 857 | 'scope_sumupto': '总结到此处', |
| 858 | 'delete_confirm': '删除此会话?', |
| 859 | 'welcome_tag': 'AI 编码助手', |
| 860 | 'hint_commands': '/ 命令', |
| 861 | 'hint_mode': '计划', |
| 862 | 'hint_yolo': 'YOLO', |
| 863 | 'hint_rewind': 'Esc×2 回退', |
| 864 | 'example_explain': '解释项目结构', |
| 865 | 'example_fix': '查找并修复错误', |
| 866 | 'example_test': '为主模块编写测试', |
| 867 | 'total': '总计', |
| 868 | 'in': '输入', |
| 869 | 'out': '输出', |
| 870 | 'nav_jk': 'j/k 或 ↑↓ 导航', |
| 871 | 'nav_enter_esc': 'Enter 选择 · Esc 关闭', |
| 872 | 'nav_keys': 'b/c/d/f/s/u 快捷键', |
| 873 | 'nav_apply_esc': 'Enter 应用 · Esc 返回', |
| 874 | 'rewind_title': '回退 — 选择轮次', |
| 875 | 'action_title': '第 #{turn} 轮 — 选择操作', |
| 876 | 'files': '个文件', |
| 877 | } |
| 878 | }; |
| 879 | const __ = (key, ...args) => { |
| 880 | let s = __T[__LANG]?.[key] ?? __T.en[key] ?? key; |
| 881 | if (args.length) args.forEach((v, i) => { s = s.replace('#{' + ['turn','n'][i] + '}', v); }); |
| 882 | return s; |
| 883 | }; |
| 884 | function translateTokens(text) { |
| 885 | return String(text).replace(/__\('([^']+)'\)/g, (_, key) => __(key)); |
| 886 | } |
| 887 | function applyStaticI18n() { |
| 888 | const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { |
| 889 | acceptNode(node) { |
| 890 | const parent = node.parentElement; |
| 891 | if (!parent || parent.closest('script,style')) return NodeFilter.FILTER_REJECT; |
| 892 | return node.nodeValue.includes("__('") ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; |
| 893 | } |
| 894 | }); |
| 895 | const nodes = []; |
| 896 | for (let node = walker.nextNode(); node; node = walker.nextNode()) nodes.push(node); |
| 897 | nodes.forEach(node => { node.nodeValue = translateTokens(node.nodeValue); }); |
| 898 | document.querySelectorAll('[title],[placeholder]').forEach(node => { |
| 899 | if (node.hasAttribute('title')) node.setAttribute('title', translateTokens(node.getAttribute('title'))); |
| 900 | if (node.hasAttribute('placeholder')) node.setAttribute('placeholder', translateTokens(node.getAttribute('placeholder'))); |
| 901 | }); |
| 902 | } |
| 903 | applyStaticI18n(); |
| 904 | |
| 905 | // ── populate welcome examples ── |
| 906 | document.addEventListener('DOMContentLoaded', () => { |
| 907 | document.querySelectorAll('.welcome__ex').forEach((btn, i) => { |
| 908 | const keys = ['example_explain', 'example_fix', 'example_test']; |
| 909 | if (i < keys.length) btn.dataset.prompt = __(keys[i]); |
| 910 | }); |
| 911 | }); |
| 912 | |
| 913 | |
| 914 | |
| 915 | const $ = s => document.querySelector(s); |
| 916 | const $$ = s => document.querySelectorAll(s); |
| 917 | const log = $('#log'), input = $('#in'), btnSend = $('#btn-send'), btnStop = $('#btn-stop'); |
| 918 | const statusDot = $('#status-dot-footer'), statusText = $('#status-text'); |
| 919 | const turnInfo = $('#turn-info'), balanceInfo = $('#balance-info'); |
| 920 | const ctxFill = $('#ctx-fill'), ctxUsed = $('#ctx-used'), ctxWindow = $('#ctx-window'); |
| 921 | const statusDotSidebar = $('#status-dot'), statusModel = $('#status-model'); |
| 922 | const welcome = $('#welcome'); |
| 923 | const welcomeModel = $('#welcome-model'), welcomeCwd = $('#welcome-cwd'); |
| 924 | const slashAnchor = $('#slash-menu-anchor'); |
| 925 | |
| 926 | // ── state ── |
| 927 | let running = false, planMode = false, bypassMode = false, toolApprovalMode = 'ask', yoloRestoreMode = 'ask'; |
| 928 | let goalMode = false, goalActive = false, goalText = ''; |
| 929 | let currentMsg = null, currentText = null, currentReasoning = null; |
| 930 | let turnStartAt = 0, turnTokens = 0, tickTimer = null, retryStatus = null; |
| 931 | let toolCards = {}; |
| 932 | let escTimer = null; |
| 933 | let hasVisibleHistory = false, checkpointCount = 0; |
| 934 | let sessionsCache = [], sessionFilter = '', pendingDeleteSession = null; |
| 935 | |
| 936 | // ── slash commands registry ── |
| 937 | const SLASH_CMDS = [ |
| 938 | {cmd:'new',sig:'/new',desc:__('cmd_new'),group:'session'}, |
| 939 | {cmd:'resume',sig:'/resume [n]',desc:__('cmd_resume'),group:'session'}, |
| 940 | {cmd:'compact',sig:'/compact',desc:__('cmd_compact'),group:'session'}, |
| 941 | {cmd:'rewind',sig:'/rewind',desc:__('cmd_rewind'),group:'session'}, |
| 942 | {cmd:'tree',sig:'/tree',desc:__('cmd_tree'),group:'branch'}, |
| 943 | {cmd:'branch',sig:'/branch <name>',desc:__('cmd_branch'),group:'branch'}, |
| 944 | {cmd:'switch',sig:'/switch <id>',desc:__('cmd_switch'),group:'branch'}, |
| 945 | {cmd:'model',sig:'/model [provider/model]',desc:__('cmd_model'),group:'model'}, |
| 946 | {cmd:'effort',sig:'/effort <level>',desc:__('cmd_effort'),group:'model'}, |
| 947 | {cmd:'goal',sig:'/goal <task>',desc:__('cmd_goal'),group:'agent'}, |
| 948 | {cmd:'thinking',sig:'/thinking <level>',desc:__('cmd_thinking'),group:'agent'}, |
| 949 | {cmd:'verbose',sig:'/verbose',desc:__('cmd_verbose'),group:'agent'}, |
| 950 | {cmd:'mcp',sig:'/mcp',desc:__('cmd_mcp'),group:'system'}, |
| 951 | {cmd:'skill',sig:'/skill',desc:__('cmd_skill'),group:'system'}, |
| 952 | {cmd:'hooks',sig:'/hooks',desc:__('cmd_hooks'),group:'system'}, |
| 953 | {cmd:'reload',sig:'/reload',desc:__('cmd_reload'),group:'system'}, |
| 954 | {cmd:'memory',sig:'/memory',desc:__('cmd_memory'),group:'memory'}, |
| 955 | {cmd:'forget',sig:'/forget <item>',desc:__('cmd_forget'),group:'memory',danger:true}, |
| 956 | {cmd:'help',sig:'/help',desc:__('cmd_help'),group:'help'}, |
| 957 | ]; |
| 958 | const SLASH_GROUPS = ['session','branch','model','agent','system','memory','help']; |
| 959 | function slashGroupLabel(group){return __('cmd_group_'+group)||group;} |
| 960 | |
| 961 | // ── helpers ── |
| 962 | function post(path, body) { |
| 963 | return fetch(path, {method:'POST',headers:{'content-type':'application/json'},body:body?JSON.stringify(body):undefined}); |
| 964 | } |
| 965 | // Follow streaming output only while the user is pinned to the bottom of the |
| 966 | // transcript; scrolling up detaches so history stays readable mid-stream. |
| 967 | // Detach triggers on wheel intent (any upward notch on scrollable content), |
| 968 | // not only on position: during fast streaming every frame re-bottoms the |
| 969 | // viewport, so a slow trackpad drag would never accumulate the 40px position |
| 970 | // delta. force re-pins (own message sent, session loaded, action prompts, |
| 971 | // click feedback) and wins over a concurrent user scroll — an approval needs |
| 972 | // a decision and its key handler is document-level, so it must not sit |
| 973 | // off-screen. Scrolls are instant: a smooth animation lags behind rapid |
| 974 | // chunks, and its intermediate positions would read as "not at bottom" and |
| 975 | // break pin detection. |
| 976 | let pinnedToBottom=true; |
| 977 | function atBottom() { return log.scrollHeight-log.scrollTop-log.clientHeight<40; } |
| 978 | log.addEventListener('scroll',()=>{pinnedToBottom=atBottom();}); |
| 979 | log.addEventListener('wheel',e=>{if(e.deltaY<0&&log.scrollHeight>log.clientHeight)pinnedToBottom=false;},{passive:true}); |
| 980 | function scrollDown(force) { if(force)pinnedToBottom=true; if(!pinnedToBottom)return; requestAnimationFrame(()=>{if(force)pinnedToBottom=true; else if(!pinnedToBottom)return; log.scrollTo({top:log.scrollHeight,behavior:'instant'});});} |
| 981 | function hideWelcome() { if(welcome) welcome.style.display='none';} |
| 982 | function showWelcome() { if(welcome) welcome.style.display='';} |
| 983 | // Global single-key shortcuts must stay dead while the user is typing (the |
| 984 | // composer is autofocused, so this is the default state) and when a |
| 985 | // browser/OS chord is held (Cmd+A must never resolve an approval). |
| 986 | function isTextEntry(t) { return !!t&&(t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.tagName==='SELECT'||t.isContentEditable); } |
| 987 | function isPlainKey(e) { return !e.ctrlKey&&!e.metaKey&&!e.altKey&&!isTextEntry(e.target); } |
| 988 | function fmtTok(n) { return n>=1000?(n/1000).toFixed(1).replace(/\.0$/,'')+'k':String(n);} |
| 989 | function currencySymbol(c){const v=String(c||'¥').trim();if(/^(cny|rmb|yuan)$/i.test(v))return '¥';if(/^(usd|dollar)$/i.test(v))return '$';return v||'¥';} |
| 990 | function fmtMoney(n,c){if(typeof n!=='number'||!isFinite(n))return '—';const s=currencySymbol(c);return s+(n<1?n.toFixed(4):n.toFixed(2));} |
| 991 | function fmtElapsed(ms) { const s=Math.floor(ms/1000); return s<60?s+'s':Math.floor(s/60)+'m '+s%60+'s';} |
| 992 | function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');} |
| 993 | function escAttr(s) { return escHtml(s).replace(/"/g,'"').replace(/'/g,''');} |
| 994 | function el(tag,cls,text) { const e=document.createElement(tag); if(cls)e.className=cls; if(text)e.textContent=text; return e;} |
| 995 | function compactText(s, max) { |
| 996 | const text=String(s||'').replace(/\s+/g,' ').trim(); |
| 997 | return text.length>max?text.slice(0,max-1)+'…':text; |
| 998 | } |
| 999 | function lineCount(s) { |
| 1000 | const text=String(s||''); |
| 1001 | return text?text.split(/\r\n|\r|\n/).length:0; |
| 1002 | } |
| 1003 | function actionDisabled(id){return $('#'+id)?.classList.contains('sidebar__item--disabled');} |
| 1004 | function setActionDisabled(id, disabled, title) { |
| 1005 | const node = $('#'+id); |
| 1006 | if(!node)return; |
| 1007 | node.classList.toggle('sidebar__item--disabled', !!disabled); |
| 1008 | node.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
| 1009 | if(title)node.setAttribute('title', title); |
| 1010 | else node.removeAttribute('title'); |
| 1011 | } |
| 1012 | function showNotice(text, tone) { |
| 1013 | hideWelcome(); |
| 1014 | const n=el('div','notice'+(tone==='warn'?' notice--warn':''),text); |
| 1015 | log.appendChild(n); |
| 1016 | scrollDown(true); // synchronous feedback to a user action — always reveal |
| 1017 | } |
| 1018 | function updateActionAvailability() { |
| 1019 | setActionDisabled('btn-compact', running || !hasVisibleHistory, running ? __('thinking') : __('no_conversation')); |
| 1020 | setActionDisabled('btn-rewind', running || checkpointCount === 0, running ? __('thinking') : __('no_checkpoints')); |
| 1021 | } |
| 1022 | |
| 1023 | function setRunning(on) { |
| 1024 | running=on; |
| 1025 | retryStatus=null; |
| 1026 | btnSend.style.display=on?'none':''; |
| 1027 | btnStop.style.display=on?'':'none'; |
| 1028 | statusDot.className=on?'status__dot status__dot--busy':'status__dot'; |
| 1029 | statusDotSidebar.className=statusDot.className; |
| 1030 | statusText.textContent=on?(goalActive?__('goal_active')+' · ':'')+__('thinking'):__('ready'); |
| 1031 | updateActionAvailability(); |
| 1032 | if(on){turnStartAt=Date.now();turnTokens=0;tickTimer=setInterval(()=>{ |
| 1033 | const ms=Date.now()-turnStartAt; const tok=turnTokens>0?' · ↓ '+fmtTok(turnTokens)+' tok':''; |
| 1034 | turnInfo.textContent=fmtElapsed(ms)+tok; |
| 1035 | },1000);} else {clearInterval(tickTimer);turnInfo.textContent='';} |
| 1036 | } |
| 1037 | |
| 1038 | function setRetrying(attempt,max) { |
| 1039 | retryStatus={attempt:attempt||0,max:max||0}; |
| 1040 | statusDot.className='status__dot status__dot--busy'; |
| 1041 | statusDotSidebar.className=statusDot.className; |
| 1042 | statusText.textContent=__('retrying_status').replace('{attempt}',retryStatus.attempt).replace('{max}',retryStatus.max); |
| 1043 | } |
| 1044 | |
| 1045 | function clearRetrying() { |
| 1046 | if(!retryStatus)return; |
| 1047 | retryStatus=null; |
| 1048 | if(running)statusText.textContent=__('thinking'); |
| 1049 | } |
| 1050 | |
| 1051 | function setConnState(state) { |
| 1052 | // state: 'connected' | 'reconnecting' | 'disconnected' |
| 1053 | const colors={connected:'var(--success)',reconnecting:'var(--warning)',disconnected:'var(--danger)'}; |
| 1054 | const labels={connected:__('connected'),reconnecting:__('reconnecting'),disconnected:__('disconnected')}; |
| 1055 | if(!running){ |
| 1056 | statusDot.style.background=colors[state]||''; |
| 1057 | statusDot.className='status__dot'+(state==='reconnecting'?' status__dot--busy':''); |
| 1058 | statusText.textContent=labels[state]||state; |
| 1059 | } |
| 1060 | statusDotSidebar.style.background=colors[state]||''; |
| 1061 | } |
| 1062 | |
| 1063 | // ── messages ── |
| 1064 | function addUserMsg(text) { hasVisibleHistory=true; updateActionAvailability(); hideWelcome(); const d=el('div','msg msg--user'); d.appendChild(el('span','msg__caret','›')); d.appendChild(el('div','msg__text',text)); log.appendChild(d); scrollDown(true); currentMsg=null;currentText=null;currentReasoning=null; } |
| 1065 | function ensureMsg() { if(!currentMsg){hasVisibleHistory=true; updateActionAvailability(); hideWelcome();currentMsg=el('div','msg msg--assistant');log.appendChild(currentMsg);} return currentMsg; } |
| 1066 | function appendText(t) { const m=ensureMsg(); if(!currentText){currentText=document.createElement('span');currentText.className='msg__text'; const old=m.querySelector('.cursor');if(old)old.remove(); m.appendChild(currentText); m.appendChild(el('span','cursor'));} currentText.textContent+=t; scrollDown(); } |
| 1067 | function appendReasoning(t) { const m=ensureMsg(); if(!currentReasoning){const w=el('div','reasoning'); const b=el('button','reasoning__toggle'); b.innerHTML='<span class="reasoning__chevron">▶</span> '+__('thinking'); const body=el('div','reasoning__body'); body.style.display='none'; b.onclick=()=>{body.style.display=body.style.display==='none'?'':'none';b.querySelector('.reasoning__chevron').className='reasoning__chevron'+(body.style.display!=='none'?' reasoning__chevron--open':'');}; w.appendChild(b);w.appendChild(body); if(currentText) m.insertBefore(w,currentText); else m.appendChild(w); currentReasoning=body;} currentReasoning.textContent+=t; scrollDown(); } |
| 1068 | function finalizeMsg() { if(currentMsg){const c=currentMsg.querySelector('.cursor');if(c)c.remove();} currentMsg=null;currentText=null;currentReasoning=null; } |
| 1069 | |
| 1070 | // ── tool cards (v1 pattern) ── |
| 1071 | function toolIcon(kind) { |
| 1072 | if(kind==='success')return '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>'; |
| 1073 | if(kind==='danger')return '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'; |
| 1074 | return '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10" stroke-dasharray="60" stroke-dashoffset="20"/></svg>'; |
| 1075 | } |
| 1076 | function toolArgsSummary(args) { |
| 1077 | const raw=String(args||'').trim(); |
| 1078 | if(!raw)return ''; |
| 1079 | try{ |
| 1080 | const parsed=JSON.parse(raw); |
| 1081 | if(parsed&&typeof parsed==='object'&&!Array.isArray(parsed)){ |
| 1082 | const key=['cmd','command','path','file','query','q','url','prompt','input'].find(k=>parsed[k]); |
| 1083 | if(key)return compactText(parsed[key],90); |
| 1084 | } |
| 1085 | }catch{} |
| 1086 | return compactText(raw,90); |
| 1087 | } |
| 1088 | function updateToolMeta(card, text) { |
| 1089 | const meta=card.querySelector('.card-meta'); |
| 1090 | if(meta)meta.textContent=text; |
| 1091 | } |
| 1092 | function setToolStatus(card, tone, label) { |
| 1093 | card.dataset.tone=tone; |
| 1094 | const badge=card.querySelector('.card-badge'); |
| 1095 | if(badge)badge.textContent=label; |
| 1096 | const ico=card.querySelector('.ico'); |
| 1097 | if(ico){ico.className='ico'+(tone==='accent'?' spin':'');ico.innerHTML=toolIcon(tone);} |
| 1098 | } |
| 1099 | function copyText(text) { |
| 1100 | if(!text)return Promise.resolve(false); |
| 1101 | if(navigator.clipboard?.writeText)return navigator.clipboard.writeText(text).then(()=>true).catch(()=>false); |
| 1102 | return Promise.resolve(false); |
| 1103 | } |
| 1104 | function renderToolDispatch(tool) { |
| 1105 | |
| 1106 | // Partial, full, and post-write preview-refresh dispatches share one ID. |
| 1107 | // Update the existing card in place so a refreshed dependent writer never |
| 1108 | // appears as a duplicate operation in the timeline. |
| 1109 | if(!tool.id)return; |
| 1110 | const existing=toolCards[tool.id]; |
| 1111 | if(existing){ |
| 1112 | const summary=toolArgsSummary(tool.args); |
| 1113 | const title=existing.querySelector('.card-title'); |
| 1114 | if(title)title.innerHTML='<span class="name">'+escHtml(tool.name)+'</span>'+(summary?'<span class="subject">'+escHtml(summary)+'</span>':''); |
| 1115 | existing.dataset.toolArgs=String(tool.args||existing.dataset.toolArgs||''); |
| 1116 | updateToolMeta(existing,__('tool_args')+' '+fmtTok(existing.dataset.toolArgs.length)); |
| 1117 | scrollDown(); return; |
| 1118 | } |
| 1119 | hideWelcome(); const card=el('div','card'); card.id='tool-'+tool.id; card.dataset.open='false'; card.dataset.tone='accent'; |
| 1120 | card.dataset.startedAt=String(Date.now()); |
| 1121 | card.dataset.toolArgs=String(tool.args||''); |
| 1122 | const head=el('div','card-head'); |
| 1123 | const summary=toolArgsSummary(tool.args); |
| 1124 | head.innerHTML='<span class="ico spin">'+toolIcon('accent')+'</span><div class="card-main"><div class="card-title"><span class="name">'+escHtml(tool.name)+'</span>'+(summary?'<span class="subject">'+escHtml(summary)+'</span>':'')+'</div><div class="card-meta">'+escHtml(__('tool_args'))+' '+fmtTok(String(tool.args||'').length)+'</div></div><span class="card-badge">'+escHtml(__('tool_running'))+'</span><div class="card-actions"><button type="button" class="card-action card-copy" title="'+escAttr(__('tool_copy'))+'" aria-label="'+escAttr(__('tool_copy'))+'"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><span class="chev"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></span></div>'; |
| 1125 | const body=el('div','card-body'); body.style.display='none'; |
| 1126 | head.onclick=e=>{if(e.target.closest('button'))return;const open=card.dataset.open==='true';card.dataset.open=open?'false':'true';body.style.display=open?'none':'';}; |
| 1127 | const copy=head.querySelector('.card-copy'); |
| 1128 | if(copy)copy.onclick=async e=>{e.stopPropagation();const ok=await copyText(body.textContent||card.dataset.toolArgs||'');copy.title=ok?__('tool_copied'):__('tool_copy');setTimeout(()=>{copy.title=__('tool_copy');},1200);}; |
| 1129 | card.appendChild(head);card.appendChild(body);log.appendChild(card);toolCards[tool.id]=card;scrollDown(); |
| 1130 | } |
| 1131 | function shellDisplayName(ex){ |
| 1132 | if(!ex||!ex.shell)return 'bash'; |
| 1133 | if(ex.shell==='git-bash')return 'Git Bash'; |
| 1134 | if(ex.shell==='powershell')return 'Windows PowerShell'; |
| 1135 | if(ex.shell==='pwsh')return 'PowerShell 7+'; |
| 1136 | return ex.shell; |
| 1137 | } |
| 1138 | function renderToolResult(tool) { |
| 1139 | const card=toolCards[tool.id]; if(!card)return; |
| 1140 | const elapsed=Math.max(0,Date.now()-Number(card.dataset.startedAt||Date.now())); |
| 1141 | const ex=tool.execution||null; |
| 1142 | if(ex&&tool.name==='bash'){ |
| 1143 | const title=card.querySelector('.card-title .name'); |
| 1144 | if(title)title.textContent=shellDisplayName(ex); |
| 1145 | } |
| 1146 | setToolStatus(card,tool.err?'danger':'success',tool.err?__('tool_failed'):__('tool_done')); |
| 1147 | let meta=fmtElapsed(elapsed); |
| 1148 | if(ex){ |
| 1149 | const bits=[]; |
| 1150 | if(typeof ex.exitCode==='number')bits.push('exit '+ex.exitCode); |
| 1151 | if(ex.failurePhase)bits.push(ex.failurePhase); |
| 1152 | if(ex.verification&&ex.verification!=='not_verification')bits.push(ex.verification); |
| 1153 | if(bits.length)meta=bits.join(' · ')+' · '+meta; |
| 1154 | } |
| 1155 | if(tool.err){ |
| 1156 | const err=el('div','err-body',tool.err); |
| 1157 | if(ex&&ex.outputTail){const pre=el('pre','err-stderr',ex.outputTail.slice(0,16000));err.appendChild(pre);} |
| 1158 | card.appendChild(err); |
| 1159 | updateToolMeta(card,meta); |
| 1160 | } else { |
| 1161 | const body=card.querySelector('.card-body'); |
| 1162 | const output=String(tool.output||''); |
| 1163 | if(body)body.textContent=output?output.slice(0,2000)+(tool.truncated?'\n...[truncated]':''):__('tool_no_output'); |
| 1164 | updateToolMeta(card,meta+' · '+lineCount(output)+' '+__('tool_lines')); |
| 1165 | } |
| 1166 | scrollDown(); |
| 1167 | } |
| 1168 | function renderToolProgress(tool) { |
| 1169 | // Reserved sub-agent progress channels (reasonix.subagent.*) are an internal |
| 1170 | // local-frontend contract; they must never land in ordinary tool output. |
| 1171 | if (tool.name && tool.name.indexOf('reasonix.subagent.') === 0) return; |
| 1172 | const card=toolCards[tool.id]; if(!card)return; |
| 1173 | const body=card.querySelector('.card-body'); if(!body)return; |
| 1174 | body.style.display=''; card.dataset.open='true'; |
| 1175 | body.textContent+=tool.output||''; |
| 1176 | if(body.textContent.length>4000) body.textContent=body.textContent.slice(-3000); |
| 1177 | updateToolMeta(card,fmtElapsed(Date.now()-Number(card.dataset.startedAt||Date.now()))+' · '+lineCount(body.textContent)+' '+__('tool_lines')); |
| 1178 | scrollDown(); |
| 1179 | } |
| 1180 | |
| 1181 | // Serve renders the structured extension contract as safe native text. It |
| 1182 | // never injects extension-provided HTML/Markdown into innerHTML, so a sidecar |
| 1183 | // cannot turn a card, form, status, or notification into executable content. |
| 1184 | function renderExtensionSurface(p) { |
| 1185 | if(!p)return; |
| 1186 | const lines=[]; |
| 1187 | const plugin=p.pluginId||'extension'; |
| 1188 | let severity=''; |
| 1189 | if(p.status){ |
| 1190 | severity=p.status.severity||''; |
| 1191 | lines.push(p.status.label||''); |
| 1192 | if(p.status.detail)lines.push(p.status.detail); |
| 1193 | if(typeof p.status.progress==='number')lines.push(Math.round(p.status.progress*100)+'%'); |
| 1194 | } else if(p.card){ |
| 1195 | lines.push(p.card.title||''); |
| 1196 | lines.push(p.card.text||p.card.markdown||''); |
| 1197 | (p.card.fields||[]).forEach(f=>lines.push((f.key||'')+': '+(f.value||''))); |
| 1198 | (p.card.actions||[]).forEach(a=>lines.push('['+(a.label||a.actionId||'action')+']')); |
| 1199 | } else if(p.form){ |
| 1200 | lines.push(p.form.title||''); |
| 1201 | lines.push(p.form.message||''); |
| 1202 | (p.form.fields||[]).forEach(f=>{ |
| 1203 | let detail=(f.label||f.key||'')+(f.required?' *':''); |
| 1204 | if((f.options||[]).length)detail+=' — '+f.options.join(', '); |
| 1205 | lines.push(detail); |
| 1206 | }); |
| 1207 | } else if(p.notification){ |
| 1208 | severity=p.notification.severity||''; |
| 1209 | lines.push(p.notification.title||''); |
| 1210 | lines.push(p.notification.body||''); |
| 1211 | } |
| 1212 | const text='['+plugin+'] '+lines.filter(Boolean).join('\n'); |
| 1213 | const node=el('div','notice'+((severity==='warn'||severity==='error')?' notice--warn':''),text); |
| 1214 | log.appendChild(node);scrollDown(); |
| 1215 | } |
| 1216 | |
| 1217 | // ── approval ── |
| 1218 | function approvalSessionLabel(a) { |
| 1219 | return __('session')+' '+approvalSessionRule(a); |
| 1220 | } |
| 1221 | function approvalPersistentLabel(a) { |
| 1222 | return __('persist_tool')+' '+approvalPersistentRule(a); |
| 1223 | } |
| 1224 | function approvalSessionRule(a) { |
| 1225 | if (a.tool === 'bash' && a.subject) return 'Bash('+String(a.subject).trim()+')'; |
| 1226 | if (['write_file','edit_file','multi_edit','move_file','notebook_edit','delete_range','delete_symbol'].includes(a.tool)) return 'Edit'; |
| 1227 | return a.tool; |
| 1228 | } |
| 1229 | function approvalPersistentRule(a) { |
| 1230 | if (a.tool === 'bash' && a.subject) return 'Bash('+String(a.subject).trim()+')'; |
| 1231 | if (['write_file','edit_file','multi_edit','move_file','notebook_edit','delete_range','delete_symbol'].includes(a.tool)) return a.subject ? 'Edit('+String(a.subject).trim()+')' : 'Edit'; |
| 1232 | return a.tool; |
| 1233 | } |
| 1234 | function bashCommandPrefix(subject) { |
| 1235 | const command = String(subject || '').trim(); |
| 1236 | if (!command || command.includes('`') || command.includes('$(') || /[;|&<>\n]/.test(command)) return ''; |
| 1237 | const fields = command.split(/\s+/).filter(Boolean); |
| 1238 | if (fields.length < 2) return ''; |
| 1239 | if (dangerousBashCommand(command)) return ''; |
| 1240 | const base = fields[0].toLowerCase(); |
| 1241 | if (['npm','pnpm','yarn','bun'].includes(base) && fields[1] && fields[1].toLowerCase() === 'run') return fields.length >= 3 ? fields[0]+' '+fields[1]+' '+fields[2]+':*' : ''; |
| 1242 | return fields[0]+' '+fields[1]+':*'; |
| 1243 | } |
| 1244 | function dangerousBashCommand(command) { |
| 1245 | return /^rm\s+-[^\s]*[rf][^\s]*\b/.test(command) |
| 1246 | || /^git\s+push\b.*\s--force\b/.test(command) |
| 1247 | || /^git\s+push\b.*\s-f\b/.test(command) |
| 1248 | || /^git\s+reset\s+--hard\b/.test(command) |
| 1249 | || /^git\s+clean\s+-f\b/.test(command) |
| 1250 | || /^chmod\s+(?:-R\s+)?777\b/.test(command) |
| 1251 | || /^chown\b/.test(command) |
| 1252 | || /^sudo\b/.test(command) |
| 1253 | || /^mkfs\b/.test(command) |
| 1254 | || /^dd\s+if=/.test(command) |
| 1255 | || /^fdisk\b/.test(command); |
| 1256 | } |
| 1257 | // A turn's approval/ask prompts die with the turn: cancel (composer Esc, stop |
| 1258 | // button) or turn end must remove the cards and their key listeners, or a |
| 1259 | // stale card keeps soliciting a decision for a dead request and the approval |
| 1260 | // key listener would resolve it on a later unrelated keypress. |
| 1261 | let pendingPrompts=[]; |
| 1262 | function clearPendingPrompts(){pendingPrompts.splice(0).forEach(fn=>fn());} |
| 1263 | function showApproval(a) { |
| 1264 | const d=el('div','approval'); |
| 1265 | const prefix = a.tool === 'bash' ? bashCommandPrefix(a.subject) : ''; |
| 1266 | const hasPrefix = prefix !== ''; |
| 1267 | const prefixRule = hasPrefix ? 'Bash('+prefix+')' : ''; |
| 1268 | const actions = [ |
| 1269 | '<button class="approval__btn approval__btn--allow" data-allow="true" data-session="false"><span class="approval__key">Y</span> '+__('allow')+'</button>', |
| 1270 | ]; |
| 1271 | if (hasPrefix) { |
| 1272 | actions.push('<button class="approval__btn approval__btn--allow" data-allow="true" data-session="true" data-scope="prefix"><span class="approval__key">A</span> '+escHtml(__('session')+' '+prefixRule)+'</button>'); |
| 1273 | actions.push('<button class="approval__btn approval__btn--allow" data-allow="true" data-session="true" data-persist="true" data-scope="prefix"><span class="approval__key">P</span> '+escHtml(__('persist_tool')+' '+prefixRule)+'</button>'); |
| 1274 | } else { |
| 1275 | actions.push('<button class="approval__btn approval__btn--allow" data-allow="true" data-session="true"><span class="approval__key">A</span> '+escHtml(approvalSessionLabel(a))+'</button>'); |
| 1276 | actions.push('<button class="approval__btn approval__btn--allow" data-allow="true" data-session="true" data-persist="true"><span class="approval__key">P</span> '+escHtml(approvalPersistentLabel(a))+'</button>'); |
| 1277 | } |
| 1278 | actions.push('<button class="approval__btn approval__btn--deny" data-allow="false"><span class="approval__key">N</span> '+__('deny')+'</button>'); |
| 1279 | d.innerHTML='<div class="approval__header"><svg class="approval__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg><span class="approval__title">'+__('approval_title')+'</span></div><div class="approval__subject">'+escHtml(a.tool)+(a.subject?' — '+escHtml(a.subject):'')+'</div><div class="approval__actions">'+actions.join('')+'</div>'; |
| 1280 | log.appendChild(d);scrollDown(true); |
| 1281 | // The advertised Y/A/P/N keys need a non-text-entry target (isPlainKey), but |
| 1282 | // the composer is autofocused, so in the default state every shortcut would |
| 1283 | // just type into the draft. Move focus onto the card so the keys route here — |
| 1284 | // unless the user is mid-entry (a non-empty composer draft, or any other |
| 1285 | // text field such as the session search): stealing focus there would turn |
| 1286 | // their next keystroke into an unintended approval. Cleanup hands focus back |
| 1287 | // to the composer only when it still sits inside the card, so a user who |
| 1288 | // deliberately moved elsewhere is not yanked back. |
| 1289 | d.tabIndex=-1; |
| 1290 | if(!isTextEntry(document.activeElement)||(document.activeElement===input&&!input.value))d.focus({preventScroll:true}); |
| 1291 | const cleanup=()=>{pendingPrompts=pendingPrompts.filter(f=>f!==cleanup);const hadFocus=d.contains(document.activeElement);d.remove();document.removeEventListener('keydown',onkey);if(hadFocus)input.focus();}; |
| 1292 | pendingPrompts.push(cleanup); |
| 1293 | const resolve=payload=>{post('/approve',Object.assign({id:a.id},payload));cleanup();}; |
| 1294 | d.querySelectorAll('.approval__btn').forEach(btn=>{btn.onclick=()=>resolve({allow:btn.dataset.allow==='true',session:btn.dataset.session==='true',persist:btn.dataset.persist==='true',scope:btn.dataset.scope||''});}); |
| 1295 | // A consumed shortcut must preventDefault: cleanup refocuses the composer |
| 1296 | // synchronously, and the keystroke's default text insertion runs after |
| 1297 | // keydown — without it the approving key lands in the draft as a stray char. |
| 1298 | const onkey=e=>{if(!isPlainKey(e))return; const k=e.key.toLowerCase(); let payload=null;if(k==='y'||k==='1'){payload={allow:true,session:false};}else if(k==='a'||k==='2'){payload={allow:true,session:true};}else if(hasPrefix&&k==='3'){payload={allow:true,session:true,scope:'prefix'};}else if(k==='p'||(!hasPrefix&&k==='3')||(hasPrefix&&k==='4')){payload={allow:true,session:true,persist:true,scope:hasPrefix?'prefix':''};}else if(k==='n'||k==='escape'||(!hasPrefix&&k==='4')||(hasPrefix&&k==='5')){payload={allow:false,session:false};}if(payload){e.preventDefault();resolve(payload);}}; |
| 1299 | document.addEventListener('keydown',onkey); |
| 1300 | } |
| 1301 | |
| 1302 | // ── ask ── |
| 1303 | function showAsk(ask) { |
| 1304 | const d=el('div','ask'); |
| 1305 | const cleanup=()=>{pendingPrompts=pendingPrompts.filter(f=>f!==cleanup);d.remove();}; |
| 1306 | ask.questions.forEach(q=>{ |
| 1307 | const qDiv=el('div'); qDiv.appendChild(el('div','ask__prompt',q.prompt)); |
| 1308 | const opts=el('div','ask__options'); const selected=new Set(); |
| 1309 | q.options.forEach((o,i)=>{ |
| 1310 | const opt=el('button','ask__opt'); opt.innerHTML='<div><div class="ask__opt-label">'+escHtml(o.label)+'</div>'+(o.description?'<div class="ask__opt-desc">'+escHtml(o.description)+'</div>':'')+'</div>'; |
| 1311 | opt.onclick=()=>{if(q.multi){selected.has(i)?selected.delete(i):selected.add(i);opt.classList.toggle('ask__opt--selected',selected.has(i));}else{selected.clear();selected.add(i);opts.querySelectorAll('.ask__opt').forEach((o,j)=>o.classList.toggle('ask__opt--selected',j===i));}}; |
| 1312 | opts.appendChild(opt); |
| 1313 | }); |
| 1314 | qDiv.appendChild(opts); |
| 1315 | const submit=el('button','ask__submit',__('submit')); submit.disabled=true; |
| 1316 | opts.addEventListener('click',()=>{submit.disabled=selected.size===0;}); |
| 1317 | submit.onclick=()=>{const answers=ask.questions.map(qq=>({questionId:qq.id,selected:Array.from(selected).map(i=>qq.options[i].label)}));post('/answer',{id:ask.id,answers});cleanup();}; |
| 1318 | qDiv.appendChild(submit); d.appendChild(qDiv); |
| 1319 | }); |
| 1320 | log.appendChild(d);scrollDown(true); |
| 1321 | pendingPrompts.push(cleanup); |
| 1322 | } |
| 1323 | |
| 1324 | // ── compaction ── |
| 1325 | function showCompaction(c) { |
| 1326 | const d=el('div','compaction'); |
| 1327 | if(c.summary){const head=el('div','compaction__head');head.innerHTML='<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>';head.appendChild(el('span','compaction__title',__('compacted')));head.appendChild(el('span','',c.messages+' '+__('messages')));const body=el('div','compaction__body',c.summary);head.onclick=()=>body.classList.toggle('compaction__body--open');d.appendChild(head);d.appendChild(body);} |
| 1328 | else d.textContent=__('compacting'); |
| 1329 | log.appendChild(d);scrollDown(); |
| 1330 | } |
| 1331 | |
| 1332 | // ── rewind picker ── |
| 1333 | let rewindCheckpoints = []; |
| 1334 | let rewindStage = 0, rewindSelected = 0, rewindScope = 0; |
| 1335 | const SCOPES = [ |
| 1336 | {key:'b',label:__('scope_both'),scope:'both'}, |
| 1337 | {key:'c',label:__('scope_conversation'),scope:'conversation'}, |
| 1338 | {key:'d',label:__('scope_code'),scope:'code'}, |
| 1339 | {key:'f',label:__('scope_fork'),scope:'fork'}, |
| 1340 | {key:'s',label:__('scope_sumfrom'),scope:'sumfrom'}, |
| 1341 | {key:'u',label:__('scope_sumupto'),scope:'sumupto'}, |
| 1342 | ]; |
| 1343 | |
| 1344 | function openRewindPicker() { |
| 1345 | fetch('/checkpoints').then(r=>r.json()).then(cps=>{ |
| 1346 | checkpointCount=Array.isArray(cps)?cps.length:0; updateActionAvailability(); |
| 1347 | if(!cps||cps.length===0){showNotice(__('no_checkpoints'),'warn');return;} |
| 1348 | rewindCheckpoints=cps; rewindStage=0; rewindSelected=0; rewindScope=0; |
| 1349 | renderRewindPicker(); |
| 1350 | }).catch(()=>{}); |
| 1351 | } |
| 1352 | function refreshCheckpointAvailability(){ |
| 1353 | fetch('/checkpoints').then(r=>r.json()).then(cps=>{ |
| 1354 | checkpointCount=Array.isArray(cps)?cps.length:0; |
| 1355 | updateActionAvailability(); |
| 1356 | }).catch(()=>{}); |
| 1357 | } |
| 1358 | |
| 1359 | function renderRewindPicker() { |
| 1360 | let overlay=$('#rewind-overlay'); if(overlay)overlay.remove(); |
| 1361 | overlay=el('div','rewind-overlay'); overlay.id='rewind-overlay'; |
| 1362 | const picker=el('div','rewind-picker'); |
| 1363 | |
| 1364 | if(rewindStage===0){ |
| 1365 | picker.innerHTML='<div class="rewind-picker__head"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg> '+__('rewind_title')+'</div>'; |
| 1366 | const list=el('div','rewind-picker__list'); |
| 1367 | rewindCheckpoints.forEach((cp,i)=>{ |
| 1368 | const item=el('div','rewind-picker__item'+(i===rewindSelected?' rewind-picker__item--active':'')); |
| 1369 | item.innerHTML='<span class="rewind-picker__turn">#'+cp.turn+'</span><span class="rewind-picker__prompt">'+escHtml((cp.prompt||'').slice(0,80))+'</span><span class="rewind-picker__files">'+cp.files+' '+__('files')+'</span>'; |
| 1370 | item.onclick=()=>{rewindSelected=i;renderRewindPicker();}; |
| 1371 | list.appendChild(item); |
| 1372 | }); |
| 1373 | picker.appendChild(list); |
| 1374 | picker.appendChild(el('div','rewind-picker__foot','<span>'+__('nav_jk')+'</span><span>'+__('nav_enter_esc')+'</span>')); |
| 1375 | } else { |
| 1376 | const cp=rewindCheckpoints[rewindSelected]; |
| 1377 | picker.innerHTML='<div class="rewind-picker__head"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg> '+__('action_title').replace('#{turn}',cp.turn)+'</div>'; |
| 1378 | const scopes=el('div','rewind-picker__scopes'); |
| 1379 | SCOPES.forEach((s,i)=>{ |
| 1380 | const item=el('div','rewind-picker__scope'+(i===rewindScope?' rewind-picker__scope--active':'')); |
| 1381 | item.innerHTML='<span class="rewind-picker__scope-key">'+s.key+'</span>'+s.label; |
| 1382 | item.onclick=()=>{rewindScope=i;applyRewind();}; |
| 1383 | scopes.appendChild(item); |
| 1384 | }); |
| 1385 | picker.appendChild(scopes); |
| 1386 | picker.appendChild(el('div','rewind-picker__foot','<span>'+__('nav_keys')+'</span><span>'+__('nav_apply_esc')+'</span>')); |
| 1387 | } |
| 1388 | |
| 1389 | overlay.appendChild(picker); |
| 1390 | overlay.onclick=e=>{if(e.target===overlay)overlay.remove();}; |
| 1391 | document.body.appendChild(overlay); |
| 1392 | } |
| 1393 | |
| 1394 | function applyRewind() { |
| 1395 | const cp=rewindCheckpoints[rewindSelected]; const sc=SCOPES[rewindScope]; |
| 1396 | document.getElementById('rewind-overlay')?.remove(); |
| 1397 | if(sc.scope==='fork'){post('/fork',{turn:cp.turn,name:''});} |
| 1398 | else if(sc.scope==='sumfrom'){post('/summarize',{turn:cp.turn,mode:'from'});} |
| 1399 | else if(sc.scope==='sumupto'){post('/summarize',{turn:cp.turn,mode:'upto'});} |
| 1400 | else{post('/rewind',{turn:cp.turn,scope:sc.scope});} |
| 1401 | } |
| 1402 | |
| 1403 | document.addEventListener('keydown',e=>{ |
| 1404 | const overlay=$('#rewind-overlay'); if(!overlay)return; |
| 1405 | // The picker is modal and runs in the capture phase: plain keys it owns are |
| 1406 | // swallowed before they reach the composer underneath — Enter would |
| 1407 | // otherwise both send the composer draft and advance the picker, and |
| 1408 | // letters would type into the hidden draft. Modifier chords |
| 1409 | // (copy/reload/devtools), IME composition, and non-printable keys (F5, Tab) |
| 1410 | // pass through — a bare Cmd+C must not hit a scope key and apply a rewind. |
| 1411 | if(e.ctrlKey||e.metaKey||e.altKey||e.isComposing||e.key==='Process')return; |
| 1412 | if(!(e.key==='Escape'||e.key==='Enter'||e.key==='ArrowDown'||e.key==='ArrowUp'||e.key.length===1))return; |
| 1413 | e.preventDefault();e.stopPropagation(); |
| 1414 | if(e.key==='Escape'){if(rewindStage===0)overlay.remove();else{rewindStage=0;renderRewindPicker();}return;} |
| 1415 | if(rewindStage===0){ |
| 1416 | if(e.key==='j'||e.key==='ArrowDown'){rewindSelected=Math.min(rewindSelected+1,rewindCheckpoints.length-1);renderRewindPicker();} |
| 1417 | if(e.key==='k'||e.key==='ArrowUp'){rewindSelected=Math.max(rewindSelected-1,0);renderRewindPicker();} |
| 1418 | if(e.key==='Enter'){rewindStage=1;rewindScope=0;renderRewindPicker();} |
| 1419 | } else { |
| 1420 | if(e.key==='j'||e.key==='ArrowDown'){rewindScope=Math.min(rewindScope+1,SCOPES.length-1);renderRewindPicker();} |
| 1421 | if(e.key==='k'||e.key==='ArrowUp'){rewindScope=Math.max(rewindScope-1,0);renderRewindPicker();} |
| 1422 | if(e.key==='Enter'){applyRewind();} |
| 1423 | const idx=SCOPES.findIndex(s=>s.key===e.key); if(idx>=0){rewindScope=idx;applyRewind();} |
| 1424 | } |
| 1425 | },true); |
| 1426 | |
| 1427 | // ── slash menu ── |
| 1428 | let slashOpen=false, slashIndex=0, slashFiltered=[]; |
| 1429 | function updateSlashMenu() { |
| 1430 | const v=input.value; |
| 1431 | if(!v.startsWith('/')||v.includes(' ')){closeSlashMenu();return;} |
| 1432 | const q=v.slice(1).toLowerCase(); |
| 1433 | slashFiltered=SLASH_CMDS.filter(c=>{ |
| 1434 | const hay=[c.cmd,c.sig,c.desc,slashGroupLabel(c.group)].join(' ').toLowerCase(); |
| 1435 | return hay.includes(q); |
| 1436 | }).sort((a,b)=>{ |
| 1437 | const ap=a.cmd.startsWith(q)?0:1, bp=b.cmd.startsWith(q)?0:1; |
| 1438 | return ap-bp || SLASH_CMDS.indexOf(a)-SLASH_CMDS.indexOf(b); |
| 1439 | }); |
| 1440 | if(slashFiltered.length===0){closeSlashMenu();return;} |
| 1441 | slashOpen=true; slashIndex=0; |
| 1442 | renderSlashMenu(); |
| 1443 | } |
| 1444 | function renderSlashMenu() { |
| 1445 | let menu=$('#slash-menu'); if(!menu){menu=el('div','slash-menu');menu.id='slash-menu';slashAnchor.appendChild(menu);} |
| 1446 | const query=input.value.slice(1); |
| 1447 | menu.innerHTML='<div class="slash-menu__head"><span>'+escHtml(__('command_palette'))+'</span><span class="slash-menu__query">/'+escHtml(query)+'</span></div><div class="slash-menu__list" id="slash-menu-list" role="listbox"></div><div class="slash-menu__foot"><span>'+escHtml(__('command_nav'))+'</span><span>'+slashFiltered.length+'</span></div>'; |
| 1448 | const list=$('#slash-menu-list'); |
| 1449 | let lastGroup=''; |
| 1450 | slashFiltered.forEach((c,i)=>{ |
| 1451 | if(c.group!==lastGroup){ |
| 1452 | lastGroup=c.group; |
| 1453 | const group=el('div','slash-menu__group',slashGroupLabel(c.group)); |
| 1454 | list.appendChild(group); |
| 1455 | } |
| 1456 | const item=el('button','slash-menu__item'+(i===slashIndex?' slash-menu__item--active':'')); |
| 1457 | item.type='button'; |
| 1458 | item.setAttribute('role','option'); |
| 1459 | item.setAttribute('aria-selected',i===slashIndex?'true':'false'); |
| 1460 | item.innerHTML='<span class="slash-menu__name">/'+escHtml(c.cmd)+'</span><span class="slash-menu__desc">'+escHtml(c.desc)+'</span><span class="slash-menu__pill'+(c.danger?' slash-menu__pill--danger':'')+'">'+escHtml(c.danger?__('danger'):slashGroupLabel(c.group))+'</span><span class="slash-menu__sig">'+escHtml(c.sig)+'</span>'; |
| 1461 | item.onmouseenter=()=>{slashIndex=i;renderSlashMenu();}; |
| 1462 | item.onclick=()=>{input.value='/'+c.cmd+' ';closeSlashMenu();input.focus();}; |
| 1463 | list.appendChild(item); |
| 1464 | }); |
| 1465 | const active=menu.querySelector('.slash-menu__item--active'); |
| 1466 | if(active)active.scrollIntoView({block:'nearest'}); |
| 1467 | } |
| 1468 | function closeSlashMenu(){slashOpen=false;const m=$('#slash-menu');if(m)m.remove();} |
| 1469 | function acceptSlash(){if(!slashOpen)return;const c=slashFiltered[slashIndex];if(c){input.value='/'+c.cmd+' ';}closeSlashMenu();input.focus();} |
| 1470 | |
| 1471 | // ── SSE ── |
| 1472 | const es=new EventSource('/events'); |
| 1473 | es.onopen=()=>{setConnState('connected');fetchStatus();fetchTodos();}; |
| 1474 | es.onmessage=ev=>{setConnState('connected'); |
| 1475 | const e=JSON.parse(ev.data); |
| 1476 | if(e.kind!=='retrying')clearRetrying(); |
| 1477 | switch(e.kind){ |
| 1478 | case 'turn_started': setRunning(true); clearPendingPrompts(); finalizeMsg(); toolCards={}; todosDismissed=false; break; |
| 1479 | case 'reasoning': appendReasoning(e.reasoning||e.text||''); break; |
| 1480 | case 'text': currentReasoning=null; appendText(e.text||''); break; |
| 1481 | case 'message': finalizeMsg(); break; |
| 1482 | case 'tool_dispatch': if(e.tool)renderToolDispatch(e.tool); break; |
| 1483 | case 'tool_result': if(e.tool){renderToolResult(e.tool);if(e.tool.name==='todo_write'&&!e.tool.parentId&&!e.tool.err){try{const ts=parseTodos(e.tool.args);if(ts.length){todosState=ts;renderTodoPanel();}}catch{}}} break; |
| 1484 | case 'tool_progress': if(e.tool)renderToolProgress(e.tool); break; |
| 1485 | case 'usage': |
| 1486 | if(e.usage){turnTokens=e.usage.completionTokens||0; |
| 1487 | cumulativeTokens+=e.usage.totalTokens||0; |
| 1488 | const usageCost=e.usage.cost??e.usage.costUsd; |
| 1489 | if(typeof usageCost==='number')cumulativeCost+=usageCost; |
| 1490 | cumulativeCacheHit+=e.usage.cacheHitTokens||0; |
| 1491 | cumulativeCacheMiss+=e.usage.cacheMissTokens||0; |
| 1492 | const strip=el('div','metric-strip'); |
| 1493 | [{l:__('total'),v:fmtTok(e.usage.totalTokens),c:''},{l:__('in'),v:fmtTok(e.usage.promptTokens),c:'acc'},{l:__('out'),v:fmtTok(e.usage.completionTokens),c:'ok'}].forEach(it=>{ |
| 1494 | const sp=el('span','item');sp.innerHTML=it.l+' <span class="v '+it.c+'">'+it.v+'</span>';strip.appendChild(sp); |
| 1495 | }); |
| 1496 | if(e.usage.cacheHitTokens){const sp=el('span','item');sp.innerHTML=__('cache')+' <span class="v acc">'+Math.round(e.usage.cacheHitTokens/Math.max(1,e.usage.cacheHitTokens+e.usage.cacheMissTokens)*100)+'%</span>';strip.appendChild(sp);} |
| 1497 | if(typeof usageCost==='number'&&usageCost>0){const sp=el('span','item');sp.innerHTML=__('cost')+' <span class="v">'+fmtMoney(usageCost,e.usage.currency)+'</span>';strip.appendChild(sp);} |
| 1498 | log.appendChild(strip);scrollDown(); |
| 1499 | } break; |
| 1500 | case 'notice': log.appendChild(el('div','notice'+(e.level==='warn'?' notice--warn':''),(e.level==='warn'?'! ':'')+(e.text||'')));scrollDown();break; |
| 1501 | case 'phase': finalizeMsg(); log.appendChild(el('div','phase',e.text||''));scrollDown();break; |
| 1502 | case 'approval_request': if(e.approval)showApproval(e.approval); break; |
| 1503 | case 'ask_request': if(e.ask)showAsk(e.ask); break; |
| 1504 | case 'extension_surface': if(e.extension)renderExtensionSurface(e.extension); break; |
| 1505 | case 'extension_status': if(e.extension)renderExtensionSurface(e.extension); break; |
| 1506 | case 'compaction_started': showCompaction({trigger:e.compaction?.trigger}); break; |
| 1507 | case 'compaction_done': showCompaction(e.compaction||{}); break; |
| 1508 | case 'retrying': setRetrying(e.retryAttempt,e.retryMax); break; |
| 1509 | case 'turn_done': clearPendingPrompts(); finalizeMsg(); setRunning(false); if(e.outcome==='recovery_paused'){showNotice('⏸ '+__('recovery_paused'));}else if(e.err){log.appendChild(el('div','msg--error','✗ '+e.err));scrollDown();} fetchStatus(); fetchTodos(); refreshCheckpointAvailability(); break; |
| 1510 | } |
| 1511 | }; |
| 1512 | es.onerror=()=>{ |
| 1513 | if(es.readyState===EventSource.CONNECTING){setConnState('reconnecting');} |
| 1514 | else{setConnState('disconnected');} |
| 1515 | }; |
| 1516 | |
| 1517 | // ── status polling ── |
| 1518 | function fetchStatus(){ |
| 1519 | fetch('/status').then(r=>r.json()).then(s=>{ |
| 1520 | if(s.label)statusModel.textContent=s.label; |
| 1521 | if(welcomeModel)welcomeModel.textContent=s.label||'-'; |
| 1522 | if(welcomeCwd)welcomeCwd.textContent=(s.cwd||'-').replace(/^.*\//,''); |
| 1523 | // sync mode UI without triggering POST (server is source of truth) |
| 1524 | planMode=!!s.plan; |
| 1525 | toolApprovalMode=s.toolApprovalMode || ((s.autoApproveTools??s.bypass)?'yolo':'ask'); |
| 1526 | bypassMode=toolApprovalMode==='yolo'; |
| 1527 | if(!bypassMode&&toolApprovalMode==='auto')yoloRestoreMode='auto'; |
| 1528 | updateModeButtons(); |
| 1529 | if(s.window){const pct=Math.min(100,Math.round(s.used/s.window*100));ctxFill.style.width=pct+'%';ctxFill.style.background=pct>85?'var(--warning)':pct>95?'var(--danger)':'var(--accent)';ctxUsed.textContent=fmtTok(s.used)+' tok';ctxWindow.textContent=fmtTok(s.window)+' tok';} |
| 1530 | // goal state |
| 1531 | goalText=(s.goal||'').trim(); |
| 1532 | goalActive=goalText!==''&&(s.goalStatus||'')==='running'; |
| 1533 | updateGoalUI(); |
| 1534 | // sidebar metrics |
| 1535 | const cacheTotal=(s.cacheHit||0)+(s.cacheMiss||0); |
| 1536 | $('#sm-cache').textContent=cacheTotal>0?Math.round((s.cacheHit||0)/cacheTotal*100)+'%':'—'; |
| 1537 | const lastCost=s.lastUsage?.cost??s.lastUsage?.costUsd??s.lastUsage?.totalCost; |
| 1538 | if(typeof lastCost==='number')$('#sm-cost').textContent=fmtMoney(lastCost,s.lastUsage?.currency); |
| 1539 | if(s.balance){$('#sm-balance').textContent=s.balance.display||'--';} |
| 1540 | if(s.balance)balanceInfo.textContent='💰 '+(s.balance.display||'--'); |
| 1541 | }).catch(()=>{}); |
| 1542 | } |
| 1543 | setInterval(fetchStatus,30000); |
| 1544 | |
| 1545 | // ── history ── |
| 1546 | function renderHistoryMessages(ms){ |
| 1547 | if(!ms||ms.length===0){hasVisibleHistory=false;showWelcome();updateActionAvailability();return false;} |
| 1548 | const visible = ms.some(m => { |
| 1549 | if(m.role==='user')return !!m.content; |
| 1550 | if(m.role==='assistant')return !!(m.content||m.reasoning||(m.toolCalls&&m.toolCalls.length)); |
| 1551 | if(m.role==='tool')return !!(m.content||m.toolCallId||m.toolName); |
| 1552 | return false; |
| 1553 | }); |
| 1554 | hasVisibleHistory=visible; |
| 1555 | if(!visible){showWelcome();updateActionAvailability();return false;} |
| 1556 | hideWelcome(); toolCards={}; |
| 1557 | const resultById=new Map(); |
| 1558 | ms.forEach(m=>{if(m.role==='tool'&&m.toolCallId&&!resultById.has(m.toolCallId))resultById.set(m.toolCallId,m);}); |
| 1559 | const consumed=new Set(); |
| 1560 | let seq=0; |
| 1561 | ms.forEach(m=>{ |
| 1562 | if(m.role==='system')return; |
| 1563 | if(m.role==='user'){if(m.content)addUserMsg(m.content);return;} |
| 1564 | if(m.role==='assistant'){ |
| 1565 | if(m.reasoning)appendReasoning(m.reasoning); |
| 1566 | if(m.content)appendText(m.content); |
| 1567 | if(m.content||m.reasoning)finalizeMsg(); |
| 1568 | (m.toolCalls||[]).forEach(tc=>{ |
| 1569 | const id=tc.id||'hist-tool-'+(seq++); |
| 1570 | renderToolDispatch({id,name:tc.name,args:tc.arguments||'',readOnly:false}); |
| 1571 | const result=resultById.get(tc.id); |
| 1572 | if(tc.id)consumed.add(tc.id); |
| 1573 | if(result)renderToolResult({id,name:tc.name,output:result.content||''}); |
| 1574 | }); |
| 1575 | return; |
| 1576 | } |
| 1577 | if(m.role==='tool'){ |
| 1578 | if(m.toolCallId&&consumed.has(m.toolCallId))return; |
| 1579 | const id=m.toolCallId||'hist-tool-'+(seq++); |
| 1580 | renderToolDispatch({id,name:m.toolName||'tool',args:'',readOnly:false}); |
| 1581 | renderToolResult({id,name:m.toolName||'tool',output:m.content||''}); |
| 1582 | } |
| 1583 | }); |
| 1584 | currentMsg=null;currentText=null;currentReasoning=null; scrollDown(true); updateActionAvailability(); return true; |
| 1585 | } |
| 1586 | fetch('/history').then(r=>r.json()).then(msgs=>{renderHistoryMessages(msgs);fetchTodos();}).catch(()=>{}); |
| 1587 | |
| 1588 | // ── session list ── |
| 1589 | let sessionCount=0; |
| 1590 | function sessionTitle(s){ |
| 1591 | const name=String(s.name||'').replace(/^.*\//,'').replace(/\.jsonl$/,''); |
| 1592 | return s.title||name.replace(/^\w+-/,'').replace(/T/,' ').replace(/[-_]/g,' ').slice(0,30); |
| 1593 | } |
| 1594 | function renderSessions(){ |
| 1595 | const list=$('#session-list'); if(!list)return; |
| 1596 | const count=$('#session-count'); |
| 1597 | const q=sessionFilter.trim().toLowerCase(); |
| 1598 | const filtered=sessionsCache.filter(s=>{ |
| 1599 | if(!q)return true; |
| 1600 | return [sessionTitle(s),s.name,s.path,String(s.turns||'')].some(v=>String(v||'').toLowerCase().includes(q)); |
| 1601 | }); |
| 1602 | sessionCount=sessionsCache.length; |
| 1603 | if(count)count.textContent=sessionCount?String(sessionCount):''; |
| 1604 | if(sessionsCache.length===0){list.innerHTML='<div style="padding:10px;color:var(--muted-2);font-size:12px">'+__('no_sessions')+'</div>';return;} |
| 1605 | if(filtered.length===0){list.innerHTML='<div style="padding:10px;color:var(--muted-2);font-size:12px">'+__('no_sessions')+'</div>';return;} |
| 1606 | list.innerHTML=''; |
| 1607 | filtered.forEach(s=>{ |
| 1608 | const item=el('div','session-item'+(s.current?' session-item--active':'')); |
| 1609 | const title=sessionTitle(s); |
| 1610 | const meta=s.turns?s.turns+' turns':''; |
| 1611 | item.innerHTML='<svg class="session-item__icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><div class="session-item__body"><div class="session-item__title">'+escHtml(title)+'</div><div class="session-item__meta">'+escHtml(meta)+'</div></div><button type="button" class="session-del" data-name="'+escAttr(s.name)+'" title="'+escAttr(__('delete_session'))+'">×</button>'; |
| 1612 | item.onclick=ev=>{if(ev.target.closest('.session-del')||running||s.current)return;post('/resume',{path:s.path}).then(()=>{log.innerHTML='';log.appendChild(welcome);showWelcome();hasVisibleHistory=false;checkpointCount=0;todosDismissed=false;resetCumulativeStats();loadSessions();updateActionAvailability();setTimeout(()=>fetch('/history').then(r=>r.json()).then(msgs=>{renderHistoryMessages(msgs);fetchTodos();refreshCheckpointAvailability();}),300);});}; |
| 1613 | list.appendChild(item); |
| 1614 | }); |
| 1615 | } |
| 1616 | function loadSessions(){ |
| 1617 | fetch('/sessions').then(r=>r.json()).then(ss=>{ |
| 1618 | sessionsCache=Array.isArray(ss)?ss:[]; |
| 1619 | renderSessions(); |
| 1620 | }).catch(()=>{const list=$('#session-list');if(list)list.innerHTML='<div style="padding:10px;color:var(--muted-2);font-size:12px">'+__('error_loading')+'</div>';}); |
| 1621 | } |
| 1622 | loadSessions(); |
| 1623 | |
| 1624 | // ── branches panel ── |
| 1625 | function branchValue(b, ...keys) { |
| 1626 | for(const k of keys){ if(b && b[k] != null && b[k] !== '') return b[k]; } |
| 1627 | return ''; |
| 1628 | } |
| 1629 | function branchTitle(b) { |
| 1630 | return branchValue(b,'custom_title','CustomTitle','name','Name','topic_title','TopicTitle','id','ID') || 'branch'; |
| 1631 | } |
| 1632 | function renderBranches(data) { |
| 1633 | const tree=$('#branches-tree'), list=$('#branches-list'); |
| 1634 | if(!tree||!list)return; |
| 1635 | tree.textContent=data?.tree||'branches: none'; |
| 1636 | const branches=Array.isArray(data?.branches)?data.branches:[]; |
| 1637 | if(branches.length===0){ |
| 1638 | list.innerHTML='<div class="empty-note">'+escHtml(__('no_branches'))+'</div>'; |
| 1639 | return; |
| 1640 | } |
| 1641 | list.innerHTML=''; |
| 1642 | branches.forEach(b=>{ |
| 1643 | const id=String(branchValue(b,'id','ID')); |
| 1644 | const preview=String(branchValue(b,'preview','Preview')); |
| 1645 | const turns=branchValue(b,'turns','Turns'); |
| 1646 | const model=String(branchValue(b,'model','Model')); |
| 1647 | const item=el('div','branch-item'); |
| 1648 | const meta=[turns?turns+' turns':'',model,preview].filter(Boolean).join(' · '); |
| 1649 | item.innerHTML='<div><div class="branch-item__title">'+escHtml(branchTitle(b))+'</div><div class="branch-item__meta">'+escHtml(meta||id)+'</div></div><button class="branch-item__btn" data-branch-id="'+escAttr(id)+'">'+escHtml(__('switch'))+'</button>'; |
| 1650 | list.appendChild(item); |
| 1651 | }); |
| 1652 | } |
| 1653 | function openBranches() { |
| 1654 | const modal=$('#branches-modal'); |
| 1655 | if(!modal)return; |
| 1656 | $('#branches-tree').textContent=__('loading'); |
| 1657 | $('#branches-list').innerHTML=''; |
| 1658 | modal.style.display='flex'; |
| 1659 | fetch('/branches').then(r=>r.json()).then(renderBranches).catch(()=>{ |
| 1660 | $('#branches-tree').textContent=__('error_loading'); |
| 1661 | }); |
| 1662 | } |
| 1663 | function closeBranches(){const m=$('#branches-modal');if(m)m.style.display='none';} |
| 1664 | |
| 1665 | // ── models panel ── |
| 1666 | function renderModelsPanel(data) { |
| 1667 | const list=$('#models-list'); |
| 1668 | if(!list)return; |
| 1669 | const models=Array.isArray(data?.models)?data.models:[]; |
| 1670 | if(models.length===0){list.innerHTML='<div class="empty-note">'+escHtml(__('error_loading'))+'</div>';return;} |
| 1671 | list.innerHTML=''; |
| 1672 | models.forEach(m=>{ |
| 1673 | const active=!!m.active; |
| 1674 | const item=el('div','model-item'+(active?' model-item--active':'')); |
| 1675 | item.innerHTML='<div><div class="model-item__title">'+escHtml(m.ref||'')+'</div><div class="model-item__meta">'+escHtml([m.kind,m.default?'default':''].filter(Boolean).join(' · '))+'</div></div>'; |
| 1676 | if(active){ |
| 1677 | item.innerHTML+='<span class="model-item__status model-item__status--active">'+escHtml(__('active'))+'</span>'; |
| 1678 | }else{ |
| 1679 | item.innerHTML+='<button class="branch-item__btn" data-model-ref="'+escAttr(m.ref||'')+'">'+escHtml(__('use_model'))+'</button>'; |
| 1680 | } |
| 1681 | list.appendChild(item); |
| 1682 | }); |
| 1683 | } |
| 1684 | function openModels() { |
| 1685 | const modal=$('#models-modal'); |
| 1686 | if(!modal)return; |
| 1687 | $('#models-list').innerHTML='<div class="empty-note">'+escHtml(__('loading'))+'</div>'; |
| 1688 | modal.style.display='flex'; |
| 1689 | fetch('/models').then(r=>r.json()).then(renderModelsPanel).catch(()=>{ |
| 1690 | $('#models-list').innerHTML='<div class="empty-note">'+escHtml(__('error_loading'))+'</div>'; |
| 1691 | }); |
| 1692 | } |
| 1693 | function closeModels(){const m=$('#models-modal');if(m)m.style.display='none';} |
| 1694 | |
| 1695 | // ── delete confirmation ── |
| 1696 | function openDeleteSession(name) { |
| 1697 | if(!name)return; |
| 1698 | pendingDeleteSession=name; |
| 1699 | const title=$('#delete-session-title'); |
| 1700 | if(title)title.textContent=name; |
| 1701 | const modal=$('#delete-modal'); |
| 1702 | if(modal)modal.style.display='flex'; |
| 1703 | } |
| 1704 | function closeDeleteSession() { |
| 1705 | pendingDeleteSession=null; |
| 1706 | const modal=$('#delete-modal'); |
| 1707 | if(modal)modal.style.display='none'; |
| 1708 | } |
| 1709 | |
| 1710 | // ── input handling ── |
| 1711 | async function syncModeBeforeSubmit(){ |
| 1712 | await post('/plan',{on:planMode}); |
| 1713 | await post('/tool-approval-mode',{mode:bypassMode?'yolo':toolApprovalMode}); |
| 1714 | // If goalMode is on, we'll send as /goal command — no separate POST needed |
| 1715 | } |
| 1716 | async function send(){ |
| 1717 | const v=input.value.trim(); if(!v)return; |
| 1718 | if(v==='/reload'){ |
| 1719 | input.value='';input.style.height='';closeSlashMenu(); |
| 1720 | showNotice(__('extensions_reloading')); |
| 1721 | const response=await post('/extensions/reload',{}); |
| 1722 | if(response.ok){showNotice(__('extensions_reloaded'));fetchStatus();} |
| 1723 | else{showNotice((await response.text()).trim()||__('extensions_reload_failed'),'warn');} |
| 1724 | return; |
| 1725 | } |
| 1726 | await syncModeBeforeSubmit(); |
| 1727 | let submitInput=v; |
| 1728 | if(goalMode && !v.startsWith('/goal')){ |
| 1729 | // Send as /goal command for goal-draft mode |
| 1730 | submitInput='/goal '+v; |
| 1731 | goalMode=false; |
| 1732 | updateGoalUI(); |
| 1733 | } else if(goalMode){ |
| 1734 | // User typed /goal manually, exit goal-draft mode |
| 1735 | goalMode=false; |
| 1736 | updateGoalUI(); |
| 1737 | } |
| 1738 | addUserMsg(v); |
| 1739 | post('/submit',{input:submitInput}).then(r=>{if(r.ok&&r.status===204){fetchStatus();loadSessions();}}); |
| 1740 | input.value='';input.style.height='';closeSlashMenu(); |
| 1741 | } |
| 1742 | |
| 1743 | input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,140)+'px';updateSlashMenu();}); |
| 1744 | input.addEventListener('keydown',e=>{ |
| 1745 | // slash menu nav |
| 1746 | if(slashOpen){ |
| 1747 | if(e.key==='ArrowDown'){e.preventDefault();slashIndex=Math.min(slashIndex+1,slashFiltered.length-1);renderSlashMenu();return;} |
| 1748 | if(e.key==='ArrowUp'){e.preventDefault();slashIndex=Math.max(slashIndex-1,0);renderSlashMenu();return;} |
| 1749 | if(e.key==='Tab'||e.key==='Enter'){e.preventDefault();acceptSlash();return;} |
| 1750 | if(e.key==='Escape'){e.preventDefault();closeSlashMenu();return;} |
| 1751 | } |
| 1752 | // main keys |
| 1753 | if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();return;} |
| 1754 | if(e.key==='Escape'){ |
| 1755 | if(goalMode&&!running){goalMode=false;updateGoalUI();input.value='';closeSlashMenu();return;} |
| 1756 | if(running){post('/cancel');return;} |
| 1757 | // double-Esc for rewind |
| 1758 | if(input.value===''){if(escTimer){clearTimeout(escTimer);escTimer=null;openRewindPicker();}else{escTimer=setInterval(()=>escTimer=null,600);}return;} |
| 1759 | } |
| 1760 | }); |
| 1761 | |
| 1762 | // shift+tab mode cycle |
| 1763 | document.addEventListener('keydown',e=>{ |
| 1764 | if(e.target===input&&e.key==='Tab'&&e.shiftKey){e.preventDefault();cycleMode();return;} |
| 1765 | if(e.target===input&&(e.key==='y'||e.key==='Y')&&(e.ctrlKey||e.metaKey)&&!e.altKey&&!e.shiftKey){e.preventDefault();toggleYolo();return;} |
| 1766 | if(e.key==='/'&&isPlainKey(e)){e.preventDefault();input.focus();return;} |
| 1767 | }); |
| 1768 | |
| 1769 | // mode helpers — Shift+Tab toggles Plan; Ctrl+Y toggles YOLO separately. |
| 1770 | function updateModeButtons(){ |
| 1771 | const auto=!bypassMode&&toolApprovalMode==='auto'; |
| 1772 | $('#btn-auto').classList.toggle('toolbar__btn--ok',auto); |
| 1773 | $('#btn-plan').classList.toggle('toolbar__btn--active',planMode); |
| 1774 | $('#btn-bypass').classList.toggle('toolbar__btn--danger',bypassMode); |
| 1775 | $('#btn-goal').classList.toggle('toolbar__btn--goal',goalMode&&!goalActive); |
| 1776 | $('#btn-goal').classList.toggle('toolbar__btn--goal-active',goalActive); |
| 1777 | } |
| 1778 | function updateGoalUI(){ |
| 1779 | updateModeButtons(); |
| 1780 | const bar=$('#goal-active-bar'); |
| 1781 | if(goalActive&&goalText){ |
| 1782 | bar.style.display=''; |
| 1783 | $('#goal-chip-text').textContent=goalText; |
| 1784 | } else { |
| 1785 | bar.style.display='none'; |
| 1786 | } |
| 1787 | input.placeholder=goalMode?__('goal_placeholder'):__('placeholder'); |
| 1788 | } |
| 1789 | async function setToolApprovalMode(mode){toolApprovalMode=mode;bypassMode=mode==='yolo';updateModeButtons();await post('/tool-approval-mode',{mode});} |
| 1790 | async function setPlan(on){planMode=on;updateModeButtons();await post('/plan',{on});} |
| 1791 | async function cycleMode(){if(goalMode){goalMode=false;updateGoalUI();return;}await setPlan(!planMode);setTimeout(fetchStatus,200);} |
| 1792 | async function toggleYolo(){if(bypassMode){const restore=yoloRestoreMode==='auto'?'auto':'ask';await setToolApprovalMode(restore);}else{yoloRestoreMode=toolApprovalMode==='auto'?'auto':'ask';await setToolApprovalMode('yolo');}setTimeout(fetchStatus,200);} |
| 1793 | async function setMode(m){if(m==='auto')await setToolApprovalMode('auto');else if(m==='plan')await setPlan(!planMode);else if(m==='yolo')await toggleYolo();else if(m==='goal')toggleGoalMode();} |
| 1794 | function toggleGoalMode(){ |
| 1795 | if(goalActive){ |
| 1796 | // Clear active goal |
| 1797 | post('/goal',{goal:''}).then(()=>{goalActive=false;goalText='';updateGoalUI();fetchStatus();}); |
| 1798 | return; |
| 1799 | } |
| 1800 | if(goalMode){ |
| 1801 | goalMode=false; |
| 1802 | updateGoalUI(); |
| 1803 | } else { |
| 1804 | goalMode=true; |
| 1805 | updateGoalUI(); |
| 1806 | input.focus(); |
| 1807 | } |
| 1808 | } |
| 1809 | |
| 1810 | // ── todo panel ── |
| 1811 | let todosState=[],todosDismissed=false; |
| 1812 | function parseTodos(args){ |
| 1813 | try{const a=JSON.parse(args);return Array.isArray(a.todos)?a.todos:[];} |
| 1814 | catch{return[];} |
| 1815 | } |
| 1816 | function todoStatusLabel(s){ |
| 1817 | switch(String(s||'').trim()){ |
| 1818 | case'completed':return '✓'; |
| 1819 | case'in_progress':return '▶'; |
| 1820 | default:return '○'; |
| 1821 | } |
| 1822 | } |
| 1823 | function hasIncompleteTodos(ts){return ts.some(t=>String(t.status||'').trim()!=='completed');} |
| 1824 | function renderTodoPanel(){ |
| 1825 | const panel=$('#todo-panel'),list=$('#todos-list'),badge=$('#todos-badge'); |
| 1826 | const summary=$('#todos-summary'),dismiss=$('#todos-dismiss'),title=$('#todos-title'); |
| 1827 | if(!panel||todosDismissed){panel&&(panel.classList.remove('todos--visible'));return;} |
| 1828 | if(!todosState.length){panel.classList.remove('todos--visible');return;} |
| 1829 | const done=todosState.filter(t=>String(t.status||'').trim()==='completed').length; |
| 1830 | const total=todosState.length; |
| 1831 | const current=todosState.find(t=>String(t.status||'').trim()==='in_progress'); |
| 1832 | const allDone=done===total; |
| 1833 | badge.textContent=done+'/'+total; |
| 1834 | summary.textContent=(current?.activeForm||current?.content||todosState[todosState.length-1]?.content||'').slice(0,60); |
| 1835 | dismiss.style.display=allDone?'':'none'; |
| 1836 | if(allDone)panel.classList.add('todos--collapsed'); |
| 1837 | list.innerHTML=''; |
| 1838 | todosState.forEach((t,i)=>{ |
| 1839 | const st=String(t.status||'').trim(); |
| 1840 | const li=el('li','todos__item todos__item--'+st+(t.level?' todos__item--sub':'')); |
| 1841 | li.innerHTML='<span class="todos__status todos__status--'+st+'">'+todoStatusLabel(st)+'</span><span class="todos__text">'+escHtml((st==='in_progress'&&t.activeForm)?t.activeForm:t.content)+'</span>'; |
| 1842 | list.appendChild(li); |
| 1843 | }); |
| 1844 | panel.classList.add('todos--visible'); |
| 1845 | } |
| 1846 | function fetchTodos(){ |
| 1847 | fetch('/todos').then(r=>r.json()).then(ts=>{ |
| 1848 | if(Array.isArray(ts)){todosState=ts;renderTodoPanel();} |
| 1849 | }).catch(()=>{}); |
| 1850 | } |
| 1851 | $('#todos-head').onclick=function(e){ |
| 1852 | if(e.target.closest('.todos__dismiss')){todosDismissed=true;renderTodoPanel();return;} |
| 1853 | const panel=$('#todo-panel');panel.classList.toggle('todos--collapsed'); |
| 1854 | }; |
| 1855 | |
| 1856 | // toolbar buttons |
| 1857 | btnSend.onclick=()=>void send(); |
| 1858 | btnStop.onclick=()=>post('/cancel'); |
| 1859 | $('#btn-auto').onclick=()=>void setMode('auto'); |
| 1860 | $('#btn-plan').onclick=()=>void setPlan(!planMode); |
| 1861 | $('#btn-bypass').onclick=()=>void toggleYolo(); |
| 1862 | $('#btn-goal').onclick=()=>toggleGoalMode(); |
| 1863 | $('#goal-chip').onclick=()=>toggleGoalMode(); |
| 1864 | $('#btn-new').onclick=()=>{if(running)return;post('/new').then(()=>{log.innerHTML='';log.appendChild(welcome);showWelcome();hasVisibleHistory=false;checkpointCount=0;todosState=[];todosDismissed=false;renderTodoPanel();resetCumulativeStats();loadSessions();updateActionAvailability();});}; |
| 1865 | $('#btn-compact').onclick=()=>{if(actionDisabled('btn-compact')){showNotice(__('no_conversation'),'warn');return;}post('/compact');}; |
| 1866 | $('#btn-rewind').onclick=()=>{if(actionDisabled('btn-rewind')){showNotice(__('no_checkpoints'),'warn');return;}openRewindPicker();}; |
| 1867 | $('#btn-tree').onclick=()=>openBranches(); |
| 1868 | $('#btn-models').onclick=()=>openModels(); |
| 1869 | $('#btn-stats').onclick=()=>openStats(); |
| 1870 | $('#branches-modal-close').onclick=()=>closeBranches(); |
| 1871 | $('#branches-modal').onclick=e=>{if(e.target===e.currentTarget)closeBranches();}; |
| 1872 | $('#branches-list').onclick=e=>{ |
| 1873 | const btn=e.target.closest('[data-branch-id]'); |
| 1874 | if(!btn||running)return; |
| 1875 | const id=btn.dataset.branchId; |
| 1876 | if(!id)return; |
| 1877 | closeBranches(); |
| 1878 | post('/submit',{input:'/switch '+id}).then(()=>{setTimeout(()=>{log.innerHTML='';log.appendChild(welcome);showWelcome();loadSessions();fetch('/history').then(r=>r.json()).then(msgs=>{renderHistoryMessages(msgs);fetchTodos();refreshCheckpointAvailability();});},300);}); |
| 1879 | }; |
| 1880 | $('#models-modal-close').onclick=()=>closeModels(); |
| 1881 | $('#models-modal').onclick=e=>{if(e.target===e.currentTarget)closeModels();}; |
| 1882 | $('#models-list').onclick=e=>{ |
| 1883 | const btn=e.target.closest('[data-model-ref]'); |
| 1884 | if(!btn||running)return; |
| 1885 | const ref=btn.dataset.modelRef; |
| 1886 | if(!ref)return; |
| 1887 | closeModels(); |
| 1888 | post('/submit',{input:'/model '+ref}).then(()=>setTimeout(fetchStatus,300)); |
| 1889 | }; |
| 1890 | $('#session-search').addEventListener('input',e=>{sessionFilter=e.target.value;renderSessions();}); |
| 1891 | |
| 1892 | // ── mobile sidebar toggle ── |
| 1893 | const sidebar=$('.sidebar'), overlay=$('#sidebar-overlay'), menuBtn=$('#menu-btn'); |
| 1894 | function openSidebar(){sidebar.classList.add('sidebar--open');overlay.classList.add('sidebar-overlay--visible');menuBtn.style.opacity='0';} |
| 1895 | function closeSidebar(){sidebar.classList.remove('sidebar--open');overlay.classList.remove('sidebar-overlay--visible');menuBtn.style.opacity='';} |
| 1896 | menuBtn.onclick=()=>openSidebar(); |
| 1897 | overlay.onclick=()=>closeSidebar(); |
| 1898 | document.addEventListener('keydown',e=>{if(e.key==='Escape'&&sidebar.classList.contains('sidebar--open'))closeSidebar();}); |
| 1899 | |
| 1900 | // ── cumulative stats ── |
| 1901 | // Session-scoped counters accumulated while this page is open. They reset on |
| 1902 | // session rotation (/new, /resume) so the stats card labeled "session cost" |
| 1903 | // never quietly turns into an all-sessions running total. |
| 1904 | let cumulativeTokens=0, cumulativeCost=0, cumulativeCacheHit=0, cumulativeCacheMiss=0; |
| 1905 | function resetCumulativeStats(){cumulativeTokens=0;cumulativeCost=0;cumulativeCacheHit=0;cumulativeCacheMiss=0;} |
| 1906 | function openStats(){ |
| 1907 | const modal=$('#stats-modal'); |
| 1908 | if(!modal)return; |
| 1909 | // populate from latest status |
| 1910 | fetch('/status').then(r=>r.json()).then(s=>{ |
| 1911 | $('#stats-model').textContent=s.label||'-'; |
| 1912 | $('#stats-sessions').textContent=sessionCount||'0'; |
| 1913 | const total=cumulativeTokens||s.used||0; |
| 1914 | $('#stats-total-tokens').textContent=total>=1000?fmtTok(total):String(total); |
| 1915 | const hit=cumulativeCacheHit||s.cacheHit||0, miss=cumulativeCacheMiss||s.cacheMiss||0; |
| 1916 | const rate=hit+miss>0?Math.round(hit/(hit+miss)*100)+'%':'0%'; |
| 1917 | $('#stats-cache-rate').textContent=rate; |
| 1918 | // No single-request fallback here: rendering lastUsage.cost (one turn) |
| 1919 | // under the session-cost label displayed one turn's spend as the session's. |
| 1920 | const cost=cumulativeCost; |
| 1921 | $('#stats-total-cost').textContent=cost>0?fmtMoney(cost,s.lastUsage?.currency):'-'; |
| 1922 | $('#stats-balance').textContent=s.balance?.display||'-'; |
| 1923 | if(s.window){const pct=Math.min(100,Math.round(s.used/s.window*100));$('#stats-ctx-fill').style.width=pct+'%';$('#stats-ctx-used').textContent=fmtTok(s.used)+' tok';$('#stats-ctx-window').textContent=fmtTok(s.window)+' tok';} |
| 1924 | }).catch(()=>{}); |
| 1925 | modal.style.display='flex'; |
| 1926 | } |
| 1927 | $('#stats-modal-close').onclick=()=>{const m=$('#stats-modal');if(m)m.style.display='none';}; |
| 1928 | $('#stats-modal').onclick=e=>{if(e.target===e.currentTarget){const m=$('#stats-modal');if(m)m.style.display='none';}}; |
| 1929 | |
| 1930 | // ── session delete ── |
| 1931 | document.addEventListener('click',e=>{ |
| 1932 | const del=e.target.closest('.session-del'); |
| 1933 | if(!del)return; |
| 1934 | e.stopPropagation(); |
| 1935 | const name=del.dataset.name; |
| 1936 | const target=sessionsCache.find(s=>s.name===name); |
| 1937 | if(target&&target.current){showNotice(__('cannot_delete_active'),'warn');return;} |
| 1938 | openDeleteSession(name); |
| 1939 | }); |
| 1940 | $('#delete-modal-close').onclick=()=>closeDeleteSession(); |
| 1941 | $('#delete-cancel').onclick=()=>closeDeleteSession(); |
| 1942 | $('#delete-modal').onclick=e=>{if(e.target===e.currentTarget)closeDeleteSession();}; |
| 1943 | $('#delete-confirm').onclick=()=>{ |
| 1944 | const name=pendingDeleteSession; |
| 1945 | if(!name)return; |
| 1946 | closeDeleteSession(); |
| 1947 | post('/delete-session',{name}).then(async r=>{ |
| 1948 | if(!r.ok){showNotice((await r.text()).trim()||('HTTP '+r.status),'warn');} |
| 1949 | loadSessions(); |
| 1950 | }).catch(()=>showNotice(__('delete_failed'),'warn')); |
| 1951 | }; |
| 1952 | |
| 1953 | // welcome examples |
| 1954 | $$('.welcome__ex').forEach(btn=>{btn.onclick=()=>{input.value=btn.dataset.prompt;send();};}); |
| 1955 | |
| 1956 | // initial fetch |
| 1957 | fetchStatus(); |
| 1958 | refreshCheckpointAvailability(); |
| 1959 | updateActionAvailability(); |
| 1960 | </script> |
| 1961 | </body> |
| 1962 | </html> |
| 1963 |