@kevin0181/rcodex 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,7 +18,7 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
18
18
  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
19
19
  text-rendering:geometricPrecision;-webkit-text-size-adjust:100%}
20
20
 
21
- /* ?�?� Header ?�?� */
21
+ /* Header */
22
22
  .hdr{height:46px;display:flex;align-items:center;justify-content:space-between;
23
23
  padding:0 14px 0 10px;background:rgba(13,13,18,.98);border-bottom:1px solid var(--b1);
24
24
  position:relative;z-index:100;flex-shrink:0;gap:8px}
@@ -40,10 +40,10 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
40
40
  box-shadow:0 0 5px var(--gr);animation:blink 2s infinite}
41
41
  @keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}
42
42
 
43
- /* ?�?� Layout ?�?� */
43
+ /* Layout */
44
44
  .layout{display:flex;flex:1;overflow:hidden;position:relative}
45
45
 
46
- /* ?�?� Sidebar shell ?�?� */
46
+ /* Sidebar shell */
47
47
  .sb{position:absolute;left:0;top:0;bottom:0;width:280px;
48
48
  background:var(--s1);border-right:1px solid var(--b1);
49
49
  display:flex;flex-direction:column;z-index:60;
@@ -62,13 +62,13 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
62
62
  .sb-x:hover{background:var(--s2);color:var(--tx)}
63
63
  .sb-body{flex:1;overflow-y:auto}
64
64
 
65
- /* ?�?� Scrollbars (visible, styled) ?�?� */
65
+ /* Scrollbars */
66
66
  .sb-body::-webkit-scrollbar,.mn-body::-webkit-scrollbar{width:5px}
67
67
  .sb-body::-webkit-scrollbar-track,.mn-body::-webkit-scrollbar-track{background:transparent}
68
68
  .sb-body::-webkit-scrollbar-thumb,.mn-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}
69
69
  .sb-body::-webkit-scrollbar-thumb:hover,.mn-body::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.28)}
70
70
 
71
- /* ?�?� Home nav items ?�?� */
71
+ /* Home nav items */
72
72
  .nav-item{display:flex;align-items:center;gap:11px;padding:11px 16px;
73
73
  cursor:pointer;transition:background .12s;user-select:none}
74
74
  .nav-item:hover{background:var(--s2)}
@@ -82,7 +82,7 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
82
82
  background:rgba(99,102,241,.15);color:var(--bl2);border:1px solid rgba(99,102,241,.2)}
83
83
  .sb-sep{height:1px;background:var(--b1);margin:4px 0}
84
84
 
85
- /* ?�?� Provider type list ?�?� */
85
+ /* Provider type list */
86
86
  .ptype{display:flex;align-items:center;gap:10px;padding:10px 14px;
87
87
  cursor:pointer;transition:background .12s;border-radius:0}
88
88
  .ptype:hover{background:var(--s2)}
@@ -96,7 +96,7 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
96
96
  cursor:pointer;transition:all .15s;white-space:nowrap;flex-shrink:0}
97
97
  .add-btn:hover{background:rgba(99,102,241,.22);border-color:var(--bl)}
98
98
 
99
- /* ?�?� Connected accounts ?�?� */
99
+ /* Connected accounts */
100
100
  .sb-section{font-size:9px;font-weight:700;text-transform:uppercase;
101
101
  letter-spacing:.1em;color:var(--mu);padding:12px 16px 6px}
102
102
  .acc-item{display:flex;align-items:center;gap:10px;padding:9px 14px;
@@ -119,7 +119,7 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
119
119
  justify-content:center;transition:all .12s}
120
120
  .del-btn:hover{background:rgba(239,68,68,.1);color:var(--rd)}
121
121
 
122
- /* ?�?� Auth method cards ?�?� */
122
+ /* Auth method cards */
123
123
  .auth-cards{display:flex;flex-direction:column;gap:8px;padding:12px 14px}
124
124
  .auth-card{border:1px solid var(--b1);border-radius:11px;padding:12px 14px;
125
125
  cursor:pointer;transition:all .15s;background:var(--s2)}
@@ -132,7 +132,7 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
132
132
  border-radius:8px;padding:8px 11px;font-size:10px;color:#fcd34d;
133
133
  margin:0 14px 10px;line-height:1.5}
134
134
 
135
- /* ?�?� Auth form ?�?� */
135
+ /* Auth form */
136
136
  .auth-form{padding:12px 14px;display:flex;flex-direction:column;gap:8px}
137
137
  .form-label{font-size:10px;color:var(--mu)}
