File size: 152,175 Bytes
f67267e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
<!DOCTYPE html>
<!--
  content/apps/Agent.html
  Runs a standalone configurable AI chatbot with streaming, MCP tools, attachments, agent traces, saved chats, and sandboxed HTML artifacts.
-->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="color-scheme" content="dark">
  <title>Agent</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link rel="preconnect" href="https://cdn.jsdelivr.net">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/styles/github-dark.min.css">
  <script src="https://cdn.jsdelivr.net/npm/lucide@0.468.0/dist/umd/lucide.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
  <style>
    * { box-sizing: border-box; }
    :root {
      color-scheme: dark;
      --bg: #0b0d10; --bg-2: #0f1216; --surface: #14181d; --surface-2: #191e24;
      --surface-3: #20262e; --text: #f3f5f7; --text-2: #c9d0d8; --muted: #8b949f;
      --faint: #626c78; --line: rgba(196, 208, 222, .13); --line-2: rgba(196, 208, 222, .23);
      --accent: #78d6c6; --accent-2: #9be8dc; --accent-soft: rgba(120, 214, 198, .11);
      --blue: #7cb7ff; --green: #6bd69b; --amber: #eac36a; --red: #ef8181; --purple: #bba0f7;
      --sidebar: 278px; --radius: 10px; --shadow: 0 18px 48px rgba(0,0,0,.34);
      --sans: "Inter", system-ui, sans-serif; --mono: "IBM Plex Mono", ui-monospace, monospace;
    }
    html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: var(--bg); color: var(--text); }
    body { font: 14px/1.5 var(--sans); -webkit-font-smoothing: antialiased; }
    button, input, textarea, select { font: inherit; }
    button { cursor: pointer; }
    button:disabled, input:disabled, textarea:disabled, select:disabled { cursor: not-allowed; opacity: .5; }
    a { color: var(--accent); }
    :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    ::-webkit-scrollbar { width: 9px; height: 9px; }
    ::-webkit-scrollbar-thumb { background: rgba(178,191,207,.19); border: 2px solid transparent; border-radius: 10px; background-clip: padding-box; }
    ::-webkit-scrollbar-thumb:hover { background: rgba(178,191,207,.32); border: 2px solid transparent; background-clip: padding-box; }
    .hidden { display: none !important; }
    .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
    .app { height: 100dvh; display: grid; grid-template-columns: var(--sidebar) minmax(0,1fr); overflow: hidden; }

    .sidebar { min-width: 0; display: flex; flex-direction: column; border-right: 1px solid var(--line); background: #0e1115; transition: width .18s ease, transform .18s ease; z-index: 30; }
    .side-head { padding: 10px; border-bottom: 1px solid var(--line); }
    .side-head-actions { display:flex; align-items:center; gap:6px; }
    .side-head .new-chat { flex:1; width: 100%; min-height: 31px; height: 31px; font-size: .73rem; }
    .side-head .new-chat svg { width: 14px; height: 14px; }
    .chat-list { min-height: 0; flex: 1; overflow: auto; padding: 0 7px 10px; }
    .chat-row { position: relative; display: grid; grid-template-columns: minmax(0,1fr) 22px; align-items: center; gap: 1px; border-radius: 7px; }
    .chat-row:hover { background: var(--surface); }
    .chat-row.active { background: var(--surface-2); }
    .chat-open { min-width: 0; display: flex; align-items: center; gap: 7px; height: 32px; padding: 0 7px; border: 0; background: transparent; color: var(--text-2); text-align: left; font-size: .72rem; }
    .chat-open svg { width: 13px; height: 13px; flex: 0 0 auto; }
    .chat-open span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
    .chat-delete { opacity: 0; width: 20px; height: 20px; padding: 0; display: grid; place-items: center; border: 0; border-radius: 5px; background: transparent; color: var(--muted); }
    .chat-delete svg { width: 12px; height: 12px; }
    .chat-row:hover .chat-delete, .chat-row:focus-within .chat-delete { opacity: 1; }
    .chat-delete:hover { color: var(--red); background: rgba(239,129,129,.1); }
    .empty-chats { padding: 24px 12px; color: var(--faint); text-align: center; font-size: .78rem; }
    .side-foot { padding: 8px; border-top: 1px solid var(--line); display: grid; gap: 5px; }
    .side-foot .btn { min-height: 31px; justify-content: flex-start; padding-inline: 9px; font-size: .72rem; }
    .side-foot .btn svg { width: 14px; height: 14px; }

    .workspace { min-width: 0; min-height: 0; display: grid; grid-template-rows: minmax(0,1fr); background: var(--bg); }
    .model-loading { width: 13px; height: 13px; border: 2px solid var(--line-2); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; }
    @keyframes spin { to { transform: rotate(360deg); } }

    .main { min-height: 0; position: relative; display: grid; grid-template-rows: minmax(0,1fr) auto; }
    .messages { min-height: 0; overflow-y: auto; overflow-anchor: auto; }
    .messages-inner { width: min(860px, calc(100% - 24px)); margin: 0 auto; padding: 18px 0 13px; }
    .welcome { min-height: calc(100dvh - 160px); display: grid; place-items: center; padding: 20px 0; }
    .welcome-card { width: min(680px,100%); text-align: center; }
    .welcome-icon { width: 44px; height: 44px; margin: 0 auto 13px; display: grid; place-items: center; border-radius: 12px; color: var(--accent); background: var(--accent-soft); border: 1px solid rgba(120,214,198,.25); box-shadow: 0 0 36px rgba(120,214,198,.08); }
    .welcome h1 { margin: 0 0 5px; font-size: clamp(1.3rem, 3vw, 1.7rem); letter-spacing: -.03em; }
    .prompt-grid { margin-top: 15px; display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 7px; text-align: left; }
    .prompt-card { min-height: 60px; padding: 9px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--text-2); }
    .prompt-card:hover { border-color: var(--line-2); background: var(--surface-2); }
    .prompt-card strong { display: block; margin-bottom: 2px; color: var(--text); font-size: .75rem; }
    .prompt-card span { color: var(--muted); font-size: .68rem; }

    .message { position: relative; display: flex; flex-direction: column; margin: 0 0 16px; animation: rise .16s ease; }
    @keyframes rise { from { opacity: 0; transform: translateY(4px); } }
    .message.user { align-items: flex-end; }
    .message.assistant { align-items: flex-start; }
    .message-timeline { width: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 5px; }
    .message-label { margin: 0 4px 3px; color: var(--muted); font-size: .62rem; font-weight: 600; letter-spacing: .03em; }
    .bubble { max-width: min(94%, 730px); border: 1px solid var(--line); border-radius: 12px; padding: 9px 11px; background: var(--surface); box-shadow: 0 2px 9px rgba(0,0,0,.12); overflow-wrap: anywhere; }
    .user .bubble { max-width: min(84%, 650px); color: #08110f; background: var(--accent); border-color: transparent; border-top-right-radius: 5px; }
    .assistant .bubble { border-top-left-radius: 5px; }
    .bubble.error { border-color: rgba(239,129,129,.35); background: rgba(239,129,129,.06); }
    .message-actions { display: flex; gap: 1px; min-height: 23px; margin: 3px 2px 0; opacity: 0; transition: opacity .14s; }
    .message:hover .message-actions, .message:focus-within .message-actions { opacity: 1; }
    .user .message-actions { justify-content: flex-end; }
    .message-actions button { width: 23px; height: 23px; padding: 0; border: 0; border-radius: 5px; background: transparent; color: var(--muted); }
    .message-actions button svg { width: 12px; height: 12px; }
    .message-actions button:hover { color: var(--text); background: var(--surface-2); }
    .model-badge { padding: 0 6px; align-self: center; color: var(--faint); font-size: .65rem; }

    .markdown { min-width: 0; font-size: .82rem; line-height: 1.6; }
    .markdown > :first-child { margin-top: 0; } .markdown > :last-child { margin-bottom: 0; }
    .markdown p, .markdown ul, .markdown ol, .markdown blockquote, .markdown pre, .markdown table { margin: .65em 0; }
    .markdown h1, .markdown h2, .markdown h3 { margin: 1em 0 .45em; line-height: 1.25; }
    .markdown h1 { font-size: 1.32rem; } .markdown h2 { font-size: 1.14rem; } .markdown h3 { font-size: 1rem; }
    .markdown ul, .markdown ol { padding-left: 1.4rem; }
    .markdown blockquote { border-left: 3px solid var(--line-2); padding-left: 12px; color: var(--muted); }
    .markdown :not(pre) > code { padding: 2px 5px; border: 1px solid var(--line); border-radius: 5px; background: rgba(0,0,0,.22); color: var(--accent-2); font: .88em var(--mono); }
    .user .markdown :not(pre) > code { color: #07100e; border-color: rgba(0,0,0,.15); }
    .code-wrap { margin: .8em 0; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; background: #0d1014; }
    .code-head { height: 30px; display: flex; align-items: center; gap: 5px; padding: 0 6px 0 9px; border-bottom: 1px solid var(--line); background: #11151a; }
    .code-head span { min-width: 0; flex: 1; color: var(--muted); font: .67rem var(--mono); text-transform: uppercase; }
    .code-head button { height: 22px; padding: 0 7px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface); color: var(--text-2); font-size: .64rem; }
    .code-head button:hover { border-color: var(--line-2); color: var(--text); }
    .code-wrap pre { margin: 0; padding: 10px; overflow: auto; background: transparent !important; }
    .code-wrap code { font: .78rem/1.55 var(--mono); background: transparent !important; }
    .markdown table { width: 100%; border-collapse: collapse; display: block; overflow-x: auto; }
    .markdown th, .markdown td { padding: 7px 9px; border: 1px solid var(--line); text-align: left; }
    .markdown th { background: var(--surface-2); }
    .markdown img { max-width: 100%; border-radius: 8px; }

    .trace { width: min(94%,730px); margin-bottom: 5px; border: 1px solid var(--line); border-radius: 8px; background: rgba(20,24,29,.65); overflow: hidden; }
    .trace-row + .trace-row { border-top: 1px solid var(--line); }
    .trace-row summary { min-height: 31px; display: flex; align-items: center; gap: 7px; padding: 5px 8px; color: var(--text-2); cursor: pointer; list-style: none; font-size: .7rem; }
    .trace-row summary svg { width: 13px; height: 13px; }
    .trace-row summary::-webkit-details-marker { display: none; }
    .trace-row summary .trace-title { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .trace-row .chev { transition: transform .15s; color: var(--faint); }
    .trace-row[open] .chev { transform: rotate(180deg); }
    .trace-detail { padding: 0 8px 8px 28px; }
    .trace-detail pre { max-height: 240px; margin: 0; padding: 7px; overflow: auto; border: 1px solid var(--line); border-radius: 6px; background: #0c0f12; color: var(--text-2); white-space: pre-wrap; font: .66rem/1.5 var(--mono); }
    .trace-tool { color: var(--amber); } .trace-done { color: var(--green); } .trace-error { color: var(--red); } .trace-thought { color: var(--purple); }

    .attachment-list { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 6px; }
    .attachment { max-width: 230px; display: grid; grid-template-columns: 26px minmax(0,1fr) auto; align-items: center; gap: 6px; padding: 5px; border: 1px solid var(--line); border-radius: 7px; background: rgba(0,0,0,.14); }
    .attachment img { width: 26px; height: 26px; object-fit: cover; border-radius: 4px; }
    .attachment-icon { width: 26px; height: 26px; display: grid; place-items: center; border-radius: 4px; background: var(--surface-3); color: var(--blue); }
    .attachment-copy { min-width: 0; }
    .attachment-copy strong, .attachment-copy span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .attachment-copy strong { font-size: .7rem; } .attachment-copy span { color: var(--muted); font-size: .62rem; }
    .attachment-remove { width: 19px; height: 19px; padding: 0; border: 0; border-radius: 4px; background: transparent; color: var(--muted); }
    .attachment-remove svg { width: 11px; height: 11px; }
    .attachment-remove:hover { color: var(--red); background: rgba(239,129,129,.1); }

    .status-line { width: min(860px,calc(100% - 24px)); min-height: 23px; margin: 0 auto 3px; display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: .68rem; }
    .status-line .pulse { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 0 rgba(120,214,198,.4); animation: pulse 1.25s infinite; }
    @keyframes pulse { 70% { box-shadow: 0 0 0 7px rgba(120,214,198,0); } 100% { box-shadow: 0 0 0 0 rgba(120,214,198,0); } }
    .composer-shell { position: relative; padding: 0 11px 10px; background: linear-gradient(transparent, var(--bg) 22%); }
    .composer { width: min(860px,100%); margin: 0 auto; border: 1px solid var(--line-2); border-radius: 11px; background: var(--surface); box-shadow: 0 10px 28px rgba(0,0,0,.24); overflow: hidden; }
    .pending-attachments { padding: 7px 8px 0; }
    .composer textarea { width: 100%; min-height: 46px; max-height: 170px; display: block; resize: none; padding: 10px 11px 5px; border: 0; outline: 0; background: transparent; color: var(--text); font-size: .82rem; line-height: 1.45; }
    .composer textarea::placeholder { color: var(--faint); }
    .composer-foot { min-height: 36px; display: flex; align-items: center; gap: 3px; padding: 3px 5px; }
    .composer-spacer { flex: 1; }
    .reasoning-btn { width: auto; max-width: 125px; height: 27px; padding: 0 7px; display: inline-flex; align-items: center; gap: 4px; border: 0; border-radius: 6px; background: transparent; color: var(--muted); font-size: .68rem; }
    .reasoning-btn:hover:not(:disabled) { background: var(--surface-2); color: var(--text-2); }
    .reasoning-btn span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .reasoning-btn svg { width: 13px; height: 13px; flex: 0 0 auto; }
    .model-picker-btn { width: auto; max-width: 210px; height: 27px; padding: 0 7px; display: inline-flex; align-items: center; gap: 4px; border: 0; border-radius: 6px; background: transparent; color: var(--accent); font-size: .68rem; }
    .model-picker-btn:hover { background: var(--accent-soft); }
    .model-picker-btn span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .model-picker-btn svg { width: 13px; height: 13px; flex: 0 0 auto; }
    .send-btn { color: #07110f !important; background: var(--accent) !important; border-color: transparent !important; }
    .send-btn:hover { background: var(--accent-2) !important; }
    .stop-btn { color: var(--red) !important; background: rgba(239,129,129,.1) !important; }
    .command-panel { position:absolute; left:50%; bottom:calc(100% + 4px); z-index:45; width:min(860px,calc(100% - 22px)); max-height:min(360px,48dvh); transform:translateX(-50%); padding:5px; border:1px solid var(--line-2); border-radius:12px; background:rgba(27,29,32,.98); box-shadow:0 18px 48px rgba(0,0,0,.38); overflow:auto; }
    .command-row { width:100%; min-width:0; display:grid; grid-template-columns:24px minmax(0,1fr); align-items:center; gap:5px; min-height:35px; padding:5px 7px; border:0; border-radius:8px; background:transparent; color:var(--text-2); text-align:left; }
    .command-row:hover, .command-row.selected { background:rgba(255,255,255,.08); color:var(--text); }
    .command-row > svg { width:15px; height:15px; color:var(--muted); }
    .command-copy { min-width:0; display:flex; align-items:baseline; gap:7px; overflow:hidden; }
    .command-copy strong { flex:0 0 auto; font-size:.72rem; font-weight:500; }
    .command-copy span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--muted); font-size:.68rem; }
    .command-empty { padding:18px 10px; color:var(--muted); text-align:center; font-size:.72rem; }
    .composer-popover { position:absolute; bottom:calc(100% + 4px); z-index:46; padding:7px; border:1px solid var(--line-2); border-radius:10px; background:rgba(27,29,32,.98); box-shadow:0 16px 42px rgba(0,0,0,.36); }
    .model-popover { width:min(340px,calc(100% - 22px)); }
    .reasoning-popover { width:min(290px,calc(100% - 22px)); }
    .tools-popover { width:min(440px,calc(100% - 22px)); }
    .config-popover { width:min(570px,calc(100% - 22px)); }
    .data-controls-popover { width:min(420px,calc(100% - 22px)); }
    .panel-popover { max-height:min(70dvh,640px); padding:0; overflow:hidden; grid-template-rows:auto minmax(0,1fr) auto; }
    .panel-popover:not(.hidden) { display:grid; }
    .panel-popover .dialog-body { min-height:0; overflow:auto; }
    .composer-popover .model-search-wrap { margin-bottom:4px; border:1px solid var(--line); border-radius:6px; background:rgba(255,255,255,.025); }
    .composer-popover .model-search-wrap:focus-within { border-color:rgba(120,214,198,.45); box-shadow:inset 0 0 0 1px rgba(120,214,198,.08); }
    .composer-popover .model-search-wrap input { min-height:27px; height:27px; padding-left:27px; padding-right:28px; border:0; border-radius:6px; background:transparent; font-size:.68rem; }
    .composer-popover .model-search-wrap input:focus-visible { outline:none; }
    .composer-popover .model-search-wrap svg { left:8px; width:12px; height:12px; }
    .composer-popover .model-search-wrap .model-loading { position:absolute; left:auto; right:8px; top:7px; width:11px; height:11px; }
    .composer-popover .model-list { min-height:80px; max-height:min(310px,42dvh); }
    .composer-popover .reasoning-options { display:grid; grid-template-columns:1fr; }

    .btn, .icon-btn, select, input[type="text"], input[type="password"], input[type="number"], textarea.field-input {
      border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2); color: var(--text-2);
    }
    .btn { min-height: 30px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 9px; font-weight: 600; font-size: .71rem; }
    .btn:hover, .icon-btn:hover { border-color: var(--line-2); background: var(--surface-3); color: var(--text); }
    .btn.primary { color: #07110f; border-color: transparent; background: var(--accent); }
    .btn.danger { color: var(--red); background: rgba(239,129,129,.08); }
    .btn.ghost { background: transparent; }
    .icon-btn { width: 30px; height: 30px; padding: 0; display: grid; place-items: center; }
    .icon-btn svg, .btn svg, .attachment-icon svg, .welcome-icon svg { width: 14px; height: 14px; }
    select, input[type="text"], input[type="url"], input[type="password"], input[type="number"] { min-height: 31px; padding: 0 8px; font-size: .75rem; }

    .dialog-head { min-height: 46px; display: flex; align-items: center; gap: 8px; padding: 8px 9px 8px 13px; border-bottom: 1px solid var(--line); }
    .dialog-head > div { min-width: 0; flex: 1; }
    .dialog-head h2 { margin: 0; font-size: .86rem; } .dialog-head p { margin: 1px 0 0; color: var(--muted); font-size: .65rem; }
    .dialog-body { min-height:0; max-height:none; padding: 12px; overflow:auto; }
    .config-popover .dialog-body, .data-controls-popover .dialog-body { padding-block: 10px; }
    .dialog-foot { min-height: 46px; display: flex; justify-content: flex-end; align-items: center; gap: 6px; padding: 7px 9px; border-top: 1px solid var(--line); }
    .settings-section + .settings-section { margin-top: 15px; padding-top: 13px; border-top: 1px solid var(--line); }
    .settings-section h3 { margin: 0 0 3px; font-size: .78rem; } .settings-section > p { margin: 0 0 11px; color: var(--muted); font-size: .7rem; }
    .mcp-settings-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:7px; }
    .mcp-settings-head h3 { margin:0; }
    .mcp-server-list { display:grid; gap:6px; }
    .mcp-server-row { display:grid; grid-template-columns:22px minmax(100px,.38fr) minmax(180px,1fr) 30px; align-items:center; gap:6px; }
    .mcp-server-row input[type="checkbox"] { accent-color:var(--accent); justify-self:center; }
    .mcp-server-row input[type="text"], .mcp-server-row input[type="url"] { min-width:0; }
    .mcp-server-row .icon-btn { width:30px; height:30px; }
    .provider-settings-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:9px; }
    .provider-settings-head h3 { margin-bottom:2px; }
    .provider-settings-head p { margin:0; color:var(--muted); font-size:.66rem; }
    .provider-picker { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; margin-bottom:10px; }
    .provider-card { min-width:0; display:grid; grid-template-columns:30px minmax(0,1fr) auto; align-items:center; gap:8px; padding:9px; border:1px solid var(--line); border-radius:8px; background:var(--surface-2); color:var(--text-2); text-align:left; }
    .provider-card:hover { border-color:var(--line-2); background:var(--surface-3); }
    .provider-card.active { border-color:rgba(120,214,198,.45); background:var(--accent-soft); color:var(--text); box-shadow:inset 0 0 0 1px rgba(120,214,198,.08); }
    .provider-card-icon { width:30px; height:30px; display:grid; place-items:center; border-radius:7px; background:rgba(255,255,255,.045); color:var(--accent); }
    .provider-card-icon svg { width:15px; height:15px; }
    .provider-card-copy { min-width:0; display:grid; gap:2px; }
    .provider-card-copy strong, .provider-card-copy span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .provider-card-copy strong { font-size:.72rem; }
    .provider-card-copy span { color:var(--muted); font-size:.63rem; }
    .provider-card-check { width:17px; height:17px; color:var(--accent); opacity:0; }
    .provider-card.active .provider-card-check { opacity:1; }
    .provider-editor-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin:2px 0 9px; padding-top:10px; border-top:1px solid var(--line); }
    .provider-editor-title { min-width:0; display:flex; align-items:center; gap:7px; }
    .provider-editor-title strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:.75rem; }
    .provider-badge { padding:2px 6px; border-radius:999px; background:var(--surface-3); color:var(--muted); font-size:.58rem; text-transform:uppercase; letter-spacing:.04em; }
    .field { display: grid; gap: 4px; margin-bottom: 8px; }
    .field label, .field-row label { color: var(--text-2); font-size: .7rem; font-weight: 600; }
    .field small { color: var(--faint); font-size: .65rem; }
    .field input, .field select, .field textarea { width: 100%; }
    .field textarea { min-height: 82px; padding: 7px; resize: vertical; font-size: .75rem; }
    .field-row { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 10px; }
    .range-row { display: grid; grid-template-columns: minmax(0,1fr) 58px; align-items: center; gap: 7px; }
    input[type="range"] { width: 100%; accent-color: var(--accent); }
    .check-row { display: flex; align-items: flex-start; gap: 8px; color: var(--text-2); font-size: .72rem; }
    .check-row input { margin-top: 3px; accent-color: var(--accent); }
    .security-note, .server-note { display: flex; gap: 8px; padding: 9px 10px; border-radius: 8px; color: var(--muted); font-size: .68rem; }
    .security-note { background: rgba(107,214,155,.06); border: 1px solid rgba(107,214,155,.15); }
    .server-note { background: rgba(124,183,255,.06); border: 1px solid rgba(124,183,255,.15); }
    .tool-grid { margin-top:8px; border:1px solid var(--line); border-radius:7px; overflow:hidden; background:var(--surface-2); }
    .tool-server-group + .tool-server-group { border-top:1px solid var(--line-2); }
    .tool-server-head { min-height:34px; display:grid; grid-template-columns:16px minmax(0,1fr); align-items:center; gap:8px; padding:6px 9px; background:rgba(255,255,255,.035); cursor:pointer; }
    .tool-server-head:hover { background:rgba(255,255,255,.055); }
    .tool-server-head input { margin:0; accent-color:var(--accent); }
    .tool-server-head span { min-width:0; display:flex; align-items:baseline; justify-content:space-between; gap:8px; }
    .tool-server-head strong { overflow:hidden; color:var(--text); font-size:.7rem; text-overflow:ellipsis; white-space:nowrap; }
    .tool-server-head small { flex:0 0 auto; color:var(--muted); font-size:.61rem; }
    .tool-card { display:grid; grid-template-columns:16px 17px minmax(0,1fr); align-items:start; gap:8px; min-height:43px; padding:7px 9px; border:0; border-bottom:1px solid var(--line); background:transparent; cursor:pointer; }
    .tool-card:last-child { border-bottom:0; }
    .tool-card:hover { background:var(--surface-3); }
    .tool-card.disabled { opacity: .55; }
    .tool-card input { margin:2px 0 0; accent-color:var(--accent); }
    .tool-card strong, .tool-card span { display:block; }
    .tool-card > span:nth-child(2) { color:var(--muted); }
    .tool-card > span:last-child { min-width:0; }
    .tool-card strong { color:var(--text-2); font-size:.72rem; line-height:1.25; }
    .tool-card > span:last-child > span { overflow:hidden; color:var(--muted); font-size:.63rem; line-height:1.35; text-overflow:ellipsis; white-space:nowrap; }
    .tool-card svg { width:15px; height:15px; margin-top:1px; }
    .data-actions { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 8px; }
    .data-actions .btn { min-width: 0; padding-inline: 8px; }
    .model-search-wrap { position: relative; margin-bottom: 7px; }
    .model-search-wrap svg { position: absolute; left: 10px; top: 50%; width: 14px; height: 14px; color: var(--muted); transform: translateY(-50%); pointer-events: none; }
    .model-search-wrap input { width: 100%; padding-left: 32px; }
    .model-list { min-height: 100px; max-height: min(56dvh,400px); overflow-y: auto; display: grid; align-content: start; gap: 2px; }
    .model-option { width: 100%; min-width: 0; min-height: 33px; padding: 5px 7px; display: flex; align-items: center; gap: 7px; border: 0; border-radius: 6px; background: transparent; color: var(--text-2); text-align: left; font-size: .74rem; }
    .model-option:hover, .model-option.selected { background: var(--surface-2); color: var(--text); }
    .model-option.selected { color: var(--accent); }
    .model-option span { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .model-option small { color: var(--faint); font: .62rem var(--mono); }
    .model-empty { padding: 34px 12px; color: var(--muted); text-align: center; font-size: .75rem; }
    .reasoning-options { display: flex; flex-wrap: wrap; gap: 5px; }
    .reasoning-option { min-height: 30px; padding: 0 9px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-2); color: var(--text-2); font-size: .69rem; text-transform: capitalize; }
    .reasoning-option:hover { border-color: var(--line-2); background: var(--surface-3); }
    .reasoning-option.selected { border-color: rgba(120,214,198,.45); background: var(--accent-soft); color: var(--accent); }
    .reasoning-budget { margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--line); display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: end; gap: 6px; }
    .reasoning-budget .field { margin: 0; }
    .reasoning-unavailable { padding: 24px 10px; color: var(--muted); text-align: center; }

    .artifact { position: fixed; inset: 0 0 0 auto; width: min(780px,92vw); display: grid; grid-template-rows: 48px minmax(0,1fr); transform: translateX(102%); transition: transform .2s ease; background: var(--surface); border-left: 1px solid var(--line-2); box-shadow: var(--shadow); z-index: 60; }
    .artifact.open { transform: translateX(0); }
    .artifact-head { display: flex; align-items: center; gap: 7px; padding: 7px 9px; border-bottom: 1px solid var(--line); }
    .artifact-head > div { min-width: 0; flex: 1; } .artifact-head strong, .artifact-head span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .artifact-head span { color: var(--muted); font-size: .67rem; }
    .artifact iframe { width: 100%; height: 100%; border: 0; background: white; }
    .artifact-backdrop { position: fixed; inset: 0; z-index: 55; background: rgba(0,0,0,.45); }
    .drop-overlay { position: fixed; inset: 12px; z-index: 90; display: grid; place-items: center; border: 2px dashed var(--accent); border-radius: 15px; background: rgba(11,13,16,.9); color: var(--text); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); pointer-events: none; }
    .drop-overlay div { text-align: center; } .drop-overlay strong { display: block; margin-top: 8px; font-size: 1rem; }
    .toast-stack { position: fixed; right: 14px; bottom: 14px; z-index: 100; display: grid; gap: 7px; pointer-events: none; }
    .toast { max-width: min(340px,calc(100vw - 24px)); padding: 7px 9px; border: 1px solid var(--line-2); border-radius: 7px; background: var(--surface-2); color: var(--text-2); box-shadow: var(--shadow); animation: toast-in .18s ease; font-size: .68rem; }
    .toast.error { border-color: rgba(239,129,129,.35); } @keyframes toast-in { from { opacity: 0; transform: translateY(5px); } }
    .mobile-only { display: none; }

    @media (max-width: 760px) {
      .app { display: block; }
      .sidebar { position: fixed; inset: 0 auto 0 0; width: min(300px,88vw); transform: translateX(-102%); box-shadow: var(--shadow); }
      .sidebar.open { transform: translateX(0); }
      .sidebar-backdrop { position: fixed; inset: 0; z-index: 25; background: rgba(0,0,0,.5); }
      .workspace { height: 100dvh; }
      .mobile-only { display: grid; }
      .top-title span { display: none; }
      .messages-inner { width: calc(100% - 18px); padding-top: 16px; }
      .bubble { max-width: 97%; } .user .bubble { max-width: 91%; }
      .message-actions { opacity: 1; }
      .prompt-grid { grid-template-columns: 1fr; }
      .welcome { min-height: 0; padding: 28px 0 18px; }
      .composer-shell { padding-inline: 8px; }
      input:not([type="range"]):not([type="checkbox"]):not([type="file"]), textarea, select { font-size: 16px !important; }
      .composer-foot { min-height: 50px; gap: 4px; padding-block: 4px; }
      .composer-foot .icon-btn { width: 42px; height: 42px; }
      .composer-foot .icon-btn svg { width: 17px; height: 17px; }
      .field-row, .provider-picker, .tool-grid { grid-template-columns: 1fr; }
      .mcp-server-row { grid-template-columns:22px minmax(0,1fr) 30px; }
      .mcp-server-row input[type="url"] { grid-column:2 / 4; }
      .data-actions { grid-template-columns: 1fr; }
      .reasoning-btn, .model-picker-btn { height: 42px; max-width: 96px; padding-inline: 8px; font-size: .72rem; }
      .reasoning-btn svg, .model-picker-btn svg { width: 15px; height: 15px; }
    }
    @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; transition-duration: .01ms !important; } }
    @media (max-width: 360px) { .reasoning-btn, .model-picker-btn { max-width: 60px; } }
  </style>
