返回 DeepSeek-Reasonix
index.html
根目录 / internal / serve / index.html
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&times;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">&times;</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">&times;</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">&#8250;</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">&times;</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">&times;</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">&times;</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">&times;</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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
993 function escAttr(s) { return escHtml(s).replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
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'))+'">&times;</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
1963 lines HTML