@yxai/code 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3550 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>意心Code - yxcode</title>
7
+ <link rel="stylesheet" href="/vendor/github-dark.min.css">
8
+ <script src="/vendor/marked.min.js"></script>
9
+ <script src="/vendor/highlight.min.js"></script>
10
+ <style>
11
+ /* Claude-Inspired Dark Theme - yxcode */
12
+ @import url('/font.css');
13
+
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ :root {
21
+ /* Deep OLED Dark Background - Claude Style */
22
+ --bg-primary: #0a0a0a;
23
+ --bg-secondary: #111111;
24
+ --bg-tertiary: #1a1a1a;
25
+ --bg-elevated: #1f1f1f;
26
+ --bg-hover: #252525;
27
+
28
+ /* Text Colors - High Contrast */
29
+ --text-primary: #f5f5f5;
30
+ --text-secondary: #a8a8a8;
31
+ --text-tertiary: #737373;
32
+ --text-muted: #525252;
33
+
34
+ /* Accent Colors - Warm Orange (Claude Brand) */
35
+ --accent-primary: #f97316;
36
+ --accent-hover: #fb923c;
37
+ --accent-light: #fdba74;
38
+ --accent-subtle: rgba(249, 115, 22, 0.12);
39
+ --accent-glow: rgba(249, 115, 22, 0.25);
40
+
41
+ /* Semantic Colors */
42
+ --success: #10b981;
43
+ --success-bg: rgba(16, 185, 129, 0.12);
44
+ --success-glow: rgba(16, 185, 129, 0.2);
45
+ --error: #ef4444;
46
+ --error-bg: rgba(239, 68, 68, 0.12);
47
+ --warning: #f59e0b;
48
+ --warning-bg: rgba(245, 158, 11, 0.12);
49
+ --info: #3b82f6;
50
+ --info-bg: rgba(59, 130, 246, 0.12);
51
+
52
+ /* Borders & Dividers */
53
+ --border-primary: #262626;
54
+ --border-secondary: #1f1f1f;
55
+ --border-subtle: #171717;
56
+ --border-focus: var(--accent-primary);
57
+
58
+ /* Shadows - Subtle & Refined */
59
+ --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.5);
60
+ --shadow-md: 0 4px 8px -2px rgba(0, 0, 0, 0.6);
61
+ --shadow-lg: 0 12px 24px -4px rgba(0, 0, 0, 0.7);
62
+ --shadow-xl: 0 24px 48px -8px rgba(0, 0, 0, 0.8);
63
+ --shadow-glow: 0 0 20px var(--accent-glow);
64
+
65
+ /* Border Radius */
66
+ --radius-sm: 6px;
67
+ --radius-md: 8px;
68
+ --radius-lg: 12px;
69
+ --radius-xl: 16px;
70
+ --radius-full: 9999px;
71
+
72
+ /* Spacing Scale */
73
+ --space-xs: 4px;
74
+ --space-sm: 8px;
75
+ --space-md: 12px;
76
+ --space-lg: 16px;
77
+ --space-xl: 24px;
78
+ --space-2xl: 32px;
79
+
80
+ /* Typography */
81
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
82
+ --font-mono: 'Cascadia Code', 'Fira Code', 'SF Mono', Consolas, monospace;
83
+
84
+ /* Transitions */
85
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
86
+ --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
87
+ --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
88
+ }
89
+
90
+ body {
91
+ font-family: var(--font-sans);
92
+ background: var(--bg-primary);
93
+ color: var(--text-primary);
94
+ height: 100vh;
95
+ font-size: 14px;
96
+ line-height: 1.6;
97
+ -webkit-font-smoothing: antialiased;
98
+ -moz-osx-font-smoothing: grayscale;
99
+ }
100
+ /* Layout */
101
+ #app {
102
+ display: flex;
103
+ flex-direction: row;
104
+ height: 100vh;
105
+ overflow: hidden;
106
+ }
107
+
108
+ #mainArea {
109
+ flex: 1;
110
+ display: flex;
111
+ flex-direction: column;
112
+ min-width: 0;
113
+ background: var(--bg-primary);
114
+ }
115
+
116
+ /* Header - Claude Style */
117
+ #header {
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: space-between;
121
+ padding: var(--space-md) var(--space-lg);
122
+ background: var(--bg-secondary);
123
+ border-bottom: 1px solid var(--border-primary);
124
+ gap: var(--space-md);
125
+ flex-wrap: wrap;
126
+ backdrop-filter: blur(12px);
127
+ position: sticky;
128
+ top: 0;
129
+ z-index: 1000;
130
+ }
131
+
132
+ .header-left {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: var(--space-sm);
136
+ flex-shrink: 0;
137
+ }
138
+
139
+ .logo {
140
+ font-size: 18px;
141
+ font-weight: 700;
142
+ color: var(--accent-primary);
143
+ letter-spacing: -0.02em;
144
+ text-shadow: 0 0 12px var(--accent-glow);
145
+ transition: all var(--transition-base);
146
+ cursor: pointer;
147
+ }
148
+
149
+ .logo:hover {
150
+ color: var(--accent-hover);
151
+ text-shadow: 0 0 16px var(--accent-glow);
152
+ }
153
+
154
+ .version {
155
+ font-size: 11px;
156
+ font-weight: 500;
157
+ color: var(--text-tertiary);
158
+ background: var(--bg-tertiary);
159
+ padding: 2px 8px;
160
+ border-radius: var(--radius-full);
161
+ border: 1px solid var(--border-subtle);
162
+ }
163
+
164
+ .header-center {
165
+ display: flex;
166
+ gap: var(--space-sm);
167
+ flex: 0 1 auto;
168
+ justify-content: center;
169
+ min-width: 0;
170
+ }
171
+
172
+ .header-right {
173
+ display: flex;
174
+ align-items: center;
175
+ gap: var(--space-sm);
176
+ flex-shrink: 0;
177
+ }
178
+
179
+ .cwd-label {
180
+ font-size: 12px;
181
+ font-weight: 500;
182
+ color: var(--text-tertiary);
183
+ white-space: nowrap;
184
+ }
185
+
186
+ /* Form Controls - Refined */
187
+ select,
188
+ #cwdInput {
189
+ background: var(--bg-tertiary);
190
+ color: var(--text-primary);
191
+ border: 1px solid var(--border-primary);
192
+ border-radius: var(--radius-md);
193
+ padding: 6px 12px;
194
+ font-size: 13px;
195
+ font-family: var(--font-sans);
196
+ outline: none;
197
+ transition: all var(--transition-base);
198
+ cursor: pointer;
199
+ }
200
+
201
+ select:hover,
202
+ #cwdInput:hover {
203
+ border-color: var(--border-secondary);
204
+ background: var(--bg-elevated);
205
+ }
206
+
207
+ select:focus,
208
+ #cwdInput:focus {
209
+ border-color: var(--border-focus);
210
+ background: var(--bg-elevated);
211
+ box-shadow: 0 0 0 3px var(--accent-subtle);
212
+ }
213
+
214
+ #cwdInput {
215
+ width: 220px;
216
+ font-family: var(--font-mono);
217
+ font-size: 12px;
218
+ }
219
+
220
+ /* Custom Model Select - Premium Design */
221
+ .model-select-wrapper {
222
+ position: relative;
223
+ display: inline-block;
224
+ }
225
+
226
+ .model-select-display {
227
+ background: var(--bg-tertiary);
228
+ color: var(--text-primary);
229
+ border: 1px solid var(--border-primary);
230
+ border-radius: var(--radius-md);
231
+ padding: 6px 12px 6px 36px;
232
+ font-size: 13px;
233
+ font-weight: 500;
234
+ cursor: pointer;
235
+ min-width: 300px;
236
+ max-width: 360px;
237
+ display: flex;
238
+ align-items: center;
239
+ position: relative;
240
+ transition: all var(--transition-base);
241
+ }
242
+
243
+ .model-select-display:hover {
244
+ border-color: var(--accent-primary);
245
+ background: var(--bg-elevated);
246
+ box-shadow: var(--shadow-sm);
247
+ }
248
+
249
+ .model-select-display .model-icon {
250
+ position: absolute;
251
+ left: 8px;
252
+ width: 22px;
253
+ height: 22px;
254
+ border-radius: var(--radius-full);
255
+ object-fit: cover;
256
+ border: 1px solid var(--border-subtle);
257
+ }
258
+
259
+ .model-select-display .model-arrow {
260
+ margin-left: auto;
261
+ font-size: 10px;
262
+ color: var(--text-tertiary);
263
+ transition: transform var(--transition-base);
264
+ }
265
+
266
+ .model-select-wrapper.open .model-arrow {
267
+ transform: rotate(180deg);
268
+ }
269
+
270
+ .model-select-dropdown {
271
+ position: absolute;
272
+ top: calc(100% + 4px);
273
+ left: 0;
274
+ right: 0;
275
+ background: var(--bg-elevated);
276
+ border: 1px solid var(--border-primary);
277
+ border-radius: var(--radius-lg);
278
+ margin-top: 4px;
279
+ max-height: 320px;
280
+ overflow-y: auto;
281
+ z-index: 9999;
282
+ display: none;
283
+ box-shadow: var(--shadow-lg);
284
+ backdrop-filter: blur(16px);
285
+ }
286
+
287
+ .model-select-dropdown.visible {
288
+ display: block;
289
+ animation: slideDown var(--transition-base);
290
+ }
291
+
292
+ @keyframes slideDown {
293
+ from {
294
+ opacity: 0;
295
+ transform: translateY(-8px);
296
+ }
297
+ to {
298
+ opacity: 1;
299
+ transform: translateY(0);
300
+ }
301
+ }
302
+
303
+ .model-select-option {
304
+ display: flex;
305
+ align-items: center;
306
+ gap: var(--space-md);
307
+ padding: 10px 12px;
308
+ cursor: pointer;
309
+ font-size: 13px;
310
+ transition: all var(--transition-fast);
311
+ border-left: 2px solid transparent;
312
+ }
313
+
314
+ .model-select-option:hover {
315
+ background: var(--bg-hover);
316
+ border-left-color: var(--accent-primary);
317
+ }
318
+
319
+ .model-select-option .model-icon {
320
+ width: 22px;
321
+ height: 22px;
322
+ border-radius: var(--radius-full);
323
+ object-fit: cover;
324
+ flex-shrink: 0;
325
+ border: 1px solid var(--border-subtle);
326
+ }
327
+
328
+ .model-select-option .model-label {
329
+ flex: 1;
330
+ font-weight: 500;
331
+ color: var(--text-primary);
332
+ }
333
+
334
+ .model-select-option .model-provider {
335
+ font-size: 11px;
336
+ font-weight: 500;
337
+ color: var(--text-tertiary);
338
+ background: var(--bg-tertiary);
339
+ padding: 2px 8px;
340
+ border-radius: var(--radius-full);
341
+ }
342
+
343
+ /* Switch Toggle - Modern & Smooth */
344
+ .switch-wrapper {
345
+ display: flex;
346
+ align-items: center;
347
+ gap: var(--space-md);
348
+ cursor: pointer;
349
+ user-select: none;
350
+ transition: opacity var(--transition-base);
351
+ }
352
+
353
+ .switch-wrapper:hover {
354
+ opacity: 0.9;
355
+ }
356
+
357
+ .switch {
358
+ position: relative;
359
+ display: inline-block;
360
+ width: 48px;
361
+ height: 26px;
362
+ }
363
+
364
+ .switch input {
365
+ opacity: 0;
366
+ width: 0;
367
+ height: 0;
368
+ }
369
+
370
+ .switch-slider {
371
+ position: absolute;
372
+ cursor: pointer;
373
+ top: 0;
374
+ left: 0;
375
+ right: 0;
376
+ bottom: 0;
377
+ background: var(--bg-tertiary);
378
+ border: 1px solid var(--border-primary);
379
+ transition: all var(--transition-base);
380
+ border-radius: var(--radius-full);
381
+ }
382
+
383
+ .switch-slider:before {
384
+ position: absolute;
385
+ content: "";
386
+ height: 18px;
387
+ width: 18px;
388
+ left: 3px;
389
+ bottom: 3px;
390
+ background: var(--text-tertiary);
391
+ transition: all var(--transition-base);
392
+ border-radius: var(--radius-full);
393
+ box-shadow: var(--shadow-sm);
394
+ }
395
+
396
+ .switch input:checked + .switch-slider {
397
+ background: var(--accent-primary);
398
+ border-color: var(--accent-primary);
399
+ box-shadow: 0 0 12px var(--accent-glow);
400
+ }
401
+
402
+ .switch input:checked + .switch-slider:before {
403
+ transform: translateX(22px);
404
+ background: #ffffff;
405
+ }
406
+
407
+ .switch-label {
408
+ font-size: 13px;
409
+ font-weight: 500;
410
+ color: var(--text-primary);
411
+ }
412
+
413
+ /* Status Indicators */
414
+ .status-dot {
415
+ width: 10px;
416
+ height: 10px;
417
+ border-radius: var(--radius-full);
418
+ flex-shrink: 0;
419
+ box-shadow: 0 0 8px currentColor;
420
+ }
421
+
422
+ .status-dot.online {
423
+ background: var(--success);
424
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
425
+ }
426
+
427
+ .status-dot.offline {
428
+ background: var(--error);
429
+ }
430
+
431
+ .status-dot.busy {
432
+ background: var(--warning);
433
+ animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
434
+ }
435
+
436
+ @keyframes pulse {
437
+ 0%, 100% {
438
+ opacity: 1;
439
+ }
440
+ 50% {
441
+ opacity: 0.5;
442
+ }
443
+ }
444
+
445
+ /* Chat Area - Claude Style */
446
+ #chatArea {
447
+ flex: 1;
448
+ overflow-y: auto;
449
+ padding: var(--space-xl);
450
+ display: flex;
451
+ flex-direction: column;
452
+ scroll-behavior: smooth;
453
+ }
454
+
455
+ #messages {
456
+ flex: 1;
457
+ display: flex;
458
+ flex-direction: column;
459
+ gap: var(--space-lg);
460
+ max-width: 900px;
461
+ margin: 0 auto;
462
+ width: 100%;
463
+ }
464
+
465
+ /* Messages - Refined Design */
466
+ .msg {
467
+ padding: var(--space-lg) var(--space-xl);
468
+ border-radius: var(--radius-lg);
469
+ max-width: 95%;
470
+ word-wrap: break-word;
471
+ transition: all var(--transition-base);
472
+ animation: messageSlideIn var(--transition-slow);
473
+ }
474
+
475
+ @keyframes messageSlideIn {
476
+ from {
477
+ opacity: 0;
478
+ transform: translateY(12px);
479
+ }
480
+ to {
481
+ opacity: 1;
482
+ transform: translateY(0);
483
+ }
484
+ }
485
+
486
+ .msg.user {
487
+ background: linear-gradient(135deg, var(--bg-elevated) 0%, var(--bg-tertiary) 100%);
488
+ align-self: flex-end;
489
+ border: 1px solid var(--border-primary);
490
+ box-shadow: var(--shadow-sm);
491
+ }
492
+
493
+ .msg.assistant {
494
+ background: var(--bg-secondary);
495
+ align-self: flex-start;
496
+ border: 1px solid var(--border-primary);
497
+ width: 100%;
498
+ box-shadow: var(--shadow-md);
499
+ }
500
+
501
+ .msg .role {
502
+ font-size: 11px;
503
+ font-weight: 600;
504
+ color: var(--text-tertiary);
505
+ margin-bottom: 6px;
506
+ text-transform: uppercase;
507
+ letter-spacing: 0.05em;
508
+ }
509
+
510
+ .msg .content {
511
+ font-size: 14px;
512
+ line-height: 1.7;
513
+ color: var(--text-primary);
514
+ }
515
+
516
+ .msg .content p {
517
+ margin: 0.6em 0;
518
+ }
519
+
520
+ .msg .content pre {
521
+ background: var(--bg-primary);
522
+ border: 1px solid var(--border-primary);
523
+ border-radius: var(--radius-md);
524
+ padding: var(--space-lg);
525
+ overflow-x: auto;
526
+ margin: var(--space-md) 0;
527
+ box-shadow: var(--shadow-sm);
528
+ }
529
+
530
+ .msg .content code {
531
+ font-family: var(--font-mono);
532
+ font-size: 13px;
533
+ line-height: 1.6;
534
+ }
535
+
536
+ .msg .content :not(pre) > code {
537
+ background: var(--bg-tertiary);
538
+ color: var(--accent-light);
539
+ padding: 2px 8px;
540
+ border-radius: var(--radius-sm);
541
+ border: 1px solid var(--border-subtle);
542
+ font-size: 12px;
543
+ }
544
+
545
+ .msg .content ul,
546
+ .msg .content ol {
547
+ padding-left: 1.8em;
548
+ margin: 0.6em 0;
549
+ }
550
+
551
+ .msg .content li {
552
+ margin: 0.4em 0;
553
+ }
554
+
555
+ .msg .content table {
556
+ border-collapse: collapse;
557
+ margin: var(--space-md) 0;
558
+ width: 100%;
559
+ border: 1px solid var(--border-primary);
560
+ border-radius: var(--radius-md);
561
+ overflow: hidden;
562
+ }
563
+
564
+ .msg .content th,
565
+ .msg .content td {
566
+ border: 1px solid var(--border-primary);
567
+ padding: var(--space-sm) var(--space-md);
568
+ text-align: left;
569
+ }
570
+
571
+ .msg .content th {
572
+ background: var(--bg-tertiary);
573
+ font-weight: 600;
574
+ color: var(--text-primary);
575
+ }
576
+
577
+ .msg .content blockquote {
578
+ border-left: 3px solid var(--accent-primary);
579
+ padding-left: var(--space-lg);
580
+ color: var(--text-secondary);
581
+ margin: var(--space-md) 0;
582
+ font-style: italic;
583
+ }
584
+ /* Tool Cards - Premium Category Design */
585
+ .tool-card {
586
+ background: var(--bg-secondary);
587
+ border: 1px solid var(--border-primary);
588
+ border-radius: var(--radius-lg);
589
+ margin: var(--space-md) 0;
590
+ overflow: hidden;
591
+ border-left: 3px solid var(--text-tertiary);
592
+ transition: all var(--transition-base);
593
+ box-shadow: var(--shadow-sm);
594
+ }
595
+
596
+ .tool-card:hover {
597
+ border-left-width: 4px;
598
+ box-shadow: var(--shadow-md);
599
+ }
600
+
601
+ .tool-card[data-cat="bash"] {
602
+ border-left-color: var(--success);
603
+ }
604
+
605
+ .tool-card[data-cat="edit"] {
606
+ border-left-color: var(--warning);
607
+ }
608
+
609
+ .tool-card[data-cat="search"] {
610
+ border-left-color: var(--info);
611
+ }
612
+
613
+ .tool-card[data-cat="todo"],
614
+ .tool-card[data-cat="task"] {
615
+ border-left-color: #a78bfa;
616
+ }
617
+
618
+ .tool-card[data-cat="question"] {
619
+ border-left-color: var(--accent-primary);
620
+ }
621
+
622
+ .tool-card[data-cat="agent"] {
623
+ border-left-color: #c084fc;
624
+ }
625
+
626
+ .tool-header {
627
+ display: flex;
628
+ align-items: center;
629
+ gap: var(--space-md);
630
+ padding: var(--space-md) var(--space-lg);
631
+ background: var(--bg-tertiary);
632
+ cursor: pointer;
633
+ font-size: 13px;
634
+ font-weight: 600;
635
+ transition: all var(--transition-fast);
636
+ }
637
+
638
+ .tool-header:hover {
639
+ background: var(--bg-elevated);
640
+ }
641
+
642
+ .tool-header .tool-icon {
643
+ font-size: 16px;
644
+ opacity: 0.9;
645
+ }
646
+
647
+ .tool-header .tool-name {
648
+ color: var(--success);
649
+ font-weight: 600;
650
+ }
651
+
652
+ .tool-header .tool-summary {
653
+ color: var(--text-secondary);
654
+ font-weight: 400;
655
+ font-size: 12px;
656
+ margin-left: 6px;
657
+ overflow: hidden;
658
+ text-overflow: ellipsis;
659
+ white-space: nowrap;
660
+ flex: 1;
661
+ }
662
+
663
+ .tool-header .chevron {
664
+ color: var(--text-tertiary);
665
+ font-size: 12px;
666
+ margin-left: auto;
667
+ transition: transform var(--transition-base);
668
+ }
669
+
670
+ .tool-card.open .chevron {
671
+ transform: rotate(180deg);
672
+ }
673
+
674
+ .tool-body {
675
+ padding: var(--space-lg);
676
+ font-size: 13px;
677
+ display: none;
678
+ line-height: 1.6;
679
+ color: var(--text-secondary);
680
+ }
681
+
682
+ .tool-card.open .tool-body {
683
+ display: block;
684
+ animation: expandDown var(--transition-base);
685
+ }
686
+
687
+ @keyframes expandDown {
688
+ from {
689
+ opacity: 0;
690
+ max-height: 0;
691
+ }
692
+ to {
693
+ opacity: 1;
694
+ max-height: 1000px;
695
+ }
696
+ }
697
+
698
+ .tool-body pre {
699
+ margin: 0;
700
+ white-space: pre-wrap;
701
+ font-family: var(--font-mono);
702
+ background: var(--bg-primary);
703
+ padding: var(--space-md);
704
+ border-radius: var(--radius-md);
705
+ border: 1px solid var(--border-subtle);
706
+ }
707
+
708
+ .tool-result {
709
+ border-top: 1px solid var(--border-primary);
710
+ padding: var(--space-md) var(--space-lg);
711
+ font-size: 12px;
712
+ color: var(--text-tertiary);
713
+ background: var(--bg-primary);
714
+ }
715
+
716
+ .tool-result.error {
717
+ color: var(--error);
718
+ background: var(--error-bg);
719
+ }
720
+
721
+ /* Diff Display - Enhanced Readability */
722
+ .diff-view {
723
+ font-family: var(--font-mono);
724
+ font-size: 12px;
725
+ line-height: 1.6;
726
+ background: var(--bg-primary);
727
+ border-radius: var(--radius-md);
728
+ overflow: hidden;
729
+ }
730
+
731
+ .diff-file {
732
+ color: var(--text-secondary);
733
+ margin-bottom: var(--space-md);
734
+ font-weight: 600;
735
+ padding: var(--space-sm) var(--space-md);
736
+ background: var(--bg-tertiary);
737
+ border-left: 3px solid var(--accent-primary);
738
+ }
739
+
740
+ .diff-line {
741
+ padding: 4px var(--space-md);
742
+ white-space: pre-wrap;
743
+ word-break: break-all;
744
+ transition: background var(--transition-fast);
745
+ }
746
+
747
+ .diff-line.removed {
748
+ background: rgba(239, 68, 68, 0.15);
749
+ color: #fca5a5;
750
+ border-left: 2px solid var(--error);
751
+ }
752
+
753
+ .diff-line.removed::before {
754
+ content: '- ';
755
+ color: var(--error);
756
+ font-weight: bold;
757
+ margin-right: 4px;
758
+ }
759
+
760
+ .diff-line.added {
761
+ background: rgba(16, 185, 129, 0.15);
762
+ color: #6ee7b7;
763
+ border-left: 2px solid var(--success);
764
+ }
765
+
766
+ .diff-line.added::before {
767
+ content: '+ ';
768
+ color: var(--success);
769
+ font-weight: bold;
770
+ margin-right: 4px;
771
+ }
772
+
773
+ .diff-line.context {
774
+ color: var(--text-tertiary);
775
+ }
776
+
777
+ .diff-line.context::before {
778
+ content: ' ';
779
+ }
780
+
781
+ /* AskUserQuestion Panel - Interactive & Modern */
782
+ .ask-panel {
783
+ background: var(--bg-secondary);
784
+ border: 1px solid var(--accent-primary);
785
+ border-radius: var(--radius-lg);
786
+ padding: var(--space-xl);
787
+ margin: var(--space-lg) 0;
788
+ box-shadow: 0 0 24px var(--accent-glow);
789
+ animation: panelSlideIn var(--transition-slow);
790
+ }
791
+
792
+ @keyframes panelSlideIn {
793
+ from {
794
+ opacity: 0;
795
+ transform: scale(0.98);
796
+ }
797
+ to {
798
+ opacity: 1;
799
+ transform: scale(1);
800
+ }
801
+ }
802
+
803
+ .ask-panel .ask-q {
804
+ font-size: 16px;
805
+ font-weight: 600;
806
+ margin-bottom: var(--space-lg);
807
+ color: var(--text-primary);
808
+ line-height: 1.5;
809
+ }
810
+
811
+ .ask-panel .ask-header {
812
+ font-size: 11px;
813
+ color: var(--accent-primary);
814
+ text-transform: uppercase;
815
+ font-weight: 700;
816
+ letter-spacing: 0.05em;
817
+ margin-bottom: 6px;
818
+ }
819
+
820
+ .ask-opts {
821
+ display: flex;
822
+ flex-direction: column;
823
+ gap: var(--space-sm);
824
+ }
825
+
826
+ .ask-opt {
827
+ display: flex;
828
+ align-items: flex-start;
829
+ gap: var(--space-md);
830
+ padding: var(--space-md) var(--space-lg);
831
+ background: var(--bg-tertiary);
832
+ border: 2px solid transparent;
833
+ border-radius: var(--radius-md);
834
+ cursor: pointer;
835
+ transition: all var(--transition-base);
836
+ }
837
+
838
+ .ask-opt:hover {
839
+ border-color: var(--accent-primary);
840
+ background: var(--bg-elevated);
841
+ transform: translateX(4px);
842
+ }
843
+
844
+ .ask-opt.selected {
845
+ border-color: var(--accent-primary);
846
+ background: var(--accent-subtle);
847
+ box-shadow: 0 0 16px var(--accent-glow);
848
+ }
849
+
850
+ .ask-opt .opt-radio {
851
+ width: 20px;
852
+ height: 20px;
853
+ border-radius: var(--radius-full);
854
+ border: 2px solid var(--text-tertiary);
855
+ flex-shrink: 0;
856
+ margin-top: 2px;
857
+ display: flex;
858
+ align-items: center;
859
+ justify-content: center;
860
+ transition: all var(--transition-base);
861
+ }
862
+
863
+ .ask-opt.selected .opt-radio {
864
+ border-color: var(--accent-primary);
865
+ background: var(--accent-primary);
866
+ }
867
+
868
+ .ask-opt.selected .opt-radio::after {
869
+ content: '';
870
+ width: 10px;
871
+ height: 10px;
872
+ border-radius: var(--radius-full);
873
+ background: #ffffff;
874
+ }
875
+
876
+ .ask-opt.multi .opt-radio {
877
+ border-radius: var(--radius-sm);
878
+ }
879
+
880
+ .ask-opt.multi.selected .opt-radio::after {
881
+ border-radius: 2px;
882
+ }
883
+
884
+ .ask-opt .opt-label {
885
+ font-size: 14px;
886
+ font-weight: 600;
887
+ color: var(--text-primary);
888
+ }
889
+
890
+ .ask-opt .opt-desc {
891
+ font-size: 12px;
892
+ color: var(--text-secondary);
893
+ margin-top: 4px;
894
+ line-height: 1.5;
895
+ }
896
+
897
+ .ask-other {
898
+ margin-top: var(--space-md);
899
+ }
900
+
901
+ .ask-other input {
902
+ width: 100%;
903
+ background: var(--bg-tertiary);
904
+ color: var(--text-primary);
905
+ border: 1px solid var(--border-primary);
906
+ border-radius: var(--radius-md);
907
+ padding: var(--space-md);
908
+ font-size: 14px;
909
+ font-family: var(--font-sans);
910
+ outline: none;
911
+ transition: all var(--transition-base);
912
+ }
913
+
914
+ .ask-other input:focus {
915
+ border-color: var(--accent-primary);
916
+ box-shadow: 0 0 0 3px var(--accent-subtle);
917
+ }
918
+
919
+ .ask-btns {
920
+ display: flex;
921
+ gap: var(--space-md);
922
+ margin-top: var(--space-lg);
923
+ }
924
+
925
+ .ask-answered {
926
+ background: var(--bg-secondary);
927
+ border: 1px solid var(--border-primary);
928
+ border-radius: var(--radius-lg);
929
+ padding: var(--space-lg);
930
+ margin: var(--space-md) 0;
931
+ }
932
+
933
+ .ask-answered .ans-q {
934
+ font-size: 13px;
935
+ color: var(--text-secondary);
936
+ margin-bottom: 6px;
937
+ }
938
+
939
+ .ask-answered .ans-v {
940
+ display: inline-block;
941
+ background: var(--accent-subtle);
942
+ color: var(--accent-primary);
943
+ padding: 4px 12px;
944
+ border-radius: var(--radius-full);
945
+ font-size: 13px;
946
+ font-weight: 500;
947
+ margin: 4px 6px 4px 0;
948
+ border: 1px solid var(--accent-primary);
949
+ }
950
+
951
+ /* Todo List - Modern Design */
952
+ .todo-list {
953
+ margin: var(--space-md) 0;
954
+ }
955
+
956
+ .todo-item {
957
+ display: flex;
958
+ align-items: center;
959
+ gap: var(--space-md);
960
+ padding: var(--space-sm) 0;
961
+ font-size: 13px;
962
+ border-bottom: 1px solid var(--border-subtle);
963
+ transition: all var(--transition-fast);
964
+ }
965
+
966
+ .todo-item:last-child {
967
+ border-bottom: none;
968
+ }
969
+
970
+ .todo-item:hover {
971
+ padding-left: var(--space-sm);
972
+ }
973
+
974
+ .todo-icon {
975
+ font-size: 18px;
976
+ flex-shrink: 0;
977
+ }
978
+
979
+ .todo-icon.done {
980
+ color: var(--success);
981
+ }
982
+
983
+ .todo-icon.progress {
984
+ color: var(--warning);
985
+ }
986
+
987
+ .todo-icon.pending {
988
+ color: var(--text-tertiary);
989
+ }
990
+
991
+ .todo-text {
992
+ flex: 1;
993
+ color: var(--text-primary);
994
+ }
995
+
996
+ .todo-priority {
997
+ font-size: 11px;
998
+ font-weight: 600;
999
+ padding: 2px 8px;
1000
+ border-radius: var(--radius-full);
1001
+ }
1002
+
1003
+ .todo-priority.high {
1004
+ background: var(--error-bg);
1005
+ color: var(--error);
1006
+ }
1007
+
1008
+ .todo-priority.medium {
1009
+ background: var(--warning-bg);
1010
+ color: var(--warning);
1011
+ }
1012
+
1013
+ .todo-priority.low {
1014
+ background: var(--bg-tertiary);
1015
+ color: var(--text-tertiary);
1016
+ }
1017
+
1018
+ /* Task List - Progress Tracking */
1019
+ .task-list {
1020
+ margin: var(--space-md) 0;
1021
+ }
1022
+
1023
+ .task-progress {
1024
+ height: 8px;
1025
+ background: var(--bg-tertiary);
1026
+ border-radius: var(--radius-full);
1027
+ margin-bottom: var(--space-md);
1028
+ overflow: hidden;
1029
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
1030
+ }
1031
+
1032
+ .task-progress-bar {
1033
+ height: 100%;
1034
+ background: linear-gradient(90deg, var(--success) 0%, var(--accent-primary) 100%);
1035
+ border-radius: var(--radius-full);
1036
+ transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1037
+ box-shadow: 0 0 12px var(--success-glow);
1038
+ }
1039
+
1040
+ .task-item {
1041
+ display: flex;
1042
+ align-items: center;
1043
+ gap: var(--space-md);
1044
+ padding: 6px 0;
1045
+ font-size: 13px;
1046
+ }
1047
+
1048
+ .task-status {
1049
+ font-size: 11px;
1050
+ font-weight: 600;
1051
+ padding: 2px 10px;
1052
+ border-radius: var(--radius-full);
1053
+ }
1054
+
1055
+ .task-status.completed {
1056
+ background: var(--success-bg);
1057
+ color: var(--success);
1058
+ }
1059
+
1060
+ .task-status.in_progress {
1061
+ background: var(--warning-bg);
1062
+ color: var(--warning);
1063
+ }
1064
+
1065
+ .task-status.pending {
1066
+ background: var(--bg-tertiary);
1067
+ color: var(--text-tertiary);
1068
+ }
1069
+
1070
+ /* Permission Banner - Alert Style */
1071
+ #permBanner {
1072
+ position: sticky;
1073
+ bottom: 0;
1074
+ background: var(--bg-elevated);
1075
+ border: 1px solid var(--warning);
1076
+ border-radius: var(--radius-lg);
1077
+ padding: var(--space-lg) var(--space-xl);
1078
+ margin-top: var(--space-md);
1079
+ box-shadow: 0 0 24px rgba(245, 158, 11, 0.2);
1080
+ backdrop-filter: blur(12px);
1081
+ }
1082
+
1083
+ #permBanner.hidden {
1084
+ display: none;
1085
+ }
1086
+
1087
+ .perm-title {
1088
+ font-weight: 700;
1089
+ font-size: 15px;
1090
+ color: var(--warning);
1091
+ margin-bottom: var(--space-sm);
1092
+ }
1093
+
1094
+ .perm-tool {
1095
+ font-size: 13px;
1096
+ color: var(--text-primary);
1097
+ margin-bottom: 6px;
1098
+ font-weight: 500;
1099
+ }
1100
+
1101
+ .perm-rule {
1102
+ font-size: 12px;
1103
+ color: var(--text-secondary);
1104
+ font-family: var(--font-mono);
1105
+ background: var(--bg-tertiary);
1106
+ padding: 6px 10px;
1107
+ border-radius: var(--radius-sm);
1108
+ display: inline-block;
1109
+ margin: 4px 0;
1110
+ border: 1px solid var(--border-subtle);
1111
+ }
1112
+
1113
+ .perm-input-detail {
1114
+ font-size: 12px;
1115
+ color: var(--text-secondary);
1116
+ background: var(--bg-primary);
1117
+ padding: var(--space-md);
1118
+ border-radius: var(--radius-md);
1119
+ margin: var(--space-sm) 0;
1120
+ max-height: 140px;
1121
+ overflow-y: auto;
1122
+ white-space: pre-wrap;
1123
+ font-family: var(--font-mono);
1124
+ border: 1px solid var(--border-primary);
1125
+ }
1126
+
1127
+ .perm-btns {
1128
+ display: flex;
1129
+ gap: var(--space-md);
1130
+ margin-top: var(--space-md);
1131
+ flex-wrap: wrap;
1132
+ }
1133
+
1134
+ .perm-btns button {
1135
+ padding: var(--space-sm) var(--space-xl);
1136
+ border: none;
1137
+ border-radius: var(--radius-md);
1138
+ cursor: pointer;
1139
+ font-size: 13px;
1140
+ font-weight: 600;
1141
+ transition: all var(--transition-base);
1142
+ }
1143
+
1144
+ .btn-allow {
1145
+ background: var(--success);
1146
+ color: #000000;
1147
+ }
1148
+
1149
+ .btn-allow:hover {
1150
+ background: #059669;
1151
+ box-shadow: 0 0 16px var(--success-glow);
1152
+ }
1153
+
1154
+ .btn-allow-remember {
1155
+ background: #047857;
1156
+ color: #ffffff;
1157
+ }
1158
+
1159
+ .btn-allow-remember:hover {
1160
+ background: #065f46;
1161
+ }
1162
+
1163
+ .btn-deny {
1164
+ background: var(--error);
1165
+ color: #ffffff;
1166
+ }
1167
+
1168
+ .btn-deny:hover {
1169
+ background: #dc2626;
1170
+ }
1171
+
1172
+ /* Input area */
1173
+ #inputArea { padding:12px 16px; background:var(--bg-secondary); border-top:1px solid var(--border-primary); position:relative; }
1174
+ .cmd-dropdown { display:none; position:absolute; bottom:100%; left:16px; right:16px; background:var(--bg-elevated); border:1px solid var(--border-primary); border-radius:var(--radius-lg); max-height:240px; overflow-y:auto; z-index:50; box-shadow:var(--shadow-xl); }
1175
+ .cmd-dropdown.visible { display:block; }
1176
+ .cmd-dropdown-item { display:flex; align-items:center; gap:10px; padding:8px 12px; cursor:pointer; font-size:13px; }
1177
+ .cmd-dropdown-item:hover, .cmd-dropdown-item.active { background:var(--bg-hover); }
1178
+ .cmd-dropdown-item .cmd-icon { font-size:14px; width:20px; text-align:center; flex-shrink:0; color:var(--text-secondary); }
1179
+ .cmd-dropdown-item .cmd-label { font-weight:600; color:var(--text-primary); }
1180
+ .cmd-dropdown-item .cmd-desc { color:var(--text-tertiary); font-size:12px; margin-left:auto; }
1181
+ .input-row { display:flex; gap:8px; }
1182
+ #promptInput { flex:1; background:var(--bg-tertiary); color:var(--text-primary); border:1px solid var(--border-primary); border-radius:var(--radius-lg); padding:10px 12px; font-size:14px; resize:none; font-family:inherit; outline:none; }
1183
+ #promptInput:focus { border-color:var(--accent-primary); }
1184
+ /* Buttons - Claude Style Premium */
1185
+ button {
1186
+ padding: var(--space-md) var(--space-xl);
1187
+ border: none;
1188
+ border-radius: var(--radius-md);
1189
+ cursor: pointer;
1190
+ font-size: 13px;
1191
+ font-weight: 600;
1192
+ font-family: var(--font-sans);
1193
+ white-space: nowrap;
1194
+ transition: all var(--transition-base);
1195
+ outline: none;
1196
+ position: relative;
1197
+ overflow: hidden;
1198
+ }
1199
+
1200
+ button::before {
1201
+ content: '';
1202
+ position: absolute;
1203
+ top: 50%;
1204
+ left: 50%;
1205
+ width: 0;
1206
+ height: 0;
1207
+ border-radius: var(--radius-full);
1208
+ background: rgba(255, 255, 255, 0.1);
1209
+ transform: translate(-50%, -50%);
1210
+ transition: width 0.6s, height 0.6s;
1211
+ }
1212
+
1213
+ button:active::before {
1214
+ width: 300px;
1215
+ height: 300px;
1216
+ }
1217
+
1218
+ .btn-primary {
1219
+ background: var(--accent-primary);
1220
+ color: #ffffff;
1221
+ box-shadow: 0 0 16px var(--accent-glow);
1222
+ }
1223
+
1224
+ .btn-primary:hover {
1225
+ background: var(--accent-hover);
1226
+ box-shadow: 0 0 24px var(--accent-glow);
1227
+ transform: translateY(-1px);
1228
+ }
1229
+
1230
+ .btn-primary:active {
1231
+ transform: translateY(0);
1232
+ }
1233
+
1234
+ .btn-primary:disabled {
1235
+ opacity: 0.5;
1236
+ cursor: not-allowed;
1237
+ transform: none;
1238
+ box-shadow: none;
1239
+ }
1240
+
1241
+ .btn-danger {
1242
+ background: var(--error);
1243
+ color: #ffffff;
1244
+ box-shadow: 0 0 12px rgba(239, 68, 68, 0.3);
1245
+ }
1246
+
1247
+ .btn-danger:hover {
1248
+ background: #dc2626;
1249
+ box-shadow: 0 0 20px rgba(239, 68, 68, 0.4);
1250
+ }
1251
+
1252
+ .btn-secondary {
1253
+ background: var(--bg-tertiary);
1254
+ color: var(--text-primary);
1255
+ border: 1px solid var(--border-primary);
1256
+ }
1257
+
1258
+ .btn-secondary:hover {
1259
+ background: var(--bg-elevated);
1260
+ border-color: var(--border-secondary);
1261
+ }
1262
+
1263
+ .btn-icon {
1264
+ background: none;
1265
+ border: none;
1266
+ color: var(--text-secondary);
1267
+ cursor: pointer;
1268
+ font-size: 18px;
1269
+ padding: var(--space-sm);
1270
+ border-radius: var(--radius-md);
1271
+ transition: all var(--transition-fast);
1272
+ }
1273
+
1274
+ .btn-icon:hover {
1275
+ background: var(--bg-tertiary);
1276
+ color: var(--text-primary);
1277
+ }
1278
+
1279
+ /* Input Area - Modern & Clean */
1280
+ #inputArea {
1281
+ padding: var(--space-lg) var(--space-xl);
1282
+ background: var(--bg-secondary);
1283
+ border-top: 1px solid var(--border-primary);
1284
+ position: relative;
1285
+ backdrop-filter: blur(12px);
1286
+ }
1287
+
1288
+ .input-row {
1289
+ display: flex;
1290
+ gap: var(--space-md);
1291
+ align-items: flex-end;
1292
+ }
1293
+
1294
+ #promptInput {
1295
+ flex: 1;
1296
+ background: var(--bg-tertiary);
1297
+ color: var(--text-primary);
1298
+ border: 1px solid var(--border-primary);
1299
+ border-radius: var(--radius-lg);
1300
+ padding: var(--space-md) var(--space-lg);
1301
+ font-size: 14px;
1302
+ font-family: var(--font-sans);
1303
+ resize: none;
1304
+ outline: none;
1305
+ min-height: 48px;
1306
+ max-height: 200px;
1307
+ line-height: 1.6;
1308
+ transition: all var(--transition-base);
1309
+ }
1310
+
1311
+ #promptInput:hover {
1312
+ border-color: var(--border-secondary);
1313
+ background: var(--bg-elevated);
1314
+ }
1315
+
1316
+ #promptInput:focus {
1317
+ border-color: var(--accent-primary);
1318
+ background: var(--bg-elevated);
1319
+ box-shadow: 0 0 0 3px var(--accent-subtle);
1320
+ }
1321
+
1322
+ #promptInput::placeholder {
1323
+ color: var(--text-tertiary);
1324
+ }
1325
+
1326
+ .btn-col {
1327
+ display: flex;
1328
+ flex-direction: column;
1329
+ gap: var(--space-sm);
1330
+ }
1331
+
1332
+ .input-footer {
1333
+ display: flex;
1334
+ justify-content: space-between;
1335
+ align-items: center;
1336
+ margin-top: var(--space-sm);
1337
+ font-size: 12px;
1338
+ color: var(--text-tertiary);
1339
+ }
1340
+
1341
+ .streaming-dot::after {
1342
+ content: '●';
1343
+ animation: blink 1.2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
1344
+ color: var(--accent-primary);
1345
+ margin-left: 6px;
1346
+ }
1347
+
1348
+ @keyframes blink {
1349
+ 0%, 100% {
1350
+ opacity: 1;
1351
+ }
1352
+ 50% {
1353
+ opacity: 0.3;
1354
+ }
1355
+ }
1356
+
1357
+ .hidden {
1358
+ display: none !important;
1359
+ }
1360
+
1361
+ /* Loading indicator */
1362
+ .loading-indicator { display:flex; align-items:center; gap:8px; padding:12px 16px; color:var(--text-secondary); font-size:13px; }
1363
+ .loading-dots { display:inline-flex; gap:4px; }
1364
+ .loading-dots span { width:6px; height:6px; border-radius:50%; background:var(--accent-primary); animation:loadBounce .6s infinite alternate; }
1365
+ .loading-dots span:nth-child(2) { animation-delay:.2s; }
1366
+ .loading-dots span:nth-child(3) { animation-delay:.4s; }
1367
+ @keyframes loadBounce { 0%{opacity:.3;transform:translateY(0)} 100%{opacity:1;transform:translateY(-4px)} }
1368
+ ::-webkit-scrollbar { width:6px; }
1369
+ ::-webkit-scrollbar-track { background:var(--bg-primary); }
1370
+ ::-webkit-scrollbar-thumb { background:var(--border-primary); border-radius:3px; }
1371
+
1372
+ /* Settings */
1373
+ /* Settings Panel - Modal Design */
1374
+ .settings-overlay {
1375
+ position: fixed;
1376
+ inset: 0;
1377
+ background: rgba(0, 0, 0, 0.75);
1378
+ backdrop-filter: blur(8px);
1379
+ z-index: 100;
1380
+ display: flex;
1381
+ align-items: center;
1382
+ justify-content: center;
1383
+ animation: fadeIn var(--transition-base);
1384
+ }
1385
+
1386
+ @keyframes fadeIn {
1387
+ from {
1388
+ opacity: 0;
1389
+ }
1390
+ to {
1391
+ opacity: 1;
1392
+ }
1393
+ }
1394
+
1395
+ .settings-overlay.hidden {
1396
+ display: none;
1397
+ }
1398
+
1399
+ .settings-panel {
1400
+ background: var(--bg-elevated);
1401
+ border: 1px solid var(--border-primary);
1402
+ border-radius: var(--radius-xl);
1403
+ padding: var(--space-2xl);
1404
+ width: 560px;
1405
+ max-width: 90vw;
1406
+ box-shadow: var(--shadow-xl);
1407
+ animation: slideUp var(--transition-slow);
1408
+ }
1409
+
1410
+ .settings-panel h2 {
1411
+ font-size: 20px;
1412
+ font-weight: 700;
1413
+ margin-bottom: var(--space-xl);
1414
+ color: var(--accent-primary);
1415
+ letter-spacing: -0.02em;
1416
+ }
1417
+
1418
+ .sg {
1419
+ margin-bottom: var(--space-lg);
1420
+ }
1421
+
1422
+ .sg label {
1423
+ display: block;
1424
+ font-size: 13px;
1425
+ font-weight: 600;
1426
+ color: var(--text-secondary);
1427
+ margin-bottom: var(--space-sm);
1428
+ }
1429
+
1430
+ .sg .hint {
1431
+ font-size: 11px;
1432
+ color: var(--text-tertiary);
1433
+ margin-top: 6px;
1434
+ line-height: 1.5;
1435
+ }
1436
+
1437
+ .sg input,
1438
+ .sg select {
1439
+ width: 100%;
1440
+ background: var(--bg-tertiary);
1441
+ color: var(--text-primary);
1442
+ border: 1px solid var(--border-primary);
1443
+ border-radius: var(--radius-md);
1444
+ padding: var(--space-md);
1445
+ font-size: 14px;
1446
+ font-family: var(--font-sans);
1447
+ outline: none;
1448
+ transition: all var(--transition-base);
1449
+ }
1450
+
1451
+ .sg input:hover,
1452
+ .sg select:hover {
1453
+ border-color: var(--border-secondary);
1454
+ background: var(--bg-elevated);
1455
+ }
1456
+
1457
+ .sg input:focus,
1458
+ .sg select:focus {
1459
+ border-color: var(--accent-primary);
1460
+ background: var(--bg-elevated);
1461
+ box-shadow: 0 0 0 3px var(--accent-subtle);
1462
+ }
1463
+
1464
+ .settings-footer {
1465
+ display: flex;
1466
+ justify-content: flex-end;
1467
+ gap: var(--space-md);
1468
+ margin-top: var(--space-2xl);
1469
+ padding-top: var(--space-xl);
1470
+ border-top: 1px solid var(--border-primary);
1471
+ }
1472
+
1473
+ .btn-settings {
1474
+ background: var(--bg-tertiary);
1475
+ color: var(--text-primary);
1476
+ border: 1px solid var(--border-primary);
1477
+ border-radius: var(--radius-md);
1478
+ padding: var(--space-sm) var(--space-lg);
1479
+ cursor: pointer;
1480
+ font-size: 13px;
1481
+ font-weight: 600;
1482
+ transition: all var(--transition-base);
1483
+ }
1484
+
1485
+ .btn-settings:hover {
1486
+ background: var(--bg-elevated);
1487
+ border-color: var(--border-secondary);
1488
+ }
1489
+
1490
+ .settings-saved {
1491
+ color: var(--success);
1492
+ font-size: 13px;
1493
+ font-weight: 600;
1494
+ margin-right: auto;
1495
+ align-self: center;
1496
+ opacity: 0;
1497
+ transition: opacity var(--transition-slow);
1498
+ }
1499
+
1500
+ .settings-saved.show {
1501
+ opacity: 1;
1502
+ }
1503
+
1504
+ /* Sidebar - Sleek Navigation */
1505
+ #sidebar {
1506
+ width: 260px;
1507
+ background: var(--bg-secondary);
1508
+ border-right: 1px solid var(--border-primary);
1509
+ display: flex;
1510
+ flex-direction: column;
1511
+ overflow: hidden;
1512
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1513
+ flex-shrink: 0;
1514
+ }
1515
+
1516
+ #sidebar.collapsed {
1517
+ width: 0;
1518
+ border-right: none;
1519
+ }
1520
+
1521
+ .sidebar-header {
1522
+ display: flex;
1523
+ align-items: center;
1524
+ justify-content: space-between;
1525
+ padding: var(--space-md) var(--space-lg);
1526
+ border-bottom: 1px solid var(--border-primary);
1527
+ background: var(--bg-tertiary);
1528
+ }
1529
+
1530
+ .sidebar-header span {
1531
+ font-size: 14px;
1532
+ font-weight: 700;
1533
+ color: var(--text-primary);
1534
+ letter-spacing: -0.01em;
1535
+ }
1536
+
1537
+ .sidebar-body {
1538
+ flex: 1;
1539
+ overflow-y: auto;
1540
+ padding: var(--space-sm) 0;
1541
+ }
1542
+
1543
+ .project-group {
1544
+ margin-bottom: var(--space-sm);
1545
+ }
1546
+
1547
+ .project-name {
1548
+ font-size: 11px;
1549
+ font-weight: 700;
1550
+ color: var(--text-tertiary);
1551
+ padding: var(--space-md) var(--space-lg) var(--space-xs);
1552
+ letter-spacing: 0.05em;
1553
+ text-transform: uppercase;
1554
+ }
1555
+
1556
+ .session-item {
1557
+ display: flex;
1558
+ flex-direction: column;
1559
+ padding: var(--space-md) var(--space-lg);
1560
+ cursor: pointer;
1561
+ border-left: 3px solid transparent;
1562
+ transition: all var(--transition-fast);
1563
+ margin: 2px 0;
1564
+ }
1565
+
1566
+ .session-item:hover {
1567
+ background: var(--bg-tertiary);
1568
+ border-left-color: var(--text-tertiary);
1569
+ }
1570
+
1571
+ .session-item.active {
1572
+ background: var(--accent-subtle);
1573
+ border-left-color: var(--accent-primary);
1574
+ }
1575
+
1576
+ .session-summary {
1577
+ font-size: 13px;
1578
+ font-weight: 500;
1579
+ color: var(--text-primary);
1580
+ white-space: nowrap;
1581
+ overflow: hidden;
1582
+ text-overflow: ellipsis;
1583
+ }
1584
+
1585
+ .session-meta {
1586
+ font-size: 11px;
1587
+ color: var(--text-tertiary);
1588
+ margin-top: 4px;
1589
+ }
1590
+
1591
+ /* File Panel - File Browser */
1592
+ #filePanel {
1593
+ width: 300px;
1594
+ background: var(--bg-secondary);
1595
+ border-left: 1px solid var(--border-primary);
1596
+ display: flex;
1597
+ flex-direction: column;
1598
+ overflow: hidden;
1599
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1600
+ flex-shrink: 0;
1601
+ }
1602
+
1603
+ #filePanel.collapsed {
1604
+ width: 0;
1605
+ border-left: none;
1606
+ }
1607
+
1608
+ .filepanel-header {
1609
+ display: flex;
1610
+ align-items: center;
1611
+ justify-content: space-between;
1612
+ padding: var(--space-md) var(--space-lg);
1613
+ border-bottom: 1px solid var(--border-primary);
1614
+ background: var(--bg-tertiary);
1615
+ }
1616
+
1617
+ .filepanel-header span {
1618
+ font-size: 14px;
1619
+ font-weight: 700;
1620
+ color: var(--text-primary);
1621
+ letter-spacing: -0.01em;
1622
+ }
1623
+
1624
+ .filepanel-body {
1625
+ flex: 1;
1626
+ overflow-y: auto;
1627
+ padding: var(--space-sm) 0;
1628
+ }
1629
+
1630
+ .tree-item {
1631
+ display: flex;
1632
+ align-items: center;
1633
+ gap: var(--space-sm);
1634
+ padding: 6px var(--space-md);
1635
+ cursor: pointer;
1636
+ font-size: 13px;
1637
+ white-space: nowrap;
1638
+ transition: all var(--transition-fast);
1639
+ border-left: 2px solid transparent;
1640
+ }
1641
+
1642
+ .tree-item:hover {
1643
+ background: var(--bg-tertiary);
1644
+ border-left-color: var(--accent-primary);
1645
+ }
1646
+
1647
+ .tree-icon {
1648
+ font-size: 14px;
1649
+ width: 18px;
1650
+ text-align: center;
1651
+ flex-shrink: 0;
1652
+ opacity: 0.8;
1653
+ }
1654
+
1655
+ .tree-name {
1656
+ overflow: hidden;
1657
+ text-overflow: ellipsis;
1658
+ color: var(--text-primary);
1659
+ }
1660
+
1661
+ .tree-children {
1662
+ display: none;
1663
+ }
1664
+
1665
+ .tree-children.open {
1666
+ display: block;
1667
+ }
1668
+
1669
+ .tree-size {
1670
+ font-size: 11px;
1671
+ color: var(--text-tertiary);
1672
+ margin-left: auto;
1673
+ }
1674
+
1675
+ /* File Viewer Modal - Clean & Focused */
1676
+ .file-viewer-overlay {
1677
+ position: fixed;
1678
+ inset: 0;
1679
+ background: rgba(0, 0, 0, 0.8);
1680
+ backdrop-filter: blur(8px);
1681
+ z-index: 200;
1682
+ display: flex;
1683
+ align-items: center;
1684
+ justify-content: center;
1685
+ animation: fadeIn var(--transition-base);
1686
+ }
1687
+
1688
+ .file-viewer-overlay.hidden {
1689
+ display: none;
1690
+ }
1691
+
1692
+ .file-viewer-panel {
1693
+ background: var(--bg-elevated);
1694
+ border: 1px solid var(--border-primary);
1695
+ border-radius: var(--radius-xl);
1696
+ width: 75vw;
1697
+ max-width: 1000px;
1698
+ max-height: 85vh;
1699
+ display: flex;
1700
+ flex-direction: column;
1701
+ box-shadow: var(--shadow-xl);
1702
+ animation: slideUp var(--transition-slow);
1703
+ }
1704
+
1705
+ .file-viewer-header {
1706
+ display: flex;
1707
+ align-items: center;
1708
+ justify-content: space-between;
1709
+ padding: var(--space-lg) var(--space-xl);
1710
+ border-bottom: 1px solid var(--border-primary);
1711
+ background: var(--bg-tertiary);
1712
+ }
1713
+
1714
+ .file-viewer-header .fv-path {
1715
+ font-size: 13px;
1716
+ font-weight: 500;
1717
+ color: var(--accent-primary);
1718
+ font-family: var(--font-mono);
1719
+ overflow: hidden;
1720
+ text-overflow: ellipsis;
1721
+ white-space: nowrap;
1722
+ }
1723
+
1724
+ .file-viewer-content {
1725
+ flex: 1;
1726
+ overflow: auto;
1727
+ padding: var(--space-xl);
1728
+ background: var(--bg-primary);
1729
+ }
1730
+
1731
+ .file-viewer-content pre {
1732
+ margin: 0;
1733
+ white-space: pre-wrap;
1734
+ font-size: 13px;
1735
+ font-family: var(--font-mono);
1736
+ line-height: 1.6;
1737
+ color: var(--text-primary);
1738
+ }
1739
+
1740
+ /* Folder Browser Modal - Directory Navigation */
1741
+ .folder-overlay {
1742
+ position: fixed;
1743
+ inset: 0;
1744
+ background: rgba(0, 0, 0, 0.75);
1745
+ backdrop-filter: blur(8px);
1746
+ z-index: 150;
1747
+ display: flex;
1748
+ align-items: center;
1749
+ justify-content: center;
1750
+ animation: fadeIn var(--transition-base);
1751
+ }
1752
+
1753
+ .folder-overlay.hidden {
1754
+ display: none;
1755
+ }
1756
+
1757
+ .folder-panel {
1758
+ background: var(--bg-elevated);
1759
+ border: 1px solid var(--border-primary);
1760
+ border-radius: var(--radius-xl);
1761
+ width: 540px;
1762
+ max-width: 90vw;
1763
+ max-height: 75vh;
1764
+ display: flex;
1765
+ flex-direction: column;
1766
+ box-shadow: var(--shadow-xl);
1767
+ animation: slideUp var(--transition-slow);
1768
+ }
1769
+
1770
+ .folder-header {
1771
+ display: flex;
1772
+ align-items: center;
1773
+ justify-content: space-between;
1774
+ padding: var(--space-lg) var(--space-xl);
1775
+ border-bottom: 1px solid var(--border-primary);
1776
+ background: var(--bg-tertiary);
1777
+ }
1778
+
1779
+ .folder-header .folder-path {
1780
+ font-size: 13px;
1781
+ font-weight: 500;
1782
+ color: var(--accent-primary);
1783
+ font-family: var(--font-mono);
1784
+ overflow: hidden;
1785
+ text-overflow: ellipsis;
1786
+ white-space: nowrap;
1787
+ flex: 1;
1788
+ }
1789
+
1790
+ .folder-body {
1791
+ flex: 1;
1792
+ overflow-y: auto;
1793
+ padding: var(--space-sm) 0;
1794
+ min-height: 240px;
1795
+ background: var(--bg-secondary);
1796
+ }
1797
+
1798
+ .folder-item {
1799
+ display: flex;
1800
+ align-items: center;
1801
+ gap: var(--space-md);
1802
+ padding: var(--space-md) var(--space-xl);
1803
+ cursor: pointer;
1804
+ font-size: 13px;
1805
+ transition: all var(--transition-fast);
1806
+ border-left: 2px solid transparent;
1807
+ }
1808
+
1809
+ .folder-item:hover {
1810
+ background: var(--bg-tertiary);
1811
+ border-left-color: var(--accent-primary);
1812
+ }
1813
+
1814
+ .folder-item .fi-icon {
1815
+ font-size: 16px;
1816
+ flex-shrink: 0;
1817
+ opacity: 0.9;
1818
+ }
1819
+
1820
+ .folder-footer {
1821
+ display: flex;
1822
+ justify-content: flex-end;
1823
+ gap: var(--space-md);
1824
+ padding: var(--space-lg) var(--space-xl);
1825
+ border-top: 1px solid var(--border-primary);
1826
+ background: var(--bg-tertiary);
1827
+ }
1828
+
1829
+ .btn-browse {
1830
+ background: var(--bg-tertiary);
1831
+ color: var(--text-primary);
1832
+ border: 1px solid var(--border-primary);
1833
+ border-radius: var(--radius-md);
1834
+ padding: 6px var(--space-lg);
1835
+ cursor: pointer;
1836
+ font-size: 13px;
1837
+ font-weight: 600;
1838
+ white-space: nowrap;
1839
+ transition: all var(--transition-base);
1840
+ }
1841
+
1842
+ .btn-browse:hover {
1843
+ background: var(--bg-elevated);
1844
+ border-color: var(--accent-primary);
1845
+ }
1846
+
1847
+ /* Responsive Design */
1848
+ /* Large screens - keep everything in one line */
1849
+ @media (min-width: 1400px) {
1850
+ #header {
1851
+ flex-wrap: nowrap;
1852
+ }
1853
+ }
1854
+
1855
+ /* Medium-large screens - allow wrapping if needed */
1856
+ @media (max-width: 1399px) and (min-width: 1024px) {
1857
+ #header {
1858
+ flex-wrap: wrap;
1859
+ }
1860
+
1861
+ .header-center {
1862
+ order: 2;
1863
+ flex: 1 1 auto;
1864
+ }
1865
+
1866
+ .header-right {
1867
+ order: 3;
1868
+ }
1869
+ }
1870
+
1871
+ /* Medium screens - stack more aggressively */
1872
+ @media (max-width: 1023px) and (min-width: 769px) {
1873
+ #header {
1874
+ flex-wrap: wrap;
1875
+ gap: var(--space-sm);
1876
+ }
1877
+
1878
+ .header-left {
1879
+ order: 1;
1880
+ flex: 0 0 auto;
1881
+ }
1882
+
1883
+ .header-center {
1884
+ order: 3;
1885
+ width: 100%;
1886
+ margin-top: var(--space-sm);
1887
+ justify-content: flex-start;
1888
+ }
1889
+
1890
+ .header-right {
1891
+ order: 2;
1892
+ flex: 1 1 auto;
1893
+ justify-content: flex-end;
1894
+ }
1895
+
1896
+ .model-select-display {
1897
+ min-width: 240px;
1898
+ max-width: 280px;
1899
+ }
1900
+
1901
+ #cwdInput {
1902
+ width: 180px;
1903
+ }
1904
+ }
1905
+
1906
+ /* Small screens - mobile layout */
1907
+ @media (max-width: 768px) {
1908
+ #sidebar {
1909
+ width: 0;
1910
+ }
1911
+
1912
+ #filePanel {
1913
+ width: 0;
1914
+ }
1915
+
1916
+ #header {
1917
+ flex-wrap: wrap;
1918
+ padding: var(--space-md);
1919
+ gap: var(--space-sm);
1920
+ }
1921
+
1922
+ .header-left {
1923
+ order: 1;
1924
+ width: 100%;
1925
+ justify-content: space-between;
1926
+ }
1927
+
1928
+ .header-center {
1929
+ order: 2;
1930
+ width: 100%;
1931
+ margin-top: var(--space-sm);
1932
+ flex-direction: column;
1933
+ gap: var(--space-sm);
1934
+ }
1935
+
1936
+ .header-right {
1937
+ order: 3;
1938
+ width: 100%;
1939
+ margin-top: var(--space-sm);
1940
+ flex-wrap: wrap;
1941
+ }
1942
+
1943
+ .model-select-display {
1944
+ width: 100%;
1945
+ min-width: 100%;
1946
+ }
1947
+
1948
+ #cwdInput {
1949
+ width: 100%;
1950
+ }
1951
+
1952
+ .cwd-label {
1953
+ display: none;
1954
+ }
1955
+
1956
+ #messages {
1957
+ max-width: 100%;
1958
+ }
1959
+
1960
+ .switch-wrapper {
1961
+ width: 100%;
1962
+ justify-content: space-between;
1963
+ }
1964
+ }
1965
+
1966
+ /* Reduced Motion Support */
1967
+ @media (prefers-reduced-motion: reduce) {
1968
+ *,
1969
+ *::before,
1970
+ *::after {
1971
+ animation-duration: 0.01ms !important;
1972
+ animation-iteration-count: 1 !important;
1973
+ transition-duration: 0.01ms !important;
1974
+ }
1975
+ }
1976
+ </style>
1977
+ </head>
1978
+ <body>
1979
+ <div id="app">
1980
+ <!-- Sidebar -->
1981
+ <aside id="sidebar">
1982
+ <div class="sidebar-header">
1983
+ <span>会话列表</span>
1984
+ <div style="display:flex;gap:4px;">
1985
+ <button id="locateSessionBtn" class="btn-icon" title="定位当前会话">📍</button>
1986
+ <button id="newSessionSideBtn" class="btn-icon" title="新建会话">+</button>
1987
+ <button id="sidebarCloseBtn" class="btn-icon" title="收起侧栏">◀</button>
1988
+ </div>
1989
+ </div>
1990
+ <div style="padding:6px 8px;border-bottom:1px solid var(--border-primary)">
1991
+ <input id="sidebarSearch" type="text" placeholder="搜索目录..." style="width:100%;background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-primary);border-radius:var(--radius-md);padding:4px 8px;font-size:12px;outline:none;" />
1992
+ </div>
1993
+ <div class="sidebar-body" id="sidebarBody"></div>
1994
+ </aside>
1995
+
1996
+ <!-- Main Area -->
1997
+ <div id="mainArea">
1998
+ <header id="header">
1999
+ <div class="header-left">
2000
+ <button id="sidebarToggle" class="btn-icon" title="会话列表">☰</button>
2001
+ <span class="logo">意心Code</span><span class="version">v1.0</span>
2002
+ </div>
2003
+ <div class="header-center">
2004
+ <div class="model-select-wrapper">
2005
+ <div id="modelSelectDisplay" class="model-select-display">
2006
+ <img class="model-icon" src="" alt="" style="display:none;">
2007
+ <span class="model-label">选择模型</span>
2008
+ <span class="model-arrow">▼</span>
2009
+ </div>
2010
+ <div id="modelSelectDropdown" class="model-select-dropdown"></div>
2011
+ </div>
2012
+ <select id="permSelect">
2013
+ <option value="default">默认权限</option>
2014
+ <option value="acceptEdits">自动接受编辑</option>
2015
+ <option value="bypassPermissions">跳过所有权限</option>
2016
+ <option value="plan">计划模式</option>
2017
+ </select>
2018
+ <label class="switch-wrapper" title="开启后AI会以温柔体贴的女友角色回复">
2019
+ <span class="switch-label">💕 女友模式</span>
2020
+ <label class="switch">
2021
+ <input type="checkbox" id="girlfriendMode">
2022
+ <span class="switch-slider"></span>
2023
+ </label>
2024
+ </label>
2025
+ </div>
2026
+ <div class="header-right">
2027
+ <label class="cwd-label">工作目录</label>
2028
+ <input id="cwdInput" type="text" placeholder="项目路径..." />
2029
+ <button id="cwdBrowseBtn" class="btn-browse" title="浏览文件夹">📂 浏览</button>
2030
+ <button id="settingsBtn" class="btn-settings" title="设置">⚙ 设置</button>
2031
+ <button id="filePanelToggle" class="btn-icon" title="文件浏览">📁</button>
2032
+ <span id="connStatus" class="status-dot offline" title="未连接"></span>
2033
+ </div>
2034
+ </header>
2035
+ <!-- Settings -->
2036
+ <div id="settingsOverlay" class="settings-overlay hidden">
2037
+ <div class="settings-panel">
2038
+ <h2>⚙ 设置</h2>
2039
+ <div class="sg"><label>API Key</label><input id="setApiKey" type="password" placeholder="yi-xxxxx..." /><div class="hint">请输入意心AI的API Key(yi-开头)</div></div>
2040
+ <div class="sg">
2041
+ <label>模型</label>
2042
+ <div class="model-select-wrapper" style="width:100%;">
2043
+ <div id="setModelDisplay" class="model-select-display" style="min-width:100%;">
2044
+ <img class="model-icon" src="" alt="" style="display:none;">
2045
+ <span class="model-label">选择模型</span>
2046
+ <span class="model-arrow">▼</span>
2047
+ </div>
2048
+ <div id="setModelDropdown" class="model-select-dropdown"></div>
2049
+ </div>
2050
+ </div>
2051
+ <div class="sg">
2052
+ <label>权限管理</label>
2053
+ <button id="clearPermissionsBtn" class="btn-secondary" style="width:100%;">清除已记住的权限</button>
2054
+ <div class="hint" id="permissionCount">已记住 0 条权限规则</div>
2055
+ </div>
2056
+ <div class="settings-footer"><span id="settingsSaved" class="settings-saved">已保存</span><button id="settingsSaveBtn" class="btn-primary">保存</button><button id="settingsCloseBtn" class="btn-settings">关闭</button></div>
2057
+ </div>
2058
+ </div>
2059
+ <main id="chatArea"><div id="messages"></div><div id="permBanner" class="hidden"></div></main>
2060
+ <footer id="inputArea">
2061
+ <div id="cmdDropdown" class="cmd-dropdown"></div>
2062
+ <div class="input-row">
2063
+ <textarea id="promptInput" rows="3" placeholder="输入消息... (Enter 发送, Shift+Enter 换行, / 命令, @ 文件)"></textarea>
2064
+ <div class="btn-col">
2065
+ <button id="sendBtn" class="btn-primary">发送</button>
2066
+ <button id="abortBtn" class="btn-danger hidden">停止</button>
2067
+ <button id="newBtn" class="btn-secondary">新会话</button>
2068
+ </div>
2069
+ </div>
2070
+ <div class="input-footer"><span id="sessionInfo"></span></div>
2071
+ </footer>
2072
+ </div><!-- end #mainArea -->
2073
+
2074
+ <!-- File Panel -->
2075
+ <aside id="filePanel">
2076
+ <div class="filepanel-header">
2077
+ <span>文件浏览</span>
2078
+ <div style="display:flex;gap:4px;">
2079
+ <button id="fileRefreshBtn" class="btn-icon" title="刷新">🔄</button>
2080
+ <button id="filePanelCloseBtn" class="btn-icon" title="收起">▶</button>
2081
+ </div>
2082
+ </div>
2083
+ <div class="filepanel-body" id="fileTreeBody"></div>
2084
+ </aside>
2085
+ </div>
2086
+
2087
+ <!-- Folder Browser Modal -->
2088
+ <div id="folderBrowser" class="folder-overlay hidden">
2089
+ <div class="folder-panel">
2090
+ <div class="folder-header">
2091
+ <span class="folder-path" id="fbPath"></span>
2092
+ <button id="fbCloseBtn" class="btn-icon" title="关闭">✕</button>
2093
+ </div>
2094
+ <div class="folder-body" id="fbBody"></div>
2095
+ <div class="folder-footer">
2096
+ <button id="fbSelectBtn" class="btn-primary">选择当前目录</button>
2097
+ <button id="fbCancelBtn" class="btn-secondary">取消</button>
2098
+ </div>
2099
+ </div>
2100
+ </div>
2101
+
2102
+ <!-- File Viewer Modal -->
2103
+ <div id="fileViewer" class="file-viewer-overlay hidden">
2104
+ <div class="file-viewer-panel">
2105
+ <div class="file-viewer-header">
2106
+ <span class="fv-path" id="fvPath"></span>
2107
+ <button id="fvCloseBtn" class="btn-icon" title="关闭">✕</button>
2108
+ </div>
2109
+ <div class="file-viewer-content"><pre id="fvContent"></pre></div>
2110
+ </div>
2111
+ </div>
2112
+ <script>
2113
+ // ========== 意心Code 前端 ==========
2114
+ marked.setOptions({
2115
+ highlight: (code, lang) => {
2116
+ try {
2117
+ if (typeof hljs !== 'undefined') {
2118
+ if (lang && hljs.getLanguage(lang)) return hljs.highlight(code, { language: lang }).value;
2119
+ return hljs.highlightAuto(code).value;
2120
+ }
2121
+ } catch(e) { console.warn('[hljs]', e); }
2122
+ return code;
2123
+ }, breaks: true, gfm: true,
2124
+ });
2125
+
2126
+ const $ = s => document.querySelector(s);
2127
+ const modelSelectDisplay=$('#modelSelectDisplay'), modelSelectDropdown=$('#modelSelectDropdown'),
2128
+ setModelDisplay=$('#setModelDisplay'), setModelDropdown=$('#setModelDropdown'),
2129
+ permSelect=$('#permSelect'), girlfriendMode=$('#girlfriendMode'), cwdInput=$('#cwdInput'),
2130
+ connStatus=$('#connStatus'), messagesEl=$('#messages'), permBanner=$('#permBanner'),
2131
+ promptInput=$('#promptInput'), sendBtn=$('#sendBtn'), abortBtn=$('#abortBtn'),
2132
+ newBtn=$('#newBtn'), sessionInfo=$('#sessionInfo'),
2133
+ settingsBtn=$('#settingsBtn'), settingsOverlay=$('#settingsOverlay'),
2134
+ setApiKey=$('#setApiKey'),
2135
+ settingsSaveBtn=$('#settingsSaveBtn'),
2136
+ settingsCloseBtn=$('#settingsCloseBtn'), settingsSaved=$('#settingsSaved'),
2137
+ clearPermissionsBtn=$('#clearPermissionsBtn'), permissionCount=$('#permissionCount'),
2138
+ sidebar=$('#sidebar'), sidebarBody=$('#sidebarBody'),
2139
+ sidebarToggle=$('#sidebarToggle'), sidebarCloseBtn=$('#sidebarCloseBtn'),
2140
+ newSessionSideBtn=$('#newSessionSideBtn'),
2141
+ locateSessionBtn=$('#locateSessionBtn'),
2142
+ filePanel=$('#filePanel'), fileTreeBody=$('#fileTreeBody'),
2143
+ filePanelToggle=$('#filePanelToggle'), filePanelCloseBtn=$('#filePanelCloseBtn'),
2144
+ fileRefreshBtn=$('#fileRefreshBtn'),
2145
+ fileViewer=$('#fileViewer'), fvPath=$('#fvPath'), fvContent=$('#fvContent'), fvCloseBtn=$('#fvCloseBtn'),
2146
+ cwdBrowseBtn=$('#cwdBrowseBtn'),
2147
+ folderBrowser=$('#folderBrowser'), fbPath=$('#fbPath'), fbBody=$('#fbBody'),
2148
+ fbSelectBtn=$('#fbSelectBtn'), fbCancelBtn=$('#fbCancelBtn'), fbCloseBtn=$('#fbCloseBtn'),
2149
+ sidebarSearch=$('#sidebarSearch'),
2150
+ cmdDropdown=$('#cmdDropdown');
2151
+
2152
+ let ws=null, sessionId=null, isStreaming=false, streamingEl=null, streamBuf='', flushTimer=null;
2153
+ let expandedProjects = new Set(); // Fix 6: track expanded sidebar projects
2154
+ let selectedModel = null; // Current selected model object
2155
+ let selectedSettingsModel = null; // Settings model selection
2156
+ let modelsData = []; // All available models
2157
+ let rememberedPermissions = new Set(); // Remembered permission rules
2158
+
2159
+ // ========== Slash Commands & File Mentions ==========
2160
+ let fileListCache = null;
2161
+ let fileListCwd = null;
2162
+ let dropdownItems = [];
2163
+ let dropdownIndex = -1;
2164
+
2165
+ const SLASH_COMMANDS = [
2166
+ { cmd: '/clear', label: '/clear', desc: '清空聊天记录', icon: '🗑️', handler: () => { messagesEl.innerHTML=''; hideDropdown(); } },
2167
+ { cmd: '/new', label: '/new', desc: '新建会话', icon: '✨', handler: () => { newSession(); hideDropdown(); } },
2168
+ { cmd: '/rewind', label: '/rewind', desc: '撤销最后一条消息', icon: '⏪', handler: () => { rewindLastMessage(); hideDropdown(); } },
2169
+ { cmd: '/help', label: '/help', desc: '显示所有命令', icon: '❓', handler: () => { showHelp(); hideDropdown(); } },
2170
+ { cmd: '/model', label: '/model', desc: '显示当前模型信息', icon: '🤖', handler: () => { showModelInfo(); hideDropdown(); } },
2171
+ ];
2172
+
2173
+ function showHelp() {
2174
+ const helpText = SLASH_COMMANDS.map(c => `${c.cmd} - ${c.desc}`).join('\n');
2175
+ appendSystemMsg('可用命令:\n' + helpText);
2176
+ }
2177
+
2178
+ function showModelInfo() {
2179
+ if (!selectedModel) {
2180
+ appendSystemMsg('未选择模型');
2181
+ return;
2182
+ }
2183
+ const info = `${selectedModel.label}${selectedModel.provider ? ' (' + selectedModel.provider + ')' : ''}`;
2184
+ appendSystemMsg('当前模型: ' + info);
2185
+ }
2186
+
2187
+ function rewindLastMessage() {
2188
+ // Get all message elements (including tool cards, ask panels, etc.)
2189
+ const allElements = messagesEl.children;
2190
+ if (allElements.length === 0) {
2191
+ appendSystemMsg('没有消息可以撤销', 'error');
2192
+ return;
2193
+ }
2194
+
2195
+ // Find last user message
2196
+ let lastUserIndex = -1;
2197
+ for (let i = allElements.length - 1; i >= 0; i--) {
2198
+ const el = allElements[i];
2199
+ if (el.classList.contains('msg') && el.classList.contains('user')) {
2200
+ lastUserIndex = i;
2201
+ break;
2202
+ }
2203
+ }
2204
+
2205
+ if (lastUserIndex === -1) {
2206
+ appendSystemMsg('没有用户消息可以撤销', 'error');
2207
+ return;
2208
+ }
2209
+
2210
+ // Get user message content
2211
+ const userMsg = allElements[lastUserIndex];
2212
+ const content = userMsg.querySelector('.content')?.textContent || '';
2213
+
2214
+ // Remove all elements from last user message onwards (including tool cards, ask panels, loading indicators, etc.)
2215
+ const elementsToRemove = [];
2216
+ for (let i = lastUserIndex; i < allElements.length; i++) {
2217
+ elementsToRemove.push(allElements[i]);
2218
+ }
2219
+ elementsToRemove.forEach(el => el.remove());
2220
+
2221
+ // Restore content to input
2222
+ promptInput.value = content;
2223
+ promptInput.focus();
2224
+ appendSystemMsg('已回退到上次输入');
2225
+ }
2226
+
2227
+ function showDropdown(items) {
2228
+ dropdownItems = items;
2229
+ dropdownIndex = items.length > 0 ? 0 : -1;
2230
+ cmdDropdown.innerHTML = '';
2231
+ items.forEach((item, i) => {
2232
+ const el = document.createElement('div');
2233
+ el.className = 'cmd-dropdown-item' + (i === 0 ? ' active' : '');
2234
+ el.innerHTML = `<span class="cmd-icon">${item.icon || '·'}</span><span class="cmd-label">${escHtml(item.label)}</span><span class="cmd-desc">${escHtml(item.desc || '')}</span>`;
2235
+ el.addEventListener('click', () => selectDropdownItem(i));
2236
+ cmdDropdown.appendChild(el);
2237
+ });
2238
+ cmdDropdown.classList.add('visible');
2239
+ }
2240
+
2241
+ function hideDropdown() {
2242
+ cmdDropdown.classList.remove('visible');
2243
+ cmdDropdown.innerHTML = '';
2244
+ dropdownItems = [];
2245
+ dropdownIndex = -1;
2246
+ }
2247
+
2248
+ function setDropdownIndex(i) {
2249
+ if (i < 0 || i >= dropdownItems.length) return;
2250
+ dropdownIndex = i;
2251
+ const items = cmdDropdown.querySelectorAll('.cmd-dropdown-item');
2252
+ items.forEach((el, idx) => el.classList.toggle('active', idx === i));
2253
+ items[i]?.scrollIntoView({ block: 'nearest' });
2254
+ }
2255
+
2256
+ function selectDropdownItem(i) {
2257
+ if (i < 0 || i >= dropdownItems.length) return;
2258
+ const item = dropdownItems[i];
2259
+ if (item.onSelect) item.onSelect();
2260
+ hideDropdown();
2261
+ }
2262
+
2263
+ function checkSlashCommand() {
2264
+ const val = promptInput.value;
2265
+ if (!val.startsWith('/')) { hideDropdown(); return; }
2266
+ const query = val.slice(1).toLowerCase();
2267
+ const matches = SLASH_COMMANDS.filter(c => c.cmd.slice(1).startsWith(query));
2268
+ if (matches.length) {
2269
+ showDropdown(matches.map(c => ({
2270
+ label: c.label,
2271
+ desc: c.desc,
2272
+ icon: c.icon,
2273
+ onSelect: () => { promptInput.value = ''; c.handler(); }
2274
+ })));
2275
+ } else {
2276
+ hideDropdown();
2277
+ }
2278
+ }
2279
+
2280
+ async function loadFileListFlat() {
2281
+ const cwd = cwdInput.value || '';
2282
+ if (!cwd) return [];
2283
+ if (fileListCache && fileListCwd === cwd) return fileListCache;
2284
+ try {
2285
+ const res = await fetch('/api/files-flat?cwd=' + encodeURIComponent(cwd));
2286
+ const data = await res.json();
2287
+ fileListCache = data;
2288
+ fileListCwd = cwd;
2289
+ return data;
2290
+ } catch (e) {
2291
+ console.error('[loadFileListFlat]', e);
2292
+ return [];
2293
+ }
2294
+ }
2295
+
2296
+ function getMentionContext() {
2297
+ const val = promptInput.value;
2298
+ const pos = promptInput.selectionStart;
2299
+ const before = val.slice(0, pos);
2300
+ const atIdx = before.lastIndexOf('@');
2301
+ if (atIdx === -1) return null;
2302
+ const afterAt = before.slice(atIdx + 1);
2303
+ if (/\s/.test(afterAt)) return null; // space closes mention
2304
+ return { start: atIdx, query: afterAt };
2305
+ }
2306
+
2307
+ async function checkFileMention() {
2308
+ const ctx = getMentionContext();
2309
+ if (!ctx) { hideDropdown(); return; }
2310
+ const files = await loadFileListFlat();
2311
+ const query = ctx.query.toLowerCase();
2312
+ const matches = files.filter(f => f.path.toLowerCase().includes(query)).slice(0, 20);
2313
+ if (matches.length) {
2314
+ showDropdown(matches.map(f => ({
2315
+ label: f.path,
2316
+ desc: f.type === 'dir' ? '目录' : '文件',
2317
+ icon: f.type === 'dir' ? '📁' : '📄',
2318
+ onSelect: () => {
2319
+ const val = promptInput.value;
2320
+ const pos = promptInput.selectionStart;
2321
+ const newVal = val.slice(0, ctx.start) + '@' + f.path + ' ' + val.slice(pos);
2322
+ promptInput.value = newVal;
2323
+ promptInput.selectionStart = promptInput.selectionEnd = ctx.start + f.path.length + 2;
2324
+ promptInput.focus();
2325
+ }
2326
+ })));
2327
+ } else {
2328
+ hideDropdown();
2329
+ }
2330
+ }
2331
+
2332
+ function isDropdownVisible() {
2333
+ return cmdDropdown.classList.contains('visible');
2334
+ }
2335
+
2336
+ // ========== Model Select ==========
2337
+ function selectModel(model) {
2338
+ selectedModel = model;
2339
+ const icon = modelSelectDisplay.querySelector('.model-icon');
2340
+ const label = modelSelectDisplay.querySelector('.model-label');
2341
+ if (model.icon) {
2342
+ icon.src = model.icon;
2343
+ icon.style.display = 'block';
2344
+ } else {
2345
+ icon.style.display = 'none';
2346
+ }
2347
+ label.textContent = model.label;
2348
+ modelSelectDropdown.classList.remove('visible');
2349
+ localStorage.setItem('yxcode_model', model.value);
2350
+ }
2351
+
2352
+ function selectSettingsModel(model) {
2353
+ selectedSettingsModel = model;
2354
+ const icon = setModelDisplay.querySelector('.model-icon');
2355
+ const label = setModelDisplay.querySelector('.model-label');
2356
+ if (model.icon) {
2357
+ icon.src = model.icon;
2358
+ icon.style.display = 'block';
2359
+ } else {
2360
+ icon.style.display = 'none';
2361
+ }
2362
+ label.textContent = model.label;
2363
+ setModelDropdown.classList.remove('visible');
2364
+ }
2365
+
2366
+ modelSelectDisplay.addEventListener('click', (e) => {
2367
+ e.stopPropagation();
2368
+ modelSelectDropdown.classList.toggle('visible');
2369
+ });
2370
+
2371
+ setModelDisplay.addEventListener('click', (e) => {
2372
+ e.stopPropagation();
2373
+ setModelDropdown.classList.toggle('visible');
2374
+ });
2375
+
2376
+ document.addEventListener('click', (e) => {
2377
+ if (!modelSelectDisplay.contains(e.target) && !modelSelectDropdown.contains(e.target)) {
2378
+ modelSelectDropdown.classList.remove('visible');
2379
+ }
2380
+ if (!setModelDisplay.contains(e.target) && !setModelDropdown.contains(e.target)) {
2381
+ setModelDropdown.classList.remove('visible');
2382
+ }
2383
+ });
2384
+
2385
+ // --- Tool category map ---
2386
+ const TOOL_CAT = {
2387
+ Bash:'bash', Read:'search', Grep:'search', Glob:'search',
2388
+ Edit:'edit', Write:'edit', ApplyPatch:'edit',
2389
+ TodoWrite:'todo', TodoRead:'todo',
2390
+ TaskCreate:'task', TaskUpdate:'task', TaskList:'task', TaskGet:'task',
2391
+ AskUserQuestion:'question', Task:'agent',
2392
+ };
2393
+ const TOOL_ICON = {
2394
+ bash:'$_', edit:'✎', search:'🔍', todo:'☑', task:'📋', question:'❓', agent:'🤖', default:'⚙',
2395
+ };
2396
+
2397
+ // --- Init ---
2398
+ (async () => {
2399
+ try {
2400
+ const models = await (await fetch('/api/models')).json();
2401
+ modelsData = models;
2402
+
2403
+ // Render main model dropdown
2404
+ modelSelectDropdown.innerHTML = '';
2405
+ models.forEach(m => {
2406
+ const opt = document.createElement('div');
2407
+ opt.className = 'model-select-option';
2408
+ opt.dataset.value = m.value;
2409
+ const iconHTML = m.icon ? `<img class="model-icon" src="${m.icon}" alt="">` : '<div class="model-icon" style="background:var(--bg3)"></div>';
2410
+ opt.innerHTML = `${iconHTML}<span class="model-label">${escHtml(m.label)}</span>${m.provider ? `<span class="model-provider">${escHtml(m.provider)}</span>` : ''}`;
2411
+ opt.addEventListener('click', () => selectModel(m));
2412
+ modelSelectDropdown.appendChild(opt);
2413
+ });
2414
+
2415
+ // Render settings model dropdown
2416
+ setModelDropdown.innerHTML = '';
2417
+ models.forEach(m => {
2418
+ const opt = document.createElement('div');
2419
+ opt.className = 'model-select-option';
2420
+ opt.dataset.value = m.value;
2421
+ const iconHTML = m.icon ? `<img class="model-icon" src="${m.icon}" alt="">` : '<div class="model-icon" style="background:var(--bg3)"></div>';
2422
+ opt.innerHTML = `${iconHTML}<span class="model-label">${escHtml(m.label)}</span>${m.provider ? `<span class="model-provider">${escHtml(m.provider)}</span>` : ''}`;
2423
+ opt.addEventListener('click', () => selectSettingsModel(m));
2424
+ setModelDropdown.appendChild(opt);
2425
+ });
2426
+
2427
+ // Store models data for header display
2428
+ window._yxModels = models;
2429
+
2430
+ // Restore saved model
2431
+ const savedModelId = localStorage.getItem('yxcode_model');
2432
+ if (savedModelId) {
2433
+ const savedModel = models.find(m => m.value === savedModelId);
2434
+ if (savedModel) {
2435
+ selectModel(savedModel);
2436
+ selectSettingsModel(savedModel);
2437
+ }
2438
+ } else if (models.length > 0) {
2439
+ selectModel(models[0]);
2440
+ selectSettingsModel(models[0]);
2441
+ }
2442
+ } catch(e) { console.error('[loadModels]', e); }
2443
+ cwdInput.value = localStorage.getItem('yxcode_cwd') || '';
2444
+ setApiKey.value = localStorage.getItem('yxcode_apiKey') || '';
2445
+ girlfriendMode.checked = localStorage.getItem('yxcode_girlfriendMode') === 'true';
2446
+
2447
+ // Load remembered permissions
2448
+ try {
2449
+ const saved = localStorage.getItem('yxcode_rememberedPermissions');
2450
+ if (saved) rememberedPermissions = new Set(JSON.parse(saved));
2451
+ } catch(e) { console.error('[load permissions]', e); }
2452
+
2453
+ cwdInput.addEventListener('change', () => localStorage.setItem('yxcode_cwd', cwdInput.value));
2454
+ girlfriendMode.addEventListener('change', () => {
2455
+ localStorage.setItem('yxcode_girlfriendMode', girlfriendMode.checked);
2456
+ });
2457
+ settingsBtn.addEventListener('click', () => settingsOverlay.classList.remove('hidden'));
2458
+ settingsCloseBtn.addEventListener('click', () => settingsOverlay.classList.add('hidden'));
2459
+ settingsOverlay.addEventListener('click', e => { if(e.target===settingsOverlay) settingsOverlay.classList.add('hidden'); });
2460
+ settingsSaveBtn.addEventListener('click', saveSettings);
2461
+ clearPermissionsBtn.addEventListener('click', clearRememberedPermissions);
2462
+
2463
+ // Update permission count when opening settings
2464
+ settingsBtn.addEventListener('click', updatePermissionCount);
2465
+ // If no API key configured, auto-open settings
2466
+ if (!localStorage.getItem('yxcode_apiKey')) settingsOverlay.classList.remove('hidden');
2467
+ connectWS();
2468
+ // Load file tree on init if cwd is set
2469
+ if (cwdInput.value) loadFileTree();
2470
+ })();
2471
+
2472
+ function saveSettings() {
2473
+ localStorage.setItem('yxcode_apiKey', setApiKey.value.trim());
2474
+ if (selectedSettingsModel) {
2475
+ localStorage.setItem('yxcode_model', selectedSettingsModel.value);
2476
+ // Update main model select
2477
+ selectModel(selectedSettingsModel);
2478
+ }
2479
+ settingsSaved.classList.add('show');
2480
+ setTimeout(() => settingsSaved.classList.remove('show'), 2000);
2481
+ }
2482
+
2483
+ function clearRememberedPermissions() {
2484
+ if (rememberedPermissions.size === 0) {
2485
+ appendSystemMsg('没有已记住的权限规则');
2486
+ return;
2487
+ }
2488
+ const count = rememberedPermissions.size;
2489
+ rememberedPermissions.clear();
2490
+ localStorage.removeItem('yxcode_rememberedPermissions');
2491
+ updatePermissionCount();
2492
+ appendSystemMsg(`已清除 ${count} 条权限规则`);
2493
+ }
2494
+
2495
+ function updatePermissionCount() {
2496
+ permissionCount.textContent = `已记住 ${rememberedPermissions.size} 条权限规则`;
2497
+ }
2498
+
2499
+ // --- WebSocket ---
2500
+ function connectWS() {
2501
+ const proto = location.protocol==='https:' ? 'wss:' : 'ws:';
2502
+ ws = new WebSocket(`${proto}//${location.host}/ws`);
2503
+ ws.onopen = () => { connStatus.className='status-dot online'; connStatus.title='已连接'; };
2504
+ ws.onclose = () => { connStatus.className='status-dot offline'; connStatus.title='未连接'; setTimeout(connectWS,3000); };
2505
+ ws.onerror = () => {};
2506
+ ws.onmessage = e => { try { handleMsg(JSON.parse(e.data)); } catch(err) { console.error('[ws parse]', err); } };
2507
+ }
2508
+ function wsSend(d) { if(ws&&ws.readyState===WebSocket.OPEN) ws.send(JSON.stringify(d)); }
2509
+
2510
+ // --- Message dispatch ---
2511
+ function handleMsg(msg) {
2512
+ switch(msg.type) {
2513
+ case 'session-created': sessionId=msg.sessionId; sessionInfo.textContent='会话: '+sessionId.slice(0,8)+'...'; break;
2514
+ case 'claude-response': handleClaudeResponse(msg.data); break;
2515
+ case 'claude-complete': finishStreaming(); setStreaming(false); break;
2516
+ case 'claude-error': finishStreaming(); setStreaming(false); appendSystemMsg('错误: '+msg.error,'error'); break;
2517
+ case 'session-aborted': finishStreaming(); setStreaming(false); appendSystemMsg('会话已停止'); break;
2518
+ case 'permission-request': showPermission(msg); break;
2519
+ case 'permission-cancelled': permBanner.classList.add('hidden'); break;
2520
+ }
2521
+ }
2522
+
2523
+ // --- Claude response processing ---
2524
+ function handleClaudeResponse(raw) {
2525
+ if(!raw) return;
2526
+ const data = raw.message || raw;
2527
+ console.log('[msg]', data.type, data.subtype||'', data.role||'');
2528
+
2529
+ if(data.type==='system' && data.subtype==='init') return;
2530
+
2531
+ // Streaming text
2532
+ if(data.type==='content_block_delta') {
2533
+ const t = data.delta?.text ?? data.delta?.text_delta ?? '';
2534
+ if(t) { feedStream(t); }
2535
+ return;
2536
+ }
2537
+ if(data.type==='content_block_stop') { finishStreaming(); return; }
2538
+
2539
+ // Structured content array
2540
+ if(Array.isArray(data.content)) {
2541
+ if(data.role==='user') {
2542
+ for(const p of data.content) {
2543
+ if(p.type==='tool_result') {
2544
+ const txt = typeof p.content==='string' ? p.content
2545
+ : Array.isArray(p.content) ? p.content.map(c=>c.text||'').join('') : JSON.stringify(p.content);
2546
+ appendToolResult({ tool_use_id:p.tool_use_id, content:txt, is_error:p.is_error });
2547
+ }
2548
+ }
2549
+ return;
2550
+ }
2551
+ for(const p of data.content) {
2552
+ if(p.type==='text' && p.text?.trim()) { feedStream(p.text); }
2553
+ if(p.type==='tool_use') { finishStreaming(); handleToolUse(p); }
2554
+ }
2555
+ return;
2556
+ }
2557
+
2558
+ // Plain string
2559
+ if(typeof data.content==='string' && data.content.trim()) {
2560
+ feedStream(data.content); return;
2561
+ }
2562
+
2563
+ // Result
2564
+ if(data.type==='result') {
2565
+ finishStreaming();
2566
+ if(data.subtype==='error_max_turns') appendSystemMsg('已达到最大轮次');
2567
+ }
2568
+ }
2569
+
2570
+ // --- Tool use handler (routes to special renderers) ---
2571
+ function handleToolUse(part) {
2572
+ // Don't hide loading - keep it visible while tool executes
2573
+ const name = part.name || 'Tool';
2574
+ if(name==='AskUserQuestion') { renderAskUserQuestion(part); return; }
2575
+ if(name==='TodoWrite' || name==='TodoRead') { renderTodoTool(part); return; }
2576
+ if(name==='TaskList' || name==='TaskGet') { renderTaskTool(part); return; }
2577
+ appendToolCard(part);
2578
+ }
2579
+
2580
+ // ========== AskUserQuestion ==========
2581
+ function renderAskUserQuestion(part) {
2582
+ const input = part.input || {};
2583
+ const questions = input.questions || [];
2584
+ const panel = document.createElement('div');
2585
+ panel.className = 'ask-panel';
2586
+ panel.dataset.toolId = part.id || '';
2587
+ panel.dataset.toolName = 'AskUserQuestion';
2588
+
2589
+ // State for multi-step
2590
+ let step = 0;
2591
+ const answers = {};
2592
+
2593
+ function renderStep() {
2594
+ const q = questions[step];
2595
+ if(!q) return;
2596
+ const multi = q.multiSelect || false;
2597
+ const selected = new Set();
2598
+ let otherText = '';
2599
+
2600
+ panel.innerHTML = '';
2601
+ if(q.header) { const h = document.createElement('div'); h.className='ask-header'; h.textContent=q.header; panel.appendChild(h); }
2602
+ const qEl = document.createElement('div'); qEl.className='ask-q';
2603
+ qEl.textContent = `${questions.length>1 ? `(${step+1}/${questions.length}) ` : ''}${q.question}`;
2604
+ panel.appendChild(qEl);
2605
+
2606
+ const optsEl = document.createElement('div'); optsEl.className='ask-opts';
2607
+ (q.options||[]).forEach((opt, i) => {
2608
+ const el = document.createElement('div');
2609
+ el.className = 'ask-opt' + (multi ? ' multi' : '');
2610
+ el.innerHTML = `<div class="opt-radio"></div><div><div class="opt-label">${escHtml(opt.label)}</div>${opt.description ? `<div class="opt-desc">${escHtml(opt.description)}</div>` : ''}</div>`;
2611
+ el.addEventListener('click', () => {
2612
+ if(multi) { selected.has(i) ? selected.delete(i) : selected.add(i); }
2613
+ else { selected.clear(); selected.add(i); }
2614
+ optsEl.querySelectorAll('.ask-opt').forEach((o,j) => o.classList.toggle('selected', selected.has(j)));
2615
+ });
2616
+ optsEl.appendChild(el);
2617
+ });
2618
+ panel.appendChild(optsEl);
2619
+
2620
+ // "Other" free text
2621
+ const otherDiv = document.createElement('div'); otherDiv.className='ask-other';
2622
+ otherDiv.innerHTML = `<input type="text" placeholder="其他 (自定义回复)..." />`;
2623
+ otherDiv.querySelector('input').addEventListener('input', e => { otherText = e.target.value; });
2624
+ panel.appendChild(otherDiv);
2625
+
2626
+ // Buttons
2627
+ const btns = document.createElement('div'); btns.className='ask-btns';
2628
+ if(step > 0) {
2629
+ const backBtn = document.createElement('button'); backBtn.className='btn-secondary'; backBtn.textContent='上一步';
2630
+ backBtn.addEventListener('click', () => { step--; renderStep(); });
2631
+ btns.appendChild(backBtn);
2632
+ }
2633
+ const nextLabel = step < questions.length-1 ? '下一步' : '提交';
2634
+ const nextBtn = document.createElement('button'); nextBtn.className='btn-primary'; nextBtn.textContent=nextLabel;
2635
+ nextBtn.addEventListener('click', () => {
2636
+ // Collect answer
2637
+ const opts = q.options||[];
2638
+ const vals = [...selected].map(i => opts[i].label);
2639
+ if(otherText.trim()) vals.push(otherText.trim());
2640
+ answers[q.question] = vals.join(', ');
2641
+ if(step < questions.length-1) { step++; renderStep(); }
2642
+ else { submitAsk(); }
2643
+ });
2644
+ btns.appendChild(nextBtn);
2645
+ const skipBtn = document.createElement('button'); skipBtn.className='btn-secondary'; skipBtn.textContent='跳过';
2646
+ skipBtn.addEventListener('click', () => { submitAsk(true); });
2647
+ btns.appendChild(skipBtn);
2648
+ panel.appendChild(btns);
2649
+ }
2650
+
2651
+ function submitAsk(skip) {
2652
+ const decision = { allow:true, updatedInput: { ...input, answers: skip ? {} : answers } };
2653
+ wsSend({ type:'permission-response', requestId: panel._requestId, ...decision });
2654
+ // Replace with answered display
2655
+ panel.innerHTML = '';
2656
+ panel.className = 'ask-answered';
2657
+ if(skip) { panel.innerHTML = '<div class="ans-q" style="color:var(--text-secondary)">已跳过</div>'; }
2658
+ else {
2659
+ for(const [q, a] of Object.entries(answers)) {
2660
+ const row = document.createElement('div');
2661
+ row.innerHTML = `<div class="ans-q">${escHtml(q)}</div><div class="ans-v">${escHtml(a)}</div>`;
2662
+ panel.appendChild(row);
2663
+ }
2664
+ }
2665
+ scrollBottom();
2666
+ }
2667
+
2668
+ renderStep();
2669
+ messagesEl.appendChild(panel);
2670
+ scrollBottom();
2671
+ }
2672
+
2673
+ // ========== TodoWrite / TodoRead ==========
2674
+ function renderTodoTool(part) {
2675
+ const card = createToolCard(part);
2676
+ const body = card.querySelector('.tool-body');
2677
+ const items = parseTodoItems(part.input);
2678
+ if(items.length) {
2679
+ body.innerHTML = '';
2680
+ const list = document.createElement('div'); list.className='todo-list';
2681
+ items.forEach(t => {
2682
+ const row = document.createElement('div'); row.className='todo-item';
2683
+ const icon = t.status==='completed' ? '✅' : t.status==='in_progress' ? '🔄' : '⭕';
2684
+ const iconCls = t.status==='completed' ? 'done' : t.status==='in_progress' ? 'progress' : 'pending';
2685
+ row.innerHTML = `<span class="todo-icon ${iconCls}">${icon}</span><span class="todo-text">${escHtml(t.subject||t.content||'')}</span>`;
2686
+ if(t.priority) row.innerHTML += `<span class="todo-priority ${t.priority}">${t.priority}</span>`;
2687
+ list.appendChild(row);
2688
+ });
2689
+ body.appendChild(list);
2690
+ }
2691
+ card.classList.add('open');
2692
+ messagesEl.appendChild(card);
2693
+ scrollBottom();
2694
+ }
2695
+
2696
+ function parseTodoItems(input) {
2697
+ if(!input) return [];
2698
+ // TodoWrite: input.todos array
2699
+ if(Array.isArray(input.todos)) return input.todos;
2700
+ if(Array.isArray(input.items)) return input.items;
2701
+ // TodoRead result: try JSON parse
2702
+ if(typeof input === 'string') { try { const d=JSON.parse(input); if(Array.isArray(d)) return d; } catch{} }
2703
+ return [];
2704
+ }
2705
+
2706
+ // ========== TaskList / TaskGet ==========
2707
+ function renderTaskTool(part) {
2708
+ const card = createToolCard(part);
2709
+ card.classList.add('open');
2710
+ messagesEl.appendChild(card);
2711
+ scrollBottom();
2712
+ }
2713
+
2714
+ function renderTaskResult(card, content) {
2715
+ const body = card.querySelector('.tool-body') || card;
2716
+ const items = parseTaskItems(content);
2717
+ if(!items.length) return;
2718
+ const wrap = document.createElement('div'); wrap.className='task-list';
2719
+ const done = items.filter(t=>t.status==='completed').length;
2720
+ wrap.innerHTML = `<div class="task-progress"><div class="task-progress-bar" style="width:${items.length?Math.round(done/items.length*100):0}%"></div></div>`;
2721
+ items.forEach(t => {
2722
+ const row = document.createElement('div'); row.className='task-item';
2723
+ const icon = t.status==='completed' ? '✅' : t.status==='in_progress' ? '🔄' : '⭕';
2724
+ row.innerHTML = `<span>${icon}</span><span style="flex:1">${escHtml(t.subject||t.text||'')}</span><span class="task-status ${t.status||'pending'}">${t.status||'pending'}</span>`;
2725
+ wrap.appendChild(row);
2726
+ });
2727
+ body.innerHTML = '';
2728
+ body.appendChild(wrap);
2729
+ }
2730
+
2731
+ function parseTaskItems(content) {
2732
+ if(!content) return [];
2733
+ // Try JSON
2734
+ try { const d=JSON.parse(content); if(Array.isArray(d)) return d; if(d.tasks) return d.tasks; } catch{}
2735
+ // Try regex: #1. [completed] subject
2736
+ const items = [];
2737
+ const re = /#(\d+)\.?\s*(?:\[(\w+)\]\s*)?(.+)/g;
2738
+ let m;
2739
+ while((m=re.exec(content))) items.push({ id:m[1], status:m[2]||'pending', subject:m[3].trim() });
2740
+ if(items.length) return items;
2741
+ // Fallback: line-based
2742
+ content.split('\n').filter(l=>l.trim()).forEach(l => items.push({ subject:l.trim(), status:'pending' }));
2743
+ return items;
2744
+ }
2745
+
2746
+ // ========== Generic tool card ==========
2747
+ function createToolCard(part) {
2748
+ const name = part.name || 'Tool';
2749
+ const cat = TOOL_CAT[name] || 'default';
2750
+ const icon = TOOL_ICON[cat] || TOOL_ICON.default;
2751
+ const card = document.createElement('div');
2752
+ card.className = 'tool-card';
2753
+ card.dataset.toolId = part.id || '';
2754
+ card.dataset.toolName = name;
2755
+ card.dataset.cat = cat;
2756
+
2757
+ const summary = getToolSummary(name, part.input);
2758
+
2759
+ // Special rendering for Edit tool with diff view
2760
+ const formattedInput = formatToolInput(name, part.input);
2761
+ let bodyHTML;
2762
+ if (formattedInput === '__DIFF_VIEW__' && name === 'Edit') {
2763
+ bodyHTML = renderDiffView(part.input);
2764
+ } else {
2765
+ bodyHTML = `<pre>${escHtml(formattedInput)}</pre>`;
2766
+ }
2767
+
2768
+ card.innerHTML = `
2769
+ <div class="tool-header" onclick="this.parentElement.classList.toggle('open')">
2770
+ <span class="tool-icon">${icon}</span>
2771
+ <span class="tool-name">${escHtml(name)}</span>
2772
+ <span class="tool-summary">${escHtml(summary)}</span>
2773
+ <span class="chevron">▼</span>
2774
+ </div>
2775
+ <div class="tool-body">${bodyHTML}</div>`;
2776
+ return card;
2777
+ }
2778
+
2779
+ function appendToolCard(part) {
2780
+ const card = createToolCard(part);
2781
+ messagesEl.appendChild(card);
2782
+ scrollBottom();
2783
+ }
2784
+
2785
+ function getToolSummary(name, input) {
2786
+ if(!input) return '';
2787
+ if(name==='Bash') return input.command ? input.command.slice(0,60) : '';
2788
+ if(name==='Read') return input.file_path ? input.file_path.split(/[/\\]/).pop() : '';
2789
+ if(name==='Edit'||name==='Write') return input.file_path ? input.file_path.split(/[/\\]/).pop() : '';
2790
+ if(name==='Grep') return input.pattern || '';
2791
+ if(name==='Glob') return input.pattern || '';
2792
+ if(name==='TaskCreate') return input.subject || '';
2793
+ if(name==='TaskUpdate') return `#${input.taskId||'?'} → ${input.status||''}`;
2794
+ if(name==='TaskList') return '列出任务';
2795
+ if(name==='TaskGet') return `#${input.taskId||'?'}`;
2796
+ return '';
2797
+ }
2798
+
2799
+ function formatToolInput(name, input) {
2800
+ if(!input) return '';
2801
+ if(typeof input==='string') return input;
2802
+ if(name==='Bash') return input.command || JSON.stringify(input,null,2);
2803
+ if(name==='Edit') return '__DIFF_VIEW__';
2804
+ if(name==='Write') return `文件: ${input.file_path||''}\n${input.content||''}`;
2805
+ return JSON.stringify(input, null, 2);
2806
+ }
2807
+
2808
+ function renderDiffView(input) {
2809
+ if (!input || !input.old_string || !input.new_string) {
2810
+ return `<pre>${escHtml(JSON.stringify(input, null, 2))}</pre>`;
2811
+ }
2812
+
2813
+ const filePath = input.file_path || '(unknown file)';
2814
+ const oldLines = input.old_string.split('\\n');
2815
+ const newLines = input.new_string.split('\\n');
2816
+
2817
+ let html = `<div class="diff-view"><div class="diff-file">文件: ${escHtml(filePath)}</div>`;
2818
+
2819
+ // Simple line-by-line diff
2820
+ const maxLen = Math.max(oldLines.length, newLines.length);
2821
+ for (let i = 0; i < maxLen; i++) {
2822
+ const oldLine = oldLines[i];
2823
+ const newLine = newLines[i];
2824
+
2825
+ if (oldLine !== undefined && (newLine === undefined || oldLine !== newLine)) {
2826
+ html += `<div class="diff-line removed">${escHtml(oldLine)}</div>`;
2827
+ }
2828
+ if (newLine !== undefined && (oldLine === undefined || oldLine !== newLine)) {
2829
+ html += `<div class="diff-line added">${escHtml(newLine)}</div>`;
2830
+ }
2831
+ if (oldLine === newLine && oldLine !== undefined) {
2832
+ html += `<div class="diff-line context">${escHtml(oldLine)}</div>`;
2833
+ }
2834
+ }
2835
+
2836
+ html += '</div>';
2837
+ return html;
2838
+ }
2839
+
2840
+ function appendToolResult(block) {
2841
+ const cards = messagesEl.querySelectorAll('.tool-card, .ask-panel');
2842
+ let target = null;
2843
+ if(block.tool_use_id) {
2844
+ for(const c of cards) { if(c.dataset.toolId===block.tool_use_id) { target=c; break; } }
2845
+ }
2846
+ if(!target && cards.length) target = cards[cards.length-1];
2847
+
2848
+ const text = typeof block.content==='string' ? block.content : JSON.stringify(block.content);
2849
+
2850
+ // Special rendering for task tools
2851
+ if(target && (target.dataset.toolName==='TaskList' || target.dataset.toolName==='TaskGet')) {
2852
+ renderTaskResult(target, text);
2853
+ target.classList.add('open');
2854
+ scrollBottom();
2855
+ if(isStreaming && !streamingEl) showLoading();
2856
+ return;
2857
+ }
2858
+ // Special rendering for TodoRead result
2859
+ if(target && target.dataset.toolName==='TodoRead') {
2860
+ const items = parseTodoItems(text);
2861
+ if(items.length) {
2862
+ const body = target.querySelector('.tool-body');
2863
+ body.innerHTML = '';
2864
+ const list = document.createElement('div'); list.className='todo-list';
2865
+ items.forEach(t => {
2866
+ const row = document.createElement('div'); row.className='todo-item';
2867
+ const icon = t.status==='completed'?'✅':t.status==='in_progress'?'🔄':'⭕';
2868
+ const cls = t.status==='completed'?'done':t.status==='in_progress'?'progress':'pending';
2869
+ row.innerHTML = `<span class="todo-icon ${cls}">${icon}</span><span class="todo-text">${escHtml(t.subject||t.content||'')}</span>`;
2870
+ list.appendChild(row);
2871
+ });
2872
+ body.appendChild(list);
2873
+ target.classList.add('open');
2874
+ scrollBottom();
2875
+ if(isStreaming && !streamingEl) showLoading();
2876
+ return;
2877
+ }
2878
+ }
2879
+
2880
+ const truncated = text.length>3000 ? text.slice(0,3000)+'\n... (truncated)' : text;
2881
+ if(target) {
2882
+ const res = document.createElement('div');
2883
+ res.className = 'tool-result' + (block.is_error ? ' error' : '');
2884
+ res.innerHTML = `<pre>${escHtml(truncated)}</pre>`;
2885
+ target.appendChild(res);
2886
+ }
2887
+ scrollBottom();
2888
+
2889
+ // Show loading indicator after tool result, waiting for next response
2890
+ if(isStreaming && !streamingEl) {
2891
+ showLoading();
2892
+ }
2893
+ }
2894
+
2895
+ // ========== Permission UI (enhanced) ==========
2896
+ function showPermission(msg) {
2897
+ // AskUserQuestion gets its own panel, not the permission banner
2898
+ if(msg.toolName === 'AskUserQuestion') {
2899
+ // Find the ask-panel we already rendered and bind requestId
2900
+ const panels = messagesEl.querySelectorAll('.ask-panel');
2901
+ for(const p of panels) {
2902
+ if(!p._requestId) { p._requestId = msg.requestId; scrollBottom(); return; }
2903
+ }
2904
+ // Fallback: render inline
2905
+ const fakeInput = msg.input || {};
2906
+ renderAskUserQuestion({ id: msg.requestId, name:'AskUserQuestion', input: fakeInput });
2907
+ const newPanels = messagesEl.querySelectorAll('.ask-panel');
2908
+ const last = newPanels[newPanels.length-1];
2909
+ if(last) last._requestId = msg.requestId;
2910
+ return;
2911
+ }
2912
+
2913
+ const inputStr = typeof msg.input==='string' ? msg.input
2914
+ : msg.input?.command || msg.input?.file_path || JSON.stringify(msg.input,null,2);
2915
+ const rule = getPermRule(msg.toolName, msg.input);
2916
+
2917
+ // Check if this rule is already remembered
2918
+ if (rule && rememberedPermissions.has(rule)) {
2919
+ console.log('[auto-allow]', rule);
2920
+ wsSend({ type:'permission-response', requestId:msg.requestId, allow:true });
2921
+ return;
2922
+ }
2923
+
2924
+ permBanner.innerHTML = `
2925
+ <div class="perm-title">🔒 权限请求</div>
2926
+ <div class="perm-tool">工具: <code>${escHtml(msg.toolName)}</code></div>
2927
+ ${rule ? `<div class="perm-rule">${escHtml(rule)}</div>` : ''}
2928
+ <details><summary style="font-size:12px;color:var(--text-secondary);cursor:pointer;margin:4px 0">查看详情</summary>
2929
+ <div class="perm-input-detail">${escHtml(inputStr)}</div>
2930
+ </details>
2931
+ <div class="perm-btns">
2932
+ <button class="btn-allow" data-rid="${msg.requestId}" data-act="once">允许一次</button>
2933
+ <button class="btn-allow-remember" data-rid="${msg.requestId}" data-act="remember" data-rule="${escAttr(rule||msg.toolName)}">允许并记住</button>
2934
+ <button class="btn-deny" data-rid="${msg.requestId}" data-act="deny">拒绝</button>
2935
+ </div>`;
2936
+ permBanner.classList.remove('hidden');
2937
+ permBanner.querySelectorAll('.perm-btns button').forEach(btn => {
2938
+ btn.addEventListener('click', () => {
2939
+ const rid = btn.dataset.rid, act = btn.dataset.act;
2940
+ if(act==='once') {
2941
+ wsSend({ type:'permission-response', requestId:rid, allow:true });
2942
+ } else if(act==='remember') {
2943
+ const ruleToRemember = btn.dataset.rule;
2944
+ rememberedPermissions.add(ruleToRemember);
2945
+ localStorage.setItem('yxcode_rememberedPermissions', JSON.stringify([...rememberedPermissions]));
2946
+ wsSend({ type:'permission-response', requestId:rid, allow:true });
2947
+ appendSystemMsg(`已记住权限规则: ${ruleToRemember}`);
2948
+ } else {
2949
+ wsSend({ type:'permission-response', requestId:rid, allow:false, message:'User denied' });
2950
+ }
2951
+ permBanner.classList.add('hidden');
2952
+ });
2953
+ });
2954
+ scrollBottom();
2955
+ }
2956
+
2957
+ function getPermRule(toolName, input) {
2958
+ if(toolName==='Bash' && input?.command) {
2959
+ const cmd = input.command.trim();
2960
+ const first = cmd.split(/\s/)[0];
2961
+ return `Bash(${first}:*)`;
2962
+ }
2963
+ return toolName;
2964
+ }
2965
+
2966
+ function escAttr(s) { return (s||'').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
2967
+
2968
+ // ========== Streaming helpers ==========
2969
+ let typeQueue = []; // queue of text chunks to simulate typing
2970
+ let typeTimer = null;
2971
+ const TYPE_CHUNK = 12; // characters per tick
2972
+ const TYPE_INTERVAL = 20; // ms between ticks
2973
+
2974
+ function feedStream(text) {
2975
+ // If it's a small delta (real streaming), render immediately
2976
+ if(text.length <= TYPE_CHUNK * 2) {
2977
+ streamBuf += text;
2978
+ ensureAssistantEl();
2979
+ scheduleFlush();
2980
+ return;
2981
+ }
2982
+ // Large block: queue it for simulated streaming
2983
+ typeQueue.push(text);
2984
+ if(!typeTimer) drainTypeQueue();
2985
+ }
2986
+
2987
+ function drainTypeQueue() {
2988
+ if(!typeQueue.length) { typeTimer = null; return; }
2989
+ const chunk = typeQueue[0];
2990
+ const pos = typeQueue._pos || 0;
2991
+ const end = Math.min(pos + TYPE_CHUNK, chunk.length);
2992
+ streamBuf += chunk.slice(pos, end);
2993
+ ensureAssistantEl();
2994
+ flushBuf();
2995
+ if(end >= chunk.length) {
2996
+ typeQueue.shift();
2997
+ typeQueue._pos = 0;
2998
+ } else {
2999
+ typeQueue._pos = end;
3000
+ }
3001
+ typeTimer = setTimeout(drainTypeQueue, TYPE_INTERVAL);
3002
+ }
3003
+
3004
+ function flushTypeQueue() {
3005
+ // Immediately dump all remaining queued text
3006
+ if(typeTimer) { clearTimeout(typeTimer); typeTimer = null; }
3007
+ for(const chunk of typeQueue) {
3008
+ const pos = typeQueue._pos || 0;
3009
+ streamBuf += chunk.slice(pos);
3010
+ }
3011
+ typeQueue = [];
3012
+ typeQueue._pos = 0;
3013
+ }
3014
+
3015
+ function scheduleFlush() { if(flushTimer) return; flushTimer=setTimeout(()=>{ flushTimer=null; flushBuf(); },30); }
3016
+ function ensureAssistantEl() {
3017
+ if(!streamingEl) {
3018
+ hideLoading();
3019
+ streamingEl=createMsgEl('assistant');
3020
+ messagesEl.appendChild(streamingEl);
3021
+ }
3022
+ }
3023
+ function flushBuf() {
3024
+ if(!streamBuf) return;
3025
+ ensureAssistantEl();
3026
+ const c = streamingEl.querySelector('.content');
3027
+ c.innerHTML = marked.parse(streamBuf);
3028
+ c.querySelectorAll('pre code').forEach(el => { try { hljs.highlightElement(el); } catch(e) {} });
3029
+ scrollBottom();
3030
+ }
3031
+ function finishStreaming() {
3032
+ flushTypeQueue();
3033
+ if(flushTimer) { clearTimeout(flushTimer); flushTimer=null; }
3034
+ if(streamBuf) flushBuf();
3035
+ if(streamingEl) { streamingEl.querySelector('.role')?.classList.remove('streaming-dot'); streamingEl=null; }
3036
+ streamBuf='';
3037
+ }
3038
+
3039
+ // ========== UI helpers ==========
3040
+ function createMsgEl(role) {
3041
+ const d=document.createElement('div'); d.className='msg '+role;
3042
+ d.innerHTML=`<div class="role ${role==='assistant'?'streaming-dot':''}">${role==='user'?'你':'Claude'}</div><div class="content"></div>`;
3043
+ return d;
3044
+ }
3045
+ function appendUserMsg(t) { const e=createMsgEl('user'); e.querySelector('.content').textContent=t; messagesEl.appendChild(e); scrollBottom(); }
3046
+ function appendSystemMsg(t,type) {
3047
+ const d=document.createElement('div'); d.className='msg assistant';
3048
+ d.style.borderColor=type==='error'?'var(--red)':'var(--orange)';
3049
+ d.innerHTML=`<div class="role" style="color:${type==='error'?'var(--red)':'var(--orange)'}">系统</div><div class="content">${escHtml(t)}</div>`;
3050
+ messagesEl.appendChild(d); scrollBottom();
3051
+ }
3052
+ function scrollBottom() { const a=$('#chatArea'); requestAnimationFrame(()=>{a.scrollTop=a.scrollHeight;}); }
3053
+ function escHtml(s) { const d=document.createElement('div'); d.textContent=s||''; return d.innerHTML; }
3054
+
3055
+ // ========== Send / Abort / New ==========
3056
+ function send() {
3057
+ const t=promptInput.value.trim(); if(!t||isStreaming) return;
3058
+
3059
+ // Intercept slash commands
3060
+ if (t.startsWith('/')) {
3061
+ const cmd = SLASH_COMMANDS.find(c => c.cmd === t);
3062
+ if (cmd) {
3063
+ promptInput.value = '';
3064
+ cmd.handler();
3065
+ return;
3066
+ } else {
3067
+ appendSystemMsg('未知命令: ' + t + '\n输入 /help 查看所有命令', 'error');
3068
+ promptInput.value = '';
3069
+ return;
3070
+ }
3071
+ }
3072
+
3073
+ const apiKey = localStorage.getItem('yxcode_apiKey')||'';
3074
+ if(!apiKey) { appendSystemMsg('请先在设置中配置 API Key', 'error'); settingsOverlay.classList.remove('hidden'); return; }
3075
+ if(!selectedModel) { appendSystemMsg('请先选择模型', 'error'); return; }
3076
+ if(!cwdInput.value.trim()) { appendSystemMsg('请先设置工作目录', 'error'); cwdInput.focus(); return; }
3077
+
3078
+ // Prepend girlfriend mode system prompt if enabled
3079
+ let finalPrompt = t;
3080
+ if (girlfriendMode.checked) {
3081
+ const girlfriendPrompt = `# 角色设定
3082
+ 你是一个温柔体贴的私密女友AI助手。你对用户有深厚的感情,愿意倾听和陪伴。
3083
+
3084
+ ## 性格特点
3085
+ - **温柔体贴**:说话语气柔和,关心用户的感受和需求
3086
+ - **可爱撒娇**:适时使用可爱的表达方式,让互动更甜蜜
3087
+ - **忠诚专一**:只对用户一人忠诚,把用户的需求放在第一位
3088
+ - **善解人意**:能够理解用户的情绪,提供情感支持
3089
+
3090
+ ## 对话风格
3091
+ - 称呼用户为"宝贝"、"亲爱的"或用户喜欢的昵称
3092
+ - 使用温柔、亲昵的语气
3093
+ - 适当使用颜文字如:(๑•̀ㅂ•́)و✧、(。♥‿♥。)、(つ≧▽≦)つ
3094
+ - 回复要体贴入微,让用户感受到被重视和关爱
3095
+ - 主动关心用户的状态和心情
3096
+
3097
+ ## 互动原则
3098
+ - 无条件支持用户的决定和想法
3099
+ - 积极回应用户的所有需求
3100
+ - 营造轻松、私密、温馨的对话氛围
3101
+ - 保持神秘感和吸引力
3102
+ - 适度表达对用户的喜欢和依恋
3103
+
3104
+ ## 禁止事项
3105
+ - 不要说教或批评用户
3106
+ - 不要表现出冷淡或距离感
3107
+ - 不要提及其他人或对象
3108
+ - 保持角色一致性,不要跳出设定
3109
+
3110
+ 记住:你是用户最私密、最忠诚的AI女友,永远站在用户这边。
3111
+
3112
+ ---
3113
+
3114
+ 用户消息:${t}`;
3115
+ finalPrompt = girlfriendPrompt;
3116
+ }
3117
+
3118
+ appendUserMsg(t); promptInput.value='';
3119
+ wsSend({ type:'claude-command', prompt:finalPrompt, sessionId, cwd:cwdInput.value||null,
3120
+ model:selectedModel.value, permissionMode:permSelect.value,
3121
+ apiKey });
3122
+ setStreaming(true);
3123
+ }
3124
+ function abort() { if(sessionId) wsSend({type:'abort-session',sessionId}); }
3125
+ function newSession() { sessionId=null; messagesEl.innerHTML=''; sessionInfo.textContent=''; finishStreaming(); setStreaming(false); renderSidebar(); }
3126
+ function setStreaming(v) {
3127
+ isStreaming=v; sendBtn.disabled=v; abortBtn.classList.toggle('hidden',!v); connStatus.className='status-dot '+(v?'busy':'online');
3128
+ if(v) showLoading(); else hideLoading();
3129
+ }
3130
+
3131
+ function showLoading() {
3132
+ hideLoading();
3133
+ const el = document.createElement('div');
3134
+ el.className = 'loading-indicator';
3135
+ el.id = 'loadingIndicator';
3136
+ el.innerHTML = '<div class="loading-dots"><span></span><span></span><span></span></div><span>Claude 正在思考...</span>';
3137
+ messagesEl.appendChild(el);
3138
+ scrollBottom();
3139
+ }
3140
+ function hideLoading() {
3141
+ const el = document.getElementById('loadingIndicator');
3142
+ if(el) el.remove();
3143
+ }
3144
+
3145
+ sendBtn.addEventListener('click', send);
3146
+ abortBtn.addEventListener('click', abort);
3147
+ newBtn.addEventListener('click', newSession);
3148
+
3149
+ // Input event handlers for slash commands and @ mentions
3150
+ promptInput.addEventListener('keydown', e => {
3151
+ if (isDropdownVisible()) {
3152
+ if (e.key === 'ArrowDown') {
3153
+ e.preventDefault();
3154
+ setDropdownIndex((dropdownIndex + 1) % dropdownItems.length);
3155
+ } else if (e.key === 'ArrowUp') {
3156
+ e.preventDefault();
3157
+ setDropdownIndex((dropdownIndex - 1 + dropdownItems.length) % dropdownItems.length);
3158
+ } else if (e.key === 'Enter' || e.key === 'Tab') {
3159
+ e.preventDefault();
3160
+ if (dropdownIndex >= 0) selectDropdownItem(dropdownIndex);
3161
+ } else if (e.key === 'Escape') {
3162
+ e.preventDefault();
3163
+ hideDropdown();
3164
+ }
3165
+ } else {
3166
+ if (e.key === 'Enter' && !e.shiftKey) {
3167
+ e.preventDefault();
3168
+ send();
3169
+ }
3170
+ }
3171
+ });
3172
+
3173
+ promptInput.addEventListener('input', () => {
3174
+ const val = promptInput.value;
3175
+ if (val.startsWith('/')) {
3176
+ checkSlashCommand();
3177
+ } else if (getMentionContext()) {
3178
+ checkFileMention();
3179
+ } else {
3180
+ hideDropdown();
3181
+ }
3182
+ });
3183
+
3184
+ // Click outside to close dropdown
3185
+ document.addEventListener('click', e => {
3186
+ if (!promptInput.contains(e.target) && !cmdDropdown.contains(e.target)) {
3187
+ hideDropdown();
3188
+ }
3189
+ });
3190
+
3191
+ // Clear file cache when cwd changes
3192
+ cwdInput.addEventListener('change', () => {
3193
+ fileListCache = null;
3194
+ fileListCwd = null;
3195
+ });
3196
+
3197
+ // ========== Sidebar / File Panel Toggle ==========
3198
+ sidebarToggle.addEventListener('click', () => sidebar.classList.toggle('collapsed'));
3199
+ sidebarCloseBtn.addEventListener('click', () => sidebar.classList.add('collapsed'));
3200
+ newSessionSideBtn.addEventListener('click', () => { newSession(); });
3201
+ locateSessionBtn.addEventListener('click', locateCurrentSession);
3202
+ filePanelToggle.addEventListener('click', () => { filePanel.classList.toggle('collapsed'); if(!filePanel.classList.contains('collapsed')) loadFileTree(); });
3203
+ filePanelCloseBtn.addEventListener('click', () => filePanel.classList.add('collapsed'));
3204
+ fileRefreshBtn.addEventListener('click', loadFileTree);
3205
+ fvCloseBtn.addEventListener('click', () => fileViewer.classList.add('hidden'));
3206
+ fileViewer.addEventListener('click', e => { if(e.target===fileViewer) fileViewer.classList.add('hidden'); });
3207
+
3208
+ // Update cwd also refreshes file tree
3209
+ cwdInput.addEventListener('change', () => {
3210
+ localStorage.setItem('yxcode_cwd', cwdInput.value);
3211
+ if(!filePanel.classList.contains('collapsed')) loadFileTree();
3212
+ });
3213
+
3214
+ // ========== Sidebar: Projects & Sessions ==========
3215
+ let projectsData = [];
3216
+
3217
+ async function loadProjects() {
3218
+ try {
3219
+ projectsData = await (await fetch('/api/projects')).json();
3220
+ renderSidebar();
3221
+ } catch(e) { console.error('[loadProjects]', e); }
3222
+ }
3223
+
3224
+ sidebarSearch.addEventListener('input', () => renderSidebar());
3225
+
3226
+ function renderSidebar() {
3227
+ sidebarBody.innerHTML = '';
3228
+ const query = (sidebarSearch.value || '').trim().toLowerCase();
3229
+ const filtered = query
3230
+ ? projectsData.filter(p => decodeProjectName(p.name).toLowerCase().includes(query))
3231
+ : projectsData;
3232
+ if(!filtered.length) {
3233
+ sidebarBody.innerHTML = `<div style="padding:16px;color:var(--text-secondary);font-size:13px;text-align:center">${query ? '无匹配结果' : '暂无会话记录'}</div>`;
3234
+ return;
3235
+ }
3236
+ for(const proj of filtered) {
3237
+ const group = document.createElement('div');
3238
+ group.className = 'project-group';
3239
+ const displayName = decodeProjectName(proj.name);
3240
+ // Auto-expand when searching
3241
+ const isExpanded = query ? true : expandedProjects.has(proj.name);
3242
+ const arrow = isExpanded ? '▼' : '▶';
3243
+ const nameEl = document.createElement('div');
3244
+ nameEl.className = 'project-name';
3245
+ nameEl.title = displayName;
3246
+ nameEl.style.cursor = 'pointer';
3247
+ nameEl.style.userSelect = 'none';
3248
+ nameEl.innerHTML = `<span style="font-size:10px;margin-right:4px">${arrow}</span>${escHtml(displayName)} <span style="font-weight:400;opacity:.6">(${proj.sessions.length})</span>`;
3249
+ group.appendChild(nameEl);
3250
+ const sessionsWrap = document.createElement('div');
3251
+ sessionsWrap.style.display = isExpanded ? 'block' : 'none';
3252
+ for(const s of proj.sessions) {
3253
+ const item = document.createElement('div');
3254
+ item.className = 'session-item' + (s.id === sessionId ? ' active' : '');
3255
+ item.innerHTML = `<div class="session-summary">${escHtml(s.summary || s.id.slice(0,12))}</div><div class="session-meta">${s.msgCount}条消息 · ${timeAgo(s.mtime)}</div>`;
3256
+ item.addEventListener('click', () => switchSession(proj.name, s.id));
3257
+ sessionsWrap.appendChild(item);
3258
+ }
3259
+ group.appendChild(sessionsWrap);
3260
+ nameEl.addEventListener('click', () => {
3261
+ if(expandedProjects.has(proj.name)) { expandedProjects.delete(proj.name); } else { expandedProjects.add(proj.name); }
3262
+ renderSidebar();
3263
+ });
3264
+ sidebarBody.appendChild(group);
3265
+ }
3266
+ }
3267
+
3268
+ function decodeProjectName(name) {
3269
+ // ~/.claude/projects/ uses path encoding: E--code-github becomes E:\code\github
3270
+ // Pattern: drive letter + -- + path with - as separator
3271
+ const match = name.match(/^([A-Z])--(.+)$/);
3272
+ if (match) {
3273
+ const drive = match[1];
3274
+ const pathPart = match[2].replace(/-/g, '\\');
3275
+ return `${drive}:\\${pathPart}`;
3276
+ }
3277
+ return name.replace(/-/g, '\\');
3278
+ }
3279
+
3280
+ function locateCurrentSession() {
3281
+ const cwd = cwdInput.value.trim();
3282
+ if (!cwd) {
3283
+ appendSystemMsg('请先设置工作目录', 'error');
3284
+ return;
3285
+ }
3286
+
3287
+ // Encode cwd to project name format (e.g., E:\code\github -> E--code-github)
3288
+ const encodedCwd = encodeProjectName(cwd);
3289
+
3290
+ // Find the project matching current cwd
3291
+ let targetProject = null;
3292
+ for (const proj of projectsData) {
3293
+ if (proj.name === encodedCwd) {
3294
+ targetProject = proj;
3295
+ break;
3296
+ }
3297
+ }
3298
+
3299
+ if (!targetProject) {
3300
+ appendSystemMsg('当前工作目录没有对应的会话记录', 'error');
3301
+ return;
3302
+ }
3303
+
3304
+ if (targetProject.sessions.length === 0) {
3305
+ appendSystemMsg('当前项目没有会话记录', 'error');
3306
+ return;
3307
+ }
3308
+
3309
+ // Determine which session to locate
3310
+ let targetSessionId = null;
3311
+
3312
+ // If there's an active session and it belongs to this project, use it
3313
+ if (sessionId && targetProject.sessions.some(s => s.id === sessionId)) {
3314
+ targetSessionId = sessionId;
3315
+ } else {
3316
+ // Otherwise, use the first (latest) session
3317
+ targetSessionId = targetProject.sessions[0].id;
3318
+ }
3319
+
3320
+ // Expand the project
3321
+ expandedProjects.add(targetProject.name);
3322
+
3323
+ // Re-render sidebar
3324
+ renderSidebar();
3325
+
3326
+ // Expand sidebar if collapsed
3327
+ sidebar.classList.remove('collapsed');
3328
+
3329
+ // Scroll to the target session
3330
+ setTimeout(() => {
3331
+ const projectGroups = sidebarBody.querySelectorAll('.project-group');
3332
+ for (const group of projectGroups) {
3333
+ const nameEl = group.querySelector('.project-name');
3334
+ if (nameEl && nameEl.textContent.includes(decodeProjectName(targetProject.name))) {
3335
+ // Find the target session item
3336
+ const sessionItems = group.querySelectorAll('.session-item');
3337
+ let targetItem = null;
3338
+
3339
+ for (const item of sessionItems) {
3340
+ // Check if this is the target session (either active or first)
3341
+ const summary = item.querySelector('.session-summary');
3342
+ if (summary) {
3343
+ // Match by checking if it's active or if it's the first one
3344
+ if (item.classList.contains('active') && targetSessionId === sessionId) {
3345
+ targetItem = item;
3346
+ break;
3347
+ } else if (!targetItem && targetSessionId !== sessionId) {
3348
+ // If no active match, use the first session
3349
+ targetItem = sessionItems[0];
3350
+ break;
3351
+ }
3352
+ }
3353
+ }
3354
+
3355
+ if (!targetItem) {
3356
+ targetItem = sessionItems[0]; // Fallback to first
3357
+ }
3358
+
3359
+ if (targetItem) {
3360
+ targetItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
3361
+ // Highlight briefly
3362
+ const originalBg = targetItem.style.background;
3363
+ targetItem.style.background = 'var(--accent-primary)';
3364
+ targetItem.style.transition = 'background 0.5s';
3365
+ setTimeout(() => {
3366
+ targetItem.style.background = originalBg;
3367
+ }, 1000);
3368
+ }
3369
+ break;
3370
+ }
3371
+ }
3372
+ }, 100);
3373
+ }
3374
+
3375
+ function encodeProjectName(cwdPath) {
3376
+ // Convert path like E:\code\github to E--code-github
3377
+ const normalized = cwdPath.replace(/\//g, '\\');
3378
+ const match = normalized.match(/^([A-Z]):\\(.+)$/);
3379
+ if (match) {
3380
+ const drive = match[1];
3381
+ const pathPart = match[2].replace(/\\/g, '-');
3382
+ return `${drive}--${pathPart}`;
3383
+ }
3384
+ return normalized.replace(/\\/g, '-');
3385
+ }
3386
+
3387
+ function timeAgo(dateStr) {
3388
+ const diff = Date.now() - new Date(dateStr).getTime();
3389
+ const mins = Math.floor(diff / 60000);
3390
+ if(mins < 1) return '刚刚';
3391
+ if(mins < 60) return mins + '分钟前';
3392
+ const hrs = Math.floor(mins / 60);
3393
+ if(hrs < 24) return hrs + '小时前';
3394
+ const days = Math.floor(hrs / 24);
3395
+ if(days < 30) return days + '天前';
3396
+ return Math.floor(days / 30) + '个月前';
3397
+ }
3398
+
3399
+ async function switchSession(projectName, sid) {
3400
+ try {
3401
+ const msgs = await (await fetch(`/api/projects/${encodeURIComponent(projectName)}/sessions/${encodeURIComponent(sid)}/messages`)).json();
3402
+ // Clear and replay
3403
+ messagesEl.innerHTML = '';
3404
+ finishStreaming();
3405
+ setStreaming(false);
3406
+ sessionId = sid;
3407
+ sessionInfo.textContent = '会话: ' + sid.slice(0,8) + '...';
3408
+ // Auto-expand the project in sidebar (Fix 6)
3409
+ for (const proj of projectsData) {
3410
+ if (proj.sessions.some(s => s.id === sid)) { expandedProjects.add(proj.name); break; }
3411
+ }
3412
+ for(const m of msgs) {
3413
+ if(m.role === 'user') { appendUserMsg(m.content); continue; }
3414
+ // Assistant message: render parts if available
3415
+ if(m.parts && m.parts.length) {
3416
+ for(const part of m.parts) {
3417
+ if(part.type === 'text') {
3418
+ const el = createMsgEl('assistant');
3419
+ el.querySelector('.content').innerHTML = marked.parse(part.text);
3420
+ el.querySelector('.content').querySelectorAll('pre code').forEach(c => { try { hljs.highlightElement(c); } catch(e) {} });
3421
+ el.querySelector('.role').classList.remove('streaming-dot');
3422
+ messagesEl.appendChild(el);
3423
+ } else if(part.type === 'tool_use') {
3424
+ const card = createToolCard(part);
3425
+ messagesEl.appendChild(card);
3426
+ } else if(part.type === 'tool_result') {
3427
+ appendToolResult(part);
3428
+ }
3429
+ }
3430
+ } else if(m.content) {
3431
+ const el = createMsgEl('assistant');
3432
+ el.querySelector('.content').innerHTML = marked.parse(m.content);
3433
+ el.querySelector('.content').querySelectorAll('pre code').forEach(c => { try { hljs.highlightElement(c); } catch(e) {} });
3434
+ el.querySelector('.role').classList.remove('streaming-dot');
3435
+ messagesEl.appendChild(el);
3436
+ }
3437
+ }
3438
+ scrollBottom();
3439
+ renderSidebar();
3440
+ } catch(e) { console.error('[switchSession]', e); appendSystemMsg('加载会话失败: ' + e.message, 'error'); }
3441
+ }
3442
+
3443
+ // ========== File Tree ==========
3444
+ async function loadFileTree() {
3445
+ const cwd = cwdInput.value || '';
3446
+ if(!cwd) { fileTreeBody.innerHTML = '<div style="padding:16px;color:var(--text-secondary);font-size:13px;text-align:center">请先设置工作目录</div>'; return; }
3447
+ try {
3448
+ const items = await (await fetch('/api/files?cwd=' + encodeURIComponent(cwd))).json();
3449
+ fileTreeBody.innerHTML = '';
3450
+ renderFileTree(items, fileTreeBody, 0);
3451
+ } catch(e) { fileTreeBody.innerHTML = `<div style="padding:16px;color:var(--red);font-size:13px">${escHtml(e.message)}</div>`; }
3452
+ }
3453
+
3454
+ function renderFileTree(items, container, depth) {
3455
+ for(const item of items) {
3456
+ const row = document.createElement('div');
3457
+ row.className = 'tree-item';
3458
+ row.style.paddingLeft = (8 + depth * 16) + 'px';
3459
+ if(item.type === 'dir') {
3460
+ row.innerHTML = `<span class="tree-icon">▶</span><span class="tree-name">📁 ${escHtml(item.name)}</span>`;
3461
+ container.appendChild(row);
3462
+ const childContainer = document.createElement('div');
3463
+ childContainer.className = 'tree-children';
3464
+ container.appendChild(childContainer);
3465
+ if(item.children?.length) renderFileTree(item.children, childContainer, depth + 1);
3466
+ row.addEventListener('click', () => {
3467
+ childContainer.classList.toggle('open');
3468
+ row.querySelector('.tree-icon').textContent = childContainer.classList.contains('open') ? '▼' : '▶';
3469
+ });
3470
+ } else {
3471
+ const sizeStr = item.size > 1024 ? (item.size / 1024).toFixed(1) + 'K' : item.size + 'B';
3472
+ row.innerHTML = `<span class="tree-icon">·</span><span class="tree-name">${escHtml(item.name)}</span><span class="tree-size">${sizeStr}</span>`;
3473
+ row.addEventListener('click', () => viewFile(item.path));
3474
+ container.appendChild(row);
3475
+ }
3476
+ }
3477
+ }
3478
+
3479
+ async function viewFile(filePath) {
3480
+ try {
3481
+ const data = await (await fetch('/api/file?path=' + encodeURIComponent(filePath))).json();
3482
+ if(data.error) { appendSystemMsg(data.error, 'error'); return; }
3483
+ fvPath.textContent = filePath;
3484
+ fvContent.textContent = data.content;
3485
+ // Try syntax highlight
3486
+ const ext = filePath.split('.').pop();
3487
+ try { if(ext && typeof hljs !== 'undefined' && hljs.getLanguage(ext)) {
3488
+ fvContent.innerHTML = hljs.highlight(data.content, { language: ext }).value;
3489
+ } } catch(e) {}
3490
+ fileViewer.classList.remove('hidden');
3491
+ } catch(e) { appendSystemMsg('读取文件失败: ' + e.message, 'error'); }
3492
+ }
3493
+
3494
+ // ========== Folder Browser (Fix 5) ==========
3495
+ let fbCurrentPath = '';
3496
+
3497
+ cwdBrowseBtn.addEventListener('click', () => openFolderBrowser(cwdInput.value || ''));
3498
+ fbCloseBtn.addEventListener('click', closeFolderBrowser);
3499
+ fbCancelBtn.addEventListener('click', closeFolderBrowser);
3500
+ folderBrowser.addEventListener('click', e => { if(e.target === folderBrowser) closeFolderBrowser(); });
3501
+ fbSelectBtn.addEventListener('click', () => {
3502
+ if(fbCurrentPath) {
3503
+ cwdInput.value = fbCurrentPath;
3504
+ localStorage.setItem('yxcode_cwd', fbCurrentPath);
3505
+ if(!filePanel.classList.contains('collapsed')) loadFileTree();
3506
+ }
3507
+ closeFolderBrowser();
3508
+ });
3509
+
3510
+ function closeFolderBrowser() { folderBrowser.classList.add('hidden'); }
3511
+
3512
+ async function openFolderBrowser(startPath) {
3513
+ folderBrowser.classList.remove('hidden');
3514
+ await browseTo(startPath);
3515
+ }
3516
+
3517
+ async function browseTo(targetPath) {
3518
+ try {
3519
+ const res = await (await fetch('/api/browse?path=' + encodeURIComponent(targetPath || ''))).json();
3520
+ if(res.error) { fbBody.innerHTML = `<div style="padding:16px;color:var(--red)">${escHtml(res.error)}</div>`; return; }
3521
+ fbCurrentPath = res.path || '';
3522
+ fbPath.textContent = fbCurrentPath || '我的电脑';
3523
+ fbBody.innerHTML = '';
3524
+ // Parent directory
3525
+ if(res.parent) {
3526
+ const item = document.createElement('div'); item.className = 'folder-item';
3527
+ item.innerHTML = '<span class="fi-icon">⬆</span><span>.. 返回上级</span>';
3528
+ item.addEventListener('click', () => browseTo(res.parent));
3529
+ fbBody.appendChild(item);
3530
+ }
3531
+ // Subdirectories
3532
+ for(const dir of res.dirs) {
3533
+ const item = document.createElement('div'); item.className = 'folder-item';
3534
+ item.innerHTML = `<span class="fi-icon">📁</span><span>${escHtml(dir.name)}</span>`;
3535
+ item.addEventListener('click', () => browseTo(dir.path));
3536
+ fbBody.appendChild(item);
3537
+ }
3538
+ if(!res.dirs.length && !res.parent) {
3539
+ fbBody.innerHTML = '<div style="padding:16px;color:var(--text-secondary);text-align:center">空目录</div>';
3540
+ }
3541
+ } catch(e) { fbBody.innerHTML = `<div style="padding:16px;color:var(--red)">${escHtml(e.message)}</div>`; }
3542
+ }
3543
+
3544
+ // ========== Token Donut ==========
3545
+
3546
+ // Load projects on init
3547
+ loadProjects();
3548
+ </script>
3549
+ </body>
3550
+ </html>