</head>
<body>
  <div class="app">
    <aside class="sidebar" id="sidebar" aria-label="Chats">
      <div class="side-head">
        <div class="side-head-actions">
          <button class="btn new-chat" id="newChatBtn"><i data-lucide="plus"></i> New chat</button>
          <button class="icon-btn mobile-only" id="closeSidebarBtn" aria-label="Close sidebar"><i data-lucide="x"></i></button>
        </div>
      </div>
      <div class="chat-list" id="chatList"></div>
    </aside>
    <div class="sidebar-backdrop hidden" id="sidebarBackdrop"></div>

    <section class="workspace">
      <main class="main" id="main">
        <div class="messages" id="messages" aria-live="polite">
          <div class="messages-inner" id="messagesInner"></div>
        </div>
        <div>
          <div class="status-line hidden" id="statusLine"><span class="pulse"></span><span id="statusText">Thinking…</span></div>
          <div class="composer-shell">
            <div class="command-panel hidden" id="commandPanel" role="listbox" aria-label="Commands"><div id="commandList"></div></div>
            <div class="composer-popover model-popover hidden" id="modelPopover" role="dialog" aria-label="Choose a model">
              <label class="sr-only" for="modelSearchInput">Search models</label>
              <div class="model-search-wrap"><i data-lucide="search"></i><input id="modelSearchInput" type="text" placeholder="Search models…" autocomplete="off"><div class="model-loading hidden" id="modelLoading" aria-label="Loading models"></div></div>
              <div class="model-list" id="modelList" role="listbox" aria-label="Models"></div>
            </div>
            <div class="composer-popover reasoning-popover hidden" id="reasoningPopover" role="dialog" aria-label="Reasoning settings">
              <div id="reasoningControls"></div>
            </div>
            <div class="composer-popover panel-popover tools-popover hidden" id="toolsPopover" role="dialog" aria-label="MCP tools">
              <div class="dialog-head"><div><h2>MCP tools</h2></div><div class="model-loading hidden" id="toolLoading" aria-label="Loading tools"></div><button class="icon-btn" type="button" data-close-popover aria-label="Close"><i data-lucide="x"></i></button></div>
              <div class="dialog-body">
                <div class="tool-grid" id="toolGrid"></div>
              </div>
              <div class="dialog-foot"><button class="btn" type="button" id="refreshToolsBtn"><i data-lucide="refresh-cw"></i> Refresh</button><button class="btn" type="button" id="disableToolsBtn">Disable all</button><button class="btn primary" type="button" id="saveToolsBtn">Done</button></div>
            </div>
            <div class="composer-popover panel-popover config-popover hidden" id="providersPopover" role="dialog" aria-label="Providers">
              <div class="dialog-head"><div><h2>Providers</h2></div><button class="icon-btn" type="button" data-close-popover aria-label="Close"><i data-lucide="x"></i></button></div>
              <div class="dialog-body">
                <section class="settings-section">
                  <div class="provider-settings-head"><div><h3>Inference providers</h3><p>Choose where models run.</p></div><button class="btn" type="button" id="addInferenceProviderBtn"><i data-lucide="plus"></i> Add custom</button></div>
                  <div class="provider-picker" id="inferenceProviderSelect" aria-label="Inference providers"></div>
                  <div class="provider-editor-head"><div class="provider-editor-title"><strong id="providerEditorTitle">Provider</strong><span class="provider-badge" id="providerEditorBadge">Built-in</span></div><button class="icon-btn danger" type="button" id="removeInferenceProviderBtn" aria-label="Remove custom provider" title="Remove custom provider"><i data-lucide="trash-2"></i></button></div>
                  <div class="field-row"><div class="field"><label for="inferenceProviderNameInput">Provider name</label><input id="inferenceProviderNameInput" type="text" placeholder="Provider name"></div><div class="field"><label for="inferenceDefaultModelInput">Default model</label><input id="inferenceDefaultModelInput" type="text" placeholder="model-id"></div></div>
                  <div class="field"><label for="inferenceChatUrlInput">Chat completions URL</label><input id="inferenceChatUrlInput" type="url" placeholder="https://api.example.com/v1/chat/completions"></div>
                  <div class="field"><label for="inferenceModelsUrlInput">Models URL (optional)</label><input id="inferenceModelsUrlInput" type="url" placeholder="https://api.example.com/v1/models"></div>
                  <div class="field"><label for="apiKeyInput">API key</label><input id="apiKeyInput" type="password" placeholder="Optional for local providers" autocomplete="off"></div>
                  <label class="check-row"><input type="checkbox" id="rememberKeyInput"><span>Remember API key</span></label>
                </section>
              </div>
              <div class="dialog-foot"><button class="btn primary" type="button" id="saveProvidersBtn">Save providers</button></div>
            </div>
            <div class="composer-popover panel-popover config-popover hidden" id="parametersPopover" role="dialog" aria-label="Parameters">
              <div class="dialog-head"><div><h2>Parameters</h2></div><button class="icon-btn" type="button" data-close-popover aria-label="Close"><i data-lucide="x"></i></button></div>
              <div class="dialog-body">
                <section class="settings-section"><h3>System prompt</h3><div class="field"><label class="sr-only" for="systemPromptInput">System prompt</label><textarea class="field-input" id="systemPromptInput"></textarea></div></section>
                <section class="settings-section"><h3>Generation</h3><div class="field-row">
                  <div class="field"><label for="temperatureInput">Temperature</label><div class="range-row"><input id="temperatureRange" type="range" min="0" max="2" step="0.05"><input id="temperatureInput" type="number" min="0" max="2" step="0.05"></div></div>
                  <div class="field"><label for="topPInput">Top P</label><div class="range-row"><input id="topPRange" type="range" min="0" max="1" step="0.01"><input id="topPInput" type="number" min="0" max="1" step="0.01"></div></div>
                  <div class="field"><label for="topKInput">Top K</label><div class="range-row"><input id="topKRange" type="range" min="0" max="100" step="1"><input id="topKInput" type="number" min="0" max="100" step="1"></div></div>
                  <div class="field"><label for="repetitionPenaltyInput">Repetition Penalty</label><div class="range-row"><input id="repetitionPenaltyRange" type="range" min="0" max="2" step="0.1"><input id="repetitionPenaltyInput" type="number" min="0" max="2" step="0.1"></div></div>
                  <div class="field"><label for="maxTokensInput">Max output tokens</label><input id="maxTokensInput" type="number" min="64" max="32768" step="64"></div>
                  <div class="field"><label for="seedInput">Seed (0 = random)</label><input id="seedInput" type="number" min="0" step="1"></div>
                </div></section>
              </div>
              <div class="dialog-foot"><button class="btn" type="button" id="resetParametersBtn">Reset defaults</button></div>
            </div>
            <div class="composer-popover panel-popover config-popover hidden" id="mcpConfigPopover" role="dialog" aria-label="MCP configuration">
              <div class="dialog-head"><div><h2>MCP Config</h2></div><button class="icon-btn" type="button" data-close-popover aria-label="Close"><i data-lucide="x"></i></button></div>
              <div class="dialog-body"><section class="settings-section"><div class="mcp-settings-head"><h3>Servers</h3><button class="btn" type="button" id="addMcpServerBtn"><i data-lucide="plus"></i> Add server</button></div><div class="mcp-server-list" id="mcpServerList"></div></section></div>
            </div>
            <div class="composer-popover panel-popover data-controls-popover hidden" id="dataControlsPopover" role="dialog" aria-label="Data controls">
              <div class="dialog-head"><div><h2>Data Controls</h2></div><button class="icon-btn" type="button" data-close-popover aria-label="Close"><i data-lucide="x"></i></button></div>
              <div class="dialog-body"><section class="settings-section"><div class="data-actions"><button class="btn" type="button" id="exportChatSettingsBtn"><i data-lucide="download"></i> Export current chat</button><button class="btn" type="button" id="exportAllBtn"><i data-lucide="archive"></i> Export all data</button><button class="btn" type="button" id="importAllBtn"><i data-lucide="upload"></i> Import data</button><button class="btn danger" type="button" id="clearDataBtn"><i data-lucide="trash-2"></i> Clear data</button></div><input class="sr-only" id="importInput" type="file" accept="application/json,.json"></section></div>
            </div>
            <form class="composer" id="composer">
              <div class="pending-attachments hidden" id="pendingAttachments"></div>
              <label class="sr-only" for="promptInput">Message Agent</label>
              <textarea id="promptInput" rows="1" placeholder="Message Agent…" autocomplete="off"></textarea>
              <div class="composer-foot">
                <input class="sr-only" id="fileInput" type="file" multiple aria-label="Attach files" accept="text/*,.txt,.md,.mdx,.json,.jsonl,.csv,.ts,.tsx,.js,.jsx,.py,.html,.css,.xml,.yaml,.yml,.toml,.ini,.sql,.sh,.ps1,.lua,image/png,image/jpeg,image/webp,image/gif">
                <button class="icon-btn mobile-only" type="button" id="openSidebarBtn" aria-label="Open chats" title="Open chats"><i data-lucide="panel-left"></i></button>
                <button class="icon-btn" type="button" id="commandBtn" aria-label="Open commands" title="Commands"><i data-lucide="plus"></i></button>
                <button class="model-picker-btn" type="button" id="modelPickerBtn" aria-label="Choose model" title="Choose model"><span id="modelButtonLabel">Model</span><i data-lucide="chevron-down"></i></button>
                <button class="reasoning-btn" type="button" id="reasoningBtn" aria-label="Reasoning settings" title="Reasoning settings"><span id="reasoningLabel">Thinking: auto</span><i data-lucide="chevron-down"></i></button>
                <span class="composer-spacer"></span>
                <button class="icon-btn send-btn" id="sendBtn" type="submit" aria-label="Send message" title="Send"><i data-lucide="arrow-up"></i></button>
                <button class="icon-btn stop-btn hidden" id="stopBtn" type="button" aria-label="Stop generating" title="Stop"><i data-lucide="square"></i></button>
              </div>
            </form>
          </div>
        </div>
      </main>
    </section>
  </div>

  <div class="artifact-backdrop hidden" id="artifactBackdrop"></div>
  <aside class="artifact" id="artifactPanel" aria-label="HTML artifact preview">
    <div class="artifact-head"><i data-lucide="monitor-play"></i><div><strong id="artifactTitle">HTML Preview</strong><span>Sandboxed generated artifact</span></div><button class="btn" id="downloadArtifactBtn"><i data-lucide="download"></i> Download</button><button class="icon-btn" id="closeArtifactBtn" aria-label="Close preview"><i data-lucide="x"></i></button></div>
    <iframe id="artifactFrame" title="Generated HTML artifact" sandbox="allow-scripts allow-forms allow-modals allow-popups allow-downloads"></iframe>
  </aside>
  <div class="drop-overlay hidden" id="dropOverlay"><div><i data-lucide="files"></i><strong>Drop files to attach</strong><span>Text, code, JSON, or supported images</span></div></div>
  <div class="toast-stack" id="toastStack" aria-live="polite"></div>

  <script>
  (() => {
    "use strict";
    const $ = (selector, root = document) => root.querySelector(selector);
    const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
    const STORAGE = { chats: "agent:chats:v1", settings: "agent:settings:v1", key: "agent:openrouter-key:v1", providerKeys: "agent:inference-keys:v1", models: "agent:models:v1" };
    const CHAT_DB = { name: "agent-chat-storage", version: 3, chats: "chats", secrets: "secrets", preferences: "preferences" };
    const MAX_CHATS = 100, MAX_ATTACHMENTS = 6, MAX_TEXT_BYTES = 512 * 1024, MAX_IMAGE_BYTES = 2 * 1024 * 1024, MAX_TOOL_ITERATIONS = 5;
    const DEFAULT_MCP_SERVERS = [{ id:"nymbo-tools", name:"Nymbo-Tools", url:"https://nymbo-tools.hf.space/gradio_api/mcp/", enabled:true }];
    const MCP_PROTOCOL_VERSION = "2025-03-26";
    const MCP_DISCOVERY_TIMEOUT_MS = 10000, MCP_TOOL_TIMEOUT_MS = 90000;
    const DEFAULT_MODEL = "openai/gpt-oss-120b:free";
    const DEFAULT_INFERENCE_PROVIDERS = [
      { id:"openrouter", name:"OpenRouter", description:"Cloud models · API key required", chatUrl:"https://openrouter.ai/api/v1/chat/completions", modelsUrl:"https://openrouter.ai/api/v1/models", defaultModel:DEFAULT_MODEL, rememberKey:false, openRouter:true, builtIn:true },
      { id:"lmstudio", name:"LM Studio", description:"Local models · localhost:1234", chatUrl:"http://localhost:1234/v1/chat/completions", modelsUrl:"http://localhost:1234/api/v1/models", defaultModel:"local-model", rememberKey:false, openRouter:false, builtIn:true }
    ];
    const DEFAULT_PROMPT = "You are Agent, a capable, thoughtful assistant. Give direct, accurate answers; use the available tools when they materially help; explain uncertainty; and never claim a tool ran unless a tool result is present. When creating a browser artifact, return one complete HTML document in an html code fence.";
    const DEFAULTS = { activeProviderId:"openrouter", inferenceProviders:DEFAULT_INFERENCE_PROVIDERS, model:DEFAULT_MODEL, systemPrompt: DEFAULT_PROMPT, temperature: .6, topP: .95, topK: 40, repetitionPenalty: 1.1, maxTokens: 8192, seed: 0, reasoningSelections: {}, mcpServers: DEFAULT_MCP_SERVERS, disabledMcpTools: [] };
    const FALLBACK_MODELS = [
      { id: "openai/gpt-oss-120b:free", name: "OpenAI: gpt-oss-120b (free)", modalities: ["text"], supportedParameters: ["reasoning", "tools"], reasoning: { mandatory:true, supported_efforts:["low","medium","high"], default_effort:"medium" } },
      { id: "openai/gpt-oss-20b:free", name: "OpenAI: gpt-oss-20b (free)", modalities: ["text"], supportedParameters: ["reasoning", "tools"], reasoning: { mandatory:true, supported_efforts:["low","medium","high"], default_effort:"medium" } }
    ];
    const TEXT_EXT = new Set(["txt","md","mdx","markdown","json","jsonl","csv","ts","tsx","js","jsx","mjs","cjs","py","html","htm","css","scss","xml","yaml","yml","toml","ini","sql","sh","bash","ps1","bat","cmd","java","cs","cpp","c","h","hpp","go","rs","rb","php","swift","kt","r","lua","vue","svelte"]);
    const IMAGE_TYPES = new Set(["image/png","image/jpeg","image/webp","image/gif"]);
    const IMAGE_EXTENSIONS = {png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",webp:"image/webp",gif:"image/gif"};
    const VISION_HINTS = ["gpt-4o","gpt-4.1","gpt-5","gemini","claude-3","claude-opus","claude-sonnet","claude-haiku","pixtral","qwen-vl","qwen2-vl","qwen2.5-vl","llama-3.2-vision","llava","vision","mimo-v"];
    const state = { chats: [], activeId: null, settings: { ...DEFAULTS }, providerKeys:{}, providerDrafts:[], providerKeyDrafts:{}, providerDraftId:"openrouter", models: FALLBACK_MODELS, modelLoadToken:0, attachments: [], generating: false, abort: null, status: "", artifactHtml: "", dragDepth: 0, chatDbAvailable: true, persistTimer: null, modelQuery: "", streamFrame: null, traceOpen:new Set(), followOutput:true, commandOpen:false, commandIndex:0, commandFromSlash:false, commandPromptBase:"", commandMatches:[], mcpTools: [], mcpConnections: {}, mcpLoading: false };
    let chatDbPromise = null;
    const reasoningBuffers = new WeakMap();

    const els = {
      sidebar: $("#sidebar"), sidebarBackdrop: $("#sidebarBackdrop"), chatList: $("#chatList"), modelPickerBtn: $("#modelPickerBtn"), modelButtonLabel: $("#modelButtonLabel"), modelPopover: $("#modelPopover"), modelSearch: $("#modelSearchInput"), modelList: $("#modelList"), modelLoading: $("#modelLoading"), reasoningBtn: $("#reasoningBtn"), reasoningLabel: $("#reasoningLabel"), reasoningPopover: $("#reasoningPopover"), reasoningControls: $("#reasoningControls"), toolsPopover: $("#toolsPopover"), providersPopover: $("#providersPopover"), parametersPopover: $("#parametersPopover"), mcpConfigPopover: $("#mcpConfigPopover"), dataControlsPopover: $("#dataControlsPopover"), messages: $("#messages"), messagesInner: $("#messagesInner"), composer: $("#composer"), prompt: $("#promptInput"), fileInput: $("#fileInput"), pending: $("#pendingAttachments"), statusLine: $("#statusLine"), statusText: $("#statusText"), send: $("#sendBtn"), stop: $("#stopBtn"), providerSelect: $("#inferenceProviderSelect"), mcpServerList: $("#mcpServerList"), commandPanel: $("#commandPanel"), toolGrid: $("#toolGrid"), toolLoading: $("#toolLoading"), commandList: $("#commandList"), dropOverlay: $("#dropOverlay"), toastStack: $("#toastStack"), artifact: $("#artifactPanel"), artifactBackdrop: $("#artifactBackdrop"), artifactFrame: $("#artifactFrame"), artifactTitle: $("#artifactTitle")
    };

    function uid(prefix = "id") { return `${prefix}-${crypto.randomUUID ? crypto.randomUUID() : Date.now() + "-" + Math.random().toString(16).slice(2)}`; }
    function clone(value) { return JSON.parse(JSON.stringify(value)); }
    function escapeHtml(value) { const div = document.createElement("div"); div.textContent = String(value ?? ""); return div.innerHTML; }
    function formatBytes(bytes) { return bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${(bytes/1024).toFixed(1)} KB` : `${(bytes/1048576).toFixed(1)} MB`; }
    function toast(message, type = "info") { const node = document.createElement("div"); node.className = `toast ${type}`; node.textContent = message; els.toastStack.append(node); setTimeout(() => node.remove(), 3500); }
    function safeParse(value, fallback) { if (value == null || value === "") return fallback; try { return JSON.parse(value) ?? fallback; } catch { return fallback; } }
    function saveJson(key, value, notify = true) { try { const raw=JSON.stringify(value);if([STORAGE.settings,STORAGE.models].includes(key))void writePreference(key,raw).catch(error=>{console.warn(`Could not save ${key} to IndexedDB; using the legacy fallback.`,error);try{localStorage.setItem(key,raw);}catch{if(notify)toast("Could not save this preference.","error");}});else localStorage.setItem(key,raw);return true; } catch (error) { if(notify) toast("Could not save this preference.", "error"); return false; } }
    function download(name, content, type = "text/plain") { const url = URL.createObjectURL(new Blob([content], { type: `${type};charset=utf-8` })); const a = document.createElement("a"); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }
    async function copyText(text) { try { await navigator.clipboard.writeText(text); toast("Copied to clipboard."); } catch { const area = document.createElement("textarea"); area.value = text; document.body.append(area); area.select(); document.execCommand("copy"); area.remove(); toast("Copied to clipboard."); } }
    function icon(name, cls = "") { return `<i data-lucide="${name}"${cls ? ` class="${cls}"` : ""}></i>`; }
    function refreshIcons(root = document) { if (window.lucide) lucide.createIcons({ nodes: [root] }); }
    function normalizeMcpServers(value) {
      const source=Array.isArray(value)?value:DEFAULT_MCP_SERVERS, seen=new Set();
      return source.slice(0,12).map((entry,index)=>{const rawId=String(entry?.id || "").trim(),baseId=(rawId || `mcp-${index+1}`).replace(/[^a-zA-Z0-9_-]/g,"-").slice(0,48) || `mcp-${index+1}`;let id=baseId,n=2;while(seen.has(id))id=`${baseId}-${n++}`;seen.add(id);return {id,name:String(entry?.name || `MCP Server ${index+1}`).trim().slice(0,80) || `MCP Server ${index+1}`,url:String(entry?.url || "").trim(),enabled:entry?.enabled!==false};}).filter(server=>server.url);
    }
    function validateMcpServerUrl(value) { let url;try{url=new URL(String(value).trim());}catch{throw new Error("Enter a valid MCP server URL.");}if(!["http:","https:"].includes(url.protocol))throw new Error("MCP servers must use HTTP or HTTPS.");return url.href; }
    function normalizeInferenceProviders(value,saved={}) {
      const supplied=Array.isArray(value)&&value.length?value:[],source=[...supplied];
      DEFAULT_INFERENCE_PROVIDERS.forEach(defaultProvider=>{if(!source.some(entry=>entry?.id===defaultProvider.id))source.push(defaultProvider);});
      const seen=new Set();
      return source.slice(0,10).map((entry,index)=>{const rawId=String(entry?.id || "").trim(),baseId=(rawId || `provider-${index+1}`).replace(/[^a-zA-Z0-9_-]/g,"-").slice(0,48) || `provider-${index+1}`;let id=baseId,n=2;while(seen.has(id))id=`${baseId}-${n++}`;seen.add(id);const builtIn=DEFAULT_INFERENCE_PROVIDERS.find(provider=>provider.id===id),isOpenRouter=id==="openrouter" || entry?.openRouter===true,legacyLmStudioModels="http://localhost:1234/v1/models",modelsUrl=id==="lmstudio"&&entry?.modelsUrl===legacyLmStudioModels?builtIn.modelsUrl:entry?.modelsUrl || builtIn?.modelsUrl || "";return {id,name:String(entry?.name || builtIn?.name || `Custom provider ${index+1}`).trim().slice(0,80) || `Custom provider ${index+1}`,description:String(entry?.description || builtIn?.description || "OpenAI-compatible endpoint").trim().slice(0,100),chatUrl:String(entry?.chatUrl || builtIn?.chatUrl || (isOpenRouter?saved.endpoint:"") || "").trim(),modelsUrl:String(modelsUrl).trim(),defaultModel:String(entry?.defaultModel || builtIn?.defaultModel || (isOpenRouter?saved.model:"") || DEFAULT_MODEL).trim(),rememberKey:entry?.rememberKey===true || (isOpenRouter&&saved.rememberKey===true),openRouter:isOpenRouter,builtIn:Boolean(builtIn)};});
    }
    function validateInferenceUrl(value,label,optional=false) { const raw=String(value || "").trim();if(optional&&!raw)return "";let url;try{url=new URL(raw);}catch{throw new Error(`Enter a valid ${label}.`);}if(!["http:","https:"].includes(url.protocol))throw new Error(`${label} must use HTTP or HTTPS.`);return url.href; }
    function inferenceProvider(providerId) { return state.settings.inferenceProviders.find(provider=>provider.id===providerId) || state.settings.inferenceProviders[0]; }
    function activeProvider(chat=activeChat()) { return inferenceProvider(chat?.providerId || state.settings.activeProviderId); }
    function providerKey(providerId) { return state.providerKeys[providerId] || ""; }
    function providerReasoningKey(modelId,provider=activeProvider()) { return `${provider?.id || "provider"}::${modelId}`; }

    function openChatDb() {
      if (!window.indexedDB) return Promise.reject(new Error("IndexedDB is unavailable."));
      if (chatDbPromise) return chatDbPromise;
      chatDbPromise = new Promise((resolve,reject) => {
        const request = indexedDB.open(CHAT_DB.name, CHAT_DB.version);
        request.onupgradeneeded = () => { const db=request.result; if(!db.objectStoreNames.contains(CHAT_DB.chats)) db.createObjectStore(CHAT_DB.chats,{keyPath:"id"}); if(!db.objectStoreNames.contains(CHAT_DB.secrets)) db.createObjectStore(CHAT_DB.secrets); if(!db.objectStoreNames.contains(CHAT_DB.preferences)) db.createObjectStore(CHAT_DB.preferences); };
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error || new Error("Could not open chat storage."));
        request.onblocked = () => reject(new Error("Chat storage upgrade was blocked by another Agent tab."));
      });
      return chatDbPromise;
    }
    async function readChatsFromDb() {
      const db=await openChatDb();
      return new Promise((resolve,reject) => { const request=db.transaction(CHAT_DB.chats,"readonly").objectStore(CHAT_DB.chats).getAll(); request.onsuccess=()=>resolve(request.result || []); request.onerror=()=>reject(request.error); });
    }
    async function readSecret(key,fallback) {
      try { const db=await openChatDb(); return await new Promise((resolve,reject)=>{const request=db.transaction(CHAT_DB.secrets,"readonly").objectStore(CHAT_DB.secrets).get(key);request.onsuccess=()=>resolve(request.result ?? fallback);request.onerror=()=>reject(request.error);}); } catch { return fallback; }
    }
    async function writeSecret(key,value) {
      const db=await openChatDb();
      return new Promise((resolve,reject)=>{const tx=db.transaction(CHAT_DB.secrets,"readwrite");tx.objectStore(CHAT_DB.secrets).put(value,key);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);});
    }
    async function removeSecret(key) {
      try { const db=await openChatDb(); await new Promise((resolve,reject)=>{const tx=db.transaction(CHAT_DB.secrets,"readwrite");tx.objectStore(CHAT_DB.secrets).delete(key);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);}); } catch {}
    }
    async function readPreference(key,fallback=null) {
      try { const db=await openChatDb(); return await new Promise((resolve,reject)=>{const request=db.transaction(CHAT_DB.preferences,"readonly").objectStore(CHAT_DB.preferences).get(key);request.onsuccess=()=>resolve(request.result ?? fallback);request.onerror=()=>reject(request.error);}); } catch { return fallback; }
    }
    async function writePreference(key,value) {
      const db=await openChatDb();
      return new Promise((resolve,reject)=>{const tx=db.transaction(CHAT_DB.preferences,"readwrite");tx.objectStore(CHAT_DB.preferences).put(value,key);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);});
    }
    async function migratePreference(key,validate) {
      const legacy=localStorage.getItem(key);if(legacy===null)return;
      try { if(!validate(legacy))return;const existing=await readPreference(key,null);if(existing===null)await writePreference(key,legacy);if(await readPreference(key,null)===legacy)localStorage.removeItem(key);else console.warn(`IndexedDB migration conflict for ${key}; legacy value retained.`); } catch(error){console.warn(`IndexedDB migration skipped for ${key}.`,error);}
    }
    async function persistChatsNow() {
      const chats=state.chats.slice(0,MAX_CHATS);
      if (!state.chatDbAvailable) return persistLightweightFallback(chats);
      try {
        const db=await openChatDb();
        await new Promise((resolve,reject) => { const tx=db.transaction(CHAT_DB.chats,"readwrite"), store=tx.objectStore(CHAT_DB.chats); store.clear(); chats.forEach(chat=>store.put(chat)); tx.oncomplete=resolve; tx.onerror=()=>reject(tx.error); tx.onabort=()=>reject(tx.error || new Error("Chat storage transaction was aborted.")); });
        localStorage.removeItem(STORAGE.chats);
        return true;
      } catch(error) {
        state.chatDbAvailable=false;
        console.warn("IndexedDB chat persistence is unavailable.",error);
        persistLightweightFallback(chats);
        toast("Full chat storage is unavailable. Messages will be saved without attachment contents in this browser session.","error");
        return false;
      }
    }
    function persistLightweightFallback(chats) {
      const lightweight=chats.map(chat=>({...chat,messages:chat.messages.map(message=>({...message,attachments:message.attachments?.map(({dataUrl,text,...metadata})=>metadata)}))}));
      return saveJson(STORAGE.chats,lightweight);
    }
    function scheduleChatPersistence() { clearTimeout(state.persistTimer); state.persistTimer=setTimeout(()=>{state.persistTimer=null; void persistChatsNow();},80); }
    async function clearAgentDb() { try { const db=await openChatDb(); await new Promise((resolve,reject)=>{const tx=db.transaction([CHAT_DB.chats,CHAT_DB.secrets,CHAT_DB.preferences],"readwrite");tx.objectStore(CHAT_DB.chats).clear();tx.objectStore(CHAT_DB.secrets).clear();tx.objectStore(CHAT_DB.preferences).clear();tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);}); } catch {} }
    async function loadState() {
      await migratePreference(STORAGE.settings,raw=>{const value=JSON.parse(raw);return value&&typeof value==="object";});
      await migratePreference(STORAGE.models,raw=>{const value=JSON.parse(raw);return value&&typeof value==="object"&&Array.isArray(value.models);});
      const savedSettings = safeParse(await readPreference(STORAGE.settings,localStorage.getItem(STORAGE.settings) || ""), {});
      state.settings = { ...DEFAULTS, ...savedSettings };
      state.settings.inferenceProviders=normalizeInferenceProviders(state.settings.inferenceProviders,savedSettings);
      if(!state.settings.inferenceProviders.some(provider=>provider.id===state.settings.activeProviderId))state.settings.activeProviderId=state.settings.inferenceProviders[0].id;
      state.settings.mcpServers=normalizeMcpServers(state.settings.mcpServers);
      if (!Array.isArray(state.settings.disabledMcpTools)) state.settings.disabledMcpTools = [];
      delete state.settings.enabledTools;
      if (!state.settings.reasoningSelections || typeof state.settings.reasoningSelections !== "object") state.settings.reasoningSelections={};
      const legacyChats = safeParse(localStorage.getItem(STORAGE.chats), []).filter(validChat).slice(0, MAX_CHATS);
      try {
        const storedChats=(await readChatsFromDb()).filter(validChat).sort((a,b)=>b.updatedAt-a.updatedAt).slice(0,MAX_CHATS);
        state.chats=storedChats.length ? storedChats : legacyChats;
        if (legacyChats.length && !storedChats.length) await persistChatsNow();
        else if (storedChats.length) localStorage.removeItem(STORAGE.chats);
      } catch(error) {
        state.chatDbAvailable=false;
        state.chats=legacyChats;
        console.warn("Using lightweight chat persistence because IndexedDB is unavailable.",error);
      }
      const sessionKeys = safeParse(sessionStorage.getItem(STORAGE.providerKeys),{});
      const localKey = localStorage.getItem(STORAGE.key) || "";
      const sessionKey = sessionStorage.getItem(STORAGE.key) || "";
      const indexedKeys = await readSecret(STORAGE.providerKeys,{});
      const indexedKey = await readSecret(STORAGE.key,"");
      state.providerKeys={...(indexedKeys&&typeof indexedKeys==="object"?indexedKeys:{}),...(sessionKeys&&typeof sessionKeys==="object"?sessionKeys:{})};
      const legacyKey=sessionKey || indexedKey || localKey;if(legacyKey&&!state.providerKeys.openrouter)state.providerKeys.openrouter=legacyKey;
      if(indexedKey||localKey){const openrouter=state.settings.inferenceProviders.find(provider=>provider.id==="openrouter");if(openrouter)openrouter.rememberKey=true;try{await writeSecret(STORAGE.providerKeys,{...indexedKeys,openrouter:legacyKey});localStorage.removeItem(STORAGE.key);sessionStorage.removeItem(STORAGE.key);await removeSecret(STORAGE.key);}catch{}}
      if (!state.chats.length) state.chats.push(createChat());
      state.chats.forEach(chat=>{if(!chat.providerId)chat.providerId="openrouter";});
      state.activeId = state.chats[0].id;
      const cached = safeParse(await readPreference(STORAGE.models,localStorage.getItem(STORAGE.models) || ""), null);
      const provider=activeProvider();if(cached && cached.providerId===provider.id && cached.signature===provider.modelsUrl && Date.now() - cached.savedAt < 86400000 && Array.isArray(cached.models)) state.models = cached.models;else state.models=provider.openRouter?FALLBACK_MODELS:[{id:provider.defaultModel,name:provider.defaultModel}];
    }
    function validChat(chat) { return chat && typeof chat.id === "string" && Array.isArray(chat.messages); }
    function createChat(model,providerId=state.settings.activeProviderId) { const provider=inferenceProvider(providerId),now=Date.now(); return { id: uid("chat"), title: "New Chat", providerId:provider.id, model:model || provider.defaultModel, messages: [], notes: [], createdAt: now, updatedAt: now }; }
    function activeChat() { return state.chats.find(chat => chat.id === state.activeId) || state.chats[0]; }
    function saveChats() { state.chats.sort((a,b) => b.updatedAt - a.updatedAt); scheduleChatPersistence(); }
    function touchChat(chat) { chat.updatedAt = Date.now(); if (chat.title === "New Chat") { const first = chat.messages.find(m => m.role === "user"); if (first) { const basis = (first.content || first.attachments?.[0]?.name || "Attached files").trim(); chat.title = basis.length > 50 ? basis.slice(0,47) + "…" : basis; } } saveChats(); }

    function renderApp() { renderSidebar(); renderModels(); renderMessages(); renderPending(); updateControls(); }
    function renderSidebar() {
      const chats = [...state.chats].sort((a,b) => b.updatedAt - a.updatedAt);
      els.chatList.innerHTML = chats.length ? chats.map(chat => `<div class="chat-row ${chat.id === state.activeId ? "active" : ""}" data-chat-id="${chat.id}"><button class="chat-open" data-action="open-chat">${icon("message-square")}<span>${escapeHtml(chat.title)}</span></button><button class="chat-delete" data-action="delete-chat" aria-label="Delete ${escapeHtml(chat.title)}">${icon("trash-2")}</button></div>`).join("") : `<div class="empty-chats">No chats yet</div>`;
      refreshIcons(els.chatList);
    }
    function renderModels() {
      const provider=activeProvider(), current = activeChat()?.model || provider.defaultModel || DEFAULT_MODEL;
      const models = state.models.some(m => m.id === current) ? state.models : [{ id: current, name: current }, ...state.models];
      const selected=models.find(model=>model.id===current);
      els.modelButtonLabel.textContent=selected?.name || shortModel(current) || "Model";
      els.modelPickerBtn.title=`${provider.name} · ${selected?.name || current}`;
      const query=state.modelQuery.trim().toLowerCase();
      const filtered=query ? models.map(model=>{const name=(model.name || "").toLowerCase(),id=model.id.toLowerCase(),tokens=`${name} ${id}`.split(/[^a-z0-9.]+/);const score=tokens.includes(query)?0:name.startsWith(query)||id.startsWith(query)?1:name.includes(query)||id.includes(query)?2:99;return {model,score};}).filter(entry=>entry.score<99).sort((a,b)=>a.score-b.score||(a.model.name || a.model.id).localeCompare(b.model.name || b.model.id)).map(entry=>entry.model) : models;
      els.modelList.innerHTML=filtered.length ? filtered.map(model=>`<button class="model-option ${model.id===current?"selected":""}" type="button" role="option" aria-selected="${model.id===current}" data-model-id="${escapeHtml(model.id)}"><span>${escapeHtml(model.name || model.id)}</span>${modelSupportsImages(model.id)?"<small>VISION</small>":""}${model.id.endsWith(":free")?"<small>FREE</small>":""}${model.id===current?icon("check"):""}</button>`).join("") : `<div class="model-empty">No models match “${escapeHtml(state.modelQuery)}”.</div>`;
      refreshIcons(els.modelList);
      renderReasoning();
    }
    function selectedModelDetails() { const id=activeChat()?.model || activeProvider()?.defaultModel || DEFAULT_MODEL; return state.models.find(model=>model.id===id) || FALLBACK_MODELS.find(model=>model.id===id) || {id}; }
    function getReasoningCapabilities(model) {
      if(!model) return null;
      const raw=model.reasoning && typeof model.reasoning==="object" ? model.reasoning : model.capabilities?.reasoning && typeof model.capabilities.reasoning==="object" ? model.capabilities.reasoning : {};
      const advertised=Boolean(Object.keys(raw).length || model.supportedParameters?.includes("reasoning") || model.id?.includes("gpt-oss"));
      if(!advertised) return null;
      const allowed=["minimal","low","medium","high","xhigh","max"];
      const options=Array.isArray(raw.allowed_options) ? raw.allowed_options : Array.isArray(raw.allowedOptions) ? raw.allowedOptions : [];
      let efforts=Array.isArray(raw.supported_efforts) ? raw.supported_efforts : Array.isArray(raw.supportedEfforts) ? raw.supportedEfforts : options;
      efforts=efforts.filter(effort=>allowed.includes(effort));
      if(!efforts.length) efforts=options.includes("on")?["minimal","low","medium","high","xhigh"]:["low","medium","high"];
      efforts.sort((a,b)=>allowed.indexOf(a)-allowed.indexOf(b));
      const defaultOption=raw.default_effort || raw.defaultEffort || raw.default;
      return { efforts, mandatory:raw.mandatory===true || (options.length>0&&!options.includes("off")), defaultEnabled:raw.default_enabled ?? raw.defaultEnabled ?? (defaultOption==="off"?false:defaultOption==="on"?true:undefined), defaultEffort:allowed.includes(defaultOption)?defaultOption:undefined, supportsMaxTokens:raw.supports_max_tokens===true || raw.supportsMaxTokens===true };
    }
    function getReasoningSelection(model=selectedModelDetails()) {
      const capabilities=getReasoningCapabilities(model); if(!capabilities) return null;
      let selection=state.settings.reasoningSelections?.[providerReasoningKey(model.id)] || state.settings.reasoningSelections?.[model.id];
      if(!selection) { const legacy=state.settings.reasoning; selection=legacy==="off"?{mode:"off"}:["minimal","low","medium","high","xhigh","max"].includes(legacy)?{mode:"effort",effort:legacy}:null; }
      if(!selection) { if(!capabilities.mandatory && capabilities.defaultEnabled===false) selection={mode:"off"}; else if(capabilities.defaultEffort && capabilities.efforts.includes(capabilities.defaultEffort)) selection={mode:"effort",effort:capabilities.defaultEffort}; else selection={mode:"auto"}; }
      selection={...selection};
      if(selection.mode==="off" && capabilities.mandatory) selection=capabilities.defaultEffort?{mode:"effort",effort:capabilities.defaultEffort}:{mode:"auto"};
      if(selection.mode==="effort" && !capabilities.efforts.includes(selection.effort)) selection={mode:"effort",effort:capabilities.defaultEffort && capabilities.efforts.includes(capabilities.defaultEffort)?capabilities.defaultEffort:capabilities.efforts[0]};
      if(selection.mode==="max_tokens" && !capabilities.supportsMaxTokens) selection={mode:"auto"};
      if(selection.mode==="max_tokens") selection.maxTokens=Math.max(1,Math.round(Number(selection.maxTokens)||2000));
      return selection;
    }
    function reasoningLabel(selection) { if(!selection)return "Thinking: unavailable"; if(selection.mode==="off")return "Thinking: off"; if(selection.mode==="effort")return `Thinking: ${selection.effort}`; if(selection.mode==="max_tokens")return `Thinking: ${selection.maxTokens} tokens`; return "Thinking: auto"; }
    function renderReasoning() {
      const model=selectedModelDetails(), capabilities=getReasoningCapabilities(model), selection=getReasoningSelection(model);
      els.reasoningLabel.textContent=reasoningLabel(selection); els.reasoningBtn.title=capabilities?`Reasoning settings for ${model.name || model.id}`:"Reasoning is unavailable for this model"; els.reasoningBtn.disabled=state.generating || !capabilities;
      if(!capabilities) { els.reasoningControls.innerHTML=`<div class="reasoning-unavailable">Reasoning controls are unavailable for this model.</div>`;return; }
      const option=(mode,label,extra="")=>`<button class="reasoning-option ${selection.mode===mode && (!extra || selection.effort===extra)?"selected":""}" type="button" data-reasoning-mode="${mode}" ${extra?`data-reasoning-effort="${extra}"`:""}>${label}</button>`;
      els.reasoningControls.innerHTML=`<div class="reasoning-options">${option("auto","Auto")}${capabilities.mandatory?"":option("off","Off")}${capabilities.efforts.map(effort=>option("effort",effort,effort)).join("")}</div>${capabilities.supportsMaxTokens?`<div class="reasoning-budget"><div class="field"><label for="reasoningBudgetInput">Token budget</label><input id="reasoningBudgetInput" type="number" min="1" step="128" value="${selection.maxTokens || 2000}"></div>${option("max_tokens","Use budget")}</div>`:""}`;
    }
    function positionComposerPopover(popover,button) { const shell=popover.parentElement,shellRect=shell.getBoundingClientRect(),buttonRect=button.getBoundingClientRect(),width=popover.offsetWidth,margin=11;popover.style.left=`${Math.max(margin,Math.min(shellRect.width-width-margin,buttonRect.left-shellRect.left))}px`; }
    function positionCenteredComposerPopover(popover) { const shellRect=popover.parentElement.getBoundingClientRect(),width=popover.offsetWidth;popover.style.left=`${Math.max(11,(shellRect.width-width)/2)}px`; }
    function dismissComposerPopover(popover) { if(!popover||popover.classList.contains("hidden"))return true;if(popover===els.parametersPopover)persistParameters();if(popover===els.mcpConfigPopover&&!persistMcpConfig())return false;popover.classList.add("hidden");return true; }
    function closeComposerPopovers(except=null) { let closed=true;[els.modelPopover,els.reasoningPopover,els.toolsPopover,els.providersPopover,els.parametersPopover,els.mcpConfigPopover,els.dataControlsPopover].forEach(popover=>{if(popover!==except&&!dismissComposerPopover(popover))closed=false;});return closed; }
    function openReasoning() { if(!getReasoningCapabilities(selectedModelDetails()))return;if(!els.reasoningPopover.classList.contains("hidden")){els.reasoningPopover.classList.add("hidden");return;}closeCommands(true);if(!closeComposerPopovers(els.reasoningPopover))return;renderReasoning();els.reasoningPopover.classList.remove("hidden");positionComposerPopover(els.reasoningPopover,els.reasoningBtn); }
    function setReasoningSelection(selection) { const model=selectedModelDetails(); state.settings.reasoningSelections={...state.settings.reasoningSelections,[providerReasoningKey(model.id)]:selection}; saveJson(STORAGE.settings,state.settings); renderReasoning(); els.reasoningPopover.classList.add("hidden");els.prompt.focus(); }
    function renderWelcome() {
      return `<section class="welcome"><div class="welcome-card"><div class="welcome-icon">${icon("bot")}</div><h1>How can I help?</h1><div class="prompt-grid"><button class="prompt-card" data-prompt="Explain the key ideas in the attached file and identify anything that needs follow-up."><strong>Review a file</strong><span>Attach text, code, JSON, or an image.</span></button><button class="prompt-card" data-prompt="Create a polished, self-contained HTML to-do app with a dark interface. Return the complete file in an html code fence."><strong>Build an artifact</strong><span>Create and run a complete HTML app.</span></button><button class="prompt-card" data-prompt="Help me reason through a difficult decision. Start by identifying the key tradeoffs and missing information."><strong>Think through a decision</strong><span>Structure options and tradeoffs.</span></button><button class="prompt-card" data-prompt="Search the web for the latest browser AI news and summarize the key developments with sources."><strong>Use MCP tools</strong><span>Search, fetch, calculate, research, and more.</span></button></div></div></section>`;
    }
    function renderMessages() {
      const chat = activeChat();
      if (!chat || !chat.messages.length) els.messagesInner.innerHTML = renderWelcome();
      else els.messagesInner.innerHTML = chat.messages.map((message,index) => renderMessage(message,index)).join("");
      refreshIcons(els.messagesInner);
      enhanceMarkdown(els.messagesInner);
    }
    function renderMessage(message,index) {
      const attachments = message.attachments?.length ? renderAttachments(message.attachments, false) : "";
      const assistantLabel=message.modelName || state.models.find(model=>model.id===message.model)?.name || shortModel(message.model) || "Model";
      if(message.role==="assistant"&&message.segments?.length) { const timeline=renderAssistantTimeline(message);return `<article class="message assistant" data-message-index="${index}" data-message-id="${escapeHtml(message.id || `message-${index}`)}"><div class="message-label">${escapeHtml(assistantLabel)}</div><div class="message-timeline">${timeline}</div><div class="message-actions"><button data-action="copy-message" aria-label="Copy message" title="Copy">${icon("copy")}</button><button data-action="retry-message" aria-label="Retry response" title="Retry from here">${icon("rotate-ccw")}</button><span class="model-badge">${escapeHtml(shortModel(message.model))}</span></div></article>`; }
      const trace = message.trace?.length ? renderTrace(message.trace,message.id || `message-${index}`) : "";
      const body = message.content ? `<div class="markdown">${renderMarkdown(message.content)}</div>` : state.generating && index === activeChat().messages.length - 1 ? `<span style="color:var(--muted)">Preparing response…</span>` : "";
      const isError = message.error ? " error" : "";
      return `<article class="message ${message.role}" data-message-index="${index}" data-message-id="${escapeHtml(message.id || `message-${index}`)}"><div class="message-label">${message.role === "user" ? "You" : escapeHtml(assistantLabel)}</div>${trace}${attachments || body ? `<div class="bubble${isError}">${attachments}${body}</div>` : ""}<div class="message-actions"><button data-action="copy-message" aria-label="Copy message" title="Copy">${icon("copy")}</button>${message.role === "user" ? `<button data-action="edit-message" aria-label="Edit message" title="Edit from here">${icon("pencil")}</button>` : `<button data-action="retry-message" aria-label="Retry response" title="Retry from here">${icon("rotate-ccw")}</button>`}<span class="model-badge">${message.role === "assistant" ? escapeHtml(shortModel(message.model)) : ""}</span></div></article>`;
    }
    function scheduleStreamingRender(message) {
      if(state.streamFrame) return;
      state.streamFrame=requestAnimationFrame(()=>{state.streamFrame=null;renderStreamingMessage(message);});
    }
    function renderStreamingMessage(message) {
      const followOutput=state.followOutput;
      const article=els.messagesInner.querySelector(`[data-message-id="${CSS.escape(message.id)}"]`);
      if(!article) { renderMessages(); return; }
      if(message.segments?.length) { let timeline=article.querySelector(":scope > .message-timeline");if(!timeline){timeline=document.createElement("div");timeline.className="message-timeline";article.querySelector(":scope > .trace")?.remove();article.querySelector(":scope > .bubble")?.remove();article.insertBefore(timeline,article.querySelector(".message-actions"));}syncAssistantTimeline(timeline,message);const modelBadge=article.querySelector(".model-badge");if(modelBadge)modelBadge.textContent=shortModel(message.model);if(followOutput)requestAnimationFrame(followStreamOutput);return; }
      const index=Number(article.dataset.messageIndex), attachments=message.attachments?.length ? renderAttachments(message.attachments,false) : "";
      const body=message.content ? `<div class="markdown">${renderMarkdown(message.content)}</div>` : `<span style="color:var(--muted)">Preparing response…</span>`;
      let bubble=article.querySelector(":scope > .bubble");
      if(!bubble) { bubble=document.createElement("div"); bubble.className="bubble"; article.insertBefore(bubble,article.querySelector(".message-actions")); }
      bubble.classList.toggle("error",Boolean(message.error)); bubble.innerHTML=attachments+body; enhanceMarkdown(bubble);
      const traceHtml=message.trace?.length ? renderTrace(message.trace,message.id) : "", existingTrace=article.querySelector(":scope > .trace");
      if(traceHtml) { const holder=document.createElement("div");holder.innerHTML=traceHtml;const nextTrace=holder.firstElementChild;existingTrace?existingTrace.replaceWith(nextTrace):article.insertBefore(nextTrace,bubble); }
      else existingTrace?.remove();
      const modelBadge=article.querySelector(".model-badge");if(modelBadge)modelBadge.textContent=shortModel(message.model);
      refreshIcons(article); if(followOutput)requestAnimationFrame(followStreamOutput);
    }
    function renderAssistantTimeline(message) {
      return message.segments.map(segment=>renderAssistantSegment(segment,message)).join("") || `<span data-stream-placeholder style="color:var(--muted)">Preparing response…</span>`;
    }
    function renderAssistantSegment(segment,message) {
      const segmentId=escapeHtml(segment.uiKey || segment.id || `${segment.kind}:${segment.traceId || "segment"}`);
      if(segment.kind==="text")return segment.content?`<div class="bubble${message.error?" error":""}" data-segment-id="${segmentId}" data-segment-kind="text"><div class="markdown">${renderMarkdown(segment.content)}</div></div>`:"";
      if(segment.kind==="trace"){const entry=message.trace?.find(item=>item.id===segment.traceId);return entry?renderTrace([entry],message.id).replace('<div class="trace">',`<div class="trace" data-segment-id="${segmentId}" data-segment-kind="trace">`):"";}
      return "";
    }
    function syncAssistantTimeline(timeline,message) {
      timeline.querySelector("[data-stream-placeholder]")?.remove();
      const activeIds=new Set();
      let insertionPoint=timeline.firstElementChild;
      message.segments.forEach(segment=>{
        const segmentId=segment.uiKey || segment.id || `${segment.kind}:${segment.traceId || "segment"}`;
        activeIds.add(segmentId);
        let node=[...timeline.children].find(child=>child.dataset.segmentId===segmentId);
        if(!node){const holder=document.createElement("div");holder.innerHTML=renderAssistantSegment(segment,message);node=holder.firstElementChild;if(!node)return;timeline.appendChild(node);enhanceMarkdown(node);refreshIcons(node);}
        else if(segment.kind==="text"){
          node.classList.toggle("error",Boolean(message.error));
          if(node.dataset.streamContent!==segment.content){const markdown=node.querySelector(":scope > .markdown");if(markdown){markdown.innerHTML=renderMarkdown(segment.content);enhanceMarkdown(markdown);}node.dataset.streamContent=segment.content;}
        } else if(segment.kind==="trace") {
          const entry=message.trace?.find(item=>item.id===segment.traceId);if(entry)syncTraceRow(node,entry,message.id);
        }
        if(insertionPoint){if(node!==insertionPoint)timeline.insertBefore(node,insertionPoint);}
        else if(node!==timeline.lastElementChild)timeline.appendChild(node);
        insertionPoint=node.nextElementSibling;
      });
      [...timeline.children].forEach(node=>{if(node.dataset.segmentId&&!activeIds.has(node.dataset.segmentId))node.remove();});
    }
    function syncTraceRow(trace,entry,messageId) {
      const details=trace.querySelector(":scope > .trace-row");if(!details)return;
      const running=entry.status==="running",failed=entry.status==="error",cls=entry.kind==="thought"?"trace-thought":failed?"trace-error":running?"trace-tool":"trace-done",ico=entry.kind==="thought"?"brain-circuit":failed?"circle-x":running?"wrench":"circle-check";
      const detail=entry.detail || [entry.input?`Input\n${formatValue(entry.input)}`:"",entry.output?`Output\n${formatValue(entry.output)}`:"",entry.error?`Error\n${entry.error}`:""].filter(Boolean).join("\n\n");
      const traceId=String(entry.id || entry.toolName || "step"),disclosureKey=`${messageId}:${traceId}`,previousKey=details.dataset.traceKey;details.dataset.traceKey=disclosureKey;if(previousKey&&previousKey!==disclosureKey){state.traceOpen.delete(previousKey);if(details.open)state.traceOpen.add(disclosureKey);}
      const status=details.querySelector("summary > span:first-child");if(status&&status.dataset.icon!==ico){status.className=cls;status.dataset.icon=ico;status.innerHTML=icon(ico);refreshIcons(status);}else if(status)status.className=cls;
      const title=details.querySelector(".trace-title");if(title&&title.textContent!==(entry.title || entry.toolName || "Agent step"))title.textContent=entry.title || entry.toolName || "Agent step";
      const summary=details.querySelector("summary");let chevron=summary?.querySelector(".chev");if(detail&&!chevron&&summary){summary.insertAdjacentHTML("beforeend",icon("chevron-down","chev"));refreshIcons(summary);}
      if(!detail&&chevron)chevron.remove();
      let detailNode=details.querySelector(":scope > .trace-detail");if(detail){if(!detailNode){detailNode=document.createElement("div");detailNode.className="trace-detail";detailNode.innerHTML="<pre></pre>";details.appendChild(detailNode);}const pre=detailNode.querySelector("pre");if(pre&&pre.textContent!==detail)pre.textContent=detail;}else detailNode?.remove();
    }
    function renderMarkdown(text) {
      if (!window.marked || !window.DOMPurify) return `<p>${escapeHtml(text).replace(/\n/g,"<br>")}</p>`;
      marked.setOptions({ gfm: true, breaks: true });
      return DOMPurify.sanitize(marked.parse(text), { USE_PROFILES: { html: true } });
    }
    function enhanceMarkdown(root) {
      $$(".markdown a", root).forEach(a => { a.target = "_blank"; a.rel = "noopener noreferrer"; });
      $$(".markdown pre", root).forEach(pre => {
        if (pre.closest(".code-wrap")) return;
        const code = $("code", pre); if (!code) return;
        try { if (window.hljs) hljs.highlightElement(code); } catch {}
        const languageClass = [...code.classList].find(c => c.startsWith("language-")) || "";
        const language = languageClass.replace("language-","") || "code";
        const html = code.textContent || "";
        const isArtifact = (language === "html" || html.trim().startsWith("<!DOCTYPE html>")) && /<!DOCTYPE html>/i.test(html);
        const wrap = document.createElement("div"); wrap.className = "code-wrap";
        const head = document.createElement("div"); head.className = "code-head"; head.innerHTML = `<span>${escapeHtml(isArtifact ? "HTML artifact" : language)}</span><button data-code-copy>Copy</button>${isArtifact ? "<button data-run-html>Run</button>" : ""}`;
        pre.replaceWith(wrap); wrap.append(head, pre); wrap.dataset.code = html;
      });
    }
    function renderTrace(entries,messageId) {
      const display=entries;
      return `<div class="trace">${display.map(entry => { const running = entry.status === "running"; const failed = entry.status === "error"; const cls = entry.kind === "thought" ? "trace-thought" : failed ? "trace-error" : running ? "trace-tool" : "trace-done"; const ico = entry.kind === "thought" ? "brain-circuit" : failed ? "circle-x" : running ? "wrench" : "circle-check"; const detail = entry.detail || [entry.input ? `Input\n${formatValue(entry.input)}` : "", entry.output ? `Output\n${formatValue(entry.output)}` : "", entry.error ? `Error\n${entry.error}` : ""].filter(Boolean).join("\n\n"),traceId=String(entry.id || entry.toolName || "step"),disclosureKey=`${messageId}:${traceId}`; return `<details class="trace-row" data-trace-key="${escapeHtml(disclosureKey)}" ${state.traceOpen.has(disclosureKey)?"open":""}><summary><span class="${cls}">${icon(ico)}</span><span class="trace-title">${escapeHtml(entry.title || entry.toolName || "Agent step")}</span>${detail ? icon("chevron-down","chev") : ""}</summary>${detail ? `<div class="trace-detail"><pre>${escapeHtml(detail)}</pre></div>` : ""}</details>`; }).join("")}</div>`;
    }
    function formatValue(value) { if (typeof value === "string") return value; try { return JSON.stringify(value,null,2); } catch { return String(value); } }
    function renderAttachments(attachments, removable) {
      return `<div class="attachment-list">${attachments.map(file => `<div class="attachment" data-attachment-id="${file.id}">${file.kind === "image" && file.dataUrl ? `<img src="${file.dataUrl}" alt="">` : `<span class="attachment-icon">${icon(file.kind === "image" ? "image" : "file-code-2")}</span>`}<span class="attachment-copy"><strong>${escapeHtml(file.name)}</strong><span>${file.kind === "image" ? "Image" : "Text"} · ${formatBytes(file.size)}</span></span>${removable ? `<button class="attachment-remove" data-action="remove-attachment" aria-label="Remove ${escapeHtml(file.name)}">${icon("x")}</button>` : ""}</div>`).join("")}</div>`;
    }
    function renderPending() { els.pending.classList.toggle("hidden", !state.attachments.length); els.pending.innerHTML = state.attachments.length ? renderAttachments(state.attachments,true) : ""; refreshIcons(els.pending); }
    function updateControls() {
      const canSend = !state.generating && (els.prompt.value.trim() || state.attachments.length) && !state.attachments.some(a => a.error);
      els.send.disabled = !canSend; els.send.classList.toggle("hidden", state.generating); els.stop.classList.toggle("hidden", !state.generating);
      els.statusLine.classList.toggle("hidden", !state.generating); els.statusText.textContent = state.status || "Thinking…";
      els.modelPickerBtn.disabled = state.generating; els.reasoningBtn.disabled = state.generating || !getReasoningCapabilities(selectedModelDetails());
    }
    function isNearBottom() { return els.messages.scrollHeight-els.messages.scrollTop-els.messages.clientHeight<72; }
    function followStreamOutput() { if(state.followOutput)els.messages.scrollTop=els.messages.scrollHeight; }
    function scrollToBottom() { state.followOutput=true;els.messages.scrollTop=els.messages.scrollHeight; }
    function shortModel(model = "") { return model.split("/").pop()?.replace(":free","") || model; }

    async function addFiles(fileList) {
      const files = [...fileList].slice(0, Math.max(0, MAX_ATTACHMENTS - state.attachments.length));
      if ([...fileList].length > files.length) toast(`Up to ${MAX_ATTACHMENTS} attachments per turn.`, "error");
      for (const file of files) {
        const ext = file.name.toLowerCase().split(".").pop() || "";
        const imageMimeType=IMAGE_TYPES.has(file.type)?file.type:IMAGE_EXTENSIONS[ext],kind=imageMimeType?"image":(file.type.startsWith("text/") || TEXT_EXT.has(ext))?"text":null;
        if (!kind) { toast(`${file.name}: unsupported file type.`, "error"); continue; }
        const limit = kind === "image" ? MAX_IMAGE_BYTES : MAX_TEXT_BYTES;
        if (file.size > limit) { toast(`${file.name}: exceeds the ${formatBytes(limit)} limit.`, "error"); continue; }
        try { const attachment={id:uid("file"),name:file.name,mimeType:imageMimeType || file.type || "text/plain",size:file.size,kind};if(kind==="image"){attachment.dataUrl=await readFile(file,"data");if(!attachment.dataUrl.startsWith(`data:${attachment.mimeType};`))attachment.dataUrl=attachment.dataUrl.replace(/^data:[^;]*;/,`data:${attachment.mimeType};`);}else attachment.text=await readFile(file,"text");state.attachments.push(attachment); } catch { toast(`${file.name}: could not read the file.`, "error"); }
      }
      renderPending(); updateControls();
    }
    function readFile(file, mode) { return new Promise((resolve,reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || "")); reader.onerror = reject; mode === "data" ? reader.readAsDataURL(file) : reader.readAsText(file); }); }
    function modelSupportsImages(modelId) { const model=state.models.find(m=>m.id===modelId);if(model?.capabilities?.vision===true)return true;if(model?.capabilities?.vision===false)return false;if(model?.modalities?.length)return model.modalities.map(value=>String(value).toLowerCase()).includes("image");return VISION_HINTS.some(h=>modelId.toLowerCase().includes(h)); }
    function providerSupportsImageType(provider,mimeType) { const type=String(mimeType || "").toLowerCase();if(provider?.id==="lmstudio")return ["image/png","image/jpeg","image/webp"].includes(type);return IMAGE_TYPES.has(type); }
    function attachmentText(attachments = []) { const files = attachments.filter(a => a.kind === "text" && a.text); if (!files.length) return ""; return ["The user attached the following text/code files. Treat them as user-provided context.", ...files.map((a,i) => `\n### Attachment ${i+1}: ${a.name}\n\n\`\`\`${a.name.split(".").pop() || "text"}\n${a.text}\n\`\`\``)].join("\n"); }
    function providerContent(message,modelId,provider=activeProvider(),requireImageSupport=true) {
      const context = attachmentText(message.attachments); const text = [message.content?.trim(),context].filter(Boolean).join("\n\n");
      const images = (message.attachments || []).filter(a => a.kind === "image" && a.dataUrl);
      if (!images.length) return text;
      if (!modelSupportsImages(modelId)) {
        if (requireImageSupport) throw new Error("The selected model does not support image input. Choose a vision-capable model or remove the image.");
        const imageSummary=images.map(image=>`- ${image.name} (${image.mimeType || "image"}, ${formatBytes(image.size)})`).join("\n");
        return [text,"A prior user message included image attachments that are unavailable to this text-only model.",imageSummary].filter(Boolean).join("\n\n");
      }
      const unsupported=images.filter(image=>!providerSupportsImageType(provider,image.mimeType));
      if(unsupported.length) { if(requireImageSupport)throw new Error(`${provider?.name || "This provider"} does not support ${unsupported.map(image=>image.name).join(", ")} in this image format. Use PNG, JPEG, or WebP.`);const supported=images.filter(image=>providerSupportsImageType(provider,image.mimeType));if(!supported.length)return [text,"A prior user message included image attachments in a format unavailable to this provider."].filter(Boolean).join("\n\n");return [{type:"text",text:text || "Please review the attached image."},...supported.map(image=>({type:"image_url",image_url:{url:image.dataUrl,detail:"auto"}}))]; }
      return [{ type: "text", text: text || "Please review the attached image." }, ...images.map(a => ({ type: "image_url", image_url: { url: a.dataUrl, detail: "auto" } }))];
    }

    class McpStreamableHttpClient {
      constructor(url) { this.url=url.endsWith("/")?url:`${url}/`; this.requestId=0; this.sessionId=""; this.initialized=false; this.serverInfo=null; this.initializePromise=null; }
      async connect(signal) {
        if(this.initialized)return this.serverInfo;
        if(this.initializePromise)return this.initializePromise;
        this.initializePromise=(async()=>{
          const result=await this.request("initialize",{protocolVersion:MCP_PROTOCOL_VERSION,capabilities:{},clientInfo:{name:"agent-micro-app",version:"1.0.0"}},{signal,skipConnect:true,timeoutMs:MCP_DISCOVERY_TIMEOUT_MS});
          this.serverInfo=result.serverInfo || null;
          await this.notify("notifications/initialized",{},signal);
          this.initialized=true;
          return this.serverInfo;
        })();
        try{return await this.initializePromise;}finally{this.initializePromise=null;}
      }
      async request(method,params={},options={}) {
        if(!options.skipConnect)await this.connect(options.signal);
        const id=++this.requestId;
        const response=await this.post({jsonrpc:"2.0",id,method,params},options.signal,options.timeoutMs || MCP_TOOL_TIMEOUT_MS);
        if(response?.error)throw new Error(response.error.message || `MCP error ${response.error.code || ""}`.trim());
        if(!response || response.id!==id)throw new Error("The MCP server returned an invalid response.");
        return response.result;
      }
      async notify(method,params={},signal) { await this.post({jsonrpc:"2.0",method,params},signal,MCP_DISCOVERY_TIMEOUT_MS,true); }
      async post(payload,outerSignal,timeoutMs,notification=false) {
        const controller=new AbortController();
        const onAbort=()=>controller.abort(outerSignal?.reason);
        if(outerSignal?.aborted)onAbort();else outerSignal?.addEventListener("abort",onAbort,{once:true});
        const timeout=setTimeout(()=>controller.abort(new DOMException("MCP request timed out.","TimeoutError")),timeoutMs);
        const headers={"Content-Type":"application/json","Accept":"application/json, text/event-stream","MCP-Protocol-Version":MCP_PROTOCOL_VERSION};
        if(this.sessionId)headers["MCP-Session-Id"]=this.sessionId;
        try {
          const response=await fetch(this.url,{method:"POST",headers,body:JSON.stringify(payload),signal:controller.signal});
          const session=response.headers.get("MCP-Session-Id"); if(session)this.sessionId=session;
          if(!response.ok)throw new Error(`MCP ${response.status}: ${(await response.text()).slice(0,500) || response.statusText}`);
          if(notification || response.status===202)return null;
          const raw=await response.text();
          return parseMcpResponse(raw,response.headers.get("Content-Type") || "");
        } catch(error) {
          if(controller.signal.aborted && !outerSignal?.aborted)throw new Error(`MCP server did not respond within ${Math.round(timeoutMs/1000)} seconds.`);
          throw error;
        } finally { clearTimeout(timeout); outerSignal?.removeEventListener("abort",onAbort); }
      }
      async listTools(signal) { const result=await this.request("tools/list",{}, {signal,timeoutMs:MCP_DISCOVERY_TIMEOUT_MS}); return Array.isArray(result?.tools)?result.tools:[]; }
      async callTool(name,args,signal) { return this.request("tools/call",{name,arguments:args || {}},{signal,timeoutMs:MCP_TOOL_TIMEOUT_MS}); }
      reset() { this.sessionId=""; this.initialized=false; this.serverInfo=null; this.initializePromise=null; }
    }
    function parseMcpResponse(raw,contentType) {
      if(contentType.includes("application/json"))return safeParse(raw,null);
      const messages=[]; let data=[];
      for(const line of raw.split(/\r?\n/)) { if(line.startsWith("data:"))data.push(line.slice(5).trim()); else if(!line.trim() && data.length){const parsed=safeParse(data.join("\n"),null);if(parsed)messages.push(parsed);data=[];} }
      if(data.length){const parsed=safeParse(data.join("\n"),null);if(parsed)messages.push(parsed);}
      if(!messages.length)throw new Error("The MCP server returned an unreadable response.");
      return messages.find(message=>message?.result || message?.error) || messages[0];
    }
    const mcpClients=new Map();
    function getMcpClient(server) { const current=mcpClients.get(server.id);if(current?.url===server.url)return current.client;const client=new McpStreamableHttpClient(server.url);mcpClients.set(server.id,{url:server.url,client});return client; }
    function resetMcpClients() { mcpClients.clear();state.mcpTools=[];state.mcpConnections={}; }
    function mcpToolIcon(name) { const value=String(name).toLowerCase(); if(value.includes("search"))return "search";if(value.includes("fetch"))return "globe";if(value.includes("file"))return "folder";if(value.includes("code")||value.includes("terminal")||value.includes("shell"))return "square-terminal";if(value.includes("image"))return "image";if(value.includes("video"))return "video";if(value.includes("speech"))return "audio-lines";if(value.includes("research"))return "telescope";if(value.includes("memory"))return "brain";if(value.includes("skill"))return "blocks";return "wrench"; }
    function toolKey(serverId,name) { return `${serverId}::${name}`; }
    function safeToolName(value) { return String(value || "tool").replace(/[^a-zA-Z0-9_-]/g,"_").replace(/^_+|_+$/g,"").slice(0,64) || "tool"; }
    function assignExposedToolNames(tools) { const counts={};for(const tool of tools){const base=safeToolName(tool.name);counts[base]=(counts[base]||0)+1;}const used=new Set();return tools.map(tool=>{const base=safeToolName(tool.name),prefix=safeToolName(tool.serverName).slice(0,20),seed=counts[base]>1?`${prefix}__${base}`:base;let exposedName=seed.slice(0,64),n=2;while(used.has(exposedName)){const suffix=`_${n++}`;exposedName=`${seed.slice(0,64-suffix.length)}${suffix}`;}used.add(exposedName);return {...tool,exposedName};}); }
    function isMcpToolDisabled(tool) { return state.settings.disabledMcpTools.includes(tool.key) || (tool.serverId==="nymbo-tools" && state.settings.disabledMcpTools.includes(tool.name)); }
    function enabledMcpTools() { return state.mcpTools.filter(tool=>!isMcpToolDisabled(tool)); }
    function enabledToolSchemas() { return enabledMcpTools().map(tool=>({type:"function",function:{name:tool.exposedName,description:`${tool.description || ""}${tool.description?"\n":""}MCP server: ${tool.serverName}`,parameters:tool.inputSchema && typeof tool.inputSchema==="object"?tool.inputSchema:{type:"object",properties:{}}}})); }
    async function loadMcpTools(force=false,signal) {
      const servers=state.settings.mcpServers.filter(server=>server.enabled);
      if(!force && servers.length && servers.every(server=>state.mcpConnections[server.id]?.status==="connected"))return state.mcpTools;
      state.mcpLoading=true;renderMcpTools();
      try {
        const previous=state.mcpTools;
        const groups=await Promise.all(servers.map(async server=>{
          const client=getMcpClient(server);if(force)client.reset();
          try { const tools=await client.listTools(signal);state.mcpConnections[server.id]={status:"connected",count:tools.length,serverInfo:client.serverInfo};return tools.map(tool=>({...tool,key:toolKey(server.id,tool.name),serverId:server.id,serverName:server.name})); }
          catch(error) { const message=cleanError(error.message);state.mcpConnections[server.id]={status:"error",count:0,error:message};return previous.filter(tool=>tool.serverId===server.id); }
        }));
        state.mcpTools=assignExposedToolNames(groups.flat());
        const failures=servers.filter(server=>state.mcpConnections[server.id]?.status==="error");
        if(failures.length===servers.length && servers.length && !state.mcpTools.length)throw new Error(failures.map(server=>`${server.name}: ${state.mcpConnections[server.id].error}`).join(" · "));
        return state.mcpTools;
      } finally { state.mcpLoading=false;renderMcpTools(); }
    }
    function formatMcpToolResult(result) {
      const parts=[];
      if(Array.isArray(result?.content))for(const entry of result.content){if(typeof entry?.text==="string")parts.push(entry.text);else if(entry!=null)parts.push(JSON.stringify(entry));}
      if(result?.structuredContent!==undefined)parts.push(JSON.stringify(result.structuredContent));
      const output=parts.filter(Boolean).join("\n");
      if(result?.isError)throw new Error(output || "The MCP tool reported an error.");
      return output || "Tool returned no structured response.";
    }
    async function executeTool(name,args,signal) {
      const tool=state.mcpTools.find(entry=>entry.exposedName===name);
      if(!tool)throw new Error(`Unknown MCP tool: ${name}`);
      if(isMcpToolDisabled(tool))throw new Error(`${humanize(tool.name)} is disabled.`);
      const server=state.settings.mcpServers.find(entry=>entry.id===tool.serverId);
      if(!server?.enabled)throw new Error(`${tool.serverName} is disabled.`);
      return formatMcpToolResult(await getMcpClient(server).callTool(tool.name,args,signal));
    }

    async function sendMessage(content = els.prompt.value, suppliedAttachments = null) {
      if (state.generating) return;
      const text = String(content || "").trim(); const attachments = suppliedAttachments ? clone(suppliedAttachments) : clone(state.attachments);
      if (!text && !attachments.length) return;
      const chat = activeChat(),provider=activeProvider(chat),key=providerKey(provider.id);if(provider.openRouter&&!key.trim()){openProviders();toast(`Add a ${provider.name} API key first.`,"error");return;}const model = chat.model || provider.defaultModel;
      try { providerContent({content:text,attachments},model,provider); } catch(error) { toast(error.message,"error"); return; }
      const userMessage = { id:uid("msg"), role:"user", content:text, attachments, createdAt:Date.now() };
      const modelName=state.models.find(entry=>entry.id===model)?.name || model;
      const assistant = { id:uid("msg"), role:"assistant", content:"", model, modelName, providerId:provider.id, trace:[], segments:[], createdAt:Date.now() };
      const prior = chat.messages.map(m => ({ role:m.role, content:providerContent(m,model,provider,false) }));
      chat.messages.push(userMessage,assistant); state.attachments=[]; els.prompt.value=""; autoSizePrompt(); touchChat(chat); setGenerating(true,"Thinking…"); renderApp(); scrollToBottom();
      const controller = new AbortController(); state.abort = controller;
      try {
        state.status="Just a sec…";updateControls();
        try { await loadMcpTools(false,controller.signal); }
        catch(error) { console.warn("MCP tool discovery failed; continuing without tools.",error); }
        const conversation = [{ role:"system", content:state.settings.systemPrompt }, ...prior, { role:"user", content:providerContent(userMessage,model,provider) }];
        let iterations=0;
        while (iterations <= MAX_TOOL_ITERATIONS) {
          const result = await InferenceAdapter.complete({ messages:conversation, model, provider, assistant, signal:controller.signal });
          if (!result.toolCalls.length) break;
          if (iterations++ >= MAX_TOOL_ITERATIONS) throw new Error("The agent reached the tool-call limit for one turn.");
          conversation.push({ role:"assistant", content:result.content || "", tool_calls:result.toolCalls.map(call=>({id:call.id,type:call.type || "function",function:call.function})) });
          for (const call of result.toolCalls) {
            let args={}; try { args=JSON.parse(call.function.arguments || "{}"); } catch { args={ raw:call.function.arguments }; }
            const entry = upsertToolTrace(assistant,call.traceId || call.id,call.function.name,{ status:"running", input:args, title:`Using ${humanize(call.function.name)}…` });
            state.status=`Using ${humanize(call.function.name)}…`; scheduleStreamingRender(assistant); updateControls();
            try { const output=await executeTool(call.function.name,args,controller.signal); Object.assign(entry,{status:"completed",output,title:`Used ${humanize(call.function.name)}`}); conversation.push({role:"tool",tool_call_id:call.id,name:call.function.name,content:output}); }
            catch(error) { Object.assign(entry,{status:"error",error:error.message,title:`${humanize(call.function.name)} failed`}); conversation.push({role:"tool",tool_call_id:call.id,name:call.function.name,content:`Error: ${error.message}`}); }
            scheduleStreamingRender(assistant);
          }
          state.status="Reasoning over tool results…"; updateControls();
        }
        if (!assistant.content.trim() && !assistant.trace.length) appendAssistantText(assistant,"empty","The model returned an empty response.");
      } catch(error) {
        if (error.name === "AbortError") { if (!assistant.content) appendAssistantText(assistant,"stopped","Generation stopped."); }
        else { assistant.error=true;appendAssistantText(assistant,"error",`Error: ${cleanError(error.message)}`);toast(cleanError(error.message),"error"); }
      } finally { const followOutput=state.followOutput;touchChat(chat);setGenerating(false);renderStreamingMessage(assistant);renderSidebar();renderPending();updateControls();if(followOutput)requestAnimationFrame(followStreamOutput); }
    }
    function setGenerating(value,status="") { state.generating=value; state.status=status; if (!value) { state.abort=null; state.status=""; } updateControls(); }
    function cleanError(message) { let value=String(message || "Request failed.").replace(/sk-or-v1-[A-Za-z0-9_-]+/g,"[redacted]");for(const key of Object.values(state.providerKeys))if(key&&key.length>=8)value=value.split(key).join("[redacted]");return value.slice(0,1000); }
    function humanize(value) { return String(value || "tool").replace(/^Tools_/,"").replace(/_/g," ").replace(/\b\w/g,c=>c.toUpperCase()); }
    function ensureSegment(message,segment) { message.segments ||= [];let existing=message.segments.find(item=>item.id===segment.id);if(!existing){existing={...segment,uiKey:segment.uiKey || segment.id};message.segments.push(existing);}else existing.uiKey ||= existing.id;return existing; }
    function appendAssistantText(message,segmentId,text) { if(!text)return;const segment=ensureSegment(message,{id:segmentId,kind:"text",content:""});segment.content+=text;message.content+=text; }
    function upsertToolTrace(message,id,name,patch) { let entry=message.trace.find(e=>e.id===id); if (!entry) { entry={id,kind:"tool",toolName:name}; message.trace.push(entry);ensureSegment(message,{id:`trace:${id}`,kind:"trace",traceId:id}); } Object.assign(entry,patch); return entry; }
    function appendReasoning(assistant,text,source="reasoning",traceId="reasoning") { if(!text)return;let buffers=reasoningBuffers.get(assistant);if(!buffers){buffers=new Map();reasoningBuffers.set(assistant,buffers);}const bufferKey=`${traceId}:${source}`;buffers.set(bufferKey,(buffers.get(bufferKey)||"")+text);const prefix=`${traceId}:`,main=[buffers.get(`${prefix}reasoning`)||"",buffers.get(`${prefix}reasoning-content`)||""].sort((a,b)=>b.length-a.length)[0],structured=[...new Set([...buffers.entries()].filter(([key])=>key.startsWith(`${prefix}detail:`)).map(([,value])=>value))].join("\n\n"),detail=structured.length>main.length?structured:main;let entry=assistant.trace.find(item=>item.id===traceId);if(!entry){entry={id:traceId,kind:"thought",status:"completed",title:"Reasoning",detail:""};assistant.trace.push(entry);ensureSegment(assistant,{id:`trace:${traceId}`,kind:"trace",traceId});}entry.detail=detail;const preview=detail.replace(/[\s*_`#>]+/g," ").trim();entry.title=preview?`Reasoning · ${preview.slice(0,100)}`:"Reasoning"; }

    const InferenceAdapter = {
      version: 1,
      async complete({messages,model,provider,assistant,signal}) {
        const body={ model, messages, temperature:Number(state.settings.temperature), top_p:Number(state.settings.topP), top_k:Number(state.settings.topK), repetition_penalty:Number(state.settings.repetitionPenalty), max_tokens:Number(state.settings.maxTokens), stream:true };
        if (state.settings.seed) body.seed=Number(state.settings.seed);
        const tools=enabledToolSchemas(); if (tools.length) body.tools=tools;
        const modelInfo=state.models.find(entry=>entry.id===model) || {id:model}, reasoning=getReasoningSelection(modelInfo);
        if(reasoning) {
          if(provider.id==="lmstudio") { if(reasoning.mode==="off")body.reasoning_effort="none";else if(reasoning.mode==="effort")body.reasoning_effort=reasoning.effort;else if(reasoning.mode==="max_tokens")body.reasoning_tokens=reasoning.maxTokens; }
          else { if(reasoning.mode==="off")body.reasoning={effort:"none"};else if(reasoning.mode==="effort")body.reasoning={effort:reasoning.effort};else if(reasoning.mode==="max_tokens")body.reasoning={max_tokens:reasoning.maxTokens};else body.reasoning={enabled:true}; }
        }
        const headers={"Content-Type":"application/json"},key=providerKey(provider.id);if(key)headers.Authorization=`Bearer ${key.trim()}`;if(provider.openRouter){headers["HTTP-Referer"]=location.origin.startsWith("http")?location.origin:"https://localhost";headers["X-Title"]="Agent Micro-App";}
        const response=await fetch(provider.chatUrl,{method:"POST",headers,body:JSON.stringify(body),signal});
        if (!response.ok) { const raw=await response.text(); throw new Error(`${provider.name} ${response.status}: ${extractProviderError(raw,response.statusText)}`); }
        if (!response.body) throw new Error("The provider returned no response stream.");
        const reader=response.body.getReader(),decoder=new TextDecoder(),streamId=uid("stream");let buffer="",callMap=[],content="",phase=0,lastStreamKind="";const phaseId=kind=>{if(lastStreamKind!==kind){lastStreamKind=kind;phase++;}return `${streamId}:${kind}-${phase}`;};
        state.status="Thinking…"; updateControls();
        while(true) {
          const {done,value}=await reader.read(); if(done) break; buffer+=decoder.decode(value,{stream:true}); const lines=buffer.split("\n"); buffer=lines.pop() || "";
          for(const rawLine of lines) { const line=rawLine.trim(); if(!line || line==="data: [DONE]") continue; const payload=line.startsWith("data:")?line.slice(5).trim():line; let data; try{data=JSON.parse(payload);}catch{continue;} const delta=data.choices?.[0]?.delta; if(!delta) continue;
            const reasoningText=typeof delta.reasoning==="string"?delta.reasoning:"",reasoningContent=typeof delta.reasoning_content==="string"?delta.reasoning_content:"";
            if(reasoningText) appendReasoning(assistant,reasoningText,"reasoning",phaseId("reasoning")); if(reasoningContent) appendReasoning(assistant,reasoningContent,"reasoning-content",phaseId("reasoning"));
            if(Array.isArray(delta.reasoning_details)) delta.reasoning_details.forEach((d,n)=>{const detailText=d.text || d.summary || "";if(detailText)appendReasoning(assistant,detailText,`detail:${d.id ?? d.index ?? n}`,phaseId("reasoning"));});
            const contentText=typeof delta.content==="string"?delta.content:"";if(contentText) { content+=contentText;appendAssistantText(assistant,phaseId("text"),contentText);state.status="Writing response…";scheduleStreamingRender(assistant);updateControls(); }
            if(Array.isArray(delta.tool_calls)&&delta.tool_calls.length) { const parts=delta.tool_calls.filter(part=>part&&(part.id || part.function?.name || part.function?.arguments));if(parts.length){const toolPhaseId=phaseId("tool");parts.forEach(part=>{const idx=part.index ?? 0;if(!callMap[idx])callMap[idx]={id:part.id || `${streamId}:call-${idx}`,traceId:`${toolPhaseId}:${idx}`,type:"function",function:{name:"",arguments:""}};else if(part.id)callMap[idx].id=part.id;if(part.function?.name)callMap[idx].function.name+=part.function.name;if(part.function?.arguments)callMap[idx].function.arguments+=part.function.arguments;upsertToolTrace(assistant,callMap[idx].traceId,callMap[idx].function.name || "tool",{status:"running",rawInput:callMap[idx].function.arguments,title:`Preparing ${humanize(callMap[idx].function.name || "tool")}…`});});}}
            scheduleStreamingRender(assistant);
          }
        }
        return {content,toolCalls:callMap.filter(Boolean)};
      }
    };
    function extractProviderError(raw,fallback) { const parsed=safeParse(raw,null); return parsed?.error?.message || parsed?.message || raw.trim().slice(0,700) || fallback || "Request failed"; }
    function normalizeProviderModel(model) { if(typeof model==="string")return {id:model,name:model,modalities:[]};const id=model.id || model.key || model.name,capabilities=model.capabilities && typeof model.capabilities==="object"?model.capabilities:{},advertisedModalities=model.architecture?.input_modalities || model.input_modalities || model.modalities || [];const modalities=Array.isArray(advertisedModalities)&&advertisedModalities.length?advertisedModalities:capabilities.vision===true?["text","image"]:capabilities.vision===false?["text"]:[];return {id,name:model.name || model.display_name || id,modalities,capabilities,contextLength:model.context_length || model.max_context_length,reasoning:model.reasoning || capabilities.reasoning,supportedParameters:model.supported_parameters || []}; }

    async function loadModels(force=false) {
      const token=++state.modelLoadToken,provider=activeProvider(),key=providerKey(provider.id),cacheScope=key?"keyed":"anonymous";
      if(!provider.modelsUrl){state.models=[{id:provider.defaultModel,name:provider.defaultModel}];els.modelLoading.classList.add("hidden");renderModels();return;}
      if (!force) { const cached=safeParse(await readPreference(STORAGE.models,localStorage.getItem(STORAGE.models) || ""),null); if(cached && cached.providerId===provider.id && cached.signature===provider.modelsUrl && cached.scope===cacheScope && Date.now()-cached.savedAt<86400000) { state.models=cached.models;els.modelLoading.classList.add("hidden");renderModels();return; } }
      els.modelLoading.classList.remove("hidden");
      try { const headers={};if(key)headers.Authorization=`Bearer ${key}`;if(provider.openRouter){headers["HTTP-Referer"]=location.origin.startsWith("http")?location.origin:"https://localhost";headers["X-Title"]="Agent Micro-App";}const response=await fetch(provider.modelsUrl,{headers});if(!response.ok)throw new Error(`${response.status} ${response.statusText}`);const data=await response.json(),items=Array.isArray(data)?data:(data.data || data.models || []);let models=items.filter(item=>typeof item==="string" || !item?.type || item.type==="llm").map(normalizeProviderModel).filter(model=>model.id);if(provider.openRouter&&!key)models=models.filter(model=>model.id.endsWith(":free"));if(!models.length)throw new Error("No models returned");models.sort((a,b)=>(a.name || a.id).localeCompare(b.name || b.id));if(token!==state.modelLoadToken)return;state.models=models;saveJson(STORAGE.models,{models,savedAt:Date.now(),scope:cacheScope,providerId:provider.id,signature:provider.modelsUrl},false); }
      catch { if(token!==state.modelLoadToken)return;state.models=provider.openRouter?FALLBACK_MODELS:[{id:provider.defaultModel,name:provider.defaultModel}];toast(`Could not load models from ${provider.name}.`,"error"); }
      finally { if(token===state.modelLoadToken){els.modelLoading.classList.add("hidden");renderModels();} }
    }

    function openModelPicker() { if(!els.modelPopover.classList.contains("hidden")){els.modelPopover.classList.add("hidden");els.prompt.focus();return;}closeCommands(true);if(!closeComposerPopovers(els.modelPopover))return;state.modelQuery="";els.modelSearch.value="";renderModels();els.modelPopover.classList.remove("hidden");positionComposerPopover(els.modelPopover,els.modelPickerBtn);requestAnimationFrame(()=>els.modelSearch.focus()); }
    function selectModel(modelId) { const chat=activeChat(),provider=activeProvider(chat);chat.model=modelId;chat.providerId=provider.id;provider.defaultModel=modelId;state.settings.model=modelId;touchChat(chat);saveJson(STORAGE.settings,state.settings);renderModels();els.modelPopover.classList.add("hidden");els.prompt.focus(); }

    function showCommandPopover(popover) { closeCommands(true);if(!closeComposerPopovers(popover))return;popover.classList.remove("hidden");positionCenteredComposerPopover(popover); }
    function openProviders() { state.providerDrafts=clone(state.settings.inferenceProviders);state.providerKeyDrafts={...state.providerKeys};state.providerDraftId=activeProvider()?.id || state.settings.activeProviderId;renderProviderEditor();showCommandPopover(els.providersPopover); }
    function populateParameters(values=state.settings) { $("#systemPromptInput").value=values.systemPrompt;$("#temperatureInput").value=$("#temperatureRange").value=values.temperature;$("#topPInput").value=$("#topPRange").value=values.topP;$("#topKInput").value=$("#topKRange").value=values.topK;$("#repetitionPenaltyInput").value=$("#repetitionPenaltyRange").value=values.repetitionPenalty;$("#maxTokensInput").value=values.maxTokens;$("#seedInput").value=values.seed; }
    function openParameters() { populateParameters();showCommandPopover(els.parametersPopover); }
    function openMcpConfig() { renderMcpServerSettings();showCommandPopover(els.mcpConfigPopover); }
    function openDataControls() { showCommandPopover(els.dataControlsPopover); }
    function captureProviderDraft(validate=false) { const provider=state.providerDrafts.find(entry=>entry.id===state.providerDraftId);if(!provider)return;provider.name=$("#inferenceProviderNameInput").value.trim() || "Inference Provider";provider.defaultModel=$("#inferenceDefaultModelInput").value.trim();provider.chatUrl=validate?validateInferenceUrl($("#inferenceChatUrlInput").value,"chat completions URL"):$("#inferenceChatUrlInput").value.trim();provider.modelsUrl=validate?validateInferenceUrl($("#inferenceModelsUrlInput").value,"models URL",true):$("#inferenceModelsUrlInput").value.trim();if(validate&&!provider.defaultModel)throw new Error("Enter a default model ID.");provider.rememberKey=$("#rememberKeyInput").checked;state.providerKeyDrafts[provider.id]=$("#apiKeyInput").value.trim(); }
    function renderProviderEditor() { const provider=state.providerDrafts.find(entry=>entry.id===state.providerDraftId) || state.providerDrafts[0];if(!provider)return;state.providerDraftId=provider.id;els.providerSelect.innerHTML=state.providerDrafts.map(entry=>`<button class="provider-card ${entry.id===provider.id?"active":""}" type="button" data-provider-id="${escapeHtml(entry.id)}" aria-pressed="${entry.id===provider.id}"><span class="provider-card-icon">${icon(entry.id==="openrouter"?"cloud":entry.id==="lmstudio"?"monitor-cog":"server")}</span><span class="provider-card-copy"><strong>${escapeHtml(entry.name)}</strong><span>${escapeHtml(entry.description || (entry.builtIn?"Built-in provider":"OpenAI-compatible endpoint"))}</span></span>${icon("circle-check","provider-card-check")}</button>`).join("");$("#providerEditorTitle").textContent=provider.name;$("#providerEditorBadge").textContent=provider.builtIn?"Built-in":"Custom";$("#inferenceProviderNameInput").value=provider.name;$("#inferenceDefaultModelInput").value=provider.defaultModel;$("#inferenceChatUrlInput").value=provider.chatUrl;$("#inferenceModelsUrlInput").value=provider.modelsUrl;$("#apiKeyInput").value=state.providerKeyDrafts[provider.id] || "";$("#rememberKeyInput").checked=provider.rememberKey;$("#removeInferenceProviderBtn").hidden=provider.builtIn;refreshIcons(els.providerSelect); }
    function addInferenceProvider() { try{captureProviderDraft(false);}catch{}if(state.providerDrafts.length>=10){toast("Up to 10 inference providers can be configured.","error");return;}const customCount=state.providerDrafts.filter(provider=>!provider.builtIn).length+1,provider={id:uid("provider"),name:`Custom provider ${customCount}`,description:"OpenAI-compatible endpoint",chatUrl:"http://localhost:1234/v1/chat/completions",modelsUrl:"http://localhost:1234/v1/models",defaultModel:"local-model",rememberKey:false,openRouter:false,builtIn:false};state.providerDrafts.push(provider);state.providerDraftId=provider.id;renderProviderEditor();$("#inferenceProviderNameInput").select(); }
    function removeInferenceProvider() { const current=state.providerDrafts.find(entry=>entry.id===state.providerDraftId);if(!current||current.builtIn)return;const index=state.providerDrafts.indexOf(current);delete state.providerKeyDrafts[state.providerDraftId];state.providerDrafts.splice(index,1);state.providerDraftId=state.providerDrafts[Math.max(0,index-1)].id;renderProviderEditor(); }
    function renderMcpServerSettings(servers=state.settings.mcpServers) { els.mcpServerList.innerHTML=servers.length?servers.map(server=>`<div class="mcp-server-row" data-mcp-server-id="${escapeHtml(server.id)}"><input type="checkbox" data-mcp-field="enabled" ${server.enabled?"checked":""} aria-label="Enable ${escapeHtml(server.name)}"><input type="text" data-mcp-field="name" value="${escapeHtml(server.name)}" placeholder="Server name" aria-label="MCP server name"><input type="url" data-mcp-field="url" value="${escapeHtml(server.url)}" placeholder="https://server.example/mcp/" aria-label="MCP Streamable HTTP URL"><button class="icon-btn" type="button" data-action="remove-mcp-server" aria-label="Remove ${escapeHtml(server.name)}">${icon("trash-2")}</button></div>`).join(""):`<div class="empty-chats">No MCP servers configured.</div>`;refreshIcons(els.mcpServerList); }
    function readMcpServerSettings(validate=true) { return $$('[data-mcp-server-id]',els.mcpServerList).map((row,index)=>{const rawUrl=$("[data-mcp-field='url']",row).value.trim();return {id:row.dataset.mcpServerId || uid("mcp"),name:$("[data-mcp-field='name']",row).value.trim() || `MCP Server ${index+1}`,url:validate?validateMcpServerUrl(rawUrl):rawUrl,enabled:$("[data-mcp-field='enabled']",row).checked};}); }
    function addMcpServerSetting() { const servers=readMcpServerSettings(false);if(servers.length>=12){toast("Up to 12 MCP servers can be configured.","error");return;}servers.push({id:uid("mcp"),name:`MCP Server ${servers.length+1}`,url:"https://",enabled:true});renderMcpServerSettings(servers);const rows=$$('[data-mcp-server-id]',els.mcpServerList);$("[data-mcp-field='name']",rows.at(-1))?.select(); }
    async function saveProviders() {
      try{captureProviderDraft(true);}catch(error){toast(error.message,"error");return;}
      const providersChanged=JSON.stringify(state.providerDrafts)!==JSON.stringify(state.settings.inferenceProviders),chat=activeChat(),previousProviderId=chat.providerId || state.settings.activeProviderId;
      state.settings={...state.settings,activeProviderId:state.providerDraftId,inferenceProviders:clone(state.providerDrafts)};
      state.providerKeys=Object.fromEntries(state.settings.inferenceProviders.map(provider=>[provider.id,state.providerKeyDrafts[provider.id] || ""]).filter(([,key])=>key));const provider=inferenceProvider(state.settings.activeProviderId);chat.providerId=provider.id;if(previousProviderId!==provider.id)chat.model=provider.defaultModel;state.settings.model=chat.model || provider.defaultModel;touchChat(chat);
      const remembered={},session={};for(const item of state.settings.inferenceProviders){const key=state.providerKeys[item.id];if(!key)continue;(item.rememberKey?remembered:session)[item.id]=key;}
      let keyPersisted=true;try{sessionStorage.removeItem(STORAGE.providerKeys);sessionStorage.removeItem(STORAGE.key);if(Object.keys(session).length)sessionStorage.setItem(STORAGE.providerKeys,JSON.stringify(session));}catch{keyPersisted=false;}try{await removeSecret(STORAGE.providerKeys);await removeSecret(STORAGE.key);if(Object.keys(remembered).length)await writeSecret(STORAGE.providerKeys,remembered);}catch{keyPersisted=false;}try{localStorage.removeItem(STORAGE.key);}catch{}
      const settingsPersisted=saveJson(STORAGE.settings,state.settings,false);els.providersPopover.classList.add("hidden");if(providersChanged||previousProviderId!==provider.id)state.models=provider.openRouter?FALLBACK_MODELS:[{id:provider.defaultModel,name:provider.defaultModel}];loadModels(true);
      if(!keyPersisted)toast("Provider settings are active, but one or more API keys could not be persisted.","error");else if(!settingsPersisted)toast("Providers are active for this tab, but local storage is full.","error");else toast("Providers saved.");
    }
    function persistParameters() { state.settings={...state.settings,systemPrompt:$("#systemPromptInput").value.trim() || DEFAULT_PROMPT,temperature:clamp($("#temperatureInput").value,0,2),topP:clamp($("#topPInput").value,0,1),topK:Math.round(clamp($("#topKInput").value,0,100)),repetitionPenalty:clamp($("#repetitionPenaltyInput").value,0,2),maxTokens:Math.round(clamp($("#maxTokensInput").value,64,32768)),seed:Math.max(0,Math.round(Number($("#seedInput").value)||0))};saveJson(STORAGE.settings,state.settings); }
    function persistMcpConfig() { let mcpServers;try{mcpServers=readMcpServerSettings();}catch(error){toast(error.message,"error");return false;}const changed=JSON.stringify(mcpServers)!==JSON.stringify(state.settings.mcpServers);state.settings={...state.settings,mcpServers};saveJson(STORAGE.settings,state.settings);if(changed){resetMcpClients();void loadMcpTools(true).catch(error=>toast(cleanError(error.message),"error"));}return true; }
    function clamp(value,min,max) { const n=Number(value); return Number.isFinite(n)?Math.min(max,Math.max(min,n)):min; }
    function renderMcpTools() {
      if(!els.toolGrid)return;
      els.toolLoading.classList.toggle("hidden",!state.mcpLoading);
      if(state.mcpLoading && !state.mcpTools.length)els.toolGrid.innerHTML=`<div class="empty-chats">Discovering tools…</div>`;
      else if(!state.mcpTools.length)els.toolGrid.innerHTML=`<div class="empty-chats">No MCP tools available.</div>`;
      else {
        const groups=new Map();for(const tool of state.mcpTools){if(!groups.has(tool.serverId))groups.set(tool.serverId,[]);groups.get(tool.serverId).push(tool);}
        els.toolGrid.innerHTML=[...groups.entries()].map(([serverId,tools])=>{const enabledCount=tools.filter(tool=>!isMcpToolDisabled(tool)).length,serverName=tools[0]?.serverName || serverId;return `<section class="tool-server-group" data-tool-server-group="${escapeHtml(serverId)}"><label class="tool-server-head"><input type="checkbox" data-server-tools="${escapeHtml(serverId)}" ${enabledCount===tools.length?"checked":""}><span><strong>${escapeHtml(serverName)}</strong><small data-server-tool-count>${enabledCount}/${tools.length} enabled</small></span></label>${tools.map(tool=>{const checked=!isMcpToolDisabled(tool),summary=tool.description || "MCP tool";return `<label class="tool-card ${checked?"":"disabled"}" title="${escapeHtml(summary)}"><input type="checkbox" data-tool-key value="${escapeHtml(tool.key)}" data-tool-server="${escapeHtml(serverId)}" ${checked?"checked":""}><span>${icon(mcpToolIcon(tool.name))}</span><span><strong>${escapeHtml(humanize(tool.name))}</strong><span>${escapeHtml(summary)}</span></span></label>`;}).join("")}</section>`;}).join("");
        $$('[data-server-tools]',els.toolGrid).forEach(input=>syncMcpServerToggle(input.dataset.serverTools));
      }
      refreshIcons(els.toolGrid);
    }
    function syncMcpServerToggle(serverId) { const escaped=CSS.escape(serverId),tools=$$(`[data-tool-server="${escaped}"]`,els.toolGrid),toggle=$(`[data-server-tools="${escaped}"]`,els.toolGrid);if(!toggle||!tools.length)return;const enabled=tools.filter(input=>input.checked).length;toggle.checked=enabled===tools.length;toggle.indeterminate=enabled>0&&enabled<tools.length;const count=toggle.closest(".tool-server-head")?.querySelector("[data-server-tool-count]");if(count)count.textContent=`${enabled}/${tools.length} enabled`; }
    function openTools() { if(!els.toolsPopover.classList.contains("hidden")){els.toolsPopover.classList.add("hidden");return;}closeCommands(true);if(!closeComposerPopovers(els.toolsPopover))return;renderMcpTools();els.toolsPopover.classList.remove("hidden");positionCenteredComposerPopover(els.toolsPopover);void loadMcpTools().catch(()=>{}); }
    function saveTools() { if(state.mcpTools.length)state.settings.disabledMcpTools=state.mcpTools.filter(tool=>!$$('[data-tool-key]:checked',els.toolGrid).some(input=>input.value===tool.key)).map(tool=>tool.key);saveJson(STORAGE.settings,state.settings);els.toolsPopover.classList.add("hidden");toast(`${enabledMcpTools().length} MCP tools enabled.`); }
    const COMMANDS = [
      {icon:"paperclip",label:"Attach",description:"Add text, code, or images to the message",action:"attach",keywords:"file upload image"},
      {icon:"database",label:"Data Controls",description:"Export, import, or clear local data",action:"data-controls",keywords:"backup download storage"},
      {icon:"plug",label:"MCP Config",description:"Configure MCP servers and connections",action:"mcp-config",keywords:"tools server url integrations"},
      {icon:"box",label:"Models",description:"Choose a model from the active provider",action:"model-picker",keywords:"provider llm"},
      {icon:"plus",label:"New chat",description:"Start a fresh conversation",action:"new-chat",keywords:"reset conversation"},
      {icon:"sliders-horizontal",label:"Parameters",description:"Configure the system prompt and generation",action:"parameters",keywords:"temperature top p top k tokens seed repetition"},
      {icon:"server",label:"Providers",description:"Configure OpenRouter, LM Studio, and custom endpoints",action:"providers",keywords:"api key models inference"},
      {icon:"brain-circuit",label:"Reasoning",description:"Configure reasoning for the selected model",action:"reasoning",keywords:"thinking effort"},
      {icon:"refresh-cw",label:"Refresh models",description:"Reload models from the active provider",action:"refresh-models",keywords:"sync update"},
      {icon:"wrench",label:"Tools",description:"Manage MCP tools",action:"tools",keywords:"mcp integrations"}
    ];
    function renderCommands(query="") { const normalized=query.trim().toLowerCase();state.commandMatches=COMMANDS.filter(command=>!normalized||`${command.label} ${command.description} ${command.keywords}`.toLowerCase().includes(normalized));state.commandIndex=Math.min(state.commandIndex,Math.max(0,state.commandMatches.length-1));els.commandList.innerHTML=state.commandMatches.length?state.commandMatches.map((command,index)=>`<button class="command-row ${index===state.commandIndex?"selected":""}" type="button" role="option" aria-selected="${index===state.commandIndex}" data-command="${command.action}" data-command-index="${index}">${icon(command.icon)}<span class="command-copy"><strong>${escapeHtml(command.label)}</strong><span>${escapeHtml(command.description)}</span></span></button>`).join(""):`<div class="command-empty">No matching commands</div>`;refreshIcons(els.commandList); }
    function commandQuery() { if(state.commandFromSlash)return els.prompt.value.slice(1);return els.prompt.value.startsWith(state.commandPromptBase)?els.prompt.value.slice(state.commandPromptBase.length):els.prompt.value; }
    function openCommands() { if(state.commandOpen)return;if(!closeComposerPopovers())return;state.commandFromSlash=false;state.commandPromptBase=els.prompt.value;state.commandOpen=true;state.commandIndex=0;els.commandPanel.classList.remove("hidden");renderCommands();els.prompt.focus(); }
    function closeCommands(clearQuery=false) { if(!state.commandOpen)return;const restore=clearQuery?(state.commandFromSlash?"":state.commandPromptBase):null;state.commandOpen=false;state.commandFromSlash=false;state.commandPromptBase="";els.commandPanel.classList.add("hidden");if(restore!==null){els.prompt.value=restore;autoSizePrompt();} }
    function toggleCommands() { state.commandOpen?closeCommands(true):openCommands(); }
    function moveCommandSelection(delta) { if(!state.commandMatches.length)return;state.commandIndex=(state.commandIndex+delta+state.commandMatches.length)%state.commandMatches.length;renderCommands(commandQuery());els.commandList.querySelector(".command-row.selected")?.scrollIntoView({block:"nearest"}); }
    function runCommand(action) { closeCommands(true);if(action==="new-chat")newChat();if(action==="model-picker")openModelPicker();if(action==="reasoning")openReasoning();if(action==="tools")openTools();if(action==="attach")els.fileInput.click();if(action==="parameters")openParameters();if(action==="providers")openProviders();if(action==="data-controls")openDataControls();if(action==="mcp-config")openMcpConfig();if(action==="refresh-models")void loadModels(true).then(()=>toast(`Loaded ${state.models.length} models.`)); }
    function runSelectedCommand() { const command=state.commandMatches[state.commandIndex];if(command)runCommand(command.action); }
    function syncCommandPanelFromPrompt() { const slashQuery=els.prompt.value.startsWith("/")&&!els.prompt.value.includes("\n");if(slashQuery){if(!state.commandOpen){state.commandOpen=true;state.commandIndex=0;state.commandPromptBase="";els.commandPanel.classList.remove("hidden");}state.commandFromSlash=true;renderCommands(els.prompt.value.slice(1));}else if(state.commandFromSlash)closeCommands();else if(state.commandOpen)renderCommands(commandQuery()); }

    function newChat() { if(state.generating) return; const chat=createChat(undefined,state.settings.activeProviderId); state.chats.unshift(chat); state.activeId=chat.id; state.attachments=[]; state.models=activeProvider(chat).openRouter?FALLBACK_MODELS:[{id:activeProvider(chat).defaultModel,name:activeProvider(chat).defaultModel}];saveChats();renderApp();void loadModels();closeMobileSidebar();els.prompt.focus(); }
    function openChat(id) { if(state.generating) { toast("Stop the current response before switching chats.","error"); return; }const previous=activeProvider()?.id;state.activeId=id;state.attachments=[];const chat=activeChat(),provider=activeProvider(chat);chat.providerId=provider.id;state.settings.activeProviderId=provider.id;state.settings.model=chat.model;if(previous!==provider.id){state.models=provider.openRouter?FALLBACK_MODELS:[{id:provider.defaultModel,name:provider.defaultModel}];void loadModels();}saveJson(STORAGE.settings,state.settings);renderApp();closeMobileSidebar(); }
    function deleteChat(id) { if(state.generating && id===state.activeId) return; const chat=state.chats.find(c=>c.id===id); if(!chat)return; state.chats=state.chats.filter(c=>c.id!==id); if(!state.chats.length)state.chats.push(createChat()); if(id===state.activeId)state.activeId=state.chats[0].id; saveChats(); renderApp(); toast(`Deleted “${chat.title}”.`); }
    function exportChat() { const chat=activeChat(); if(!chat) return; const lines=[`# ${chat.title}`,"",`Provider: ${inferenceProvider(chat.providerId)?.name || chat.providerId}`,`Model: ${chat.model}`,""]; chat.messages.forEach(m=>{lines.push(`## ${m.role==="user"?"You":"Agent"}`,"",m.content || ""); if(m.attachments?.length) lines.push("",...m.attachments.map(a=>`- Attachment: ${a.name} (${formatBytes(a.size)})`)); if(m.trace?.length) lines.push("","> Agent trace",...m.trace.map(t=>`> - ${t.title || t.toolName}: ${t.status || "complete"}`)); lines.push("");}); download(`${slug(chat.title)}.md`,lines.join("\n"),"text/markdown"); }
    function slug(value) { return String(value).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,60)||"agent-chat"; }
    function exportAll() { download(`agent-data-${new Date().toISOString().slice(0,10)}.json`,JSON.stringify({version:1,exportedAt:new Date().toISOString(),settings:state.settings,chats:state.chats},null,2),"application/json"); }
    async function importAll(file) { try { const data=JSON.parse(await file.text()); if(data.version!==1 || !Array.isArray(data.chats)) throw new Error("Unsupported Agent export."); const incoming=data.chats.filter(validChat); if(!incoming.length) throw new Error("No valid chats were found.");state.chats=incoming.slice(0,MAX_CHATS);if(data.settings){const providers=normalizeInferenceProviders(data.settings.inferenceProviders,data.settings);state.settings={...DEFAULTS,...data.settings,inferenceProviders:providers,activeProviderId:providers.some(provider=>provider.id===data.settings.activeProviderId)?data.settings.activeProviderId:providers[0].id,mcpServers:normalizeMcpServers(data.settings.mcpServers)};}state.chats.forEach(chat=>{if(!chat.providerId||!state.settings.inferenceProviders.some(provider=>provider.id===chat.providerId))chat.providerId=state.settings.activeProviderId;});state.activeId=state.chats[0].id;state.settings.activeProviderId=activeChat().providerId;resetMcpClients();await persistChatsNow();saveJson(STORAGE.settings,state.settings);state.models=activeProvider().openRouter?FALLBACK_MODELS:[{id:activeProvider().defaultModel,name:activeProvider().defaultModel}];renderApp();void loadModels(true);void loadMcpTools(true).catch(()=>{});toast(`Imported ${state.chats.length} chats.`); } catch(error) { toast(error.message || "Could not import data.","error"); } }
    async function clearAllData() { if(!confirm("Clear every saved Agent chat and preference from this browser?")) return;clearTimeout(state.persistTimer);state.persistTimer=null;await clearAgentDb();Object.values(STORAGE).forEach(key=>{localStorage.removeItem(key);sessionStorage.removeItem(key);});state.settings={...DEFAULTS,inferenceProviders:clone(DEFAULT_INFERENCE_PROVIDERS),mcpServers:clone(DEFAULT_MCP_SERVERS)};state.providerKeys={};state.chats=[createChat(DEFAULT_MODEL,"openrouter")];state.activeId=state.chats[0].id;resetMcpClients();state.models=FALLBACK_MODELS;state.attachments=[];state.chatDbAvailable=true;[els.modelPopover,els.reasoningPopover,els.toolsPopover,els.providersPopover,els.parametersPopover,els.mcpConfigPopover,els.dataControlsPopover].forEach(popover=>popover.classList.add("hidden"));await persistChatsNow();renderApp();void loadModels(true);void loadMcpTools(true).catch(()=>{});toast("Agent was reset to its default empty state."); }

    function editMessage(index) { const chat=activeChat(),message=chat.messages[index]; if(!message || message.role!=="user" || state.generating) return; chat.messages=chat.messages.slice(0,index); els.prompt.value=message.content; state.attachments=clone(message.attachments || []); touchChat(chat); renderApp(); autoSizePrompt(); els.prompt.focus(); toast("Message loaded into the composer. Sending will replace later turns."); }
    function retryMessage(index) { const chat=activeChat(); if(state.generating) return; let userIndex=index-1; while(userIndex>=0 && chat.messages[userIndex].role!=="user") userIndex--; if(userIndex<0) return; const user=clone(chat.messages[userIndex]); chat.messages=chat.messages.slice(0,userIndex); touchChat(chat); renderApp(); sendMessage(user.content,user.attachments || []); }
    function messageCopy(index) { const message=activeChat().messages[index]; if(message) copyText(message.content || ""); }
    function autoSizePrompt() { els.prompt.style.height="auto"; els.prompt.style.height=Math.min(170,Math.max(46,els.prompt.scrollHeight))+"px"; updateControls(); }
    function openMobileSidebar() { els.sidebar.classList.add("open"); els.sidebarBackdrop.classList.remove("hidden"); }
    function closeMobileSidebar() { els.sidebar.classList.remove("open"); els.sidebarBackdrop.classList.add("hidden"); }
    function runArtifact(html) { state.artifactHtml=html; const title=html.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] || "HTML Preview"; els.artifactTitle.textContent=title; els.artifactFrame.srcdoc=html; els.artifact.classList.add("open"); els.artifactBackdrop.classList.remove("hidden"); }
    function closeArtifact() { els.artifact.classList.remove("open"); els.artifactBackdrop.classList.add("hidden"); setTimeout(()=>{els.artifactFrame.srcdoc="";},220); }
    function wireEvents() {
      $("#newChatBtn").addEventListener("click",newChat); $("#commandBtn").addEventListener("click",toggleCommands); els.modelPickerBtn.addEventListener("click",openModelPicker);
      $("#openSidebarBtn").addEventListener("click",openMobileSidebar); $("#closeSidebarBtn").addEventListener("click",closeMobileSidebar); els.sidebarBackdrop.addEventListener("click",closeMobileSidebar);
      els.chatList.addEventListener("click",event=>{const row=event.target.closest("[data-chat-id]");const action=event.target.closest("[data-action]")?.dataset.action;if(!row)return;action==="delete-chat"?deleteChat(row.dataset.chatId):openChat(row.dataset.chatId);});
      els.composer.addEventListener("submit",event=>{event.preventDefault();if(state.commandOpen)runSelectedCommand();else sendMessage();});els.prompt.addEventListener("input",()=>{autoSizePrompt();syncCommandPanelFromPrompt();});els.prompt.addEventListener("keydown",event=>{if(state.commandOpen){if(event.key==="ArrowDown"){event.preventDefault();moveCommandSelection(1);return;}if(event.key==="ArrowUp"){event.preventDefault();moveCommandSelection(-1);return;}if((event.key==="Enter"||event.key==="Tab")&&!event.isComposing){event.preventDefault();runSelectedCommand();return;}if(event.key==="Escape"){event.preventDefault();closeCommands(true);return;}}if(event.key==="Enter"&&!event.shiftKey&&!event.isComposing){event.preventDefault();sendMessage();}});
      els.fileInput.addEventListener("change",event=>{addFiles(event.target.files);event.target.value="";}); els.pending.addEventListener("click",event=>{const btn=event.target.closest('[data-action="remove-attachment"]');if(!btn)return;const id=event.target.closest("[data-attachment-id]")?.dataset.attachmentId;state.attachments=state.attachments.filter(a=>a.id!==id);renderPending();updateControls();});
      els.stop.addEventListener("click",()=>state.abort?.abort()); els.reasoningBtn.addEventListener("click",openReasoning);
      els.reasoningControls.addEventListener("click",event=>{const button=event.target.closest("[data-reasoning-mode]");if(!button)return;const mode=button.dataset.reasoningMode;if(mode==="effort")setReasoningSelection({mode,effort:button.dataset.reasoningEffort});else if(mode==="max_tokens")setReasoningSelection({mode,maxTokens:Math.max(1,Math.round(Number($("#reasoningBudgetInput")?.value)||2000))});else setReasoningSelection({mode});});
      els.modelSearch.addEventListener("input",()=>{state.modelQuery=els.modelSearch.value;renderModels();}); els.modelList.addEventListener("click",event=>{const option=event.target.closest("[data-model-id]");if(option)selectModel(option.dataset.modelId);});
      els.messagesInner.addEventListener("click",event=>{const action=event.target.closest("[data-action]")?.dataset.action;if(action==="open-settings")openProviders();const prompt=event.target.closest("[data-prompt]")?.dataset.prompt;if(prompt){els.prompt.value=prompt;autoSizePrompt();els.prompt.focus();}const article=event.target.closest("[data-message-index]");if(article){const i=Number(article.dataset.messageIndex);if(action==="copy-message")messageCopy(i);if(action==="edit-message")editMessage(i);if(action==="retry-message")retryMessage(i);}const wrap=event.target.closest(".code-wrap");if(wrap&&event.target.closest("[data-code-copy]"))copyText(wrap.dataset.code||"");if(wrap&&event.target.closest("[data-run-html]"))runArtifact(wrap.dataset.code||"");});
      els.messages.addEventListener("scroll",()=>{state.followOutput=isNearBottom();},{passive:true});
      els.messagesInner.addEventListener("toggle",event=>{const details=event.target.closest?.("[data-trace-key]");if(!details)return;details.open?state.traceOpen.add(details.dataset.traceKey):state.traceOpen.delete(details.dataset.traceKey);if(state.generating)state.followOutput=false;},true);
      $$('[data-close-popover]').forEach(btn=>btn.addEventListener("click",()=>dismissComposerPopover(btn.closest(".composer-popover"))));$("#saveProvidersBtn").addEventListener("click",saveProviders);$("#resetParametersBtn").addEventListener("click",()=>populateParameters(DEFAULTS));
      els.providerSelect.addEventListener("click",event=>{const card=event.target.closest("[data-provider-id]");if(!card||card.dataset.providerId===state.providerDraftId)return;try{captureProviderDraft(false);}catch{}state.providerDraftId=card.dataset.providerId;renderProviderEditor();});$("#inferenceProviderNameInput").addEventListener("input",()=>{const provider=state.providerDrafts.find(entry=>entry.id===state.providerDraftId);if(!provider)return;provider.name=$("#inferenceProviderNameInput").value || "Custom provider";$("#providerEditorTitle").textContent=provider.name;const card=els.providerSelect.querySelector(`[data-provider-id="${CSS.escape(provider.id)}"] .provider-card-copy strong`);if(card)card.textContent=provider.name;});$("#addInferenceProviderBtn").addEventListener("click",addInferenceProvider);$("#removeInferenceProviderBtn").addEventListener("click",removeInferenceProvider);
      $("#addMcpServerBtn").addEventListener("click",addMcpServerSetting);els.mcpServerList.addEventListener("click",event=>{const button=event.target.closest('[data-action="remove-mcp-server"]');if(!button)return;button.closest('[data-mcp-server-id]')?.remove();if(!$$('[data-mcp-server-id]',els.mcpServerList).length)renderMcpServerSettings([]);});
      [["#temperatureRange","#temperatureInput"],["#temperatureInput","#temperatureRange"],["#topPRange","#topPInput"],["#topPInput","#topPRange"],["#topKRange","#topKInput"],["#topKInput","#topKRange"],["#repetitionPenaltyRange","#repetitionPenaltyInput"],["#repetitionPenaltyInput","#repetitionPenaltyRange"]].forEach(([a,b])=>$(a).addEventListener("input",()=>$(b).value=$(a).value));
      $("#saveToolsBtn").addEventListener("click",saveTools); $("#disableToolsBtn").addEventListener("click",()=>{$$('[data-tool-key]',els.toolGrid).forEach(input=>{input.checked=false;input.closest(".tool-card")?.classList.add("disabled");});$$('[data-server-tools]',els.toolGrid).forEach(input=>syncMcpServerToggle(input.dataset.serverTools));}); $("#refreshToolsBtn").addEventListener("click",()=>{void loadMcpTools(true).then(()=>toast(`Loaded ${state.mcpTools.length} tools from ${Object.values(state.mcpConnections).filter(connection=>connection.status==="connected").length} servers.`)).catch(error=>toast(cleanError(error.message),"error"));});
      els.toolGrid.addEventListener("change",event=>{const serverToggle=event.target.closest("[data-server-tools]");if(serverToggle){const serverId=serverToggle.dataset.serverTools,escaped=CSS.escape(serverId);$$(`[data-tool-server="${escaped}"]`,els.toolGrid).forEach(input=>{input.checked=serverToggle.checked;input.closest(".tool-card")?.classList.toggle("disabled",!input.checked);});syncMcpServerToggle(serverId);return;}const toolToggle=event.target.closest("[data-tool-key]");if(toolToggle){toolToggle.closest(".tool-card")?.classList.toggle("disabled",!toolToggle.checked);syncMcpServerToggle(toolToggle.dataset.toolServer);}});
      els.commandList.addEventListener("pointermove",event=>{const row=event.target.closest("[data-command-index]");if(!row)return;const index=Number(row.dataset.commandIndex);if(index!==state.commandIndex){state.commandIndex=index;els.commandList.querySelectorAll(".command-row").forEach((button,buttonIndex)=>{button.classList.toggle("selected",buttonIndex===index);button.setAttribute("aria-selected",String(buttonIndex===index));});}});els.commandList.addEventListener("click",event=>{const command=event.target.closest("[data-command]")?.dataset.command;if(command)runCommand(command);});
      $("#exportChatSettingsBtn").addEventListener("click",exportChat); $("#exportAllBtn").addEventListener("click",exportAll); $("#importAllBtn").addEventListener("click",()=>$("#importInput").click()); $("#importInput").addEventListener("change",event=>{if(event.target.files[0])importAll(event.target.files[0]);event.target.value="";}); $("#clearDataBtn").addEventListener("click",clearAllData);
      $("#closeArtifactBtn").addEventListener("click",closeArtifact); els.artifactBackdrop.addEventListener("click",closeArtifact); $("#downloadArtifactBtn").addEventListener("click",()=>download(`${slug(els.artifactTitle.textContent)}.html`,state.artifactHtml,"text/html"));
      document.addEventListener("keydown",event=>{const mod=event.ctrlKey||event.metaKey;if(mod&&event.key.toLowerCase()==="k"){event.preventDefault();toggleCommands();}if(mod&&event.shiftKey&&event.key.toLowerCase()==="n"){event.preventDefault();newChat();}if(event.key==="Escape"){if([els.modelPopover,els.reasoningPopover,els.toolsPopover,els.providersPopover,els.parametersPopover,els.mcpConfigPopover,els.dataControlsPopover].some(popover=>!popover.classList.contains("hidden"))){closeComposerPopovers();els.prompt.focus();}if(els.artifact.classList.contains("open"))closeArtifact();}});document.addEventListener("pointerdown",event=>{if(state.commandOpen&&!event.target.closest("#commandPanel")&&!event.target.closest("#commandBtn")&&event.target!==els.prompt)closeCommands();if(!event.target.closest("#modelPopover")&&!event.target.closest("#modelPickerBtn"))els.modelPopover.classList.add("hidden");if(!event.target.closest("#reasoningPopover")&&!event.target.closest("#reasoningBtn"))els.reasoningPopover.classList.add("hidden");if(!event.target.closest("#toolsPopover"))els.toolsPopover.classList.add("hidden");[["#providersPopover",els.providersPopover],["#parametersPopover",els.parametersPopover],["#mcpConfigPopover",els.mcpConfigPopover],["#dataControlsPopover",els.dataControlsPopover]].forEach(([selector,popover])=>{if(!event.target.closest(selector))dismissComposerPopover(popover);});});window.addEventListener("resize",()=>closeComposerPopovers());
      window.addEventListener("dragenter",event=>{event.preventDefault();state.dragDepth++;els.dropOverlay.classList.remove("hidden");}); window.addEventListener("dragover",event=>event.preventDefault()); window.addEventListener("dragleave",event=>{event.preventDefault();state.dragDepth=Math.max(0,state.dragDepth-1);if(!state.dragDepth)els.dropOverlay.classList.add("hidden");}); window.addEventListener("drop",event=>{event.preventDefault();state.dragDepth=0;els.dropOverlay.classList.add("hidden");if(event.dataTransfer?.files?.length)addFiles(event.dataTransfer.files);});
    }

    async function init() { wireEvents(); await loadState(); renderApp(); autoSizePrompt(); refreshIcons(); loadModels(); void loadMcpTools().catch(()=>{}); }
    void init();
  })();
  </script>
</body>
</html>