claude-opencode-viewer 2.4.2 → 2.6.0
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.
- package/index.html +670 -4
- package/package.json +1 -1
- package/server.js +95 -3
package/index.html
CHANGED
|
@@ -487,6 +487,306 @@
|
|
|
487
487
|
border-color: #555;
|
|
488
488
|
color: #fff;
|
|
489
489
|
}
|
|
490
|
+
|
|
491
|
+
/* 选择模式:原位文本层 */
|
|
492
|
+
#terminal.select-mode .xterm-screen {
|
|
493
|
+
visibility: hidden;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
#select-text-layer {
|
|
497
|
+
display: none;
|
|
498
|
+
position: absolute;
|
|
499
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
500
|
+
overflow-y: auto;
|
|
501
|
+
-webkit-overflow-scrolling: touch;
|
|
502
|
+
background: #0a0a0a;
|
|
503
|
+
padding: 4px 8px;
|
|
504
|
+
touch-action: auto;
|
|
505
|
+
z-index: 10;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
#select-text-layer.visible {
|
|
509
|
+
display: block;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
#select-hint {
|
|
513
|
+
position: sticky;
|
|
514
|
+
top: 0;
|
|
515
|
+
background: rgba(30,30,30,0.95);
|
|
516
|
+
color: #888;
|
|
517
|
+
font-size: 11px;
|
|
518
|
+
text-align: center;
|
|
519
|
+
padding: 6px 0;
|
|
520
|
+
border-bottom: 1px solid #333;
|
|
521
|
+
z-index: 1;
|
|
522
|
+
-webkit-user-select: none;
|
|
523
|
+
user-select: none;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
#select-text-layer pre {
|
|
527
|
+
margin: 0;
|
|
528
|
+
color: #d4d4d4;
|
|
529
|
+
font-family: Menlo, Monaco, "Courier New", monospace;
|
|
530
|
+
font-size: 11px;
|
|
531
|
+
line-height: 1.4;
|
|
532
|
+
white-space: pre-wrap;
|
|
533
|
+
word-break: break-all;
|
|
534
|
+
-webkit-user-select: text;
|
|
535
|
+
user-select: text;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
#select-mode-close {
|
|
539
|
+
position: absolute;
|
|
540
|
+
top: 6px;
|
|
541
|
+
right: 6px;
|
|
542
|
+
z-index: 20;
|
|
543
|
+
display: none;
|
|
544
|
+
background: rgba(50,50,50,0.9);
|
|
545
|
+
border: 1px solid #555;
|
|
546
|
+
color: #ccc;
|
|
547
|
+
width: 28px;
|
|
548
|
+
height: 28px;
|
|
549
|
+
border-radius: 50%;
|
|
550
|
+
font-size: 14px;
|
|
551
|
+
line-height: 26px;
|
|
552
|
+
text-align: center;
|
|
553
|
+
cursor: pointer;
|
|
554
|
+
-webkit-user-select: none;
|
|
555
|
+
user-select: none;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/* 复制成功提示 */
|
|
559
|
+
#copy-toast {
|
|
560
|
+
display: none;
|
|
561
|
+
position: fixed;
|
|
562
|
+
top: 50%;
|
|
563
|
+
left: 50%;
|
|
564
|
+
transform: translate(-50%, -50%);
|
|
565
|
+
background: rgba(40, 167, 69, 0.9);
|
|
566
|
+
color: #fff;
|
|
567
|
+
padding: 10px 24px;
|
|
568
|
+
border-radius: 8px;
|
|
569
|
+
font-size: 14px;
|
|
570
|
+
z-index: 9999;
|
|
571
|
+
pointer-events: none;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#copy-toast.show {
|
|
575
|
+
display: block;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* Git Diff 面板 */
|
|
579
|
+
#git-diff-bar {
|
|
580
|
+
display: none;
|
|
581
|
+
position: absolute;
|
|
582
|
+
top: 0;
|
|
583
|
+
left: 0;
|
|
584
|
+
right: 0;
|
|
585
|
+
bottom: 0;
|
|
586
|
+
background: #0a0a0a;
|
|
587
|
+
z-index: 1000;
|
|
588
|
+
flex-direction: column;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
#git-diff-bar.visible {
|
|
592
|
+
display: flex;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
#git-diff-header {
|
|
596
|
+
display: flex;
|
|
597
|
+
align-items: center;
|
|
598
|
+
justify-content: space-between;
|
|
599
|
+
padding: 12px 16px;
|
|
600
|
+
background: #111;
|
|
601
|
+
border-bottom: 1px solid #222;
|
|
602
|
+
flex-shrink: 0;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
#git-diff-title {
|
|
606
|
+
font-size: 14px;
|
|
607
|
+
color: #ddd;
|
|
608
|
+
font-weight: 600;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.git-diff-file-list {
|
|
612
|
+
height: 250px;
|
|
613
|
+
flex-shrink: 0;
|
|
614
|
+
overflow-y: auto;
|
|
615
|
+
border-bottom: 1px solid #2a2a2a;
|
|
616
|
+
-webkit-overflow-scrolling: touch;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.git-diff-file-item {
|
|
620
|
+
display: flex;
|
|
621
|
+
align-items: center;
|
|
622
|
+
padding: 6px 12px;
|
|
623
|
+
cursor: pointer;
|
|
624
|
+
color: #ccc;
|
|
625
|
+
font-size: 13px;
|
|
626
|
+
gap: 8px;
|
|
627
|
+
white-space: nowrap;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.git-diff-file-item:hover {
|
|
631
|
+
background: #1a1a1a;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.git-diff-file-item.active {
|
|
635
|
+
background: rgba(74, 158, 255, 0.12);
|
|
636
|
+
color: #fff;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.git-diff-file-status {
|
|
640
|
+
width: 18px;
|
|
641
|
+
flex-shrink: 0;
|
|
642
|
+
font-size: 11px;
|
|
643
|
+
font-weight: 700;
|
|
644
|
+
text-align: center;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.git-diff-file-name {
|
|
648
|
+
overflow: hidden;
|
|
649
|
+
text-overflow: ellipsis;
|
|
650
|
+
flex: 1;
|
|
651
|
+
font-family: Menlo, Monaco, monospace;
|
|
652
|
+
font-size: 12px;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.git-diff-content-area {
|
|
656
|
+
flex: 1;
|
|
657
|
+
display: flex;
|
|
658
|
+
flex-direction: column;
|
|
659
|
+
min-height: 0;
|
|
660
|
+
overflow: hidden;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.git-diff-content-header {
|
|
664
|
+
display: flex;
|
|
665
|
+
align-items: center;
|
|
666
|
+
gap: 10px;
|
|
667
|
+
padding: 8px 12px;
|
|
668
|
+
border-bottom: 1px solid #2a2a2a;
|
|
669
|
+
background: #111;
|
|
670
|
+
flex-shrink: 0;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.git-diff-content-path {
|
|
674
|
+
font-size: 12px;
|
|
675
|
+
color: #ccc;
|
|
676
|
+
font-family: Menlo, Monaco, monospace;
|
|
677
|
+
overflow: hidden;
|
|
678
|
+
text-overflow: ellipsis;
|
|
679
|
+
white-space: nowrap;
|
|
680
|
+
flex: 1;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.git-diff-badge {
|
|
684
|
+
padding: 2px 8px;
|
|
685
|
+
background: #2a2a2a;
|
|
686
|
+
border: 1px solid #444;
|
|
687
|
+
border-radius: 4px;
|
|
688
|
+
font-size: 10px;
|
|
689
|
+
font-weight: 600;
|
|
690
|
+
color: #e2c08d;
|
|
691
|
+
letter-spacing: 0.5px;
|
|
692
|
+
flex-shrink: 0;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.git-diff-content-scroll {
|
|
696
|
+
flex: 1;
|
|
697
|
+
overflow: auto;
|
|
698
|
+
-webkit-overflow-scrolling: touch;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.git-diff-placeholder {
|
|
702
|
+
flex: 1;
|
|
703
|
+
display: flex;
|
|
704
|
+
flex-direction: column;
|
|
705
|
+
align-items: center;
|
|
706
|
+
justify-content: center;
|
|
707
|
+
gap: 12px;
|
|
708
|
+
color: #333;
|
|
709
|
+
font-size: 13px;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.git-diff-loading {
|
|
713
|
+
text-align: center;
|
|
714
|
+
padding: 16px;
|
|
715
|
+
color: #888;
|
|
716
|
+
font-size: 12px;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.git-diff-error {
|
|
720
|
+
color: #ff6b6b;
|
|
721
|
+
font-size: 12px;
|
|
722
|
+
padding: 16px 12px;
|
|
723
|
+
text-align: center;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/* Unified diff 行 */
|
|
727
|
+
.diff-table {
|
|
728
|
+
width: 100%;
|
|
729
|
+
border-collapse: collapse;
|
|
730
|
+
font-family: Menlo, Monaco, 'Courier New', monospace;
|
|
731
|
+
font-size: 12px;
|
|
732
|
+
line-height: 1.5;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.diff-line {
|
|
736
|
+
border: none;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.diff-line-num {
|
|
740
|
+
width: 28px;
|
|
741
|
+
min-width: 28px;
|
|
742
|
+
padding: 0 3px;
|
|
743
|
+
text-align: right;
|
|
744
|
+
color: #555;
|
|
745
|
+
font-size: 11px;
|
|
746
|
+
user-select: none;
|
|
747
|
+
-webkit-user-select: none;
|
|
748
|
+
vertical-align: top;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.diff-line-content {
|
|
752
|
+
padding: 0 8px;
|
|
753
|
+
white-space: pre-wrap;
|
|
754
|
+
word-break: break-all;
|
|
755
|
+
color: #d4d4d4;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.diff-line-add {
|
|
759
|
+
background: rgba(35, 134, 54, 0.2);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.diff-line-add .diff-line-content {
|
|
763
|
+
color: #7ee787;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.diff-line-del {
|
|
767
|
+
background: rgba(248, 81, 73, 0.2);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.diff-line-del .diff-line-content {
|
|
771
|
+
color: #ffa198;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.diff-line-hunk {
|
|
775
|
+
background: rgba(56, 139, 253, 0.1);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.diff-line-hunk .diff-line-content {
|
|
779
|
+
color: #79c0ff;
|
|
780
|
+
font-style: italic;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.diff-file-count {
|
|
784
|
+
font-size: 10px;
|
|
785
|
+
color: #555;
|
|
786
|
+
background: #1a1a1a;
|
|
787
|
+
padding: 1px 6px;
|
|
788
|
+
border-radius: 8px;
|
|
789
|
+
}
|
|
490
790
|
</style>
|
|
491
791
|
</head>
|
|
492
792
|
<body>
|
|
@@ -494,23 +794,32 @@
|
|
|
494
794
|
<div id="layout">
|
|
495
795
|
<div id="header">
|
|
496
796
|
<div style="display: flex; gap: 8px; align-items: center;">
|
|
497
|
-
<button class="history-toggle-btn" id="new-session-btn">
|
|
797
|
+
<button class="history-toggle-btn" id="new-session-btn" style="color:#73c991; border-color:#2a5a3a;">
|
|
498
798
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
499
799
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
500
800
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
501
801
|
</svg>
|
|
502
802
|
<span>新会话</span>
|
|
503
803
|
</button>
|
|
504
|
-
<button class="history-toggle-btn" id="history-toggle">
|
|
804
|
+
<button class="history-toggle-btn" id="history-toggle" style="color:#79c0ff; border-color:#2a4a7c;">
|
|
505
805
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
506
806
|
<circle cx="12" cy="12" r="10"></circle>
|
|
507
807
|
<polyline points="12 6 12 12 16 14"></polyline>
|
|
508
808
|
</svg>
|
|
509
809
|
<span>历史</span>
|
|
510
810
|
</button>
|
|
811
|
+
<button class="history-toggle-btn" id="diff-toggle" style="color:#e2c08d; border-color:#5a4a2a;">
|
|
812
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
813
|
+
<line x1="6" y1="3" x2="6" y2="15"></line>
|
|
814
|
+
<circle cx="18" cy="6" r="3"></circle>
|
|
815
|
+
<circle cx="6" cy="18" r="3"></circle>
|
|
816
|
+
<path d="M18 9a9 9 0 0 1-9 9"></path>
|
|
817
|
+
</svg>
|
|
818
|
+
<span>Diff</span>
|
|
819
|
+
</button>
|
|
511
820
|
</div>
|
|
512
821
|
<div id="mode-switcher">
|
|
513
|
-
<span id="mode-label"
|
|
822
|
+
<span id="mode-label"></span>
|
|
514
823
|
<select id="mode-select">
|
|
515
824
|
<option value="opencode">OpenCode</option>
|
|
516
825
|
<option value="claude">Claude</option>
|
|
@@ -582,9 +891,55 @@
|
|
|
582
891
|
</div>
|
|
583
892
|
</div>
|
|
584
893
|
|
|
894
|
+
<!-- Git Diff 面板 -->
|
|
895
|
+
<div id="git-diff-bar">
|
|
896
|
+
<div id="git-diff-header">
|
|
897
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
898
|
+
<span id="git-diff-title">Git Changes</span>
|
|
899
|
+
<span class="diff-file-count" id="git-diff-count">0</span>
|
|
900
|
+
</div>
|
|
901
|
+
<div style="display: flex; gap: 8px;">
|
|
902
|
+
<button class="history-toggle-btn" id="refresh-diff">
|
|
903
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
904
|
+
<polyline points="23 4 23 10 17 10"></polyline>
|
|
905
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
|
906
|
+
</svg>
|
|
907
|
+
<span>刷新</span>
|
|
908
|
+
</button>
|
|
909
|
+
<button class="history-toggle-btn" id="close-diff">
|
|
910
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
911
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
912
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
913
|
+
</svg>
|
|
914
|
+
<span>返回</span>
|
|
915
|
+
</button>
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
<div class="git-diff-file-list" id="git-diff-file-list">
|
|
919
|
+
<div class="git-diff-loading">加载中...</div>
|
|
920
|
+
</div>
|
|
921
|
+
<div class="git-diff-content-area" id="git-diff-content-area">
|
|
922
|
+
<div class="git-diff-placeholder">
|
|
923
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5">
|
|
924
|
+
<line x1="6" y1="3" x2="6" y2="15"></line>
|
|
925
|
+
<circle cx="18" cy="6" r="3"></circle>
|
|
926
|
+
<circle cx="6" cy="18" r="3"></circle>
|
|
927
|
+
<path d="M18 9a9 9 0 0 1-9 9"></path>
|
|
928
|
+
</svg>
|
|
929
|
+
<span>点击文件查看 diff</span>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
933
|
+
|
|
585
934
|
<div id="content">
|
|
586
935
|
<div id="terminal-container">
|
|
587
|
-
<div id="terminal"
|
|
936
|
+
<div id="terminal">
|
|
937
|
+
<div id="select-text-layer">
|
|
938
|
+
<div id="select-hint">长按选择文本 · 点右上角 ✕ 返回终端</div>
|
|
939
|
+
<pre id="select-text-pre"></pre>
|
|
940
|
+
</div>
|
|
941
|
+
<button id="select-mode-close">✕</button>
|
|
942
|
+
</div>
|
|
588
943
|
<div id="virtual-keybar">
|
|
589
944
|
<div class="virtual-key" data-key="up">↑</div>
|
|
590
945
|
<div class="virtual-key" data-key="down">↓</div>
|
|
@@ -596,11 +951,13 @@
|
|
|
596
951
|
<div class="virtual-key" data-key="ctrlc">Ctrl+C</div>
|
|
597
952
|
<div class="virtual-key scroll-key" data-scroll="-5">⇡ 滚动</div>
|
|
598
953
|
<div class="virtual-key scroll-key" data-scroll="5">⇣ 滚动</div>
|
|
954
|
+
<div class="virtual-key" id="btn-copy">复制</div>
|
|
599
955
|
</div>
|
|
600
956
|
</div>
|
|
601
957
|
</div>
|
|
602
958
|
</div>
|
|
603
959
|
|
|
960
|
+
<div id="copy-toast">已复制</div>
|
|
604
961
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
605
962
|
<script>
|
|
606
963
|
(function() {
|
|
@@ -837,17 +1194,40 @@
|
|
|
837
1194
|
}
|
|
838
1195
|
}
|
|
839
1196
|
|
|
1197
|
+
// 长按检测
|
|
1198
|
+
var longPressTimer = null;
|
|
1199
|
+
var longPressTriggered = false;
|
|
1200
|
+
var LONG_PRESS_DELAY = 500; // ms
|
|
1201
|
+
|
|
1202
|
+
function clearLongPress() {
|
|
1203
|
+
if (longPressTimer) {
|
|
1204
|
+
clearTimeout(longPressTimer);
|
|
1205
|
+
longPressTimer = null;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
840
1209
|
function handleTouchStart(e) {
|
|
841
1210
|
console.log('[scroll] touchstart');
|
|
842
1211
|
stopMomentum();
|
|
1212
|
+
longPressTriggered = false;
|
|
1213
|
+
clearLongPress();
|
|
843
1214
|
if (e.touches.length !== 1) return;
|
|
844
1215
|
lastY = e.touches[0].clientY;
|
|
845
1216
|
lastTime = performance.now();
|
|
846
1217
|
velocitySamples = [];
|
|
1218
|
+
|
|
1219
|
+
// 启动长按计时器
|
|
1220
|
+
longPressTimer = setTimeout(function() {
|
|
1221
|
+
longPressTriggered = true;
|
|
1222
|
+
longPressTimer = null;
|
|
1223
|
+
openSelectMode();
|
|
1224
|
+
}, LONG_PRESS_DELAY);
|
|
847
1225
|
}
|
|
848
1226
|
|
|
849
1227
|
function handleTouchMove(e) {
|
|
850
1228
|
if (e.touches.length !== 1) return;
|
|
1229
|
+
// 有移动则取消长按
|
|
1230
|
+
clearLongPress();
|
|
851
1231
|
var y = e.touches[0].clientY;
|
|
852
1232
|
var now = performance.now();
|
|
853
1233
|
var dt = now - lastTime;
|
|
@@ -871,6 +1251,16 @@
|
|
|
871
1251
|
|
|
872
1252
|
function handleTouchEnd() {
|
|
873
1253
|
console.log('[scroll] touchend');
|
|
1254
|
+
clearLongPress();
|
|
1255
|
+
|
|
1256
|
+
// 长按已触发,不执行滚动惯性
|
|
1257
|
+
if (longPressTriggered) {
|
|
1258
|
+
longPressTriggered = false;
|
|
1259
|
+
pendingDy = 0;
|
|
1260
|
+
pixelAccum = 0;
|
|
1261
|
+
velocitySamples = [];
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
874
1264
|
|
|
875
1265
|
if (scrollRaf) {
|
|
876
1266
|
cancelAnimationFrame(scrollRaf);
|
|
@@ -1563,6 +1953,282 @@
|
|
|
1563
1953
|
}
|
|
1564
1954
|
});
|
|
1565
1955
|
|
|
1956
|
+
// 提取终端缓冲区文本
|
|
1957
|
+
function getTerminalText() {
|
|
1958
|
+
var buf = term.buffer.active;
|
|
1959
|
+
var lines = [];
|
|
1960
|
+
for (var i = 0; i < buf.length; i++) {
|
|
1961
|
+
var line = buf.getLine(i);
|
|
1962
|
+
if (line) lines.push(line.translateToString(true));
|
|
1963
|
+
}
|
|
1964
|
+
// 去除尾部空行
|
|
1965
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
1966
|
+
lines.pop();
|
|
1967
|
+
}
|
|
1968
|
+
return lines.join('\n');
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// 复制到剪贴板
|
|
1972
|
+
function copyToClipboard(text) {
|
|
1973
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1974
|
+
navigator.clipboard.writeText(text).then(showCopyToast).catch(function() {
|
|
1975
|
+
fallbackCopy(text);
|
|
1976
|
+
});
|
|
1977
|
+
} else {
|
|
1978
|
+
fallbackCopy(text);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
function fallbackCopy(text) {
|
|
1983
|
+
var ta = document.createElement('textarea');
|
|
1984
|
+
ta.value = text;
|
|
1985
|
+
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
|
|
1986
|
+
document.body.appendChild(ta);
|
|
1987
|
+
ta.select();
|
|
1988
|
+
document.execCommand('copy');
|
|
1989
|
+
document.body.removeChild(ta);
|
|
1990
|
+
showCopyToast();
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function showCopyToast() {
|
|
1994
|
+
var toast = document.getElementById('copy-toast');
|
|
1995
|
+
toast.classList.add('show');
|
|
1996
|
+
setTimeout(function() { toast.classList.remove('show'); }, 1200);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// "复制" 按钮
|
|
2000
|
+
document.getElementById('btn-copy').addEventListener('touchend', function(e) {
|
|
2001
|
+
e.preventDefault();
|
|
2002
|
+
var text = getTerminalText();
|
|
2003
|
+
if (text) copyToClipboard(text);
|
|
2004
|
+
});
|
|
2005
|
+
document.getElementById('btn-copy').addEventListener('click', function(e) {
|
|
2006
|
+
e.preventDefault();
|
|
2007
|
+
var text = getTerminalText();
|
|
2008
|
+
if (text) copyToClipboard(text);
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
// 方案2: 长按进入选择模式 — 原位显示可选纯文本
|
|
2012
|
+
var selectTextLayer = document.getElementById('select-text-layer');
|
|
2013
|
+
var selectTextPre = document.getElementById('select-text-pre');
|
|
2014
|
+
var selectModeClose = document.getElementById('select-mode-close');
|
|
2015
|
+
var inSelectMode = false;
|
|
2016
|
+
|
|
2017
|
+
function openSelectMode() {
|
|
2018
|
+
if (inSelectMode) return;
|
|
2019
|
+
inSelectMode = true;
|
|
2020
|
+
// 收起键盘
|
|
2021
|
+
var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
|
|
2022
|
+
if (xtermTa) xtermTa.blur();
|
|
2023
|
+
document.activeElement && document.activeElement.blur();
|
|
2024
|
+
var text = getTerminalText();
|
|
2025
|
+
selectTextPre.textContent = text || '(终端内容为空)';
|
|
2026
|
+
terminalEl.classList.add('select-mode');
|
|
2027
|
+
selectTextLayer.classList.add('visible');
|
|
2028
|
+
selectModeClose.style.display = 'block';
|
|
2029
|
+
unbindTouchScroll();
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
function closeSelectMode() {
|
|
2033
|
+
if (!inSelectMode) return;
|
|
2034
|
+
inSelectMode = false;
|
|
2035
|
+
terminalEl.classList.remove('select-mode');
|
|
2036
|
+
selectTextLayer.classList.remove('visible');
|
|
2037
|
+
selectModeClose.style.display = 'none';
|
|
2038
|
+
window.getSelection().removeAllRanges();
|
|
2039
|
+
rebindTouchScroll();
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
selectModeClose.addEventListener('click', function(e) {
|
|
2043
|
+
e.preventDefault();
|
|
2044
|
+
e.stopPropagation();
|
|
2045
|
+
closeSelectMode();
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
selectModeClose.addEventListener('touchend', function(e) {
|
|
2049
|
+
e.preventDefault();
|
|
2050
|
+
e.stopPropagation();
|
|
2051
|
+
closeSelectMode();
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
// ======= Git Diff 功能 =======
|
|
2055
|
+
var diffBarVisible = false;
|
|
2056
|
+
var diffChanges = [];
|
|
2057
|
+
var diffSelectedFile = null;
|
|
2058
|
+
|
|
2059
|
+
var STATUS_COLORS = {
|
|
2060
|
+
'M': '#e2c08d', 'A': '#73c991', 'D': '#f14c4c',
|
|
2061
|
+
'R': '#73c991', 'C': '#73c991', 'U': '#e2c08d',
|
|
2062
|
+
'?': '#73c991', '??': '#73c991',
|
|
2063
|
+
};
|
|
2064
|
+
|
|
2065
|
+
function toggleDiffBar() {
|
|
2066
|
+
diffBarVisible = !diffBarVisible;
|
|
2067
|
+
var bar = document.getElementById('git-diff-bar');
|
|
2068
|
+
if (diffBarVisible) {
|
|
2069
|
+
bar.classList.add('visible');
|
|
2070
|
+
loadGitStatus();
|
|
2071
|
+
} else {
|
|
2072
|
+
bar.classList.remove('visible');
|
|
2073
|
+
diffSelectedFile = null;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function loadGitStatus() {
|
|
2078
|
+
var fileList = document.getElementById('git-diff-file-list');
|
|
2079
|
+
fileList.innerHTML = '<div class="git-diff-loading">加载中...</div>';
|
|
2080
|
+
document.getElementById('git-diff-count').textContent = '0';
|
|
2081
|
+
|
|
2082
|
+
fetch('/api/git-status')
|
|
2083
|
+
.then(function(r) { return r.json(); })
|
|
2084
|
+
.then(function(data) {
|
|
2085
|
+
diffChanges = data.changes || [];
|
|
2086
|
+
document.getElementById('git-diff-count').textContent = diffChanges.length;
|
|
2087
|
+
renderDiffFileList();
|
|
2088
|
+
})
|
|
2089
|
+
.catch(function() {
|
|
2090
|
+
fileList.innerHTML = '<div class="git-diff-error">无法加载 git status</div>';
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function renderDiffFileList() {
|
|
2095
|
+
var fileList = document.getElementById('git-diff-file-list');
|
|
2096
|
+
if (diffChanges.length === 0) {
|
|
2097
|
+
fileList.innerHTML = '<div class="git-diff-loading" style="color:#666;">没有变更文件</div>';
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
var html = '';
|
|
2101
|
+
diffChanges.forEach(function(c) {
|
|
2102
|
+
var color = STATUS_COLORS[c.status] || '#888';
|
|
2103
|
+
var label = c.status === '??' ? 'U' : c.status;
|
|
2104
|
+
var activeClass = diffSelectedFile === c.file ? ' active' : '';
|
|
2105
|
+
html += '<div class="git-diff-file-item' + activeClass + '" data-file="' + escapeHtml(c.file) + '">';
|
|
2106
|
+
html += '<span class="git-diff-file-status" style="color:' + color + '">' + label + '</span>';
|
|
2107
|
+
html += '<span class="git-diff-file-name">' + escapeHtml(c.file) + '</span>';
|
|
2108
|
+
html += '</div>';
|
|
2109
|
+
});
|
|
2110
|
+
fileList.innerHTML = html;
|
|
2111
|
+
|
|
2112
|
+
// 绑定点击事件
|
|
2113
|
+
fileList.querySelectorAll('.git-diff-file-item').forEach(function(item) {
|
|
2114
|
+
item.addEventListener('click', function() {
|
|
2115
|
+
var file = item.getAttribute('data-file');
|
|
2116
|
+
diffSelectedFile = file;
|
|
2117
|
+
// 更新选中状态
|
|
2118
|
+
fileList.querySelectorAll('.git-diff-file-item').forEach(function(el) {
|
|
2119
|
+
el.classList.toggle('active', el.getAttribute('data-file') === file);
|
|
2120
|
+
});
|
|
2121
|
+
loadDiffContent(file);
|
|
2122
|
+
});
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function loadDiffContent(file) {
|
|
2127
|
+
var area = document.getElementById('git-diff-content-area');
|
|
2128
|
+
area.innerHTML = '<div class="git-diff-loading">加载 diff...</div>';
|
|
2129
|
+
|
|
2130
|
+
fetch('/api/git-diff?files=' + encodeURIComponent(file))
|
|
2131
|
+
.then(function(r) { return r.json(); })
|
|
2132
|
+
.then(function(data) {
|
|
2133
|
+
if (!data.diffs || !data.diffs[0]) {
|
|
2134
|
+
area.innerHTML = '<div class="git-diff-error">无 diff 数据</div>';
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
var d = data.diffs[0];
|
|
2138
|
+
var html = '<div class="git-diff-content-header">';
|
|
2139
|
+
html += '<span class="git-diff-content-path">' + escapeHtml(d.file) + '</span>';
|
|
2140
|
+
html += '<span class="git-diff-badge">' + (d.is_new ? 'NEW' : d.is_deleted ? 'DEL' : 'DIFF') + '</span>';
|
|
2141
|
+
html += '</div>';
|
|
2142
|
+
html += '<div class="git-diff-content-scroll">';
|
|
2143
|
+
|
|
2144
|
+
if (d.is_binary) {
|
|
2145
|
+
html += '<div class="git-diff-loading" style="font-style:italic;">二进制文件</div>';
|
|
2146
|
+
} else if (d.is_large) {
|
|
2147
|
+
html += '<div class="git-diff-loading" style="color:#e2c08d;">文件过大: ' + (d.size / (1024 * 1024)).toFixed(2) + ' MB</div>';
|
|
2148
|
+
} else if (d.unified_diff) {
|
|
2149
|
+
html += renderUnifiedDiff(d.unified_diff);
|
|
2150
|
+
} else {
|
|
2151
|
+
html += '<div class="git-diff-loading" style="color:#666;">无变更内容</div>';
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
html += '</div>';
|
|
2155
|
+
area.innerHTML = html;
|
|
2156
|
+
})
|
|
2157
|
+
.catch(function(err) {
|
|
2158
|
+
area.innerHTML = '<div class="git-diff-error">加载失败: ' + escapeHtml(err.message) + '</div>';
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
function renderUnifiedDiff(diffText) {
|
|
2163
|
+
var lines = diffText.split('\n');
|
|
2164
|
+
var html = '<table class="diff-table">';
|
|
2165
|
+
var oldLine = 0, newLine = 0;
|
|
2166
|
+
|
|
2167
|
+
for (var i = 0; i < lines.length; i++) {
|
|
2168
|
+
var line = lines[i];
|
|
2169
|
+
|
|
2170
|
+
// 跳过 diff 头部信息
|
|
2171
|
+
if (line.startsWith('diff --git') || line.startsWith('index ') ||
|
|
2172
|
+
line.startsWith('---') || line.startsWith('+++') ||
|
|
2173
|
+
line.startsWith('new file') || line.startsWith('deleted file') ||
|
|
2174
|
+
line.startsWith('old mode') || line.startsWith('new mode')) {
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
if (line.startsWith('@@')) {
|
|
2179
|
+
// 解析 hunk header: @@ -old,count +new,count @@
|
|
2180
|
+
var match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
2181
|
+
if (match) {
|
|
2182
|
+
oldLine = parseInt(match[1], 10);
|
|
2183
|
+
newLine = parseInt(match[2], 10);
|
|
2184
|
+
}
|
|
2185
|
+
html += '<tr class="diff-line diff-line-hunk">';
|
|
2186
|
+
html += '<td class="diff-line-num"></td><td class="diff-line-num"></td>';
|
|
2187
|
+
html += '<td class="diff-line-content">' + escapeHtml(line) + '</td></tr>';
|
|
2188
|
+
} else if (line.startsWith('+')) {
|
|
2189
|
+
html += '<tr class="diff-line diff-line-add">';
|
|
2190
|
+
html += '<td class="diff-line-num"></td>';
|
|
2191
|
+
html += '<td class="diff-line-num">' + newLine + '</td>';
|
|
2192
|
+
html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
|
|
2193
|
+
newLine++;
|
|
2194
|
+
} else if (line.startsWith('-')) {
|
|
2195
|
+
html += '<tr class="diff-line diff-line-del">';
|
|
2196
|
+
html += '<td class="diff-line-num">' + oldLine + '</td>';
|
|
2197
|
+
html += '<td class="diff-line-num"></td>';
|
|
2198
|
+
html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
|
|
2199
|
+
oldLine++;
|
|
2200
|
+
} else if (line.startsWith(' ') || (line === '' && i < lines.length - 1)) {
|
|
2201
|
+
html += '<tr class="diff-line">';
|
|
2202
|
+
html += '<td class="diff-line-num">' + oldLine + '</td>';
|
|
2203
|
+
html += '<td class="diff-line-num">' + newLine + '</td>';
|
|
2204
|
+
html += '<td class="diff-line-content">' + escapeHtml(line.substring(1) || '') + '</td></tr>';
|
|
2205
|
+
oldLine++;
|
|
2206
|
+
newLine++;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
html += '</table>';
|
|
2211
|
+
return html;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// Diff 按钮事件绑定
|
|
2215
|
+
document.getElementById('diff-toggle').addEventListener('click', toggleDiffBar);
|
|
2216
|
+
document.getElementById('close-diff').addEventListener('click', toggleDiffBar);
|
|
2217
|
+
document.getElementById('refresh-diff').addEventListener('click', function(e) {
|
|
2218
|
+
e.stopPropagation();
|
|
2219
|
+
loadGitStatus();
|
|
2220
|
+
// 重置 diff 内容区
|
|
2221
|
+
diffSelectedFile = null;
|
|
2222
|
+
document.getElementById('git-diff-content-area').innerHTML =
|
|
2223
|
+
'<div class="git-diff-placeholder">' +
|
|
2224
|
+
'<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5">' +
|
|
2225
|
+
'<line x1="6" y1="3" x2="6" y2="15"></line>' +
|
|
2226
|
+
'<circle cx="18" cy="6" r="3"></circle>' +
|
|
2227
|
+
'<circle cx="6" cy="18" r="3"></circle>' +
|
|
2228
|
+
'<path d="M18 9a9 9 0 0 1-9 9"></path>' +
|
|
2229
|
+
'</svg><span>点击文件查看 diff</span></div>';
|
|
2230
|
+
});
|
|
2231
|
+
|
|
1566
2232
|
// 初始化虚拟按键事件
|
|
1567
2233
|
setupVirtualKeyEvents();
|
|
1568
2234
|
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
|
-
import { existsSync, createReadStream } from 'node:fs';
|
|
3
|
+
import { existsSync, createReadStream, readFileSync } from 'node:fs';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { networkInterfaces, platform, arch, homedir } from 'node:os';
|
|
7
7
|
import { chmodSync, statSync } from 'node:fs';
|
|
8
|
-
import { execSync } from 'child_process';
|
|
8
|
+
import { execSync, execFile } from 'child_process';
|
|
9
|
+
import { promisify } from 'node:util';
|
|
9
10
|
import { WebSocketServer } from 'ws';
|
|
10
11
|
import Database from 'better-sqlite3';
|
|
11
12
|
|
|
@@ -22,6 +23,7 @@ const OPENCODE_DB_PATH = process.env.OPENCODE_DB_PATH || join(
|
|
|
22
23
|
);
|
|
23
24
|
|
|
24
25
|
const MAX_BUFFER = 200000;
|
|
26
|
+
const execFileAsync = promisify(execFile);
|
|
25
27
|
|
|
26
28
|
let ptyModule = null;
|
|
27
29
|
let claudeProcess = null;
|
|
@@ -388,7 +390,7 @@ function getSessionMessages(sessionId) {
|
|
|
388
390
|
}
|
|
389
391
|
}
|
|
390
392
|
|
|
391
|
-
const server = createServer((req, res) => {
|
|
393
|
+
const server = createServer(async (req, res) => {
|
|
392
394
|
if (req.url === '/' || req.url === '/index.html') {
|
|
393
395
|
res.writeHead(200, {
|
|
394
396
|
'Content-Type': 'text/html; charset=utf-8',
|
|
@@ -409,6 +411,96 @@ const server = createServer((req, res) => {
|
|
|
409
411
|
return;
|
|
410
412
|
}
|
|
411
413
|
|
|
414
|
+
// API: 获取 git status
|
|
415
|
+
if (req.url === '/api/git-status') {
|
|
416
|
+
res.writeHead(200, {
|
|
417
|
+
'Content-Type': 'application/json',
|
|
418
|
+
'Access-Control-Allow-Origin': '*',
|
|
419
|
+
});
|
|
420
|
+
try {
|
|
421
|
+
const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
|
|
422
|
+
cwd: process.cwd(), encoding: 'utf-8', timeout: 5000,
|
|
423
|
+
});
|
|
424
|
+
const changes = stdout.split('\n').filter(Boolean).map(line => ({
|
|
425
|
+
status: line.substring(0, 2).trim(),
|
|
426
|
+
file: line.substring(3),
|
|
427
|
+
}));
|
|
428
|
+
res.end(JSON.stringify({ changes }));
|
|
429
|
+
} catch (err) {
|
|
430
|
+
res.end(JSON.stringify({ changes: [], error: err.message }));
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// API: 获取 git diff
|
|
436
|
+
if (req.url?.startsWith('/api/git-diff')) {
|
|
437
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
438
|
+
const files = url.searchParams.get('files');
|
|
439
|
+
res.writeHead(200, {
|
|
440
|
+
'Content-Type': 'application/json',
|
|
441
|
+
'Access-Control-Allow-Origin': '*',
|
|
442
|
+
});
|
|
443
|
+
if (!files) {
|
|
444
|
+
res.end(JSON.stringify({ diffs: [] }));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const fileList = files.split(',').filter(Boolean);
|
|
448
|
+
const diffs = [];
|
|
449
|
+
const cwd = process.cwd();
|
|
450
|
+
for (const file of fileList) {
|
|
451
|
+
if (file.includes('..') || file.startsWith('/')) continue;
|
|
452
|
+
try {
|
|
453
|
+
const { stdout: statusOut } = await execFileAsync('git', ['status', '--porcelain', '--', file], { cwd, encoding: 'utf-8', timeout: 3000 });
|
|
454
|
+
if (!statusOut.trim()) continue;
|
|
455
|
+
const status = statusOut.substring(0, 2).trim();
|
|
456
|
+
const is_new = status === 'A' || status === '??';
|
|
457
|
+
const is_deleted = status === 'D';
|
|
458
|
+
let is_binary = false;
|
|
459
|
+
if (!is_deleted) {
|
|
460
|
+
try {
|
|
461
|
+
const { stdout: dc } = await execFileAsync('git', ['diff', '--numstat', 'HEAD', '--', file], { cwd, encoding: 'utf-8', timeout: 3000 });
|
|
462
|
+
if (dc.includes('-\t-\t')) is_binary = true;
|
|
463
|
+
} catch {}
|
|
464
|
+
}
|
|
465
|
+
let old_content = '', new_content = '', unified_diff = '';
|
|
466
|
+
if (!is_binary) {
|
|
467
|
+
if (!is_new) {
|
|
468
|
+
try {
|
|
469
|
+
const { stdout } = await execFileAsync('git', ['show', `HEAD:${file}`], { cwd, encoding: 'utf-8', timeout: 5000, maxBuffer: 5 * 1024 * 1024 });
|
|
470
|
+
old_content = stdout;
|
|
471
|
+
} catch {}
|
|
472
|
+
}
|
|
473
|
+
if (!is_deleted) {
|
|
474
|
+
try {
|
|
475
|
+
const filePath = join(cwd, file);
|
|
476
|
+
if (existsSync(filePath)) {
|
|
477
|
+
const stat = statSync(filePath);
|
|
478
|
+
if (stat.size > 5 * 1024 * 1024) {
|
|
479
|
+
diffs.push({ file, status, is_large: true, size: stat.size });
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
new_content = readFileSync(filePath, 'utf-8');
|
|
483
|
+
}
|
|
484
|
+
} catch {}
|
|
485
|
+
}
|
|
486
|
+
// 获取 unified diff
|
|
487
|
+
try {
|
|
488
|
+
const diffArgs = is_new
|
|
489
|
+
? ['diff', '--no-color', '-U3', '--no-index', '/dev/null', file]
|
|
490
|
+
: ['diff', '--no-color', '-U3', 'HEAD', '--', file];
|
|
491
|
+
const { stdout } = await execFileAsync('git', diffArgs, { cwd, encoding: 'utf-8', timeout: 5000, maxBuffer: 5 * 1024 * 1024 });
|
|
492
|
+
unified_diff = stdout;
|
|
493
|
+
} catch (e) {
|
|
494
|
+
if (e.stdout) unified_diff = e.stdout;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
diffs.push({ file, status, old_content, new_content, unified_diff, is_binary, is_new, is_deleted });
|
|
498
|
+
} catch { continue; }
|
|
499
|
+
}
|
|
500
|
+
res.end(JSON.stringify({ diffs }));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
412
504
|
// API: 获取会话消息
|
|
413
505
|
if (req.url?.startsWith('/api/session/')) {
|
|
414
506
|
const sessionId = req.url.split('/').pop();
|