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