claude-opencode-viewer 2.5.0 → 2.6.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.
- package/index.html +469 -24
- package/package.json +1 -1
- package/server.js +95 -3
package/index.html
CHANGED
|
@@ -574,6 +574,219 @@
|
|
|
574
574
|
#copy-toast.show {
|
|
575
575
|
display: block;
|
|
576
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
|
+
}
|
|
577
790
|
</style>
|
|
578
791
|
</head>
|
|
579
792
|
<body>
|
|
@@ -581,20 +794,29 @@
|
|
|
581
794
|
<div id="layout">
|
|
582
795
|
<div id="header">
|
|
583
796
|
<div style="display: flex; gap: 8px; align-items: center;">
|
|
584
|
-
<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;">
|
|
585
798
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
586
799
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
587
800
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
588
801
|
</svg>
|
|
589
802
|
<span>新会话</span>
|
|
590
803
|
</button>
|
|
591
|
-
<button class="history-toggle-btn" id="history-toggle">
|
|
804
|
+
<button class="history-toggle-btn" id="history-toggle" style="color:#79c0ff; border-color:#2a4a7c;">
|
|
592
805
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
593
806
|
<circle cx="12" cy="12" r="10"></circle>
|
|
594
807
|
<polyline points="12 6 12 12 16 14"></polyline>
|
|
595
808
|
</svg>
|
|
596
809
|
<span>历史</span>
|
|
597
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>
|
|
598
820
|
</div>
|
|
599
821
|
<div id="mode-switcher">
|
|
600
822
|
<span id="mode-label"></span>
|
|
@@ -669,6 +891,46 @@
|
|
|
669
891
|
</div>
|
|
670
892
|
</div>
|
|
671
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
|
+
|
|
672
934
|
<div id="content">
|
|
673
935
|
<div id="terminal-container">
|
|
674
936
|
<div id="terminal">
|
|
@@ -687,8 +949,6 @@
|
|
|
687
949
|
<div class="virtual-key" data-key="tab">Tab</div>
|
|
688
950
|
<div class="virtual-key" data-key="esc">Esc</div>
|
|
689
951
|
<div class="virtual-key" data-key="ctrlc">Ctrl+C</div>
|
|
690
|
-
<div class="virtual-key scroll-key" data-scroll="-5">⇡ 滚动</div>
|
|
691
|
-
<div class="virtual-key scroll-key" data-scroll="5">⇣ 滚动</div>
|
|
692
952
|
<div class="virtual-key" id="btn-copy">复制</div>
|
|
693
953
|
</div>
|
|
694
954
|
</div>
|
|
@@ -891,14 +1151,32 @@
|
|
|
891
1151
|
pixelAccum = 0;
|
|
892
1152
|
}
|
|
893
1153
|
|
|
894
|
-
//
|
|
1154
|
+
// 触摸滚动实现 - 混合模式:alternate buffer 发鼠标滚轮,normal buffer 用 scrollLines
|
|
895
1155
|
var touchScreen = null;
|
|
896
1156
|
var touchEventsBound = false;
|
|
897
1157
|
|
|
1158
|
+
function isAlternateBuffer() {
|
|
1159
|
+
return term.buffer.active.type === 'alternate';
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function sendPageKey(direction) {
|
|
1163
|
+
if (!ws || ws.readyState !== 1) return;
|
|
1164
|
+
var seq = direction === 'up' ? '\x1b[5~' : '\x1b[6~';
|
|
1165
|
+
ws.send(JSON.stringify({ type: 'input', data: seq }));
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function doScroll(lines) {
|
|
1169
|
+
if (lines === 0) return;
|
|
1170
|
+
if (isAlternateBuffer()) {
|
|
1171
|
+
sendPageKey(lines < 0 ? 'up' : 'down');
|
|
1172
|
+
} else {
|
|
1173
|
+
term.scrollLines(lines);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
898
1177
|
function getLineHeight() {
|
|
899
1178
|
var cellDims = getCellDims();
|
|
900
1179
|
var height = (cellDims && cellDims.height) || 15;
|
|
901
|
-
console.log('[scroll] lineHeight:', height);
|
|
902
1180
|
return height;
|
|
903
1181
|
}
|
|
904
1182
|
|
|
@@ -926,8 +1204,7 @@
|
|
|
926
1204
|
var lines = Math.trunc(pixelAccum / lh);
|
|
927
1205
|
|
|
928
1206
|
if (lines !== 0) {
|
|
929
|
-
|
|
930
|
-
term.scrollLines(lines);
|
|
1207
|
+
doScroll(lines);
|
|
931
1208
|
pixelAccum -= lines * lh;
|
|
932
1209
|
}
|
|
933
1210
|
}
|
|
@@ -1011,8 +1288,7 @@
|
|
|
1011
1288
|
var lh = getLineHeight();
|
|
1012
1289
|
var lines = Math.trunc(pixelAccum / lh);
|
|
1013
1290
|
if (lines !== 0) {
|
|
1014
|
-
|
|
1015
|
-
term.scrollLines(lines);
|
|
1291
|
+
doScroll(lines);
|
|
1016
1292
|
}
|
|
1017
1293
|
pixelAccum = 0;
|
|
1018
1294
|
}
|
|
@@ -1042,8 +1318,7 @@
|
|
|
1042
1318
|
var lh = getLineHeight();
|
|
1043
1319
|
var rest = Math.round(mAccum / lh);
|
|
1044
1320
|
if (rest !== 0) {
|
|
1045
|
-
|
|
1046
|
-
term.scrollLines(rest);
|
|
1321
|
+
doScroll(rest);
|
|
1047
1322
|
}
|
|
1048
1323
|
momentumRaf = null;
|
|
1049
1324
|
return;
|
|
@@ -1052,7 +1327,7 @@
|
|
|
1052
1327
|
var lh = getLineHeight();
|
|
1053
1328
|
var lines = Math.trunc(mAccum / lh);
|
|
1054
1329
|
if (lines !== 0) {
|
|
1055
|
-
|
|
1330
|
+
doScroll(lines);
|
|
1056
1331
|
mAccum -= lines * lh;
|
|
1057
1332
|
}
|
|
1058
1333
|
velocity *= friction;
|
|
@@ -1076,12 +1351,6 @@
|
|
|
1076
1351
|
// 先解绑旧的
|
|
1077
1352
|
unbindTouchScroll();
|
|
1078
1353
|
|
|
1079
|
-
// 只在 opencode 模式下绑定
|
|
1080
|
-
if (currentMode !== 'opencode') {
|
|
1081
|
-
console.log('[scroll] Not opencode mode, skip binding');
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
1354
|
var screen = terminalEl.querySelector('.xterm-screen');
|
|
1086
1355
|
if (!screen) {
|
|
1087
1356
|
console.log('[scroll] .xterm-screen not found, retrying...');
|
|
@@ -1094,7 +1363,7 @@
|
|
|
1094
1363
|
screen.addEventListener('touchmove', handleTouchMove, { passive: true });
|
|
1095
1364
|
screen.addEventListener('touchend', handleTouchEnd, { passive: true });
|
|
1096
1365
|
touchEventsBound = true;
|
|
1097
|
-
console.log('[scroll] Touch events bound to .xterm-screen
|
|
1366
|
+
console.log('[scroll] Touch events bound to .xterm-screen');
|
|
1098
1367
|
}
|
|
1099
1368
|
|
|
1100
1369
|
function rebindTouchScroll() {
|
|
@@ -1285,10 +1554,8 @@
|
|
|
1285
1554
|
}
|
|
1286
1555
|
|
|
1287
1556
|
function scrollTerminal(lines) {
|
|
1288
|
-
if (term)
|
|
1289
|
-
|
|
1290
|
-
console.log('[scroll] scrolled by:', lines);
|
|
1291
|
-
}
|
|
1557
|
+
if (!term) return;
|
|
1558
|
+
doScroll(lines);
|
|
1292
1559
|
}
|
|
1293
1560
|
|
|
1294
1561
|
// 参考 cc-viewer 的 TerminalPanel.jsx 行 519-546: 虚拟按键触摸处理
|
|
@@ -1789,6 +2056,184 @@
|
|
|
1789
2056
|
closeSelectMode();
|
|
1790
2057
|
});
|
|
1791
2058
|
|
|
2059
|
+
// ======= Git Diff 功能 =======
|
|
2060
|
+
var diffBarVisible = false;
|
|
2061
|
+
var diffChanges = [];
|
|
2062
|
+
var diffSelectedFile = null;
|
|
2063
|
+
|
|
2064
|
+
var STATUS_COLORS = {
|
|
2065
|
+
'M': '#e2c08d', 'A': '#73c991', 'D': '#f14c4c',
|
|
2066
|
+
'R': '#73c991', 'C': '#73c991', 'U': '#e2c08d',
|
|
2067
|
+
'?': '#73c991', '??': '#73c991',
|
|
2068
|
+
};
|
|
2069
|
+
|
|
2070
|
+
function toggleDiffBar() {
|
|
2071
|
+
diffBarVisible = !diffBarVisible;
|
|
2072
|
+
var bar = document.getElementById('git-diff-bar');
|
|
2073
|
+
if (diffBarVisible) {
|
|
2074
|
+
bar.classList.add('visible');
|
|
2075
|
+
loadGitStatus();
|
|
2076
|
+
} else {
|
|
2077
|
+
bar.classList.remove('visible');
|
|
2078
|
+
diffSelectedFile = null;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function loadGitStatus() {
|
|
2083
|
+
var fileList = document.getElementById('git-diff-file-list');
|
|
2084
|
+
fileList.innerHTML = '<div class="git-diff-loading">加载中...</div>';
|
|
2085
|
+
document.getElementById('git-diff-count').textContent = '0';
|
|
2086
|
+
|
|
2087
|
+
fetch('/api/git-status')
|
|
2088
|
+
.then(function(r) { return r.json(); })
|
|
2089
|
+
.then(function(data) {
|
|
2090
|
+
diffChanges = data.changes || [];
|
|
2091
|
+
document.getElementById('git-diff-count').textContent = diffChanges.length;
|
|
2092
|
+
renderDiffFileList();
|
|
2093
|
+
})
|
|
2094
|
+
.catch(function() {
|
|
2095
|
+
fileList.innerHTML = '<div class="git-diff-error">无法加载 git status</div>';
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function renderDiffFileList() {
|
|
2100
|
+
var fileList = document.getElementById('git-diff-file-list');
|
|
2101
|
+
if (diffChanges.length === 0) {
|
|
2102
|
+
fileList.innerHTML = '<div class="git-diff-loading" style="color:#666;">没有变更文件</div>';
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
var html = '';
|
|
2106
|
+
diffChanges.forEach(function(c) {
|
|
2107
|
+
var color = STATUS_COLORS[c.status] || '#888';
|
|
2108
|
+
var label = c.status === '??' ? 'U' : c.status;
|
|
2109
|
+
var activeClass = diffSelectedFile === c.file ? ' active' : '';
|
|
2110
|
+
html += '<div class="git-diff-file-item' + activeClass + '" data-file="' + escapeHtml(c.file) + '">';
|
|
2111
|
+
html += '<span class="git-diff-file-status" style="color:' + color + '">' + label + '</span>';
|
|
2112
|
+
html += '<span class="git-diff-file-name">' + escapeHtml(c.file) + '</span>';
|
|
2113
|
+
html += '</div>';
|
|
2114
|
+
});
|
|
2115
|
+
fileList.innerHTML = html;
|
|
2116
|
+
|
|
2117
|
+
// 绑定点击事件
|
|
2118
|
+
fileList.querySelectorAll('.git-diff-file-item').forEach(function(item) {
|
|
2119
|
+
item.addEventListener('click', function() {
|
|
2120
|
+
var file = item.getAttribute('data-file');
|
|
2121
|
+
diffSelectedFile = file;
|
|
2122
|
+
// 更新选中状态
|
|
2123
|
+
fileList.querySelectorAll('.git-diff-file-item').forEach(function(el) {
|
|
2124
|
+
el.classList.toggle('active', el.getAttribute('data-file') === file);
|
|
2125
|
+
});
|
|
2126
|
+
loadDiffContent(file);
|
|
2127
|
+
});
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function loadDiffContent(file) {
|
|
2132
|
+
var area = document.getElementById('git-diff-content-area');
|
|
2133
|
+
area.innerHTML = '<div class="git-diff-loading">加载 diff...</div>';
|
|
2134
|
+
|
|
2135
|
+
fetch('/api/git-diff?files=' + encodeURIComponent(file))
|
|
2136
|
+
.then(function(r) { return r.json(); })
|
|
2137
|
+
.then(function(data) {
|
|
2138
|
+
if (!data.diffs || !data.diffs[0]) {
|
|
2139
|
+
area.innerHTML = '<div class="git-diff-error">无 diff 数据</div>';
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
var d = data.diffs[0];
|
|
2143
|
+
var html = '<div class="git-diff-content-header">';
|
|
2144
|
+
html += '<span class="git-diff-content-path">' + escapeHtml(d.file) + '</span>';
|
|
2145
|
+
html += '<span class="git-diff-badge">' + (d.is_new ? 'NEW' : d.is_deleted ? 'DEL' : 'DIFF') + '</span>';
|
|
2146
|
+
html += '</div>';
|
|
2147
|
+
html += '<div class="git-diff-content-scroll">';
|
|
2148
|
+
|
|
2149
|
+
if (d.is_binary) {
|
|
2150
|
+
html += '<div class="git-diff-loading" style="font-style:italic;">二进制文件</div>';
|
|
2151
|
+
} else if (d.is_large) {
|
|
2152
|
+
html += '<div class="git-diff-loading" style="color:#e2c08d;">文件过大: ' + (d.size / (1024 * 1024)).toFixed(2) + ' MB</div>';
|
|
2153
|
+
} else if (d.unified_diff) {
|
|
2154
|
+
html += renderUnifiedDiff(d.unified_diff);
|
|
2155
|
+
} else {
|
|
2156
|
+
html += '<div class="git-diff-loading" style="color:#666;">无变更内容</div>';
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
html += '</div>';
|
|
2160
|
+
area.innerHTML = html;
|
|
2161
|
+
})
|
|
2162
|
+
.catch(function(err) {
|
|
2163
|
+
area.innerHTML = '<div class="git-diff-error">加载失败: ' + escapeHtml(err.message) + '</div>';
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function renderUnifiedDiff(diffText) {
|
|
2168
|
+
var lines = diffText.split('\n');
|
|
2169
|
+
var html = '<table class="diff-table">';
|
|
2170
|
+
var oldLine = 0, newLine = 0;
|
|
2171
|
+
|
|
2172
|
+
for (var i = 0; i < lines.length; i++) {
|
|
2173
|
+
var line = lines[i];
|
|
2174
|
+
|
|
2175
|
+
// 跳过 diff 头部信息
|
|
2176
|
+
if (line.startsWith('diff --git') || line.startsWith('index ') ||
|
|
2177
|
+
line.startsWith('---') || line.startsWith('+++') ||
|
|
2178
|
+
line.startsWith('new file') || line.startsWith('deleted file') ||
|
|
2179
|
+
line.startsWith('old mode') || line.startsWith('new mode')) {
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
if (line.startsWith('@@')) {
|
|
2184
|
+
// 解析 hunk header: @@ -old,count +new,count @@
|
|
2185
|
+
var match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
2186
|
+
if (match) {
|
|
2187
|
+
oldLine = parseInt(match[1], 10);
|
|
2188
|
+
newLine = parseInt(match[2], 10);
|
|
2189
|
+
}
|
|
2190
|
+
html += '<tr class="diff-line diff-line-hunk">';
|
|
2191
|
+
html += '<td class="diff-line-num"></td><td class="diff-line-num"></td>';
|
|
2192
|
+
html += '<td class="diff-line-content">' + escapeHtml(line) + '</td></tr>';
|
|
2193
|
+
} else if (line.startsWith('+')) {
|
|
2194
|
+
html += '<tr class="diff-line diff-line-add">';
|
|
2195
|
+
html += '<td class="diff-line-num"></td>';
|
|
2196
|
+
html += '<td class="diff-line-num">' + newLine + '</td>';
|
|
2197
|
+
html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
|
|
2198
|
+
newLine++;
|
|
2199
|
+
} else if (line.startsWith('-')) {
|
|
2200
|
+
html += '<tr class="diff-line diff-line-del">';
|
|
2201
|
+
html += '<td class="diff-line-num">' + oldLine + '</td>';
|
|
2202
|
+
html += '<td class="diff-line-num"></td>';
|
|
2203
|
+
html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
|
|
2204
|
+
oldLine++;
|
|
2205
|
+
} else if (line.startsWith(' ') || (line === '' && i < lines.length - 1)) {
|
|
2206
|
+
html += '<tr class="diff-line">';
|
|
2207
|
+
html += '<td class="diff-line-num">' + oldLine + '</td>';
|
|
2208
|
+
html += '<td class="diff-line-num">' + newLine + '</td>';
|
|
2209
|
+
html += '<td class="diff-line-content">' + escapeHtml(line.substring(1) || '') + '</td></tr>';
|
|
2210
|
+
oldLine++;
|
|
2211
|
+
newLine++;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
html += '</table>';
|
|
2216
|
+
return html;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// Diff 按钮事件绑定
|
|
2220
|
+
document.getElementById('diff-toggle').addEventListener('click', toggleDiffBar);
|
|
2221
|
+
document.getElementById('close-diff').addEventListener('click', toggleDiffBar);
|
|
2222
|
+
document.getElementById('refresh-diff').addEventListener('click', function(e) {
|
|
2223
|
+
e.stopPropagation();
|
|
2224
|
+
loadGitStatus();
|
|
2225
|
+
// 重置 diff 内容区
|
|
2226
|
+
diffSelectedFile = null;
|
|
2227
|
+
document.getElementById('git-diff-content-area').innerHTML =
|
|
2228
|
+
'<div class="git-diff-placeholder">' +
|
|
2229
|
+
'<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5">' +
|
|
2230
|
+
'<line x1="6" y1="3" x2="6" y2="15"></line>' +
|
|
2231
|
+
'<circle cx="18" cy="6" r="3"></circle>' +
|
|
2232
|
+
'<circle cx="6" cy="18" r="3"></circle>' +
|
|
2233
|
+
'<path d="M18 9a9 9 0 0 1-9 9"></path>' +
|
|
2234
|
+
'</svg><span>点击文件查看 diff</span></div>';
|
|
2235
|
+
});
|
|
2236
|
+
|
|
1792
2237
|
// 初始化虚拟按键事件
|
|
1793
2238
|
setupVirtualKeyEvents();
|
|
1794
2239
|
|
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();
|