claude-opencode-viewer 2.6.38 → 2.6.39
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-pc.html +133 -47
- package/index.html +287 -84
- package/package.json +1 -1
- package/server.js +30 -38
package/index-pc.html
CHANGED
|
@@ -1007,6 +1007,7 @@
|
|
|
1007
1007
|
|
|
1008
1008
|
<div id="copy-toast">已复制</div>
|
|
1009
1009
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
1010
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script>
|
|
1010
1011
|
<script>
|
|
1011
1012
|
(function() {
|
|
1012
1013
|
var isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
@@ -1036,6 +1037,17 @@
|
|
|
1036
1037
|
|
|
1037
1038
|
term.open(document.getElementById('terminal'));
|
|
1038
1039
|
|
|
1040
|
+
// WebGL 渲染器:GPU 加速绘制
|
|
1041
|
+
if (window.WebglAddon) {
|
|
1042
|
+
try {
|
|
1043
|
+
var webglAddon = new WebglAddon.WebglAddon();
|
|
1044
|
+
webglAddon.onContextLoss(function() {
|
|
1045
|
+
webglAddon.dispose();
|
|
1046
|
+
});
|
|
1047
|
+
term.loadAddon(webglAddon);
|
|
1048
|
+
} catch(e) {}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1039
1051
|
// PC端复制:用 xterm.js selection API 获取纯文本,避免复制出乱码
|
|
1040
1052
|
document.getElementById('terminal').addEventListener('copy', function(e) {
|
|
1041
1053
|
var sel = term.getSelection();
|
|
@@ -1585,7 +1597,8 @@
|
|
|
1585
1597
|
|
|
1586
1598
|
ws.onclose = function() {
|
|
1587
1599
|
ws = null;
|
|
1588
|
-
term.reset();
|
|
1600
|
+
term.reset();
|
|
1601
|
+
term.write('\r\n \x1b[33m连接断开,正在重连...\x1b[0m\r\n');
|
|
1589
1602
|
setTimeout(connect, 2000);
|
|
1590
1603
|
};
|
|
1591
1604
|
|
|
@@ -1593,7 +1606,7 @@
|
|
|
1593
1606
|
try {
|
|
1594
1607
|
var msg = JSON.parse(e.data);
|
|
1595
1608
|
if (msg.type === 'data') {
|
|
1596
|
-
if (!isCreatingNewSession) {
|
|
1609
|
+
if (!isCreatingNewSession && !isTransitioning) {
|
|
1597
1610
|
throttledWrite(msg.data);
|
|
1598
1611
|
}
|
|
1599
1612
|
}
|
|
@@ -1604,14 +1617,19 @@
|
|
|
1604
1617
|
}
|
|
1605
1618
|
}
|
|
1606
1619
|
else if (msg.type === 'mode') {
|
|
1620
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1621
|
+
writeBuffer = '';
|
|
1622
|
+
term.reset();
|
|
1607
1623
|
endTransition(msg.mode);
|
|
1608
|
-
|
|
1624
|
+
if (msg.buffer) {
|
|
1625
|
+
term.write(msg.buffer);
|
|
1626
|
+
}
|
|
1609
1627
|
rebindTouchScroll();
|
|
1610
1628
|
}
|
|
1611
1629
|
else if (msg.type === 'switching') {
|
|
1612
|
-
|
|
1613
|
-
term.reset();
|
|
1630
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1614
1631
|
writeBuffer = '';
|
|
1632
|
+
term.clear();
|
|
1615
1633
|
}
|
|
1616
1634
|
else if (msg.type === 'state') {
|
|
1617
1635
|
if (msg.mode) {
|
|
@@ -1626,19 +1644,28 @@
|
|
|
1626
1644
|
}
|
|
1627
1645
|
}
|
|
1628
1646
|
else if (msg.type === 'restored') {
|
|
1629
|
-
|
|
1647
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1648
|
+
writeBuffer = '';
|
|
1630
1649
|
term.reset();
|
|
1650
|
+
if (msg.buffer) {
|
|
1651
|
+
term.write(msg.buffer);
|
|
1652
|
+
}
|
|
1631
1653
|
}
|
|
1632
1654
|
else if (msg.type === 'restore-error') {
|
|
1633
|
-
// 恢复失败
|
|
1634
1655
|
term.write('恢复失败: ' + msg.error + '\r\n');
|
|
1635
1656
|
}
|
|
1636
1657
|
else if (msg.type === 'started') {
|
|
1637
1658
|
rebindTouchScroll();
|
|
1659
|
+
preloadData();
|
|
1638
1660
|
}
|
|
1639
1661
|
else if (msg.type === 'new-session-ok') {
|
|
1640
|
-
|
|
1662
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1663
|
+
writeBuffer = '';
|
|
1641
1664
|
term.reset();
|
|
1665
|
+
if (msg.buffer) {
|
|
1666
|
+
term.write(msg.buffer);
|
|
1667
|
+
}
|
|
1668
|
+
isCreatingNewSession = false;
|
|
1642
1669
|
}
|
|
1643
1670
|
else if (msg.type === 'new-session-error') {
|
|
1644
1671
|
isCreatingNewSession = false;
|
|
@@ -1664,11 +1691,14 @@
|
|
|
1664
1691
|
});
|
|
1665
1692
|
}
|
|
1666
1693
|
|
|
1667
|
-
//
|
|
1694
|
+
// 页面卸载前保存输入缓存,并通知服务端退出
|
|
1668
1695
|
window.addEventListener('beforeunload', function() {
|
|
1669
1696
|
if (currentInputBuffer) {
|
|
1670
1697
|
saveInputCache();
|
|
1671
1698
|
}
|
|
1699
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1700
|
+
ws.send(JSON.stringify({ type: 'quit' }));
|
|
1701
|
+
}
|
|
1672
1702
|
});
|
|
1673
1703
|
|
|
1674
1704
|
// 页面可见性变化时保存缓存
|
|
@@ -2183,6 +2213,29 @@
|
|
|
2183
2213
|
var diffChanges = [];
|
|
2184
2214
|
var diffSelectedFile = null;
|
|
2185
2215
|
|
|
2216
|
+
// 预加载缓存
|
|
2217
|
+
var cachedGitStatus = null;
|
|
2218
|
+
var cachedDocs = null;
|
|
2219
|
+
var gitStatusLoading = false;
|
|
2220
|
+
var docsLoading = false;
|
|
2221
|
+
|
|
2222
|
+
function preloadData() {
|
|
2223
|
+
if (!gitStatusLoading) {
|
|
2224
|
+
gitStatusLoading = true;
|
|
2225
|
+
fetch(basePath + '/api/git-status')
|
|
2226
|
+
.then(function(r) { return r.json(); })
|
|
2227
|
+
.then(function(data) { cachedGitStatus = data; gitStatusLoading = false; })
|
|
2228
|
+
.catch(function() { gitStatusLoading = false; });
|
|
2229
|
+
}
|
|
2230
|
+
if (!docsLoading) {
|
|
2231
|
+
docsLoading = true;
|
|
2232
|
+
fetch(basePath + '/api/docs')
|
|
2233
|
+
.then(function(r) { return r.json(); })
|
|
2234
|
+
.then(function(data) { cachedDocs = data; docsLoading = false; })
|
|
2235
|
+
.catch(function() { docsLoading = false; });
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2186
2239
|
var STATUS_COLORS = {
|
|
2187
2240
|
'M': '#e2c08d', 'A': '#73c991', 'D': '#f14c4c',
|
|
2188
2241
|
'R': '#73c991', 'C': '#73c991', 'U': '#e2c08d',
|
|
@@ -2194,27 +2247,42 @@
|
|
|
2194
2247
|
var bar = document.getElementById('git-diff-bar');
|
|
2195
2248
|
if (diffBarVisible) {
|
|
2196
2249
|
bar.classList.add('visible');
|
|
2197
|
-
loadGitStatus();
|
|
2250
|
+
loadGitStatus(false);
|
|
2198
2251
|
} else {
|
|
2199
2252
|
bar.classList.remove('visible');
|
|
2200
2253
|
diffSelectedFile = null;
|
|
2201
2254
|
}
|
|
2202
2255
|
}
|
|
2203
2256
|
|
|
2204
|
-
function loadGitStatus() {
|
|
2257
|
+
function loadGitStatus(forceRefresh) {
|
|
2205
2258
|
var fileList = document.getElementById('git-diff-file-list');
|
|
2206
|
-
|
|
2207
|
-
|
|
2259
|
+
|
|
2260
|
+
if (cachedGitStatus && !forceRefresh) {
|
|
2261
|
+
diffChanges = cachedGitStatus.changes || [];
|
|
2262
|
+
document.getElementById('git-diff-count').textContent = diffChanges.length;
|
|
2263
|
+
renderDiffFileList();
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
fileList.innerHTML = '<div class="git-diff-loading">' + (forceRefresh ? '正在刷新...' : '正在查询 git status...') + '</div>';
|
|
2268
|
+
document.getElementById('git-diff-count').textContent = '...';
|
|
2269
|
+
|
|
2270
|
+
if (gitStatusLoading && !forceRefresh) return;
|
|
2271
|
+
gitStatusLoading = true;
|
|
2272
|
+
if (forceRefresh) cachedGitStatus = null;
|
|
2208
2273
|
|
|
2209
2274
|
fetch(basePath + '/api/git-status')
|
|
2210
2275
|
.then(function(r) { return r.json(); })
|
|
2211
2276
|
.then(function(data) {
|
|
2277
|
+
cachedGitStatus = data;
|
|
2278
|
+
gitStatusLoading = false;
|
|
2212
2279
|
diffChanges = data.changes || [];
|
|
2213
2280
|
document.getElementById('git-diff-count').textContent = diffChanges.length;
|
|
2214
|
-
renderDiffFileList();
|
|
2281
|
+
if (diffBarVisible) renderDiffFileList();
|
|
2215
2282
|
})
|
|
2216
2283
|
.catch(function() {
|
|
2217
|
-
|
|
2284
|
+
gitStatusLoading = false;
|
|
2285
|
+
if (diffBarVisible) fileList.innerHTML = '<div class="git-diff-error">无法加载 git status</div>';
|
|
2218
2286
|
});
|
|
2219
2287
|
}
|
|
2220
2288
|
|
|
@@ -2343,7 +2411,7 @@
|
|
|
2343
2411
|
document.getElementById('close-diff').addEventListener('click', toggleDiffBar);
|
|
2344
2412
|
document.getElementById('refresh-diff').addEventListener('click', function(e) {
|
|
2345
2413
|
e.stopPropagation();
|
|
2346
|
-
loadGitStatus();
|
|
2414
|
+
loadGitStatus(true);
|
|
2347
2415
|
// 重置 diff 内容区
|
|
2348
2416
|
diffSelectedFile = null;
|
|
2349
2417
|
document.getElementById('git-diff-content-area').innerHTML =
|
|
@@ -2365,49 +2433,67 @@
|
|
|
2365
2433
|
var bar = document.getElementById('docs-bar');
|
|
2366
2434
|
if (docsBarVisible) {
|
|
2367
2435
|
bar.classList.add('visible');
|
|
2368
|
-
loadDocs();
|
|
2436
|
+
loadDocs(false);
|
|
2369
2437
|
} else {
|
|
2370
2438
|
bar.classList.remove('visible');
|
|
2371
2439
|
docsSelectedFile = null;
|
|
2372
2440
|
}
|
|
2373
2441
|
}
|
|
2374
2442
|
|
|
2375
|
-
function
|
|
2443
|
+
function renderDocsList(data) {
|
|
2444
|
+
var fileList = document.getElementById('docs-file-list');
|
|
2445
|
+
var docs = data.docs || [];
|
|
2446
|
+
document.getElementById('docs-count').textContent = docs.length;
|
|
2447
|
+
if (!docs.length) {
|
|
2448
|
+
fileList.innerHTML = '<div class="docs-loading" style="color:#666;">无文档</div>';
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
var html = '<div style="padding:4px 12px;font-size:11px;color:#666;border-bottom:1px solid #2a2a2a;">📁 ' + escapeHtml(data.cwd || 'PROJECT_DIR') + '</div>';
|
|
2452
|
+
docs.forEach(function(doc) {
|
|
2453
|
+
var activeClass = docsSelectedFile === doc.name ? ' active' : '';
|
|
2454
|
+
var time = new Date(doc.mtime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
2455
|
+
html += '<div class="docs-file-item' + activeClass + '" data-file="' + escapeHtml(doc.name) + '">';
|
|
2456
|
+
html += '<span class="docs-file-name">' + escapeHtml(doc.name) + '</span>';
|
|
2457
|
+
html += '<span class="docs-file-time">' + time + '</span>';
|
|
2458
|
+
html += '</div>';
|
|
2459
|
+
});
|
|
2460
|
+
fileList.innerHTML = html;
|
|
2461
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(item) {
|
|
2462
|
+
item.addEventListener('click', function() {
|
|
2463
|
+
var file = this.getAttribute('data-file');
|
|
2464
|
+
docsSelectedFile = file;
|
|
2465
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(el) { el.classList.remove('active'); });
|
|
2466
|
+
this.classList.add('active');
|
|
2467
|
+
loadDocContent(file);
|
|
2468
|
+
});
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
function loadDocs(forceRefresh) {
|
|
2376
2473
|
var fileList = document.getElementById('docs-file-list');
|
|
2377
|
-
|
|
2378
|
-
|
|
2474
|
+
|
|
2475
|
+
if (cachedDocs && !forceRefresh) {
|
|
2476
|
+
renderDocsList(cachedDocs);
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
fileList.innerHTML = '<div class="docs-loading">' + (forceRefresh ? '正在刷新...' : '正在查询文档...') + '</div>';
|
|
2481
|
+
document.getElementById('docs-count').textContent = '...';
|
|
2482
|
+
|
|
2483
|
+
if (docsLoading && !forceRefresh) return;
|
|
2484
|
+
docsLoading = true;
|
|
2485
|
+
if (forceRefresh) cachedDocs = null;
|
|
2379
2486
|
|
|
2380
2487
|
fetch(basePath + '/api/docs')
|
|
2381
2488
|
.then(function(r) { return r.json(); })
|
|
2382
2489
|
.then(function(data) {
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
if (
|
|
2386
|
-
fileList.innerHTML = '<div class="docs-loading" style="color:#666;">无文档</div>';
|
|
2387
|
-
return;
|
|
2388
|
-
}
|
|
2389
|
-
var html = '<div style="padding:4px 12px;font-size:11px;color:#666;border-bottom:1px solid #2a2a2a;">📁 ' + escapeHtml(data.cwd || 'PROJECT_DIR') + '</div>';
|
|
2390
|
-
docs.forEach(function(doc) {
|
|
2391
|
-
var activeClass = docsSelectedFile === doc.name ? ' active' : '';
|
|
2392
|
-
var time = new Date(doc.mtime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
2393
|
-
html += '<div class="docs-file-item' + activeClass + '" data-file="' + escapeHtml(doc.name) + '">';
|
|
2394
|
-
html += '<span class="docs-file-name">' + escapeHtml(doc.name) + '</span>';
|
|
2395
|
-
html += '<span class="docs-file-time">' + time + '</span>';
|
|
2396
|
-
html += '</div>';
|
|
2397
|
-
});
|
|
2398
|
-
fileList.innerHTML = html;
|
|
2399
|
-
fileList.querySelectorAll('.docs-file-item').forEach(function(item) {
|
|
2400
|
-
item.addEventListener('click', function() {
|
|
2401
|
-
var file = this.getAttribute('data-file');
|
|
2402
|
-
docsSelectedFile = file;
|
|
2403
|
-
fileList.querySelectorAll('.docs-file-item').forEach(function(el) { el.classList.remove('active'); });
|
|
2404
|
-
this.classList.add('active');
|
|
2405
|
-
loadDocContent(file);
|
|
2406
|
-
});
|
|
2407
|
-
});
|
|
2490
|
+
cachedDocs = data;
|
|
2491
|
+
docsLoading = false;
|
|
2492
|
+
if (docsBarVisible) renderDocsList(data);
|
|
2408
2493
|
})
|
|
2409
2494
|
.catch(function() {
|
|
2410
|
-
|
|
2495
|
+
docsLoading = false;
|
|
2496
|
+
if (docsBarVisible) fileList.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
|
|
2411
2497
|
});
|
|
2412
2498
|
}
|
|
2413
2499
|
|
|
@@ -2432,7 +2518,7 @@
|
|
|
2432
2518
|
document.getElementById('close-docs').addEventListener('click', toggleDocsBar);
|
|
2433
2519
|
document.getElementById('refresh-docs').addEventListener('click', function(e) {
|
|
2434
2520
|
e.stopPropagation();
|
|
2435
|
-
loadDocs();
|
|
2521
|
+
loadDocs(true);
|
|
2436
2522
|
docsSelectedFile = null;
|
|
2437
2523
|
document.getElementById('docs-content-area').innerHTML =
|
|
2438
2524
|
'<div class="docs-placeholder">' +
|
package/index.html
CHANGED
|
@@ -489,6 +489,18 @@
|
|
|
489
489
|
-webkit-user-select: text;
|
|
490
490
|
user-select: text;
|
|
491
491
|
}
|
|
492
|
+
.msg-text pre {
|
|
493
|
+
background: #0d0d0d;
|
|
494
|
+
border: 1px solid #333;
|
|
495
|
+
border-radius: 4px;
|
|
496
|
+
padding: 8px;
|
|
497
|
+
overflow-x: auto;
|
|
498
|
+
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
499
|
+
font-size: 12px;
|
|
500
|
+
line-height: 1.4;
|
|
501
|
+
white-space: pre;
|
|
502
|
+
-webkit-overflow-scrolling: touch;
|
|
503
|
+
}
|
|
492
504
|
.msg-tool {
|
|
493
505
|
margin-top: 6px;
|
|
494
506
|
padding: 6px 8px;
|
|
@@ -1044,6 +1056,7 @@
|
|
|
1044
1056
|
|
|
1045
1057
|
<div id="copy-toast">已复制</div>
|
|
1046
1058
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
1059
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script>
|
|
1047
1060
|
<script>
|
|
1048
1061
|
(function() {
|
|
1049
1062
|
var isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
@@ -1052,6 +1065,7 @@
|
|
|
1052
1065
|
var fontSize = isMobile ? 11 : 13;
|
|
1053
1066
|
var currentMode = 'claude';
|
|
1054
1067
|
var isTransitioning = false;
|
|
1068
|
+
var mobileInitSent = false;
|
|
1055
1069
|
|
|
1056
1070
|
var term = new Terminal({
|
|
1057
1071
|
cursorBlink: !isMobile,
|
|
@@ -1071,6 +1085,19 @@
|
|
|
1071
1085
|
|
|
1072
1086
|
term.open(document.getElementById('terminal'));
|
|
1073
1087
|
|
|
1088
|
+
// WebGL 渲染器:GPU 加速绘制,非 iOS 设备启用(iOS WebGL 性能差)
|
|
1089
|
+
if (!isIOS && window.WebglAddon) {
|
|
1090
|
+
try {
|
|
1091
|
+
var webglAddon = new WebglAddon.WebglAddon();
|
|
1092
|
+
webglAddon.onContextLoss(function() {
|
|
1093
|
+
webglAddon.dispose();
|
|
1094
|
+
});
|
|
1095
|
+
term.loadAddon(webglAddon);
|
|
1096
|
+
} catch(e) {
|
|
1097
|
+
// WebGL 不可用时回退到 Canvas 渲染
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1074
1101
|
// PC端复制:用 xterm.js selection API 获取纯文本,避免复制出乱码
|
|
1075
1102
|
document.getElementById('terminal').addEventListener('copy', function(e) {
|
|
1076
1103
|
var sel = term.getSelection();
|
|
@@ -1132,6 +1159,8 @@
|
|
|
1132
1159
|
// 会话历史相关
|
|
1133
1160
|
var sessions = [];
|
|
1134
1161
|
var currentSessionId = null;
|
|
1162
|
+
var claudeSessionId = null;
|
|
1163
|
+
var claudeProject = null;
|
|
1135
1164
|
var currentSessionData = null;
|
|
1136
1165
|
var historyBarVisible = false;
|
|
1137
1166
|
|
|
@@ -1258,15 +1287,10 @@
|
|
|
1258
1287
|
}
|
|
1259
1288
|
}
|
|
1260
1289
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
pixelAccum = 0;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// 触摸滚动实现 - 混合模式:alternate buffer 发鼠标滚轮,normal buffer 用 scrollLines
|
|
1269
|
-
var SCROLL_SENSITIVITY = 0.7; // 滚动灵敏度,越小每次滑动行数越少
|
|
1290
|
+
// 触摸滚动实现
|
|
1291
|
+
// Claude 模式:与 cc-viewer 一致,直接 scrollLines,1:1 像素映射,无 WheelEvent
|
|
1292
|
+
// OpenCode 模式:alternate buffer 发 WheelEvent(vim/less 等需要),灵敏度 0.7
|
|
1293
|
+
var SCROLL_SENSITIVITY = 0.7;
|
|
1270
1294
|
var touchScreen = null;
|
|
1271
1295
|
var touchEventsBound = false;
|
|
1272
1296
|
|
|
@@ -1300,6 +1324,12 @@
|
|
|
1300
1324
|
|
|
1301
1325
|
function doScroll(lines) {
|
|
1302
1326
|
if (lines === 0) return;
|
|
1327
|
+
// Claude 模式:始终直接 scrollLines(与 cc-viewer 一致)
|
|
1328
|
+
if (currentMode === 'claude') {
|
|
1329
|
+
term.scrollLines(lines);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
// OpenCode 模式:alternate buffer 发 WheelEvent,normal buffer 用 scrollLines
|
|
1303
1333
|
if (isAlternateBuffer()) {
|
|
1304
1334
|
altScrollAccum += lines;
|
|
1305
1335
|
if (Math.abs(altScrollAccum) >= ALT_SCROLL_THRESHOLD) {
|
|
@@ -1335,7 +1365,8 @@
|
|
|
1335
1365
|
scrollRaf = null;
|
|
1336
1366
|
if (pendingDy === 0) return;
|
|
1337
1367
|
|
|
1338
|
-
|
|
1368
|
+
// Claude 模式 1:1 像素映射(cc-viewer 同款),OpenCode 降低灵敏度
|
|
1369
|
+
pixelAccum += pendingDy * (currentMode === 'claude' ? 1 : SCROLL_SENSITIVITY);
|
|
1339
1370
|
pendingDy = 0;
|
|
1340
1371
|
|
|
1341
1372
|
var lh = getLineHeight();
|
|
@@ -1372,22 +1403,19 @@
|
|
|
1372
1403
|
}
|
|
1373
1404
|
|
|
1374
1405
|
pendingDy += dy;
|
|
1375
|
-
console.log('[scroll] touchmove dy:', dy, 'pendingDy:', pendingDy);
|
|
1376
|
-
|
|
1377
1406
|
if (!scrollRaf) scrollRaf = requestAnimationFrame(flushScroll);
|
|
1378
1407
|
lastY = y;
|
|
1379
1408
|
lastTime = now;
|
|
1380
1409
|
}
|
|
1381
1410
|
|
|
1382
1411
|
function handleTouchEnd() {
|
|
1383
|
-
|
|
1384
1412
|
if (scrollRaf) {
|
|
1385
1413
|
cancelAnimationFrame(scrollRaf);
|
|
1386
1414
|
scrollRaf = null;
|
|
1387
1415
|
}
|
|
1388
1416
|
|
|
1389
1417
|
if (pendingDy !== 0) {
|
|
1390
|
-
pixelAccum += pendingDy * SCROLL_SENSITIVITY;
|
|
1418
|
+
pixelAccum += pendingDy * (currentMode === 'claude' ? 1 : SCROLL_SENSITIVITY);
|
|
1391
1419
|
pendingDy = 0;
|
|
1392
1420
|
var lh = getLineHeight();
|
|
1393
1421
|
var lines = Math.trunc(pixelAccum / lh);
|
|
@@ -1412,9 +1440,8 @@
|
|
|
1412
1440
|
}
|
|
1413
1441
|
velocitySamples = [];
|
|
1414
1442
|
|
|
1415
|
-
velocity *= SCROLL_SENSITIVITY;
|
|
1416
|
-
|
|
1417
|
-
if (Math.abs(velocity) < 0.3) return;
|
|
1443
|
+
velocity *= (currentMode === 'claude' ? 1 : SCROLL_SENSITIVITY);
|
|
1444
|
+
if (Math.abs(velocity) < 0.5) return;
|
|
1418
1445
|
|
|
1419
1446
|
var friction = 0.95;
|
|
1420
1447
|
var mAccum = 0;
|
|
@@ -1443,7 +1470,6 @@
|
|
|
1443
1470
|
|
|
1444
1471
|
function unbindTouchScroll() {
|
|
1445
1472
|
if (touchScreen && touchEventsBound) {
|
|
1446
|
-
console.log('[scroll] unbinding touch events');
|
|
1447
1473
|
touchScreen.removeEventListener('touchstart', handleTouchStart);
|
|
1448
1474
|
touchScreen.removeEventListener('touchmove', handleTouchMove);
|
|
1449
1475
|
touchScreen.removeEventListener('touchend', handleTouchEnd);
|
|
@@ -1553,7 +1579,8 @@
|
|
|
1553
1579
|
|
|
1554
1580
|
ws.onclose = function() {
|
|
1555
1581
|
ws = null;
|
|
1556
|
-
term.reset();
|
|
1582
|
+
term.reset();
|
|
1583
|
+
term.write('\r\n \x1b[33m连接断开,正在重连...\x1b[0m\r\n');
|
|
1557
1584
|
setTimeout(connect, 2000);
|
|
1558
1585
|
};
|
|
1559
1586
|
|
|
@@ -1561,7 +1588,7 @@
|
|
|
1561
1588
|
try {
|
|
1562
1589
|
var msg = JSON.parse(e.data);
|
|
1563
1590
|
if (msg.type === 'data') {
|
|
1564
|
-
if (!isCreatingNewSession) {
|
|
1591
|
+
if (!isCreatingNewSession && !isTransitioning) {
|
|
1565
1592
|
throttledWrite(msg.data);
|
|
1566
1593
|
}
|
|
1567
1594
|
}
|
|
@@ -1571,15 +1598,46 @@
|
|
|
1571
1598
|
throttledWrite('按 Enter 键重新启动 ' + currentMode + '...\r\n');
|
|
1572
1599
|
}
|
|
1573
1600
|
}
|
|
1601
|
+
else if (msg.type === 'state') {
|
|
1602
|
+
// 服务端还没启动进程时,查最近会话并自动恢复
|
|
1603
|
+
if (!msg.running && !mobileInitSent) {
|
|
1604
|
+
mobileInitSent = true;
|
|
1605
|
+
term.write('\r\n \x1b[36m正在恢复会话...\x1b[0m\r\n');
|
|
1606
|
+
fetch('/api/last-sessions')
|
|
1607
|
+
.then(function(r) { return r.json(); })
|
|
1608
|
+
.then(function(data) {
|
|
1609
|
+
var oc = data.opencode;
|
|
1610
|
+
var cl = data.claude;
|
|
1611
|
+
// 取 mtime 最新的那个
|
|
1612
|
+
var useOc = oc && (!cl || oc.mtime > cl.mtime);
|
|
1613
|
+
var mode = useOc ? 'opencode' : 'claude';
|
|
1614
|
+
var sessionId = useOc ? (oc && oc.id) : (cl && cl.id);
|
|
1615
|
+
if (cl) {
|
|
1616
|
+
claudeSessionId = cl.id;
|
|
1617
|
+
claudeProject = cl.project;
|
|
1618
|
+
}
|
|
1619
|
+
currentMode = mode;
|
|
1620
|
+
ws.send(JSON.stringify({ type: 'init', mode: mode, sessionId: sessionId || null }));
|
|
1621
|
+
})
|
|
1622
|
+
.catch(function() {
|
|
1623
|
+
ws.send(JSON.stringify({ type: 'init', mode: 'claude' }));
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1574
1627
|
else if (msg.type === 'mode') {
|
|
1628
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1629
|
+
writeBuffer = '';
|
|
1630
|
+
term.reset();
|
|
1575
1631
|
endTransition(msg.mode);
|
|
1576
|
-
|
|
1632
|
+
if (msg.buffer) {
|
|
1633
|
+
term.write(msg.buffer);
|
|
1634
|
+
}
|
|
1577
1635
|
rebindTouchScroll();
|
|
1578
1636
|
}
|
|
1579
1637
|
else if (msg.type === 'switching') {
|
|
1580
|
-
|
|
1581
|
-
term.reset();
|
|
1638
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1582
1639
|
writeBuffer = '';
|
|
1640
|
+
term.clear();
|
|
1583
1641
|
}
|
|
1584
1642
|
else if (msg.type === 'state') {
|
|
1585
1643
|
if (msg.mode) {
|
|
@@ -1589,22 +1647,36 @@
|
|
|
1589
1647
|
}
|
|
1590
1648
|
}
|
|
1591
1649
|
else if (msg.type === 'restored') {
|
|
1592
|
-
//
|
|
1650
|
+
// 会话恢复成功,清除所有残留
|
|
1651
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1652
|
+
writeBuffer = '';
|
|
1593
1653
|
term.reset();
|
|
1654
|
+
if (msg.buffer) {
|
|
1655
|
+
term.write(msg.buffer);
|
|
1656
|
+
}
|
|
1657
|
+
isTransitioning = false;
|
|
1594
1658
|
}
|
|
1595
1659
|
else if (msg.type === 'restore-error') {
|
|
1596
|
-
|
|
1660
|
+
isTransitioning = false;
|
|
1597
1661
|
term.write('恢复失败: ' + msg.error + '\r\n');
|
|
1598
1662
|
}
|
|
1599
1663
|
else if (msg.type === 'started') {
|
|
1600
1664
|
rebindTouchScroll();
|
|
1665
|
+
preloadData();
|
|
1601
1666
|
}
|
|
1602
1667
|
else if (msg.type === 'new-session-ok') {
|
|
1603
|
-
|
|
1668
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1669
|
+
writeBuffer = '';
|
|
1604
1670
|
term.reset();
|
|
1671
|
+
if (msg.buffer) {
|
|
1672
|
+
term.write(msg.buffer);
|
|
1673
|
+
}
|
|
1674
|
+
isCreatingNewSession = false;
|
|
1675
|
+
isTransitioning = false;
|
|
1605
1676
|
}
|
|
1606
1677
|
else if (msg.type === 'new-session-error') {
|
|
1607
1678
|
isCreatingNewSession = false;
|
|
1679
|
+
isTransitioning = false;
|
|
1608
1680
|
term.write('新会话启动失败: ' + msg.error + '\r\n');
|
|
1609
1681
|
}
|
|
1610
1682
|
} catch(err) {}
|
|
@@ -1923,11 +1995,15 @@
|
|
|
1923
1995
|
// 关闭历史栏
|
|
1924
1996
|
toggleHistoryBar();
|
|
1925
1997
|
|
|
1926
|
-
//
|
|
1998
|
+
// 阻止旧数据写入
|
|
1999
|
+
isTransitioning = true;
|
|
2000
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
2001
|
+
writeBuffer = '';
|
|
1927
2002
|
term.reset();
|
|
1928
2003
|
|
|
1929
2004
|
if (ws && ws.readyState === 1) {
|
|
1930
2005
|
if (currentMode !== 'opencode') {
|
|
2006
|
+
isTransitioning = false;
|
|
1931
2007
|
term.write('错误: 请先切换到 OpenCode 模式\r\n');
|
|
1932
2008
|
return;
|
|
1933
2009
|
}
|
|
@@ -2022,7 +2098,7 @@
|
|
|
2022
2098
|
item.appendChild(deleteBtn);
|
|
2023
2099
|
|
|
2024
2100
|
item.addEventListener('click', function() {
|
|
2025
|
-
restoreClaudeSession(s.id);
|
|
2101
|
+
restoreClaudeSession(s.id, s.project);
|
|
2026
2102
|
});
|
|
2027
2103
|
|
|
2028
2104
|
sessionList.appendChild(item);
|
|
@@ -2113,12 +2189,18 @@
|
|
|
2113
2189
|
});
|
|
2114
2190
|
}
|
|
2115
2191
|
|
|
2116
|
-
function restoreClaudeSession(sessionId) {
|
|
2192
|
+
function restoreClaudeSession(sessionId, project) {
|
|
2117
2193
|
console.log('[restore] 恢复 Claude 会话:', sessionId);
|
|
2194
|
+
claudeSessionId = sessionId;
|
|
2195
|
+
if (project) claudeProject = project;
|
|
2118
2196
|
toggleHistoryBar();
|
|
2197
|
+
isTransitioning = true;
|
|
2198
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
2199
|
+
writeBuffer = '';
|
|
2119
2200
|
term.reset();
|
|
2120
2201
|
if (ws && ws.readyState === 1) {
|
|
2121
2202
|
if (currentMode !== 'claude') {
|
|
2203
|
+
isTransitioning = false;
|
|
2122
2204
|
term.write('错误: 请先切换到 Claude 模式\r\n');
|
|
2123
2205
|
return;
|
|
2124
2206
|
}
|
|
@@ -2140,9 +2222,12 @@
|
|
|
2140
2222
|
// 新会话按钮
|
|
2141
2223
|
var isCreatingNewSession = false;
|
|
2142
2224
|
document.getElementById('new-session-btn').addEventListener('click', function() {
|
|
2143
|
-
if (!ws || ws.readyState !== 1) return;
|
|
2225
|
+
if (!ws || ws.readyState !== 1 || isTransitioning) return;
|
|
2144
2226
|
isCreatingNewSession = true;
|
|
2145
|
-
|
|
2227
|
+
isTransitioning = true;
|
|
2228
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
2229
|
+
writeBuffer = '';
|
|
2230
|
+
term.reset();
|
|
2146
2231
|
ws.send(JSON.stringify({ type: 'new-session' }));
|
|
2147
2232
|
});
|
|
2148
2233
|
|
|
@@ -2227,23 +2312,81 @@
|
|
|
2227
2312
|
msgViewerContent.innerHTML = '<div class="msg-empty">加载中...</div>';
|
|
2228
2313
|
unbindTouchScroll();
|
|
2229
2314
|
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
if (!data.sessionId) {
|
|
2235
|
-
msgViewerContent.innerHTML = '<div class="msg-empty">暂无活跃会话</div>';
|
|
2236
|
-
return;
|
|
2237
|
-
}
|
|
2238
|
-
return fetch(basePath + '/api/session/' + data.sessionId)
|
|
2315
|
+
if (currentMode === 'claude') {
|
|
2316
|
+
// Claude 模式:从 JSONL 文件读取消息
|
|
2317
|
+
var loadClaudeMessages = function(sid, proj) {
|
|
2318
|
+
fetch(basePath + '/api/claude-session-messages?id=' + encodeURIComponent(sid) + '&project=' + encodeURIComponent(proj))
|
|
2239
2319
|
.then(function(r) { return r.json(); })
|
|
2240
|
-
.then(function(
|
|
2320
|
+
.then(function(data) {
|
|
2321
|
+
if (data.error) {
|
|
2322
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">' + escapeHtml(data.error) + '</div>';
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
var messages = data.messages || [];
|
|
2241
2326
|
renderMessages(messages);
|
|
2327
|
+
})
|
|
2328
|
+
.catch(function(e) {
|
|
2329
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">加载失败: ' + e.message + '</div>';
|
|
2242
2330
|
});
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
}
|
|
2331
|
+
};
|
|
2332
|
+
if (claudeSessionId && claudeProject) {
|
|
2333
|
+
loadClaudeMessages(claudeSessionId, claudeProject);
|
|
2334
|
+
} else {
|
|
2335
|
+
// 尚未获取到 session 信息,先查
|
|
2336
|
+
fetch(basePath + '/api/last-sessions')
|
|
2337
|
+
.then(function(r) { return r.json(); })
|
|
2338
|
+
.then(function(data) {
|
|
2339
|
+
var cl = data.claude;
|
|
2340
|
+
if (cl && cl.id && cl.project) {
|
|
2341
|
+
claudeSessionId = cl.id;
|
|
2342
|
+
claudeProject = cl.project;
|
|
2343
|
+
loadClaudeMessages(cl.id, cl.project);
|
|
2344
|
+
} else {
|
|
2345
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">暂无活跃会话</div>';
|
|
2346
|
+
}
|
|
2347
|
+
})
|
|
2348
|
+
.catch(function(e) {
|
|
2349
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">加载失败: ' + e.message + '</div>';
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
} else {
|
|
2353
|
+
// OpenCode 模式:从 SQLite 读取消息
|
|
2354
|
+
fetch(basePath + '/api/current-session')
|
|
2355
|
+
.then(function(r) { return r.json(); })
|
|
2356
|
+
.then(function(data) {
|
|
2357
|
+
if (!data.sessionId) {
|
|
2358
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">暂无活跃会话</div>';
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
return fetch(basePath + '/api/session/' + data.sessionId)
|
|
2362
|
+
.then(function(r) { return r.json(); })
|
|
2363
|
+
.then(function(messages) {
|
|
2364
|
+
renderMessages(messages);
|
|
2365
|
+
});
|
|
2366
|
+
})
|
|
2367
|
+
.catch(function(e) {
|
|
2368
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">加载失败: ' + e.message + '</div>';
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function formatMsgText(text) {
|
|
2374
|
+
// 将 markdown 代码块转为 <pre>,其余部分转义
|
|
2375
|
+
var parts = text.split(/(```[\s\S]*?```)/g);
|
|
2376
|
+
var result = '';
|
|
2377
|
+
for (var i = 0; i < parts.length; i++) {
|
|
2378
|
+
var p = parts[i];
|
|
2379
|
+
if (p.startsWith('```') && p.endsWith('```')) {
|
|
2380
|
+
// 去掉首尾 ```(可能带语言标记)
|
|
2381
|
+
var inner = p.slice(3, -3);
|
|
2382
|
+
var nlIdx = inner.indexOf('\n');
|
|
2383
|
+
if (nlIdx !== -1) inner = inner.slice(nlIdx + 1);
|
|
2384
|
+
result += '<pre>' + escapeHtml(inner) + '</pre>';
|
|
2385
|
+
} else {
|
|
2386
|
+
result += escapeHtml(p);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
return result;
|
|
2247
2390
|
}
|
|
2248
2391
|
|
|
2249
2392
|
function renderMessages(messages) {
|
|
@@ -2259,7 +2402,7 @@
|
|
|
2259
2402
|
html += '<div class="msg-item ' + cls + '">';
|
|
2260
2403
|
html += '<div class="msg-role">' + roleLabel + '</div>';
|
|
2261
2404
|
if (msg.text) {
|
|
2262
|
-
html += '<div class="msg-text">' +
|
|
2405
|
+
html += '<div class="msg-text">' + formatMsgText(msg.text) + '</div>';
|
|
2263
2406
|
}
|
|
2264
2407
|
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
2265
2408
|
msg.toolCalls.forEach(function(tc) {
|
|
@@ -2269,6 +2412,7 @@
|
|
|
2269
2412
|
html += '</div>';
|
|
2270
2413
|
});
|
|
2271
2414
|
msgViewerContent.innerHTML = html;
|
|
2415
|
+
msgViewerContent.scrollTop = msgViewerContent.scrollHeight;
|
|
2272
2416
|
}
|
|
2273
2417
|
|
|
2274
2418
|
function escapeHtml(str) {
|
|
@@ -2302,6 +2446,29 @@
|
|
|
2302
2446
|
var diffChanges = [];
|
|
2303
2447
|
var diffSelectedFile = null;
|
|
2304
2448
|
|
|
2449
|
+
// 预加载缓存
|
|
2450
|
+
var cachedGitStatus = null;
|
|
2451
|
+
var cachedDocs = null;
|
|
2452
|
+
var gitStatusLoading = false;
|
|
2453
|
+
var docsLoading = false;
|
|
2454
|
+
|
|
2455
|
+
function preloadData() {
|
|
2456
|
+
if (!gitStatusLoading) {
|
|
2457
|
+
gitStatusLoading = true;
|
|
2458
|
+
fetch(basePath + '/api/git-status')
|
|
2459
|
+
.then(function(r) { return r.json(); })
|
|
2460
|
+
.then(function(data) { cachedGitStatus = data; gitStatusLoading = false; })
|
|
2461
|
+
.catch(function() { gitStatusLoading = false; });
|
|
2462
|
+
}
|
|
2463
|
+
if (!docsLoading) {
|
|
2464
|
+
docsLoading = true;
|
|
2465
|
+
fetch(basePath + '/api/docs')
|
|
2466
|
+
.then(function(r) { return r.json(); })
|
|
2467
|
+
.then(function(data) { cachedDocs = data; docsLoading = false; })
|
|
2468
|
+
.catch(function() { docsLoading = false; });
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2305
2472
|
var STATUS_COLORS = {
|
|
2306
2473
|
'M': '#e2c08d', 'A': '#73c991', 'D': '#f14c4c',
|
|
2307
2474
|
'R': '#73c991', 'C': '#73c991', 'U': '#e2c08d',
|
|
@@ -2313,27 +2480,44 @@
|
|
|
2313
2480
|
var bar = document.getElementById('git-diff-bar');
|
|
2314
2481
|
if (diffBarVisible) {
|
|
2315
2482
|
bar.classList.add('visible');
|
|
2316
|
-
loadGitStatus();
|
|
2483
|
+
loadGitStatus(false);
|
|
2317
2484
|
} else {
|
|
2318
2485
|
bar.classList.remove('visible');
|
|
2319
2486
|
diffSelectedFile = null;
|
|
2320
2487
|
}
|
|
2321
2488
|
}
|
|
2322
2489
|
|
|
2323
|
-
function loadGitStatus() {
|
|
2490
|
+
function loadGitStatus(forceRefresh) {
|
|
2324
2491
|
var fileList = document.getElementById('git-diff-file-list');
|
|
2325
|
-
|
|
2326
|
-
|
|
2492
|
+
|
|
2493
|
+
// 有缓存且非强制刷新,直接用缓存
|
|
2494
|
+
if (cachedGitStatus && !forceRefresh) {
|
|
2495
|
+
diffChanges = cachedGitStatus.changes || [];
|
|
2496
|
+
document.getElementById('git-diff-count').textContent = diffChanges.length;
|
|
2497
|
+
renderDiffFileList();
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// 正在加载中或需要发起请求
|
|
2502
|
+
fileList.innerHTML = '<div class="git-diff-loading">' + (forceRefresh ? '正在刷新...' : '正在查询 git status...') + '</div>';
|
|
2503
|
+
document.getElementById('git-diff-count').textContent = '...';
|
|
2504
|
+
|
|
2505
|
+
if (gitStatusLoading && !forceRefresh) return; // 预加载进行中,等它完成
|
|
2506
|
+
gitStatusLoading = true;
|
|
2507
|
+
if (forceRefresh) cachedGitStatus = null;
|
|
2327
2508
|
|
|
2328
2509
|
fetch(basePath + '/api/git-status')
|
|
2329
2510
|
.then(function(r) { return r.json(); })
|
|
2330
2511
|
.then(function(data) {
|
|
2512
|
+
cachedGitStatus = data;
|
|
2513
|
+
gitStatusLoading = false;
|
|
2331
2514
|
diffChanges = data.changes || [];
|
|
2332
2515
|
document.getElementById('git-diff-count').textContent = diffChanges.length;
|
|
2333
|
-
renderDiffFileList();
|
|
2516
|
+
if (diffBarVisible) renderDiffFileList();
|
|
2334
2517
|
})
|
|
2335
2518
|
.catch(function() {
|
|
2336
|
-
|
|
2519
|
+
gitStatusLoading = false;
|
|
2520
|
+
if (diffBarVisible) fileList.innerHTML = '<div class="git-diff-error">无法加载 git status</div>';
|
|
2337
2521
|
});
|
|
2338
2522
|
}
|
|
2339
2523
|
|
|
@@ -2462,7 +2646,7 @@
|
|
|
2462
2646
|
document.getElementById('close-diff').addEventListener('click', toggleDiffBar);
|
|
2463
2647
|
document.getElementById('refresh-diff').addEventListener('click', function(e) {
|
|
2464
2648
|
e.stopPropagation();
|
|
2465
|
-
loadGitStatus();
|
|
2649
|
+
loadGitStatus(true);
|
|
2466
2650
|
// 重置 diff 内容区
|
|
2467
2651
|
diffSelectedFile = null;
|
|
2468
2652
|
document.getElementById('git-diff-content-area').innerHTML =
|
|
@@ -2484,49 +2668,68 @@
|
|
|
2484
2668
|
var bar = document.getElementById('docs-bar');
|
|
2485
2669
|
if (docsBarVisible) {
|
|
2486
2670
|
bar.classList.add('visible');
|
|
2487
|
-
loadDocs();
|
|
2671
|
+
loadDocs(false);
|
|
2488
2672
|
} else {
|
|
2489
2673
|
bar.classList.remove('visible');
|
|
2490
2674
|
docsSelectedFile = null;
|
|
2491
2675
|
}
|
|
2492
2676
|
}
|
|
2493
2677
|
|
|
2494
|
-
function
|
|
2678
|
+
function renderDocsList(data) {
|
|
2679
|
+
var fileList = document.getElementById('docs-file-list');
|
|
2680
|
+
var docs = data.docs || [];
|
|
2681
|
+
document.getElementById('docs-count').textContent = docs.length;
|
|
2682
|
+
if (!docs.length) {
|
|
2683
|
+
fileList.innerHTML = '<div class="docs-loading" style="color:#666;">无文档</div>';
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
var html = '<div style="padding:4px 12px;font-size:11px;color:#666;border-bottom:1px solid #2a2a2a;">📁 ' + escapeHtml(data.cwd || 'PROJECT_DIR') + '</div>';
|
|
2687
|
+
docs.forEach(function(doc) {
|
|
2688
|
+
var activeClass = docsSelectedFile === doc.name ? ' active' : '';
|
|
2689
|
+
var time = new Date(doc.mtime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
2690
|
+
html += '<div class="docs-file-item' + activeClass + '" data-file="' + escapeHtml(doc.name) + '">';
|
|
2691
|
+
html += '<span class="docs-file-name">' + escapeHtml(doc.name) + '</span>';
|
|
2692
|
+
html += '<span class="docs-file-time">' + time + '</span>';
|
|
2693
|
+
html += '</div>';
|
|
2694
|
+
});
|
|
2695
|
+
fileList.innerHTML = html;
|
|
2696
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(item) {
|
|
2697
|
+
item.addEventListener('click', function() {
|
|
2698
|
+
var file = this.getAttribute('data-file');
|
|
2699
|
+
docsSelectedFile = file;
|
|
2700
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(el) { el.classList.remove('active'); });
|
|
2701
|
+
this.classList.add('active');
|
|
2702
|
+
loadDocContent(file);
|
|
2703
|
+
});
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
function loadDocs(forceRefresh) {
|
|
2495
2708
|
var fileList = document.getElementById('docs-file-list');
|
|
2496
|
-
|
|
2497
|
-
|
|
2709
|
+
|
|
2710
|
+
// 有缓存且非强制刷新,直接用缓存
|
|
2711
|
+
if (cachedDocs && !forceRefresh) {
|
|
2712
|
+
renderDocsList(cachedDocs);
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
fileList.innerHTML = '<div class="docs-loading">' + (forceRefresh ? '正在刷新...' : '正在查询文档...') + '</div>';
|
|
2717
|
+
document.getElementById('docs-count').textContent = '...';
|
|
2718
|
+
|
|
2719
|
+
if (docsLoading && !forceRefresh) return;
|
|
2720
|
+
docsLoading = true;
|
|
2721
|
+
if (forceRefresh) cachedDocs = null;
|
|
2498
2722
|
|
|
2499
2723
|
fetch(basePath + '/api/docs')
|
|
2500
2724
|
.then(function(r) { return r.json(); })
|
|
2501
2725
|
.then(function(data) {
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
if (
|
|
2505
|
-
fileList.innerHTML = '<div class="docs-loading" style="color:#666;">无文档</div>';
|
|
2506
|
-
return;
|
|
2507
|
-
}
|
|
2508
|
-
var html = '<div style="padding:4px 12px;font-size:11px;color:#666;border-bottom:1px solid #2a2a2a;">📁 ' + escapeHtml(data.cwd || 'PROJECT_DIR') + '</div>';
|
|
2509
|
-
docs.forEach(function(doc) {
|
|
2510
|
-
var activeClass = docsSelectedFile === doc.name ? ' active' : '';
|
|
2511
|
-
var time = new Date(doc.mtime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
2512
|
-
html += '<div class="docs-file-item' + activeClass + '" data-file="' + escapeHtml(doc.name) + '">';
|
|
2513
|
-
html += '<span class="docs-file-name">' + escapeHtml(doc.name) + '</span>';
|
|
2514
|
-
html += '<span class="docs-file-time">' + time + '</span>';
|
|
2515
|
-
html += '</div>';
|
|
2516
|
-
});
|
|
2517
|
-
fileList.innerHTML = html;
|
|
2518
|
-
fileList.querySelectorAll('.docs-file-item').forEach(function(item) {
|
|
2519
|
-
item.addEventListener('click', function() {
|
|
2520
|
-
var file = this.getAttribute('data-file');
|
|
2521
|
-
docsSelectedFile = file;
|
|
2522
|
-
fileList.querySelectorAll('.docs-file-item').forEach(function(el) { el.classList.remove('active'); });
|
|
2523
|
-
this.classList.add('active');
|
|
2524
|
-
loadDocContent(file);
|
|
2525
|
-
});
|
|
2526
|
-
});
|
|
2726
|
+
cachedDocs = data;
|
|
2727
|
+
docsLoading = false;
|
|
2728
|
+
if (docsBarVisible) renderDocsList(data);
|
|
2527
2729
|
})
|
|
2528
2730
|
.catch(function() {
|
|
2529
|
-
|
|
2731
|
+
docsLoading = false;
|
|
2732
|
+
if (docsBarVisible) fileList.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
|
|
2530
2733
|
});
|
|
2531
2734
|
}
|
|
2532
2735
|
|
|
@@ -2551,7 +2754,7 @@
|
|
|
2551
2754
|
document.getElementById('close-docs').addEventListener('click', toggleDocsBar);
|
|
2552
2755
|
document.getElementById('refresh-docs').addEventListener('click', function(e) {
|
|
2553
2756
|
e.stopPropagation();
|
|
2554
|
-
loadDocs();
|
|
2757
|
+
loadDocs(true);
|
|
2555
2758
|
docsSelectedFile = null;
|
|
2556
2759
|
document.getElementById('docs-content-area').innerHTML =
|
|
2557
2760
|
'<div class="docs-placeholder">' +
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -404,7 +404,7 @@ async function switchMode(newMode) {
|
|
|
404
404
|
} catch (e) {
|
|
405
405
|
LOG('[switchMode] 启动新进程失败:', e.message);
|
|
406
406
|
}
|
|
407
|
-
isSwitching
|
|
407
|
+
// 注意:isSwitching 由调用方在回放 buffer 后设为 false
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
function writeToPty(data) {
|
|
@@ -1042,28 +1042,7 @@ const requestHandler = async (req, res) => {
|
|
|
1042
1042
|
const httpsOpts = USE_HTTPS ? await getOrCreateCert() : null;
|
|
1043
1043
|
let server, wss;
|
|
1044
1044
|
|
|
1045
|
-
// 无客户端连接后自动退出,防止进程堆积
|
|
1046
|
-
// 首次启动等 3 分钟(给用户时间打开浏览器),连接过之后断开等 30 秒
|
|
1047
|
-
let noClientTimer = null;
|
|
1048
1045
|
let hasEverConnected = false;
|
|
1049
|
-
function startNoClientTimer() {
|
|
1050
|
-
if (noClientTimer) return;
|
|
1051
|
-
if (wss && wss.clients.size > 0) return;
|
|
1052
|
-
const timeout = hasEverConnected ? 10000 : 180000;
|
|
1053
|
-
noClientTimer = setTimeout(() => {
|
|
1054
|
-
if (!wss || wss.clients.size === 0) {
|
|
1055
|
-
LOG(`[auto-exit] ${timeout / 1000}秒无客户端连接,自动退出`);
|
|
1056
|
-
cleanupAndExit();
|
|
1057
|
-
}
|
|
1058
|
-
noClientTimer = null;
|
|
1059
|
-
}, timeout);
|
|
1060
|
-
}
|
|
1061
|
-
function cancelNoClientTimer() {
|
|
1062
|
-
if (noClientTimer) {
|
|
1063
|
-
clearTimeout(noClientTimer);
|
|
1064
|
-
noClientTimer = null;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
1046
|
|
|
1068
1047
|
function createServerAndWss() {
|
|
1069
1048
|
server = USE_HTTPS
|
|
@@ -1087,7 +1066,6 @@ function setupWss(wssInst) {
|
|
|
1087
1066
|
wssInst.on('connection', (ws, req) => {
|
|
1088
1067
|
LOG('[WS] 客户端连接 from', req.socket.remoteAddress);
|
|
1089
1068
|
hasEverConnected = true;
|
|
1090
|
-
cancelNoClientTimer();
|
|
1091
1069
|
ws.isAlive = true;
|
|
1092
1070
|
ws.on('pong', () => { ws.isAlive = true; });
|
|
1093
1071
|
|
|
@@ -1178,16 +1156,14 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1178
1156
|
}
|
|
1179
1157
|
} else if (msg.type === 'switch') {
|
|
1180
1158
|
if (msg.mode !== currentMode) {
|
|
1159
|
+
isSwitching = true;
|
|
1181
1160
|
ws.send(JSON.stringify({ type: 'switching', mode: msg.mode }));
|
|
1182
1161
|
await switchMode(msg.mode);
|
|
1183
|
-
//
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
ws.send(JSON.stringify({ type: 'data', data: outputBuffer }));
|
|
1189
|
-
}
|
|
1190
|
-
}, 100);
|
|
1162
|
+
// 切换完成后统一回放(switchMode 期间 isSwitching=true,listener 不推数据)
|
|
1163
|
+
const buf = outputBuffer;
|
|
1164
|
+
outputBuffer = '';
|
|
1165
|
+
ws.send(JSON.stringify({ type: 'mode', mode: currentMode, buffer: buf || undefined }));
|
|
1166
|
+
isSwitching = false;
|
|
1191
1167
|
}
|
|
1192
1168
|
} else if (msg.type === 'restore') {
|
|
1193
1169
|
// 恢复会话(支持 opencode 和 claude)
|
|
@@ -1223,7 +1199,9 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1223
1199
|
// 启动进程,传入 session ID
|
|
1224
1200
|
try {
|
|
1225
1201
|
await spawnProcess(currentMode, msg.sessionId);
|
|
1226
|
-
|
|
1202
|
+
const buf = outputBuffer;
|
|
1203
|
+
outputBuffer = '';
|
|
1204
|
+
ws.send(JSON.stringify({ type: 'restored', sessionId: msg.sessionId, buffer: buf || undefined }));
|
|
1227
1205
|
} catch (e) {
|
|
1228
1206
|
LOG('[restore] 启动进程失败:', e.message);
|
|
1229
1207
|
ws.send(JSON.stringify({ type: 'restore-error', error: e.message }));
|
|
@@ -1280,10 +1258,11 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1280
1258
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1281
1259
|
cleanupOrphanProcesses();
|
|
1282
1260
|
|
|
1283
|
-
// 先通知前端准备好,再启动新进程
|
|
1284
|
-
ws.send(JSON.stringify({ type: 'new-session-ok', mode }));
|
|
1285
1261
|
try {
|
|
1286
1262
|
await spawnProcess(mode);
|
|
1263
|
+
const buf = outputBuffer;
|
|
1264
|
+
outputBuffer = '';
|
|
1265
|
+
ws.send(JSON.stringify({ type: 'new-session-ok', mode, buffer: buf || undefined }));
|
|
1287
1266
|
} catch (e) {
|
|
1288
1267
|
LOG('[new-session] 启动失败:', e.message);
|
|
1289
1268
|
ws.send(JSON.stringify({ type: 'new-session-error', error: e.message }));
|
|
@@ -1315,6 +1294,17 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1315
1294
|
setTimeout(() => checkNewSession(attempt + 1), 2000);
|
|
1316
1295
|
};
|
|
1317
1296
|
setTimeout(() => checkNewSession(0), 3000);
|
|
1297
|
+
} else if (msg.type === 'quit') {
|
|
1298
|
+
// PC 端关闭浏览器时发送,延迟 5 秒退出(防止刷新页面误杀)
|
|
1299
|
+
LOG('[quit] 收到退出请求,5秒后检查是否仍无连接...');
|
|
1300
|
+
setTimeout(() => {
|
|
1301
|
+
if (!wss || wss.clients.size === 0) {
|
|
1302
|
+
LOG('[quit] 无活跃连接,退出进程');
|
|
1303
|
+
cleanupAndExit();
|
|
1304
|
+
} else {
|
|
1305
|
+
LOG('[quit] 仍有活跃连接,取消退出');
|
|
1306
|
+
}
|
|
1307
|
+
}, 5000);
|
|
1318
1308
|
}
|
|
1319
1309
|
} catch (err) {
|
|
1320
1310
|
LOG('[WS] Error:', err.message);
|
|
@@ -1343,15 +1333,19 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1343
1333
|
}
|
|
1344
1334
|
}
|
|
1345
1335
|
}
|
|
1346
|
-
// 无客户端连接时,30秒后自动退出
|
|
1347
|
-
startNoClientTimer();
|
|
1348
1336
|
});
|
|
1349
1337
|
});
|
|
1350
1338
|
|
|
1351
1339
|
// WebSocket 心跳保活,防止中间网络设备断开空闲连接
|
|
1340
|
+
// 移动端跳过心跳检测:锁屏时 JS 暂停无法回 pong,但不需要断开(进程常驻)
|
|
1352
1341
|
const HEARTBEAT_INTERVAL = 5000;
|
|
1353
1342
|
const heartbeat = setInterval(() => {
|
|
1354
1343
|
wssInst.clients.forEach((ws) => {
|
|
1344
|
+
if (mobileClients.has(ws)) {
|
|
1345
|
+
// 移动端不检测心跳,但清理已断开的僵尸连接
|
|
1346
|
+
if (ws.readyState !== 1) mobileClients.delete(ws);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1355
1349
|
if (ws.isAlive === false) return ws.terminate();
|
|
1356
1350
|
ws.isAlive = false;
|
|
1357
1351
|
ws.ping();
|
|
@@ -1397,8 +1391,6 @@ function startServer() {
|
|
|
1397
1391
|
// 移动端客户端连接时自动启动 claude(见 WS 连接处理)
|
|
1398
1392
|
LOG('[startup] 等待客户端连接并选择会话...');
|
|
1399
1393
|
|
|
1400
|
-
// 启动首次连接超时检测(3分钟无人连接则退出)
|
|
1401
|
-
startNoClientTimer();
|
|
1402
1394
|
});
|
|
1403
1395
|
|
|
1404
1396
|
server.on('error', (err) => {
|