138
138
  .form-input{width:100%;padding:8px 11px;border-radius:8px;background:var(--bg);
@@ -148,7 +148,7 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
148
148
  color:#fff;font-size:11px;font-weight:600;cursor:pointer}
149
149
  .form-submit:hover{opacity:.9}
150
150
 
151
- /* ?�?� Canvas ?�?� */
151
+ /* Canvas */
152
152
  .ws{position:relative;flex:1;overflow:hidden;cursor:default;user-select:none;
153
153
  background-color:var(--bg);
154
154
  background-image:radial-gradient(circle,var(--b1) 1px,transparent 1px);
@@ -162,8 +162,8 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
162
162
  animation:sdash .5s linear infinite}
163
163
  @keyframes sdash{to{stroke-dashoffset:-22}}
164
164
 
165
- /* ?�?� Canvas nodes ?�?� */
166
- .nd{position:absolute;width:215px;background:var(--s1);border:1px solid var(--b1);
165
+ /* Canvas nodes */
166
+ .nd{position:absolute;width:260px;background:var(--s1);border:1px solid var(--b1);
167
167
  border-radius:13px;box-shadow:0 6px 24px rgba(0,0,0,.5);
168
168
  transition:border-color .2s,box-shadow .2s}
169
169
  .nd:hover{border-color:var(--b2)}
@@ -174,7 +174,9 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
174
174
  .nh:active{cursor:grabbing}
175
175
  .nic{width:26px;height:26px;border-radius:7px;display:flex;align-items:center;
176
176
  justify-content:center;font-size:13px;flex-shrink:0}
177
- .nn{font-size:11px;font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
177
+ .nn{font-size:11px;font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
178
+ .acct-badge{font-size:9px;font-weight:800;color:var(--bl2);background:rgba(99,102,241,.16);
179
+ border:1px solid rgba(99,102,241,.28);border-radius:5px;padding:1px 5px;line-height:1.4;flex-shrink:0}
178
180
  .bk{font-size:9px;padding:2px 6px;border-radius:5px;font-weight:600;white-space:nowrap;flex-shrink:0}
179
181
  .bk-on{background:rgba(34,197,94,.12);color:var(--gr);border:1px solid rgba(34,197,94,.2)}
180
182
  .bk-off{background:rgba(96,96,128,.1);color:var(--mu);border:1px solid var(--b1)}
@@ -192,7 +194,7 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
192
194
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:.65;
193
195
  letter-spacing:.01em}
194
196
 
195
- /* ?�?� Ports ?�?� */
197
+ /* Ports */
196
198
  .po{position:absolute;right:-11px;top:50%;transform:translateY(-50%);
197
199
  width:22px;height:22px;border-radius:50%;background:var(--s1);
198
200
  border:2px solid var(--b2);cursor:crosshair;z-index:15;
@@ -210,7 +212,7 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
210
212
  .pi.live{border-color:var(--gr)}.pi.live::after{background:var(--gr)}
211
213
  .pi.acc{border-color:var(--bl);box-shadow:0 0 0 4px rgba(99,102,241,.2)}.pi.acc::after{background:var(--bl)}
212
214
 
213
- /* ?�?� OUT node ?�?� */
215
+ /* OUT node */
214
216
  .out-node{width:240px}
215
217
  .out-ic{width:26px;height:26px;border-radius:7px;
216
218
  background:linear-gradient(135deg,#6366f1,#8b5cf6);
@@ -233,7 +235,7 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
233
235
  .oi-x:hover{color:var(--rd)}
234
236
  .out-empty{font-size:10px;color:var(--mu);text-align:center;padding:12px 0;line-height:1.8}
235
237
 
236
- /* ?�?� Zoom ?�?� */
238
+ /* Zoom */
237
239
  .zbar{position:absolute;bottom:18px;right:18px;display:flex;align-items:center;gap:3px;
238
240
  background:var(--s1);border:1px solid var(--b1);border-radius:8px;padding:3px 5px;
239
241
  z-index:50;box-shadow:0 4px 16px rgba(0,0,0,.35)}
@@ -245,7 +247,7 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
245
247
  .hint{position:absolute;bottom:18px;left:18px;font-size:10px;color:var(--mu);
246
248
  pointer-events:none;z-index:50;line-height:1.8}
247
249
 
248
- /* ?�?� Monitor node ?�?� */
250
+ /* Monitor node */
249
251
  .mn{position:absolute;width:620px;background:var(--s1);border:1px solid var(--b1);
250
252
  border-radius:13px;box-shadow:0 8px 40px rgba(0,0,0,.65);z-index:20;min-width:320px;min-height:160px}
251
253
  .mn-rs-e{position:absolute;right:-4px;top:12px;bottom:12px;width:8px;cursor:ew-resize;z-index:21}
@@ -321,21 +323,21 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
321
323
  .mn-empty{display:flex;align-items:center;justify-content:center;height:100%;
322
324
  font-size:11px;color:var(--mu)}
323
325
 
324
- /* ?�?� Mode switcher ?�?� */
326
+ /* Mode switcher */
325
327
  .mode-sw{display:flex;align-items:center;background:var(--s2);border:1px solid var(--b1);border-radius:7px;padding:2px;gap:1px}
326
328
  .ms-btn{padding:3px 10px;border-radius:5px;border:none;font-size:10px;font-weight:600;cursor:pointer;
327
329
  transition:all .15s;color:var(--mu);background:transparent;white-space:nowrap}
328
330
  .ms-btn.active{background:var(--bl);color:#fff}
329
331
  .ms-btn:not(.active):hover{color:var(--tx);background:rgba(255,255,255,.06)}
330
332
 
331
- /* ?�?� OUT node bypass warning ?�?� */
333
+ /* OUT node bypass warning */
332
334
  .out-bypass{font-size:9px;color:#fcd34d;background:rgba(245,158,11,.1);
333
335
  border-top:1px solid rgba(245,158,11,.25);padding:5px 10px;text-align:center;
334
336
  border-radius:0 0 12px 12px;letter-spacing:.01em}
335
337
  .nd.bypassed{border-color:rgba(245,158,11,.5)!important;
336
338
  box-shadow:0 0 0 1px rgba(245,158,11,.1),0 6px 24px rgba(0,0,0,.5)!important}
337
339
 
338
- /* ?�?� Toast ?�?� */
340
+ /* Toast */
339
341
  .toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);
340
342
  background:var(--s1);border:1px solid var(--b1);border-radius:8px;padding:8px 16px;
341
343
  font-size:11px;box-shadow:0 8px 32px rgba(0,0,0,.45);z-index:300;
@@ -355,9 +357,9 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
355
357
  </svg>
356
358
  </button>
357
359
  <div class="logo">
358
- <div class="logo-ic">??/div>
360
+ <div class="logo-ic">R</div>
359
361
  <span class="logo-txt">rcodex Gateway</span>
360
- <span class="logo-sep"> · </span>
362
+ <span class="logo-sep"> / </span>
361
363
  <span class="logo-port">:${port}</span>
362
364
  </div>
363
365
  </div>
@@ -389,79 +391,96 @@ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
389
391
 
390
392
  <div class="layout">
391
393
 
392
- <!-- ?�?� Sidebar ?�?� -->
394
+ <!-- Sidebar -->
393
395
  <div class="sb" id="sb">
394
396
  <div class="sb-hdr">
395
- <button class="sb-back" id="sb-back" onclick="sbGoBack()" style="display:none">??/button>
397
+ <button class="sb-back" id="sb-back" onclick="sbGoBack()" style="display:none">&lt;</button>
396
398
  <span class="sb-title" id="sb-title">Menu</span>
397
- <button class="sb-x" onclick="toggleSb()">×</button>
399
+ <button class="sb-x" onclick="toggleSb()">x</button>
398
400
  </div>
399
401
  <div class="sb-body" id="sb-body"></div>
400
402
  </div>
401
403
 
402
- <!-- ?�?� Canvas ?�?� -->
404
+ <!-- Canvas -->
403
405
  <div class="ws" id="ws">
404
406
  <svg id="svgl"></svg>
405
407
  <div id="world"></div>
406
408
  <div class="zbar">
407
- <button class="zbtn" onclick="zoomStep(-1)">??/button>
409
+ <button class="zbtn" onclick="zoomStep(-1)">-</button>
408
410
  <div class="zpct" id="zpct">100%</div>
409
411
  <button class="zbtn" onclick="zoomStep(1)">+</button>
410
- <button class="zbtn" onclick="fitAll()" style="font-size:12px">??/button>
412
+ <button class="zbtn" onclick="fitAll()" style="font-size:12px">fit</button>
411
413
  </div>
412
- <div class="hint">Scroll to zoom · Drag to pan · Drag ??to connect</div>
414
+ <div class="hint">Scroll to zoom / Drag to pan / Drag ports to connect</div>
413
415
  </div>
414
416
 
415
417
  </div>
416
418
 
417
419
  <script>
418
- // ?�?�?� Provider definitions ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
419
- const PDEFS = [
420
- {id:'anthropic',name:'Claude',sub:'Anthropic',icon:'?��',ibg:'rgba(249,115,22,.15)',color:'#f97316',
421
- methods:[
422
- {id:'oauth',icon:'?��',name:'Login with Claude Code',desc:'OAuth login ??uses your Claude Pro/Max subscription',warn:null},
423
- {id:'apikey',icon:'?��',name:'API Key',desc:'Use Anthropic API key from console.anthropic.com',warn:null},
424
- {id:'session',icon:'??,name:'Session Token',desc:'Use claude.ai browser cookie (unofficial)',warn:'Unofficial ??may break. Against ToS.'},
425
- ]},
426
- {id:'openai',name:'ChatGPT / Codex',sub:'OpenAI',icon:'??,ibg:'rgba(16,163,127,.15)',color:'#10a37f',
427
- methods:[
428
- {id:'oauth',icon:'?��',name:'Login with ChatGPT',desc:'OAuth login ??uses your ChatGPT subscription',warn:null},
429
- {id:'apikey',icon:'?��',name:'API Key',desc:'Use OpenAI API key from platform.openai.com',warn:null},
430
- {id:'session',icon:'??,name:'Session Token',desc:'Use chatgpt.com browser cookie (unofficial)',warn:'Unofficial ??may break. Against ToS.'},
431
- ]},
432
- {id:'google',name:'Gemini',sub:'Google',icon:'??,ibg:'rgba(66,133,244,.15)',color:'#4285f4',
433
- methods:[
434
- {id:'apikey',icon:'?��',name:'API Key',desc:'Use Google AI Studio key from aistudio.google.com',warn:null},
435
- ]},
436
- {id:'ollama',name:'Ollama',sub:'Local models',icon:'?��',ibg:'rgba(168,85,247,.15)',color:'#a855f7',
437
- methods:[
438
- {id:'local',icon:'?��',name:'Connect Local',desc:'Use locally running Ollama (localhost:11434)',warn:null},
439
- ]},
440
- {id:'antigravity',name:'Antigravity',sub:'Google (Daily)',icon:'??,ibg:'rgba(52,211,153,.15)',color:'#34d399',
441
- methods:[
442
- {id:'oauth',icon:'?��',name:'Login with Google',desc:'OAuth login ??uses your Google Cloud / Gemini Code Assist account',warn:null},
443
- ]},
444
- {id:'copilot',name:'Copilot',sub:'GitHub',icon:'?��',ibg:'rgba(31,111,235,.15)',color:'#2f81f7',
445
- methods:[
446
- {id:'oauth',icon:'?��',name:'Login with GitHub',desc:'OAuth device login ??uses your GitHub Copilot subscription',warn:null},
447
- ]},
448
- ];
449
- const COL={anthropic:'#f97316',openai:'#10a37f',google:'#4285f4',ollama:'#a855f7',antigravity:'#34d399',copilot:'#2f81f7'};
450
- const ICONS={anthropic:'?��',openai:'??,google:'??,ollama:'?��',antigravity:'??,copilot:'?��'};
451
- const IBGS={anthropic:'rgba(249,115,22,.15)',openai:'rgba(16,163,127,.15)',google:'rgba(66,133,244,.15)',ollama:'rgba(168,85,247,.15)',antigravity:'rgba(52,211,153,.15)',copilot:'rgba(31,111,235,.15)'};
452
- const IMG_ICONS={
420
+ // Provider definitions
421
+ const PDEFS = [
422
+ {id:'anthropic',name:'Claude',sub:'Anthropic',icon:'C',ibg:'rgba(249,115,22,.15)',color:'#f97316',
423
+ methods:[
424
+ {id:'oauth',icon:'Auth',name:'Login with Claude Code',desc:'OAuth login uses your Claude Pro/Max subscription',warn:null},
425
+ {id:'apikey',icon:'Key',name:'API Key',desc:'Use Anthropic API key from console.anthropic.com',warn:null},
426
+ ]},
427
+ {id:'openai',name:'ChatGPT / Codex',sub:'OpenAI',icon:'O',ibg:'rgba(16,163,127,.15)',color:'#10a37f',
428
+ methods:[
429
+ {id:'oauth',icon:'Auth',name:'Login with ChatGPT',desc:'OAuth login uses your ChatGPT subscription',warn:null},
430
+ {id:'apikey',icon:'Key',name:'API Key',desc:'Use OpenAI API key from platform.openai.com',warn:null},
431
+ {id:'session',icon:'Cookie',name:'Session Token',desc:'Use chatgpt.com browser cookie (unofficial)',warn:'Unofficial; may break. Against ToS.'},
432
+ ]},
433
+ {id:'google',name:'Gemini',sub:'Google',icon:'G',ibg:'rgba(66,133,244,.15)',color:'#4285f4',
434
+ methods:[
435
+ {id:'apikey',icon:'Key',name:'API Key',desc:'Use Google AI Studio key from aistudio.google.com',warn:null},
436
+ ]},
437
+ {id:'ollama',name:'Ollama',sub:'Local models',icon:'L',ibg:'rgba(168,85,247,.15)',color:'#a855f7',
438
+ methods:[
439
+ {id:'local',icon:'Local',name:'Connect Local',desc:'Use locally running Ollama (localhost:11434)',warn:null},
440
+ ]},
441
+ {id:'antigravity',name:'Antigravity',sub:'Google (Daily)',icon:'A',ibg:'rgba(52,211,153,.15)',color:'#34d399',
442
+ methods:[
443
+ {id:'oauth',icon:'Auth',name:'Login with Google',desc:'OAuth login uses your Google Cloud / Gemini Code Assist account',warn:null},
444
+ ]},
445
+ {id:'copilot',name:'Copilot',sub:'GitHub',icon:'P',ibg:'rgba(31,111,235,.15)',color:'#2f81f7',
446
+ methods:[
447
+ {id:'oauth',icon:'Auth',name:'Login with GitHub',desc:'OAuth device login uses your GitHub Copilot subscription',warn:null},
448
+ ]},
449
+ ];
450
+ const COL={anthropic:'#f97316',openai:'#10a37f',google:'#4285f4',ollama:'#a855f7',antigravity:'#34d399',copilot:'#2f81f7'};
451
+ const ICONS={anthropic:'C',openai:'O',google:'G',ollama:'L',antigravity:'A',copilot:'P'};
452
+ const IBGS={anthropic:'rgba(249,115,22,.15)',openai:'rgba(16,163,127,.15)',google:'rgba(66,133,244,.15)',ollama:'rgba(168,85,247,.15)',antigravity:'rgba(52,211,153,.15)',copilot:'rgba(31,111,235,.15)'};const IMG_ICONS={
453
453
  anthropic:'data:image/png;base64,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',
454
454
  openai:'data:image/png;base64,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',
455
455
  google:'data:image/jpeg;base64,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',
456
456
  ollama:'data:image/png;base64,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',
457
- };
458
- function providerImg(provider,size){
459
- size=size||18;
460
- const src=IMG_ICONS[provider];
461
- return src?\`<img src="\${src}" style="width:\${size}px;height:\${size}px;object-fit:contain;border-radius:2px">\`:(ICONS[provider]||'?');
462
- }
457
+ };
458
+ const SVG_ICONS={
459
+ providers:'<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M12 3l7.5 4.3v8.5L12 20l-7.5-4.2V7.3L12 3z" stroke="currentColor" stroke-width="2"/><path d="M12 8v8M8.2 10.2l7.6 4.4M15.8 10.2l-7.6 4.4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
460
+ monitor:'<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M4 19V5m5 14v-8m5 8V8m5 11V3" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>',
461
+ terminal:'<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M4 7l5 5-5 5m8 0h8" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
462
+ refresh:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><path d="M20 6v5h-5M4 18v-5h5M18.2 9A7 7 0 0 0 6.3 6.8M5.8 15A7 7 0 0 0 17.7 17.2" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>',
463
+ close:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>',
464
+ login:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><path d="M10 17l5-5-5-5M15 12H3M14 4h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3h-4" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>',
465
+ key:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><circle cx="7.5" cy="14.5" r="3.5" stroke="currentColor" stroke-width="2"/><path d="M10 12l8-8m-1 1l3 3m-6 0l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
466
+ cookie:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><path d="M20 13.5A8 8 0 1 1 10.5 4a3 3 0 0 0 3 3 3 3 0 0 0 3 3 3 3 0 0 0 3.5 3.5z" stroke="currentColor" stroke-width="2"/><path d="M8 10h.01M12 16h.01M9 17h.01" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>',
467
+ };
468
+ const PROVIDER_SVG={
469
+ anthropic:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M12 4l7 16h-3l-1.4-3.4H9.4L8 20H5l7-16zm1.6 10L12 9.8 10.4 14h3.2z" fill="currentColor"/></svg>',
470
+ openai:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M12 3.5a4.2 4.2 0 0 1 4 2.9 4.2 4.2 0 0 1 3.2 6.3 4.2 4.2 0 0 1-4 5.9 4.2 4.2 0 0 1-6.4.2 4.2 4.2 0 0 1-3.9-6.1A4.2 4.2 0 0 1 8.8 6.4 4.2 4.2 0 0 1 12 3.5z" stroke="currentColor" stroke-width="1.8"/><path d="M8.8 6.4l6.4 3.7v7.8M19.2 12.7l-6.4 3.7-6.8-3.9M8.8 18.8V11l6.8-3.9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
471
+ google:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M20 12.2c0-.7-.1-1.4-.2-2H12v3.7h4.5a3.9 3.9 0 0 1-1.7 2.5v2h2.8c1.6-1.5 2.4-3.6 2.4-6.2z" fill="currentColor"/><path d="M12 20c2.2 0 4.1-.7 5.5-2l-2.8-2a5 5 0 0 1-7.4-2.6H4.4v2.1A8 8 0 0 0 12 20zM7.3 13.4a5 5 0 0 1 0-2.8V8.5H4.4a8 8 0 0 0 0 7l2.9-2.1zM12 7a4.4 4.4 0 0 1 3.1 1.2l2.4-2.4A8 8 0 0 0 4.4 8.5l2.9 2.1A4.8 4.8 0 0 1 12 7z" fill="currentColor"/></svg>',
472
+ ollama:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M7 12c0-4 2-7 5-7s5 3 5 7v7H7v-7z" stroke="currentColor" stroke-width="2"/><path d="M9 11h.01M15 11h.01M10 16h4" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></svg>',
473
+ antigravity:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M12 3l2.4 6.5L21 12l-6.6 2.5L12 21l-2.4-6.5L3 12l6.6-2.5L12 3z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>',
474
+ copilot:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M7 10V8a5 5 0 0 1 10 0v2M5 11h14v6a4 4 0 0 1-4 4H9a4 4 0 0 1-4-4v-6z" stroke="currentColor" stroke-width="2"/><path d="M9 15h.01M15 15h.01" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>',
475
+ };
476
+ function providerImg(provider,size){
477
+ size=size||18;
478
+ const src=IMG_ICONS[provider];
479
+ const fallback=PROVIDER_SVG[provider]||ICONS[provider]||'?';
480
+ return src?\`<img src="\${src}" style="width:\${size}px;height:\${size}px;object-fit:contain;border-radius:2px">\`:\`<span style="width:\${size}px;height:\${size}px;display:inline-flex;align-items:center;justify-content:center">\${fallback}</span>\`;
481
+ }
463
482
 
464
- // ?�?�?� App state ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
483
+ // App state
465
484
  let ST = { accounts:[], ollamaRunning:false, ollamaModels:[], ollamaBaseUrl:'http://localhost:11434' };
466
485
  let sbOpen = false;
467
486
  let sbScreen = 'home'; // 'home' | 'providers' | 'add-type' | 'add-method' | 'oauth-device'
@@ -486,28 +505,47 @@ let lastReqData = null;
486
505
  let codexMode = 'rcodex';
487
506
 
488
507
  // Canvas state (localStorage) ??keyed by slotId (not accountId)
489
- const LS_POS = 'rcodex-pos-v4';
490
- const LS_CANVAS = 'rcodex-canvas-v4';
491
- const LS_SLOTS = 'rcodex-slots-v1';
492
- function loadPos(){ try{return JSON.parse(localStorage.getItem(LS_POS)||'{}')}catch{return {}} }
493
- function loadCanvas(){ try{return new Set(JSON.parse(localStorage.getItem(LS_CANVAS)||'[]'))}catch{return new Set()} }
494
- function loadSlots(){ try{return JSON.parse(localStorage.getItem(LS_SLOTS)||'{}')}catch{return {}} }
495
- let NP = loadPos();
496
- let onCanvas = loadCanvas(); // Set of slotIds (+ 'out', 'monitor')
497
- let nodeSlots = loadSlots(); // {[slotId]: {accountId, model}}
498
-
499
- function saveLS(){
500
- localStorage.setItem(LS_POS, JSON.stringify(NP));
501
- localStorage.setItem(LS_CANVAS, JSON.stringify([...onCanvas]));
502
- localStorage.setItem(LS_SLOTS, JSON.stringify(nodeSlots));
503
- }
508
+ const LS_POS = 'rcodex-pos-v4';
509
+ const LS_CANVAS = 'rcodex-canvas-v4';
510
+ const LS_SLOTS = 'rcodex-slots-v1';
511
+ const LS_HIDDEN = 'rcodex-hidden-slots-v1';
512
+ function loadPos(){ try{return JSON.parse(localStorage.getItem(LS_POS)||'{}')}catch{return {}} }
513
+ function loadCanvas(){ try{return new Set(JSON.parse(localStorage.getItem(LS_CANVAS)||'[]'))}catch{return new Set()} }
514
+ function loadSlots(){ try{return JSON.parse(localStorage.getItem(LS_SLOTS)||'{}')}catch{return {}} }
515
+ function loadHidden(){ try{return new Set(JSON.parse(localStorage.getItem(LS_HIDDEN)||'[]'))}catch{return new Set()} }
516
+ let NP = loadPos();
517
+ let onCanvas = loadCanvas(); // Set of slotIds (+ 'out', 'monitor')
518
+ let nodeSlots = loadSlots(); // {[slotId]: {accountId, model}}
519
+ let hiddenSlots = loadHidden();
520
+
521
+ function saveLS(){
522
+ localStorage.setItem(LS_POS, JSON.stringify(NP));
523
+ localStorage.setItem(LS_CANVAS, JSON.stringify([...onCanvas]));
524
+ localStorage.setItem(LS_SLOTS, JSON.stringify(nodeSlots));
525
+ localStorage.setItem(LS_HIDDEN, JSON.stringify([...hiddenSlots]));
526
+ }
527
+ function accountNo(acc){
528
+ const same=(ST.accounts||[]).filter(a=>a.provider===acc.provider&&a.label===acc.label);
529
+ const idx=same.findIndex(a=>a.id===acc.id);
530
+ return same.length>1&&idx>=0?idx+1:null;
531
+ }
532
+ function accountName(acc){
533
+ const n=accountNo(acc);
534
+ return (acc.label||acc.provider)+(n?' #'+n:'');
535
+ }
536
+ function methodIcon(id){
537
+ if(id==='oauth')return SVG_ICONS.login;
538
+ if(id==='apikey')return SVG_ICONS.key;
539
+ if(id==='session')return SVG_ICONS.cookie;
540
+ return PROVIDER_SVG.ollama;
541
+ }
504
542
  if(!NP.out) NP.out={x:520,y:280};
505
543
 
506
544
  // Viewport
507
545
  let vp={x:60,y:40,s:1};
508
546
  let panD=null,nodeD=null,connD=null;
509
547
 
510
- // ?�?�?� Sidebar navigation ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
548
+ // Sidebar navigation
511
549
  function toggleSb(){
512
550
  sbOpen=!sbOpen;
513
551
  document.getElementById('sb').classList.toggle('open',sbOpen);
@@ -535,15 +573,15 @@ function renderSb(){
535
573
  body.innerHTML=\`
536
574
  <div style="padding:8px 0">
537
575
  <div class="nav-item" onclick="sbGoTo('providers')">
538
- <div class="nav-ic" style="background:rgba(99,102,241,.15)">?��</div>
576
+ <div class="nav-ic" style="background:rgba(99,102,241,.15)">\${SVG_ICONS.providers}</div>
539
577
  <div class="nav-info">
540
578
  <div class="nav-name">Providers</div>
541
579
  <div class="nav-sub">Manage AI provider accounts</div>
542
580
  </div>
543
- <span class="nav-arr">??/span>
581
+ <span class="nav-arr">&gt;</span>
544
582
  </div>
545
583
  <div class="nav-item" style="opacity:.4;pointer-events:none">
546
- <div class="nav-ic" style="background:rgba(96,96,128,.1)">??/div>
584
+ <div class="nav-ic" style="background:rgba(96,96,128,.1)">i</div>
547
585
  <div class="nav-info">
548
586
  <div class="nav-name">Settings</div>
549
587
  <div class="nav-sub">Gateway configuration</div>
@@ -551,8 +589,8 @@ function renderSb(){
551
589
  <span class="nav-badge">Soon</span>
552
590
  </div>
553
591
  <div class="nav-item" style="opacity:.4;pointer-events:none">
554
- <div class="nav-ic" style="background:rgba(96,96,128,.1)">?��</div>
555
- <div class="nav-info">
592
+ <div class="nav-ic" style="background:rgba(96,96,128,.1)">\${SVG_ICONS.monitor}</div>
593
+ <div class="nav-info">
556
594
  <div class="nav-name">Monitor</div>
557
595
  <div class="nav-sub">Request logs & metrics</div>
558
596
  </div>
@@ -587,7 +625,7 @@ function renderSb(){
587
625
  \${methods.map(m=>\`
588
626
  <div class="auth-card" onclick="sbGoToMethod('\${m.id}')">
589
627
  <div class="auth-card-hdr">
590
- <span class="auth-card-ic">\${m.icon}</span>
628
+ <span class="auth-card-ic">\${methodIcon(m.id)}</span>
591
629
  <span class="auth-card-name">\${m.name}</span>
592
630
  </div>
593
631
  <div class="auth-card-sub">\${m.desc}</div>
@@ -597,7 +635,7 @@ function renderSb(){
597
635
  else if(sbScreen==='add-method'){
598
636
  const m=sbAddingMethod;
599
637
  title.textContent=m.name;
600
- let warn=m.warn?\`<div class="auth-warn">??\${m.warn}</div>\`:'';
638
+ let warn=m.warn?\`<div class="auth-warn">\${m.warn}</div>\`:'';
601
639
 
602
640
  if(m.id==='oauth'){
603
641
  body.innerHTML=\`\${warn}
@@ -606,7 +644,7 @@ function renderSb(){
606
644
  <input class="form-input" id="f-label" placeholder="\${sbAddingDef.name} Account" value="\${sbAddingDef.name}"/>
607
645
  <div class="form-actions">
608
646
  <button class="form-cancel" onclick="sbGoBack()">Cancel</button>
609
- <button class="form-submit" onclick="doOAuth()">?�� Open Login</button>
647
+ <button class="form-submit" onclick="doOAuth()">\${SVG_ICONS.login} Open Login</button>
610
648
  </div>
611
649
  </div>\`;
612
650
  }
@@ -631,13 +669,13 @@ function renderSb(){
631
669
  const site=sbAddingDef.id==='anthropic'?'claude.ai':'chatgpt.com';
632
670
  body.innerHTML=\`\${warn}
633
671
  <div style="padding:0 14px 10px;font-size:10px;color:var(--mu);line-height:1.6">
634
- Open DevTools ??Application ??Cookies ??\${site} ??copy session token.
672
+ Open DevTools > Application > Cookies > \${site} > copy session token.
635
673
  </div>
636
674
  <div class="auth-form">
637
675
  <div class="form-label">Account label</div>
638
676
  <input class="form-input" id="f-label" placeholder="\${sbAddingDef.name} Account" value="\${sbAddingDef.name}"/>
639
677
  <div class="form-label">Session Token</div>
640
- <input class="form-input" id="f-ses" type="password" placeholder="Paste token??/>
678
+ <input class="form-input" id="f-ses" type="password" placeholder="Paste token"/>
641
679
  <div class="form-actions">
642
680
  <button class="form-cancel" onclick="sbGoBack()">Cancel</button>
643
681
  <button class="form-submit" onclick="doSession()">Connect</button>
@@ -702,11 +740,11 @@ function renderConnectedAccounts(){
702
740
  <div style="display:flex;align-items:center;gap:10px">
703
741
  <div class="acc-ic" style="background:\${IBGS[a.provider]||'rgba(96,96,128,.1)'}">\${providerImg(a.provider,16)}</div>
704
742
  <div class="acc-info">
705
- <div class="acc-name">\${a.label}</div>
706
- <div class="acc-sub">\${methodLabel[a.method]||a.method}\${activeSlots?' · <span style="color:var(--gr)">??/span> '+activeSlots+' active':canvasCount?' · '+canvasCount+' on canvas':''}</div>
743
+ <div class="acc-name">\${accountName(a)}</div>
744
+ <div class="acc-sub">\${methodLabel[a.method]||a.method}\${activeSlots?' / <span style="color:var(--gr)">active</span> '+activeSlots+' active':canvasCount?' / '+canvasCount+' on canvas':''}</div>
707
745
  \${sub?\`<div class="acc-sub" style="opacity:.6;margin-top:1px;font-size:9px">\${sub}</div>\`:''}
708
746
  </div>
709
- <button class="del-btn" onclick="deleteAccount('\${a.id}')" title="Delete">×</button>
747
+ <button class="del-btn" onclick="deleteAccount('\${a.id}')" title="Delete">\${SVG_ICONS.close}</button>
710
748
  </div>
711
749
  <div style="display:flex;gap:6px;padding:0 0 2px 40px">
712
750
  \${modelPicker}
@@ -716,7 +754,7 @@ function renderConnectedAccounts(){
716
754
  }).join('');
717
755
  if(ST.ollamaRunning&&!ST.accounts.find(a=>a.provider==='ollama')){
718
756
  html+=\`<div style="padding:8px 16px;font-size:10px;color:var(--mu)">
719
- Ollama is running ??<button onclick="sbGoToAdd('ollama')" style="background:none;border:none;color:var(--bl2);cursor:pointer;font-size:10px">add it as an account</button>
757
+ Ollama is running <button onclick="sbGoToAdd('ollama')" style="background:none;border:none;color:var(--bl2);cursor:pointer;font-size:10px">add it as an account</button>
720
758
  </div>\`;
721
759
  }
722
760
  return html;
@@ -734,14 +772,14 @@ function sbGoToMethod(methodId){
734
772
  renderSb();
735
773
  }
736
774
 
737
- // ?�?�?� Auth actions ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
775
+ // Auth actions
738
776
  async function doApiKey(){
739
777
  const label=document.getElementById('f-label')?.value.trim()||sbAddingDef.name;
740
778
  const key=document.getElementById('f-key')?.value.trim();
741
779
  if(!key){toast('API key is required',true);return}
742
780
  const r=await api('POST','/api/accounts',{provider:sbAddingDef.id,label,method:'apikey',apiKey:key});
743
781
  if(!r.ok){toast('Error: '+await r.text(),true);return}
744
- toast('??'+label+' connected');
782
+ toast(label+' connected');
745
783
  sbScreen='providers';
746
784
  await fetchStatus();
747
785
  renderSb();
@@ -753,7 +791,7 @@ async function doSession(){
753
791
  const method=sbAddingDef.id==='anthropic'?'oauth-unofficial':'oauth-unofficial';
754
792
  const r=await api('POST','/api/accounts',{provider:sbAddingDef.id,label,method,sessionToken:ses});
755
793
  if(!r.ok){toast('Error: '+await r.text(),true);return}
756
- toast('??'+label+' connected');
794
+ toast(label+' connected');
757
795
  sbScreen='providers';
758
796
  await fetchStatus();
759
797
  renderSb();
@@ -777,7 +815,7 @@ async function doOAuth(){
777
815
  pollNewAccount(0,ST.accounts.length);
778
816
  return;
779
817
  }else{
780
- toast('Complete login in the opened window??);
818
+ toast('Complete login in the opened window');
781
819
  }
782
820
  sbScreen='providers';
783
821
  renderSb();
@@ -786,7 +824,7 @@ async function doOAuth(){
786
824
  async function copyOAuthCode(){
787
825
  if(!sbOAuthDevice?.userCode)return;
788
826
  try{await navigator.clipboard?.writeText(sbOAuthDevice.userCode);toast('Code copied');}
789
- catch{toast('Copy failed ??select the code manually',true);}
827
+ catch{toast('Copy failed - select the code manually',true);}
790
828
  }
791
829
  function openOAuthDevicePage(){
792
830
  if(sbOAuthDevice?.authUrl)window.open(sbOAuthDevice.authUrl,'_blank','width=600,height=700');
@@ -797,13 +835,13 @@ async function doLocal(){
797
835
  await api('POST','/api/ollama/config',{baseUrl:url});
798
836
  const r=await api('POST','/api/accounts',{provider:'ollama',label,method:'local'});
799
837
  if(!r.ok){toast('Error: '+await r.text(),true);return}
800
- toast('??'+label+' added');
838
+ toast(label+' added');
801
839
  sbScreen='providers';
802
840
  await fetchStatus();
803
841
  renderSb();
804
842
  }
805
843
  function pollNewAccount(n,prevCount){
806
- if(n>180){toast('OAuth timed out ??no account detected. Check terminal logs.',true);return;}
844
+ if(n>180){toast('OAuth timed out - no account detected. Check terminal logs.',true);return;}
807
845
  setTimeout(async()=>{
808
846
  await fetchStatus();
809
847
  renderSb();
@@ -813,7 +851,7 @@ function pollNewAccount(n,prevCount){
813
851
  }
814
852
  if(ST.accounts.length>prevCount){
815
853
  const newAcc=ST.accounts.slice(-1)[0];
816
- toast('??'+(newAcc?.label||'Account')+' connected!');
854
+ toast((newAcc?.label||'Account')+' connected!');
817
855
  sbOAuthDevice=null;
818
856
  if(sbScreen==='oauth-device')sbScreen='providers';
819
857
  renderSb();
@@ -823,7 +861,7 @@ function pollNewAccount(n,prevCount){
823
861
  },4000);
824
862
  }
825
863
 
826
- // ?�?�?� Account / slot management ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
864
+ // Account / slot management
827
865
  async function deleteAccount(id){
828
866
  // Remove all canvas nodes belonging to this account
829
867
  for(const [slotId,info] of Object.entries(nodeSlots)){
@@ -836,13 +874,14 @@ async function deleteAccount(id){
836
874
  await fetchStatus();
837
875
  }
838
876
 
839
- function addToCanvas(accountId){
877
+ function addToCanvas(accountId){
840
878
  const acc=ST.accounts.find(a=>a.id===accountId);
841
879
  if(!acc)return;
842
880
  const models=acc.models||[];
843
881
  const model=sidebarModelSel[accountId]||models[0]||'';
844
- const slotId='slot_'+Date.now()+'_'+Math.random().toString(36).slice(2,6);
845
- nodeSlots[slotId]={accountId,model};
882
+ const slotId='slot_'+Date.now()+'_'+Math.random().toString(36).slice(2,6);
883
+ hiddenSlots.delete(slotId);
884
+ nodeSlots[slotId]={accountId,model};
846
885
  onCanvas.add(slotId);
847
886
  if(!NP[slotId]){
848
887
  const ws=document.getElementById('ws');
@@ -856,23 +895,27 @@ function addToCanvas(accountId){
856
895
  renderSb();
857
896
  }
858
897
 
859
- async function removeFromCanvas(slotId){
860
- const info=nodeSlots[slotId];
861
- onCanvas.delete(slotId);
862
- saveLS();
898
+ async function removeFromCanvas(slotId){
899
+ const info=nodeSlots[slotId];
900
+ onCanvas.delete(slotId);
901
+ hiddenSlots.add(slotId);
902
+ delete NP[slotId];
903
+ delete nodeSlots[slotId];
904
+ saveLS();
863
905
  render();
864
906
  if(sbOpen)renderSb();
865
907
  if(info){
866
908
  const acc=ST.accounts.find(a=>a.id===info.accountId);
867
909
  const slot=(acc?.activeModels||[]).find(s=>s.slotId===slotId);
868
- if(slot){
869
- await api('DELETE',\`/api/accounts/\${info.accountId}/slots/\${slotId}\`);
870
- await fetchStatus();
871
- }
872
- }
873
- }
910
+ if(slot){
911
+ const r=await api('DELETE',\`/api/accounts/\${info.accountId}/slots/\${slotId}\`);
912
+ if(!r.ok)toast('Remove failed: '+await r.text(),true);
913
+ await fetchStatus();
914
+ }
915
+ }
916
+ }
874
917
 
875
- // ?�?�?� Monitor node ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
918
+ // Monitor node
876
919
  function toggleMonitor(tab){
877
920
  if(monitorOpen && monitorTab===tab){ monitorOpen=false; }
878
921
  else{ monitorOpen=true; monitorTab=tab; if(!NP.monitor) NP.monitor={x:80,y:60}; }
@@ -938,7 +981,7 @@ function buildMonitorNode(){
938
981
  usage:'<svg width="11" height="11" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="8" width="2.5" height="4.5" rx="1" stroke="currentColor" stroke-width="1.3"/><rect x="5.75" y="5" width="2.5" height="7.5" rx="1" stroke="currentColor" stroke-width="1.3"/><rect x="10" y="2" width="2.5" height="10.5" rx="1" stroke="currentColor" stroke-width="1.3"/></svg>',
939
982
  };
940
983
  const clearBtn=(monitorTab==='logs'||monitorTab==='requests')
941
- ?\`<button onclick="clearMonitor()" style="margin:4px 8px;padding:2px 9px;border-radius:5px;border:1px solid var(--b2);background:rgba(239,68,68,.08);color:#f87171;font-size:9px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all .12s" onmouseover="this.style.background='rgba(239,68,68,.18)'" onmouseout="this.style.background='rgba(239,68,68,.08)'">??Clear</button>\`
984
+ ?\`<button onclick="clearMonitor()" style="margin:4px 8px;padding:2px 9px;border-radius:5px;border:1px solid var(--b2);background:rgba(239,68,68,.08);color:#f87171;font-size:9px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all .12s" onmouseover="this.style.background='rgba(239,68,68,.18)'" onmouseout="this.style.background='rgba(239,68,68,.08)'">Clear</button>\`
942
985
  :'';
943
986
  const tabHtml=tabs.map(t=>\`<div class="mn-t \${monitorTab===t?'on':''}" onclick="switchMonitorTab('\${t}')">\${icons[t]} \${labels[t]}</div>\`).join('')+
944
987
  \`<div style="flex:1"></div>\${clearBtn}\`;
@@ -951,22 +994,22 @@ function buildMonitorNode(){
951
994
  const short=cwd.replace(new RegExp('^/Users/[^/]+'),'~');
952
995
  inputRow=\`<div class="mn-inp-row">
953
996
  <span class="mn-cwd-lbl" title="\${cwd}">\${short}$</span>
954
- <input class="mn-inp" id="mn-inp" placeholder="Enter command?? autocomplete="off" spellcheck="false"/>
955
- <button class="mn-run" onclick="sendTerm()">??/button>
997
+ <input class="mn-inp" id="mn-inp" placeholder="Enter command" autocomplete="off" spellcheck="false"/>
998
+ <button class="mn-run" onclick="sendTerm()">Run</button>
956
999
  </div>\`;
957
1000
  } else if(monitorTab==='status'){
958
- content=\`<div id="mn-status-body"><div class="mn-empty">Loading??/div></div>\`;
1001
+ content=\`<div id="mn-status-body"><div class="mn-empty">Loading</div></div>\`;
959
1002
  } else if(monitorTab==='logs'){
960
- content=\`<div class="log-wrap" id="mn-logs-body"><div class="mn-empty">Loading??/div></div>\`;
1003
+ content=\`<div class="log-wrap" id="mn-logs-body"><div class="mn-empty">Loading</div></div>\`;
961
1004
  } else if(monitorTab==='requests'){
962
- content=\`<div class="req-wrap" id="mn-reqs-body"><div class="mn-empty">Loading??/div></div>\`;
1005
+ content=\`<div class="req-wrap" id="mn-reqs-body"><div class="mn-empty">Loading</div></div>\`;
963
1006
  } else if(monitorTab==='usage'){
964
- content=\`<div id="mn-usage-body">\${usageHtmlCache||'<div class="mn-empty">Loading??/div>'}</div>\`;
1007
+ content=\`<div id="mn-usage-body">\${usageHtmlCache||'<div class="mn-empty">Loading</div>'}</div>\`;
965
1008
  }
966
1009
 
967
1010
  return \`<div class="nd mn" id="nd-monitor" style="left:\${pos.x}px;top:\${pos.y}px;width:\${sz.w}px">
968
1011
  <div class="nh" style="border-radius:13px 13px 0 0;cursor:grab">
969
- <div class="nic" style="background:rgba(99,102,241,.15);font-size:11px">??/div>
1012
+ <div class="nic" style="background:rgba(99,102,241,.15);font-size:11px">\${SVG_ICONS.terminal}</div>
970
1013
  <span class="nn">Monitor</span>
971
1014
  <button class="nd-rm" onclick="toggleMonitor(monitorTab)" title="Close">×</button>
972
1015
  </div>
@@ -981,7 +1024,7 @@ function buildMonitorNode(){
981
1024
 
982
1025
  function escHtml(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
983
1026
 
984
- // ?�?�?� Terminal ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1027
+ // Terminal
985
1028
  function addTermLine(t,v){
986
1029
  termLines.push({t,v});
987
1030
  if(termLines.length>500)termLines.splice(0,termLines.length-500);
@@ -1031,7 +1074,7 @@ function setupTermKeys(){
1031
1074
  inp.focus();
1032
1075
  }
1033
1076
 
1034
- // ?�?�?� Status / Logs / Requests refresh ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1077
+ // Status / Logs / Requests refresh
1035
1078
  async function refreshStatus(){
1036
1079
  const body=document.getElementById('mn-status-body');
1037
1080
  if(!body)return;
@@ -1074,7 +1117,7 @@ async function refreshLogs(forceBottom=false){
1074
1117
  const d=await(await fetch('/api/logs?n=120')).json();
1075
1118
  if(!d.lines?.length){body.innerHTML='<div class="mn-empty">No log entries yet</div>';return;}
1076
1119
  body.innerHTML=d.lines.map(l=>{
1077
- const cls=l.includes('??)||l.includes('error')||l.includes('Error')?'err':l.includes('??)||l.includes('warn')?'warn':l.includes('??)?'ok':'';
1120
+ const lower=String(l).toLowerCase(); const cls=lower.includes('error')?'err':lower.includes('warn')?'warn':lower.includes('[ok]')?'ok':'';
1078
1121
  return \`<div class="log-l \${cls}">\${escHtml(l)}</div>\`;
1079
1122
  }).join('');
1080
1123
  if(scroller&&atBottom) scroller.scrollTop=scroller.scrollHeight;
@@ -1102,37 +1145,37 @@ async function refreshRequests(){
1102
1145
  } else if(r.failedModels?.length&&!r.usedModel){
1103
1146
  modelLabel=r.failedModels.map(m=>\`<div style="opacity:.5;text-decoration:line-through;line-height:1.4">\${m}</div>\`).join('');
1104
1147
  } else {
1105
- modelLabel=r.usedModel||'??;
1148
+ modelLabel=r.usedModel||'-';
1106
1149
  }
1107
1150
  const tokensTxt=r.inputTokens!=null||r.outputTokens!=null
1108
- ?\`<span style="color:#6ee7b7">\${r.inputTokens??0}</span>/<span style="color:#f9a8d4">\${r.outputTokens??0}</span>\`:'??;
1151
+ ?\`<span style="color:#6ee7b7">\${r.inputTokens??0}</span>/<span style="color:#f9a8d4">\${r.outputTokens??0}</span>\`:'-';
1109
1152
  const hasDetail=!!(r.inputPreview||r.outputPreview||r.toolCalls?.length||r.toolCallDetails?.length||r.webFetches?.length||r.error);
1110
1153
  const detailId=\`req-detail-\${r.ts}\`;
1111
- const detailBtn=hasDetail?\`<button class="req-expand-btn" onclick="toggleReqDetail('\${detailId}')" title="Show detail">??/button>\`:'';
1154
+ const detailBtn=hasDetail?\`<button class="req-expand-btn" onclick="toggleReqDetail('\${detailId}')" title="Show detail">+</button>\`:'';
1112
1155
 
1113
1156
  // Build detail panel
1114
1157
  let detailHtml='';
1115
1158
  if(hasDetail){
1116
1159
  const parts=[];
1117
- if(r.inputPreview) parts.push(\`<div class="req-detail-section"><div class="req-detail-label">??User</div><div class="req-detail-val">\${escHtml(r.inputPreview)}</div></div>\`);
1160
+ if(r.inputPreview) parts.push(\`<div class="req-detail-section"><div class="req-detail-label">User</div><div class="req-detail-val">\${escHtml(r.inputPreview)}</div></div>\`);
1118
1161
  if(r.toolCallDetails?.length){
1119
1162
  const detailLines=r.toolCallDetails.map(tc=>{
1120
1163
  let argStr='';
1121
- try{const p=JSON.parse(tc.args);const cmd=p.cmd??p.command??p.script??p.url??p.path;argStr=cmd?\` <span style="color:var(--mu)">??\${escHtml(String(cmd))}</span>\`:\`<span style="color:var(--mu);font-size:9px"> \${escHtml(tc.args.slice(0,80))}</span>\`;}catch{argStr=tc.args?\`<span style="color:var(--mu);font-size:9px"> \${escHtml(tc.args.slice(0,80))}</span>\`:'';};
1164
+ try{const p=JSON.parse(tc.args);const cmd=p.cmd??p.command??p.script??p.url??p.path;argStr=cmd?\` <span style="color:var(--mu)">\${escHtml(String(cmd))}</span>\`:\`<span style="color:var(--mu);font-size:9px"> \${escHtml(tc.args.slice(0,80))}</span>\`;}catch{argStr=tc.args?\`<span style="color:var(--mu);font-size:9px"> \${escHtml(tc.args.slice(0,80))}</span>\`:'';};
1122
1165
  return\`<code>\${escHtml(tc.name)}</code>\${argStr}\`;
1123
1166
  });
1124
- parts.push(\`<div class="req-detail-section"><div class="req-detail-label">??Tools called</div><div class="req-detail-val">\${detailLines.join('<br>')}</div></div>\`);
1125
- } else if(r.toolCalls?.length) parts.push(\`<div class="req-detail-section"><div class="req-detail-label">??Tools called</div><div class="req-detail-val">\${r.toolCalls.map(n=>\`<code>\${escHtml(n)}</code>\`).join(' ')}</div></div>\`);
1126
- if(r.webFetches?.length) parts.push(\`<div class="req-detail-section"><div class="req-detail-label">?�� Web fetched</div><div class="req-detail-val">\${r.webFetches.map(u=>\`<code>\${escHtml(u)}</code>\`).join('<br>')}</div></div>\`);
1127
- if(r.outputPreview) parts.push(\`<div class="req-detail-section"><div class="req-detail-label">??Model</div><div class="req-detail-val">\${escHtml(r.outputPreview)}</div></div>\`);
1128
- if(r.error) parts.push(\`<div class="req-detail-section"><div class="req-detail-label" style="color:#f87171">??Error</div><div class="req-detail-val" style="color:#f87171">\${escHtml(r.error)}</div></div>\`);
1167
+ parts.push(\`<div class="req-detail-section"><div class="req-detail-label">Tools called</div><div class="req-detail-val">\${detailLines.join('<br>')}</div></div>\`);
1168
+ } else if(r.toolCalls?.length) parts.push(\`<div class="req-detail-section"><div class="req-detail-label">Tools called</div><div class="req-detail-val">\${r.toolCalls.map(n=>\`<code>\${escHtml(n)}</code>\`).join(' ')}</div></div>\`);
1169
+ if(r.webFetches?.length) parts.push(\`<div class="req-detail-section"><div class="req-detail-label">Web fetched</div><div class="req-detail-val">\${r.webFetches.map(u=>\`<code>\${escHtml(u)}</code>\`).join('<br>')}</div></div>\`);
1170
+ if(r.outputPreview) parts.push(\`<div class="req-detail-section"><div class="req-detail-label">Model</div><div class="req-detail-val">\${escHtml(r.outputPreview)}</div></div>\`);
1171
+ if(r.error) parts.push(\`<div class="req-detail-section"><div class="req-detail-label" style="color:#f87171">Error</div><div class="req-detail-val" style="color:#f87171">\${escHtml(r.error)}</div></div>\`);
1129
1172
  detailHtml=\`<div id="\${detailId}" class="req-detail-panel" style="display:none">\${parts.join('')}</div>\`;
1130
1173
  }
1131
1174
 
1132
1175
  return \`<div style="flex-direction:column;display:flex;border-bottom:1px solid rgba(255,255,255,.035)">
1133
1176
  <div class="req-row \${stCls}-row" style="border-bottom:none">
1134
1177
  <span class="req-ts">\${ts}</span>
1135
- <span class="req-prov" style="color:\${col}">\${r.provider||'??}</span>
1178
+ <span class="req-prov" style="color:\${col}">\${r.provider||'-'}</span>
1136
1179
  <span class="req-model">\${modelLabel}</span>
1137
1180
  <span class="req-ms">\${r.ms}</span>
1138
1181
  <span>\${tokensTxt}</span>
@@ -1146,19 +1189,19 @@ async function refreshRequests(){
1146
1189
  body.innerHTML=hdr+rows;
1147
1190
  if(scroller&&atBottom) scroller.scrollTop=scroller.scrollHeight;
1148
1191
  // Restore open state after rebuild
1149
- openIds.forEach(id=>{const el=document.getElementById(id);if(el){el.style.display='block';const btn=el.previousElementSibling?.querySelector('.req-expand-btn');if(btn)btn.textContent='??;}});
1192
+ openIds.forEach(id=>{const el=document.getElementById(id);if(el){el.style.display='block';const btn=el.previousElementSibling?.querySelector('.req-expand-btn');if(btn)btn.textContent='-';}});
1150
1193
  }catch{body.innerHTML='<div class="mn-empty">Failed to load</div>';}
1151
1194
  }
1152
1195
  function toggleReqDetail(id){
1153
1196
  const el=document.getElementById(id);
1154
1197
  if(!el)return;
1155
1198
  const btn=el.previousElementSibling?.querySelector('.req-expand-btn');
1156
- if(el.style.display==='none'){el.style.display='block';if(btn)btn.textContent='??;}
1157
- else{el.style.display='none';if(btn)btn.textContent='??;}
1199
+ if(el.style.display==='none'){el.style.display='block';if(btn)btn.textContent='-';}
1200
+ else{el.style.display='none';if(btn)btn.textContent='-';}
1158
1201
  }
1159
1202
  function escHtml(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
1160
1203
 
1161
- // ?�?�?� Usage ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1204
+ // Usage
1162
1205
  const PRICING={
1163
1206
  'claude-opus-4-7':[15,75],'claude-opus-4-7-20250514':[15,75],'claude-opus-4-5':[15,75],
1164
1207
  'claude-sonnet-4-6':[3,15],'claude-sonnet-4-5':[3,15],
@@ -1176,17 +1219,27 @@ function getPrice(model){
1176
1219
  return key?PRICING[key]:null;
1177
1220
  }
1178
1221
  function fmtCost(usd){
1179
- if(usd==null)return'??;
1222
+ if(usd==null)return'-';
1180
1223
  if(usd<0.0001)return'<$0.0001';
1181
1224
  if(usd<0.01)return'$'+usd.toFixed(4);
1182
1225
  return'$'+usd.toFixed(3);
1183
1226
  }
1184
- function fmtTok(n){if(!n)return'??;if(n>=1e6)return(n/1e6).toFixed(2)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'K';return String(n);}
1185
-
1186
- function fmtReset(isoStr){
1187
- if(!isoStr)return'';
1188
- const diff=new Date(isoStr)-Date.now();
1189
- if(diff<=0)return'now';
1227
+ function fmtTok(n){if(!n)return'-';if(n>=1e6)return(n/1e6).toFixed(2)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'K';return String(n);}
1228
+
1229
+ function fmtReset(isoStr){
1230
+ if(isoStr==null||isoStr==='')return'';
1231
+ let t;
1232
+ if(typeof isoStr==='number'){
1233
+ t=isoStr<1e12?isoStr*1000:isoStr;
1234
+ }else if(/^\d+$/.test(String(isoStr))){
1235
+ const n=Number(isoStr);
1236
+ t=n<1e12?n*1000:n;
1237
+ }else{
1238
+ t=Date.parse(String(isoStr));
1239
+ }
1240
+ if(!Number.isFinite(t))return'';
1241
+ const diff=t-Date.now();
1242
+ if(diff<=0)return'<1m';
1190
1243
  const h=Math.floor(diff/3600000),m=Math.floor((diff%3600000)/60000);
1191
1244
  if(diff<3600000)return m+'m';
1192
1245
  if(diff<86400000)return h+'h '+m+'m';
@@ -1207,7 +1260,7 @@ function quotaRow(label,used,resetsAt){
1207
1260
  <span style="font-size:9px;color:var(--mu);width:26px;flex-shrink:0">\${label}</span>
1208
1261
  \${quotaBar(used,col)}
1209
1262
  <span style="font-size:9px;width:28px;text-align:right;flex-shrink:0;\${used>80?'color:#ef4444':used>50?'color:#f59e0b':''}">\${remaining}%</span>
1210
- \${reset?\`<span style="font-size:9px;color:var(--mu);flex-shrink:0">reset: \${reset}</span>\`:''}
1263
+ \${reset?\`<span style="font-size:9px;color:var(--mu);flex-shrink:0">reset: \${reset}</span>\`:''}
1211
1264
  </div>\`;
1212
1265
  }
1213
1266
 
@@ -1224,9 +1277,9 @@ function renderUsage(){
1224
1277
  const icon=providerImg(a.provider,14);
1225
1278
  let rows='';
1226
1279
  if(qs.loading){
1227
- rows=\`<div style="font-size:9px;color:var(--mu)">Fetching??/div>\`;
1280
+ rows=\`<div style="font-size:9px;color:var(--mu)">Fetching</div>\`;
1228
1281
  }else if(!qs.data){
1229
- rows=\`<div style="font-size:9px;color:var(--mu);opacity:.5">Click ??to load</div>\`;
1282
+ rows=\`<div style="font-size:9px;color:var(--mu);opacity:.5">Click refresh to load</div>\`;
1230
1283
  }else if(qs.data.error){
1231
1284
  rows=\`<div style="font-size:9px;color:var(--rd)">Error: \${qs.data.error}</div>\`;
1232
1285
  }else if(a.provider==='anthropic'){
@@ -1237,12 +1290,12 @@ function renderUsage(){
1237
1290
  if(qs.data.secondary!=null)rows+=quotaRow('7d',qs.data.secondary.used,qs.data.secondary.resets_at);
1238
1291
  }
1239
1292
  if(!rows&&qs.data&&!qs.data.error)rows=\`<div style="font-size:9px;color:var(--mu)">No quota data</div>\`;
1240
- const btnStyle='background:none;border:1px solid var(--b2);border-radius:5px;color:var(--di);cursor:pointer;font-size:12px;padding:0 7px;line-height:1.8;transition:all .12s;flex-shrink:0';
1293
+ const btnStyle='background:none;border:1px solid var(--b2);border-radius:5px;color:var(--di);cursor:pointer;font-size:12px;width:28px;height:24px;display:inline-flex;align-items:center;justify-content:center;transition:all .12s;flex-shrink:0';
1241
1294
  return\`<div style="background:var(--s2);border-radius:8px;padding:8px 10px">
1242
1295
  <div style="display:flex;align-items:center;gap:6px;margin-bottom:\${rows?'6':'0'}px">
1243
1296
  <div style="width:20px;height:20px;border-radius:5px;background:\${IBGS[a.provider]||'rgba(96,96,128,.15)'};display:flex;align-items:center;justify-content:center;flex-shrink:0">\${icon}</div>
1244
1297
  <span style="font-size:11px;font-weight:600;flex:1">\${escHtml(a.label||a.provider)}</span>
1245
- <button onclick="refreshQuota('\${a.id}')" \${qs.loading?'disabled':''} style="\${btnStyle}" title="Refresh quota">\${qs.loading?'??:'??}</button>
1298
+ <button onclick="refreshQuota('\${a.id}')" \${qs.loading?'disabled':''} style="\${btnStyle}" title="Refresh quota">\${qs.loading?'...':SVG_ICONS.refresh}</button>
1246
1299
  </div>
1247
1300
  \${rows}
1248
1301
  </div>\`;
@@ -1273,7 +1326,7 @@ function renderUsage(){
1273
1326
  const cards=\`<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px">
1274
1327
  <div class="usg-card"><div class="usg-val">\${fmtTok(totIn)}</div><div class="usg-lbl">Input tokens</div></div>
1275
1328
  <div class="usg-card"><div class="usg-val">\${fmtTok(totOut)}</div><div class="usg-lbl">Output tokens</div></div>
1276
- <div class="usg-card"><div class="usg-val" style="color:var(--gr)">\${hasCost?fmtCost(totCost):'??}</div><div class="usg-lbl">Est. cost</div></div>
1329
+ <div class="usg-card"><div class="usg-val" style="color:var(--gr)">\${hasCost?fmtCost(totCost):'-'}</div><div class="usg-lbl">Est. cost</div></div>
1277
1330
  </div>\`;
1278
1331
  const hdr=\`<div class="usg-row usg-hdr"><span>Model</span><span>Reqs</span><span>In</span><span>Out</span><span>Cost</span></div>\`;
1279
1332
  const rows=Object.entries(byModel).sort((a,b)=>b[1].inT+b[1].outT-(a[1].inT+a[1].outT)).map(([model,m])=>{
@@ -1283,8 +1336,8 @@ function renderUsage(){
1283
1336
  <span style="width:6px;height:6px;border-radius:50%;background:\${col};flex-shrink:0"></span>
1284
1337
  <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">\${model}</span>
1285
1338
  </span>
1286
- <span>\${m.reqs}</span><span>\${fmtTok(m.inT)||'??}</span><span>\${fmtTok(m.outT)||'??}</span>
1287
- <span>\${m.hasCost?fmtCost(m.cost):'??}</span>
1339
+ <span>\${m.reqs}</span><span>\${fmtTok(m.inT)||'-'}</span><span>\${fmtTok(m.outT)||'-'}</span>
1340
+ <span>\${m.hasCost?fmtCost(m.cost):'-'}</span>
1288
1341
  </div>\`;
1289
1342
  }).join('');
1290
1343
  tokenHtml=\`<div style="padding:10px 10px 0">
@@ -1320,7 +1373,7 @@ async function refreshQuota(accountId){
1320
1373
  renderUsage();
1321
1374
  }
1322
1375
 
1323
- // ?�?�?� Canvas viewport ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1376
+ // Canvas viewport
1324
1377
  function applyVp(){
1325
1378
  // Use CSS zoom for scaling so the browser re-renders content at the exact zoom level
1326
1379
  // (crisp text at any zoom), and translate for panning (in parent pixel space)
@@ -1362,11 +1415,11 @@ function fitAll(){
1362
1415
  applyVp();
1363
1416
  }
1364
1417
 
1365
- // ?�?�?� Canvas render ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1418
+ // Canvas render
1366
1419
  function render(){
1367
1420
  const world=document.getElementById('world');
1368
1421
  const savedScroll=document.getElementById('mn-body')?.scrollTop??0;
1369
- // Snapshot current monitor content so rebuild doesn't flash "Loading??
1422
+ // Snapshot current monitor content so rebuild doesn't flash "Loading
1370
1423
  const savedReqs=document.getElementById('mn-reqs-body')?.innerHTML;
1371
1424
  const savedLogs=document.getElementById('mn-logs-body')?.innerHTML;
1372
1425
  const savedStat=document.getElementById('mn-status-body')?.innerHTML;
@@ -1410,19 +1463,19 @@ function accountSubtext(acc){
1410
1463
  const pad=parts[1].replace(/-/g,'+').replace(/_/g,'/');
1411
1464
  const payload=JSON.parse(atob(pad+'=='.slice(0,(4-pad.length%4)%4)));
1412
1465
  const email=payload.email||payload.sub||'';
1413
- if(email)return '?�� '+email;
1466
+ if(email)return 'User '+email;
1414
1467
  }
1415
1468
  // No email in JWT ??show provider-specific label
1416
- if(acc.provider==='anthropic')return '?�� Claude Code OAuth';
1469
+ if(acc.provider==='anthropic')return 'Claude Code OAuth';
1417
1470
  }
1418
1471
  }catch{}
1419
- if(acc.method==='oauth-official')return acc.provider==='anthropic'?'?�� Claude Code OAuth':'?�� OAuth';
1472
+ if(acc.method==='oauth-official')return acc.provider==='anthropic'?'Claude Code OAuth':'OAuth';
1420
1473
  if(acc.method==='apikey'&&acc.apiKey){
1421
1474
  const k=acc.apiKey;
1422
- return '?�� '+k.slice(0,10)+'···'+k.slice(-4);
1475
+ return 'Key '+k.slice(0,10)+'...'+k.slice(-4);
1423
1476
  }
1424
- if(acc.method==='oauth-unofficial')return '?�� Session Token';
1425
- if(acc.method==='local')return '?�� '+(ST.ollamaBaseUrl||'localhost:11434');
1477
+ if(acc.method==='oauth-unofficial')return 'Session Token';
1478
+ if(acc.method==='local')return 'Local '+(ST.ollamaBaseUrl||'localhost:11434');
1426
1479
  return '';
1427
1480
  }
1428
1481
 
@@ -1432,20 +1485,24 @@ function buildAccNode(slotId){
1432
1485
  const acc=ST.accounts.find(a=>a.id===info.accountId);
1433
1486
  if(!acc)return '';
1434
1487
  const model=info.model||'(auto)';
1435
- const slot=(acc.activeModels||[]).find(s=>s.slotId===slotId);
1436
- const isOut=!!slot;
1437
- const sub=accountSubtext(acc);
1438
- const subEl=sub?\`<div class="nd-acct" title="\${sub}">\${sub}</div>\`:'';
1488
+ const slot=(acc.activeModels||[]).find(s=>s.slotId===slotId);
1489
+ const isOut=!!slot;
1490
+ const sub=accountSubtext(acc);
1491
+ const no=accountNo(acc);
1492
+ const noBadge=no?\`<span class="acct-badge">#\${no}</span>\`:'';
1493
+ const subEl=sub?\`<div class="nd-acct" title="\${sub}">\${sub}</div>\`:'';
1439
1494
  const pos=NP[slotId]||{x:80,y:80};
1440
1495
  return \`<div class="nd \${isOut?'live':''}" id="nd-\${slotId}" style="left:\${pos.x}px;top:\${pos.y}px">
1441
1496
  <div class="nh">
1442
- <div class="nic" style="background:\${IBGS[acc.provider]}">\${providerImg(acc.provider,14)}</div>
1443
- <div style="flex:1;min-width:0">
1444
- <div class="nn">\${acc.label}</div>
1445
- <div style="font-size:9px;color:var(--mu);white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="\${model}">\${model}</div>
1446
- </div>
1497
+ <div class="nic" style="background:\${IBGS[acc.provider]}">\${providerImg(acc.provider,14)}</div>
1498
+ <div style="flex:1;min-width:0">
1499
+ <div style="display:flex;align-items:center;gap:6px;min-width:0" title="\${accountName(acc)}">
1500
+ <div class="nn">\${accountName(acc)}</div>\${noBadge}
1501
+ </div>
1502
+ <div style="font-size:9px;color:var(--mu);white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="\${model}">\${model}</div>
1503
+ </div>
1447
1504
  <span class="bk \${isOut?'bk-on':'bk-off'}">\${isOut?'Active':'Idle'}</span>
1448
- <button class="nd-rm" onclick="removeFromCanvas('\${slotId}')" title="Remove from canvas">×</button>
1505
+ <button class="nd-rm" onclick="removeFromCanvas('\${slotId}')" title="Remove from canvas">\${SVG_ICONS.close}</button>
1449
1506
  </div>
1450
1507
  \${sub?\`<div class="nb" style="padding:4px 12px 6px">\${subEl}</div>\`:''}
1451
1508
  <div class="po \${isOut?'live':''}" id="po-\${slotId}" title="Drag to connect to OUT"></div>
@@ -1462,35 +1519,35 @@ function buildOutNode(){
1462
1519
  allSlots.sort((a,b)=>a.slot.order-b.slot.order);
1463
1520
  const piCls='pi'+(allSlots.length?' live':'');
1464
1521
  const multi=allSlots.length>1;
1465
- const subtitle=multi?'Priority order · fallback chain':'Codex uses these models';
1522
+ const subtitle=multi?'Priority order / fallback chain':'Codex uses these models';
1466
1523
  const body=allSlots.length
1467
1524
  ?allSlots.map(({acc,slot},i)=>{
1468
1525
  const m=slot.model||'(auto)';
1469
1526
  const upDis=i===0?'disabled':'';
1470
1527
  const dnDis=i===allSlots.length-1?'disabled':'';
1471
1528
  const orderBtns=multi?\`<div class="oi-ord">
1472
- <button class="oi-arr" onclick="moveOut('\${acc.id}','\${slot.slotId}',-1)" \${upDis}>??/button>
1473
- <button class="oi-arr" onclick="moveOut('\${acc.id}','\${slot.slotId}',1)" \${dnDis}>??/button>
1529
+ <button class="oi-arr" onclick="moveOut('\${acc.id}','\${slot.slotId}',-1)" \${upDis}>&uarr;</button>
1530
+ <button class="oi-arr" onclick="moveOut('\${acc.id}','\${slot.slotId}',1)" \${dnDis}>&darr;</button>
1474
1531
  </div>\`:'';
1475
1532
  return \`<div class="oi">
1476
1533
  \${multi?\`<span class="oi-num">\${i+1}</span>\`:''}
1477
1534
  <div class="oi-dot" style="background:\${COL[acc.provider]}"></div>
1478
1535
  <div class="oi-inf">
1479
- <div class="oi-pr">\${acc.label}</div>
1536
+ <div class="oi-pr">\${accountName(acc)}</div>
1480
1537
  <div class="oi-mo" title="\${m}">\${m}</div>
1481
1538
  </div>
1482
1539
  \${orderBtns}
1483
- <button class="oi-x" onclick="removeOut('\${acc.id}','\${slot.slotId}')">×</button>
1540
+ <button class="oi-x" onclick="removeOut('\${acc.id}','\${slot.slotId}')">\${SVG_ICONS.close}</button>
1484
1541
  </div>\`;
1485
1542
  }).join('')
1486
- :'<div class="out-empty">No providers connected<br><span style="font-size:9px">??Drag ??from a node here</span></div>';
1543
+ :'<div class="out-empty">No providers connected<br><span style="font-size:9px">Drag from a node here</span></div>';
1487
1544
  const pos=NP.out||{x:520,y:280};
1488
1545
  const bypassed=codexMode==='openai';
1489
- const bypassBanner=bypassed?'<div class="out-bypass">??Bypassed ??Codex using OpenAI directly</div>':'';
1546
+ const bypassBanner=bypassed?'<div class="out-bypass">Bypassed - Codex using OpenAI directly</div>':'';
1490
1547
  return \`<div class="nd out-node\${bypassed?' bypassed':''}" id="nd-out" style="left:\${pos.x}px;top:\${pos.y}px">
1491
1548
  <div class="\${piCls}" id="pi"></div>
1492
1549
  <div class="nh">
1493
- <div class="out-ic">??/div>
1550
+ <div class="out-ic">O</div>
1494
1551
  <div style="flex:1;min-width:0">
1495
1552
  <div class="nn">Active Output</div>
1496
1553
  <div style="font-size:9px;color:var(--mu)">\${subtitle}</div>
@@ -1501,7 +1558,7 @@ function buildOutNode(){
1501
1558
  </div>\`;
1502
1559
  }
1503
1560
 
1504
- // ?�?�?� SVG lines ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1561
+ // SVG lines
1505
1562
  function portPos(el){
1506
1563
  const ws=document.getElementById('ws').getBoundingClientRect();
1507
1564
  const r=el.getBoundingClientRect();
@@ -1535,7 +1592,7 @@ function drawLines(){
1535
1592
  });
1536
1593
  }
1537
1594
 
1538
- // ?�?�?� Node drag ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1595
+ // Node drag
1539
1596
  function startNodeDrag(e,id){
1540
1597
  if(e.target.tagName==='BUTTON'||e.target.tagName==='SELECT'||e.target.tagName==='INPUT')return;
1541
1598
  e.preventDefault();e.stopPropagation();
@@ -1636,13 +1693,14 @@ WS.addEventListener('wheel',e=>{
1636
1693
  zoomAt(e.deltaY<0?1.1:1/1.1,e.clientX-r.left,e.clientY-r.top);
1637
1694
  },{passive:false});
1638
1695
 
1639
- // ?�?�?� State changes ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1640
- async function connectOut(slotId){
1641
- const info=nodeSlots[slotId];
1642
- if(!info)return;
1643
- const r=await api('POST',\`/api/accounts/\${info.accountId}/slots\`,{slotId,model:info.model});
1644
- if(!r.ok){toast('Connection failed: '+await r.text(),true);return;}
1645
- toast('??Connected to output');
1696
+ // State changes
1697
+ async function connectOut(slotId){
1698
+ const info=nodeSlots[slotId];
1699
+ if(!info)return;
1700
+ const r=await api('POST',\`/api/accounts/\${info.accountId}/slots\`,{slotId,model:info.model});
1701
+ if(!r.ok){toast('Connection failed: '+await r.text(),true);return;}
1702
+ hiddenSlots.delete(slotId);
1703
+ toast('Connected to output');
1646
1704
  await fetchStatus();
1647
1705
  }
1648
1706
  async function removeOut(accountId,slotId){
@@ -1669,7 +1727,7 @@ async function moveOut(accountId,slotId,dir){
1669
1727
  await fetchStatus();
1670
1728
  }
1671
1729
 
1672
- // ?�?�?� Fetch ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1730
+ // Fetch
1673
1731
  async function fetchStatus(){
1674
1732
  try{
1675
1733
  const d=await(await fetch('/api/accounts')).json();
@@ -1678,9 +1736,10 @@ async function fetchStatus(){
1678
1736
  // Sync server activeModels ??canvas (add missing slots)
1679
1737
  let idx=[...onCanvas].length;
1680
1738
  for(const acc of ST.accounts){
1681
- for(const slot of (acc.activeModels||[])){
1682
- if(!nodeSlots[slot.slotId]){
1683
- nodeSlots[slot.slotId]={accountId:acc.id,model:slot.model};
1739
+ for(const slot of (acc.activeModels||[])){
1740
+ if(hiddenSlots.has(slot.slotId))continue;
1741
+ if(!nodeSlots[slot.slotId]){
1742
+ nodeSlots[slot.slotId]={accountId:acc.id,model:slot.model};
1684
1743
  }
1685
1744
  if(!onCanvas.has(slot.slotId)){
1686
1745
  onCanvas.add(slot.slotId);
@@ -1716,12 +1775,12 @@ function updateModeUI(){
1716
1775
  }
1717
1776
  async function switchProvider(mode){
1718
1777
  try{
1719
- toast(mode==='rcodex'?'Switching to rcodex Gateway??:'Switching to OpenAI direct??);
1778
+ toast(mode==='rcodex'?'Switching to rcodex Gateway':'Switching to OpenAI direct');
1720
1779
  const r=await api('POST','/api/codex-provider',{mode});
1721
1780
  if(!r.ok){toast('Switch failed',true);return;}
1722
1781
  codexMode=mode;
1723
1782
  updateModeUI();
1724
- toast(mode==='rcodex'?'??rcodex Gateway ??Codex restarted':'??OpenAI direct ??Codex restarted');
1783
+ toast(mode==='rcodex'?'rcodex Gateway - Codex restarted':'OpenAI direct - Codex restarted');
1725
1784
  }catch{toast('Switch failed',true);}
1726
1785
  }
1727
1786
  function toast(msg,err){
@@ -1732,7 +1791,7 @@ function toast(msg,err){
1732
1791
 
1733
1792
  window.addEventListener('resize',drawLines);
1734
1793
 
1735
- // ?�?�?� Init ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1794
+ // Init
1736
1795
  async function init(){
1737
1796
  await fetchStatus();
1738
1797
  try{const d=await(await fetch('/api/status')).json();if(d.home)termCwd=d.home;}catch{}