claude-opencode-viewer 2.6.38 → 2.6.40
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 +120 -47
- package/index.html +296 -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,16 @@
|
|
|
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
|
+
// 预加载已移除,改为按需查询:首次打开面板时查询并缓存,刷新按钮才重新查
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2186
2226
|
var STATUS_COLORS = {
|
|
2187
2227
|
'M': '#e2c08d', 'A': '#73c991', 'D': '#f14c4c',
|
|
2188
2228
|
'R': '#73c991', 'C': '#73c991', 'U': '#e2c08d',
|
|
@@ -2194,27 +2234,42 @@
|
|
|
2194
2234
|
var bar = document.getElementById('git-diff-bar');
|
|
2195
2235
|
if (diffBarVisible) {
|
|
2196
2236
|
bar.classList.add('visible');
|
|
2197
|
-
loadGitStatus();
|
|
2237
|
+
loadGitStatus(false);
|
|
2198
2238
|
} else {
|
|
2199
2239
|
bar.classList.remove('visible');
|
|
2200
2240
|
diffSelectedFile = null;
|
|
2201
2241
|
}
|
|
2202
2242
|
}
|
|
2203
2243
|
|
|
2204
|
-
function loadGitStatus() {
|
|
2244
|
+
function loadGitStatus(forceRefresh) {
|
|
2205
2245
|
var fileList = document.getElementById('git-diff-file-list');
|
|
2206
|
-
|
|
2207
|
-
|
|
2246
|
+
|
|
2247
|
+
if (cachedGitStatus && !forceRefresh) {
|
|
2248
|
+
diffChanges = cachedGitStatus.changes || [];
|
|
2249
|
+
document.getElementById('git-diff-count').textContent = diffChanges.length;
|
|
2250
|
+
renderDiffFileList();
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
fileList.innerHTML = '<div class="git-diff-loading">' + (forceRefresh ? '正在刷新...' : '正在查询 git status...') + '</div>';
|
|
2255
|
+
document.getElementById('git-diff-count').textContent = '...';
|
|
2256
|
+
|
|
2257
|
+
if (gitStatusLoading && !forceRefresh) return;
|
|
2258
|
+
gitStatusLoading = true;
|
|
2259
|
+
if (forceRefresh) cachedGitStatus = null;
|
|
2208
2260
|
|
|
2209
2261
|
fetch(basePath + '/api/git-status')
|
|
2210
2262
|
.then(function(r) { return r.json(); })
|
|
2211
2263
|
.then(function(data) {
|
|
2264
|
+
cachedGitStatus = data;
|
|
2265
|
+
gitStatusLoading = false;
|
|
2212
2266
|
diffChanges = data.changes || [];
|
|
2213
2267
|
document.getElementById('git-diff-count').textContent = diffChanges.length;
|
|
2214
|
-
renderDiffFileList();
|
|
2268
|
+
if (diffBarVisible) renderDiffFileList();
|
|
2215
2269
|
})
|
|
2216
2270
|
.catch(function() {
|
|
2217
|
-
|
|
2271
|
+
gitStatusLoading = false;
|
|
2272
|
+
if (diffBarVisible) fileList.innerHTML = '<div class="git-diff-error">无法加载 git status</div>';
|
|
2218
2273
|
});
|
|
2219
2274
|
}
|
|
2220
2275
|
|
|
@@ -2343,7 +2398,7 @@
|
|
|
2343
2398
|
document.getElementById('close-diff').addEventListener('click', toggleDiffBar);
|
|
2344
2399
|
document.getElementById('refresh-diff').addEventListener('click', function(e) {
|
|
2345
2400
|
e.stopPropagation();
|
|
2346
|
-
loadGitStatus();
|
|
2401
|
+
loadGitStatus(true);
|
|
2347
2402
|
// 重置 diff 内容区
|
|
2348
2403
|
diffSelectedFile = null;
|
|
2349
2404
|
document.getElementById('git-diff-content-area').innerHTML =
|
|
@@ -2365,49 +2420,67 @@
|
|
|
2365
2420
|
var bar = document.getElementById('docs-bar');
|
|
2366
2421
|
if (docsBarVisible) {
|
|
2367
2422
|
bar.classList.add('visible');
|
|
2368
|
-
loadDocs();
|
|
2423
|
+
loadDocs(false);
|
|
2369
2424
|
} else {
|
|
2370
2425
|
bar.classList.remove('visible');
|
|
2371
2426
|
docsSelectedFile = null;
|
|
2372
2427
|
}
|
|
2373
2428
|
}
|
|
2374
2429
|
|
|
2375
|
-
function
|
|
2430
|
+
function renderDocsList(data) {
|
|
2431
|
+
var fileList = document.getElementById('docs-file-list');
|
|
2432
|
+
var docs = data.docs || [];
|
|
2433
|
+
document.getElementById('docs-count').textContent = docs.length;
|
|
2434
|
+
if (!docs.length) {
|
|
2435
|
+
fileList.innerHTML = '<div class="docs-loading" style="color:#666;">无文档</div>';
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
var html = '<div style="padding:4px 12px;font-size:11px;color:#666;border-bottom:1px solid #2a2a2a;">📁 ' + escapeHtml(data.cwd || 'PROJECT_DIR') + '</div>';
|
|
2439
|
+
docs.forEach(function(doc) {
|
|
2440
|
+
var activeClass = docsSelectedFile === doc.name ? ' active' : '';
|
|
2441
|
+
var time = new Date(doc.mtime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
2442
|
+
html += '<div class="docs-file-item' + activeClass + '" data-file="' + escapeHtml(doc.name) + '">';
|
|
2443
|
+
html += '<span class="docs-file-name">' + escapeHtml(doc.name) + '</span>';
|
|
2444
|
+
html += '<span class="docs-file-time">' + time + '</span>';
|
|
2445
|
+
html += '</div>';
|
|
2446
|
+
});
|
|
2447
|
+
fileList.innerHTML = html;
|
|
2448
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(item) {
|
|
2449
|
+
item.addEventListener('click', function() {
|
|
2450
|
+
var file = this.getAttribute('data-file');
|
|
2451
|
+
docsSelectedFile = file;
|
|
2452
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(el) { el.classList.remove('active'); });
|
|
2453
|
+
this.classList.add('active');
|
|
2454
|
+
loadDocContent(file);
|
|
2455
|
+
});
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function loadDocs(forceRefresh) {
|
|
2376
2460
|
var fileList = document.getElementById('docs-file-list');
|
|
2377
|
-
|
|
2378
|
-
|
|
2461
|
+
|
|
2462
|
+
if (cachedDocs && !forceRefresh) {
|
|
2463
|
+
renderDocsList(cachedDocs);
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
fileList.innerHTML = '<div class="docs-loading">' + (forceRefresh ? '正在刷新...' : '正在查询文档...') + '</div>';
|
|
2468
|
+
document.getElementById('docs-count').textContent = '...';
|
|
2469
|
+
|
|
2470
|
+
if (docsLoading && !forceRefresh) return;
|
|
2471
|
+
docsLoading = true;
|
|
2472
|
+
if (forceRefresh) cachedDocs = null;
|
|
2379
2473
|
|
|
2380
2474
|
fetch(basePath + '/api/docs')
|
|
2381
2475
|
.then(function(r) { return r.json(); })
|
|
2382
2476
|
.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
|
-
});
|
|
2477
|
+
cachedDocs = data;
|
|
2478
|
+
docsLoading = false;
|
|
2479
|
+
if (docsBarVisible) renderDocsList(data);
|
|
2408
2480
|
})
|
|
2409
2481
|
.catch(function() {
|
|
2410
|
-
|
|
2482
|
+
docsLoading = false;
|
|
2483
|
+
if (docsBarVisible) fileList.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
|
|
2411
2484
|
});
|
|
2412
2485
|
}
|
|
2413
2486
|
|
|
@@ -2432,7 +2505,7 @@
|
|
|
2432
2505
|
document.getElementById('close-docs').addEventListener('click', toggleDocsBar);
|
|
2433
2506
|
document.getElementById('refresh-docs').addEventListener('click', function(e) {
|
|
2434
2507
|
e.stopPropagation();
|
|
2435
|
-
loadDocs();
|
|
2508
|
+
loadDocs(true);
|
|
2436
2509
|
docsSelectedFile = null;
|
|
2437
2510
|
document.getElementById('docs-content-area').innerHTML =
|
|
2438
2511
|
'<div class="docs-placeholder">' +
|
package/index.html
CHANGED
|
@@ -357,7 +357,20 @@
|
|
|
357
357
|
display: flex;
|
|
358
358
|
flex-direction: column;
|
|
359
359
|
background: #0a0a0a;
|
|
360
|
+
position: relative;
|
|
361
|
+
}
|
|
362
|
+
#loading-overlay {
|
|
363
|
+
position: fixed;
|
|
364
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
365
|
+
background: #0a0a0a;
|
|
366
|
+
color: #888;
|
|
367
|
+
display: flex;
|
|
368
|
+
align-items: center;
|
|
369
|
+
justify-content: center;
|
|
370
|
+
font-size: 15px;
|
|
371
|
+
z-index: 9999;
|
|
360
372
|
}
|
|
373
|
+
#loading-overlay.hidden { display: none; }
|
|
361
374
|
|
|
362
375
|
#terminal {
|
|
363
376
|
flex: 1;
|
|
@@ -489,6 +502,18 @@
|
|
|
489
502
|
-webkit-user-select: text;
|
|
490
503
|
user-select: text;
|
|
491
504
|
}
|
|
505
|
+
.msg-text pre {
|
|
506
|
+
background: #0d0d0d;
|
|
507
|
+
border: 1px solid #333;
|
|
508
|
+
border-radius: 4px;
|
|
509
|
+
padding: 8px;
|
|
510
|
+
overflow-x: auto;
|
|
511
|
+
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
512
|
+
font-size: 12px;
|
|
513
|
+
line-height: 1.4;
|
|
514
|
+
white-space: pre;
|
|
515
|
+
-webkit-overflow-scrolling: touch;
|
|
516
|
+
}
|
|
492
517
|
.msg-tool {
|
|
493
518
|
margin-top: 6px;
|
|
494
519
|
padding: 6px 8px;
|
|
@@ -832,6 +857,7 @@
|
|
|
832
857
|
</head>
|
|
833
858
|
<body>
|
|
834
859
|
<!-- 参考 cc-viewer 的 App.jsx 行 1315-1607: 完整的移动端布局结构 -->
|
|
860
|
+
<div id="loading-overlay">正在初始化...</div>
|
|
835
861
|
<div id="layout">
|
|
836
862
|
<div id="header">
|
|
837
863
|
<div style="display: flex; gap: 4px; align-items: center; overflow-x: auto; flex: 1; min-width: 0;">
|
|
@@ -1044,6 +1070,7 @@
|
|
|
1044
1070
|
|
|
1045
1071
|
<div id="copy-toast">已复制</div>
|
|
1046
1072
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
1073
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script>
|
|
1047
1074
|
<script>
|
|
1048
1075
|
(function() {
|
|
1049
1076
|
var isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
@@ -1052,6 +1079,7 @@
|
|
|
1052
1079
|
var fontSize = isMobile ? 11 : 13;
|
|
1053
1080
|
var currentMode = 'claude';
|
|
1054
1081
|
var isTransitioning = false;
|
|
1082
|
+
var mobileInitSent = false;
|
|
1055
1083
|
|
|
1056
1084
|
var term = new Terminal({
|
|
1057
1085
|
cursorBlink: !isMobile,
|
|
@@ -1071,6 +1099,19 @@
|
|
|
1071
1099
|
|
|
1072
1100
|
term.open(document.getElementById('terminal'));
|
|
1073
1101
|
|
|
1102
|
+
// WebGL 渲染器:GPU 加速绘制,非 iOS 设备启用(iOS WebGL 性能差)
|
|
1103
|
+
if (!isIOS && window.WebglAddon) {
|
|
1104
|
+
try {
|
|
1105
|
+
var webglAddon = new WebglAddon.WebglAddon();
|
|
1106
|
+
webglAddon.onContextLoss(function() {
|
|
1107
|
+
webglAddon.dispose();
|
|
1108
|
+
});
|
|
1109
|
+
term.loadAddon(webglAddon);
|
|
1110
|
+
} catch(e) {
|
|
1111
|
+
// WebGL 不可用时回退到 Canvas 渲染
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1074
1115
|
// PC端复制:用 xterm.js selection API 获取纯文本,避免复制出乱码
|
|
1075
1116
|
document.getElementById('terminal').addEventListener('copy', function(e) {
|
|
1076
1117
|
var sel = term.getSelection();
|
|
@@ -1132,6 +1173,8 @@
|
|
|
1132
1173
|
// 会话历史相关
|
|
1133
1174
|
var sessions = [];
|
|
1134
1175
|
var currentSessionId = null;
|
|
1176
|
+
var claudeSessionId = null;
|
|
1177
|
+
var claudeProject = null;
|
|
1135
1178
|
var currentSessionData = null;
|
|
1136
1179
|
var historyBarVisible = false;
|
|
1137
1180
|
|
|
@@ -1258,15 +1301,10 @@
|
|
|
1258
1301
|
}
|
|
1259
1302
|
}
|
|
1260
1303
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
pixelAccum = 0;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// 触摸滚动实现 - 混合模式:alternate buffer 发鼠标滚轮,normal buffer 用 scrollLines
|
|
1269
|
-
var SCROLL_SENSITIVITY = 0.7; // 滚动灵敏度,越小每次滑动行数越少
|
|
1304
|
+
// 触摸滚动实现
|
|
1305
|
+
// Claude 模式:与 cc-viewer 一致,直接 scrollLines,1:1 像素映射,无 WheelEvent
|
|
1306
|
+
// OpenCode 模式:alternate buffer 发 WheelEvent(vim/less 等需要),灵敏度 0.7
|
|
1307
|
+
var SCROLL_SENSITIVITY = 0.7;
|
|
1270
1308
|
var touchScreen = null;
|
|
1271
1309
|
var touchEventsBound = false;
|
|
1272
1310
|
|
|
@@ -1300,6 +1338,12 @@
|
|
|
1300
1338
|
|
|
1301
1339
|
function doScroll(lines) {
|
|
1302
1340
|
if (lines === 0) return;
|
|
1341
|
+
// Claude 模式:始终直接 scrollLines(与 cc-viewer 一致)
|
|
1342
|
+
if (currentMode === 'claude') {
|
|
1343
|
+
term.scrollLines(lines);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
// OpenCode 模式:alternate buffer 发 WheelEvent,normal buffer 用 scrollLines
|
|
1303
1347
|
if (isAlternateBuffer()) {
|
|
1304
1348
|
altScrollAccum += lines;
|
|
1305
1349
|
if (Math.abs(altScrollAccum) >= ALT_SCROLL_THRESHOLD) {
|
|
@@ -1335,7 +1379,8 @@
|
|
|
1335
1379
|
scrollRaf = null;
|
|
1336
1380
|
if (pendingDy === 0) return;
|
|
1337
1381
|
|
|
1338
|
-
|
|
1382
|
+
// Claude 模式 1:1 像素映射(cc-viewer 同款),OpenCode 降低灵敏度
|
|
1383
|
+
pixelAccum += pendingDy * (currentMode === 'claude' ? 1 : SCROLL_SENSITIVITY);
|
|
1339
1384
|
pendingDy = 0;
|
|
1340
1385
|
|
|
1341
1386
|
var lh = getLineHeight();
|
|
@@ -1372,22 +1417,19 @@
|
|
|
1372
1417
|
}
|
|
1373
1418
|
|
|
1374
1419
|
pendingDy += dy;
|
|
1375
|
-
console.log('[scroll] touchmove dy:', dy, 'pendingDy:', pendingDy);
|
|
1376
|
-
|
|
1377
1420
|
if (!scrollRaf) scrollRaf = requestAnimationFrame(flushScroll);
|
|
1378
1421
|
lastY = y;
|
|
1379
1422
|
lastTime = now;
|
|
1380
1423
|
}
|
|
1381
1424
|
|
|
1382
1425
|
function handleTouchEnd() {
|
|
1383
|
-
|
|
1384
1426
|
if (scrollRaf) {
|
|
1385
1427
|
cancelAnimationFrame(scrollRaf);
|
|
1386
1428
|
scrollRaf = null;
|
|
1387
1429
|
}
|
|
1388
1430
|
|
|
1389
1431
|
if (pendingDy !== 0) {
|
|
1390
|
-
pixelAccum += pendingDy * SCROLL_SENSITIVITY;
|
|
1432
|
+
pixelAccum += pendingDy * (currentMode === 'claude' ? 1 : SCROLL_SENSITIVITY);
|
|
1391
1433
|
pendingDy = 0;
|
|
1392
1434
|
var lh = getLineHeight();
|
|
1393
1435
|
var lines = Math.trunc(pixelAccum / lh);
|
|
@@ -1412,9 +1454,8 @@
|
|
|
1412
1454
|
}
|
|
1413
1455
|
velocitySamples = [];
|
|
1414
1456
|
|
|
1415
|
-
velocity *= SCROLL_SENSITIVITY;
|
|
1416
|
-
|
|
1417
|
-
if (Math.abs(velocity) < 0.3) return;
|
|
1457
|
+
velocity *= (currentMode === 'claude' ? 1 : SCROLL_SENSITIVITY);
|
|
1458
|
+
if (Math.abs(velocity) < 0.5) return;
|
|
1418
1459
|
|
|
1419
1460
|
var friction = 0.95;
|
|
1420
1461
|
var mAccum = 0;
|
|
@@ -1443,7 +1484,6 @@
|
|
|
1443
1484
|
|
|
1444
1485
|
function unbindTouchScroll() {
|
|
1445
1486
|
if (touchScreen && touchEventsBound) {
|
|
1446
|
-
console.log('[scroll] unbinding touch events');
|
|
1447
1487
|
touchScreen.removeEventListener('touchstart', handleTouchStart);
|
|
1448
1488
|
touchScreen.removeEventListener('touchmove', handleTouchMove);
|
|
1449
1489
|
touchScreen.removeEventListener('touchend', handleTouchEnd);
|
|
@@ -1553,15 +1593,24 @@
|
|
|
1553
1593
|
|
|
1554
1594
|
ws.onclose = function() {
|
|
1555
1595
|
ws = null;
|
|
1556
|
-
term.reset();
|
|
1596
|
+
term.reset();
|
|
1597
|
+
term.write('\r\n \x1b[33m连接断开,正在重连...\x1b[0m\r\n');
|
|
1557
1598
|
setTimeout(connect, 2000);
|
|
1558
1599
|
};
|
|
1559
1600
|
|
|
1601
|
+
var loadingOverlay = document.getElementById('loading-overlay');
|
|
1602
|
+
function hideLoading() {
|
|
1603
|
+
if (loadingOverlay && !loadingOverlay.classList.contains('hidden')) {
|
|
1604
|
+
loadingOverlay.classList.add('hidden');
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1560
1608
|
ws.onmessage = function(e) {
|
|
1561
1609
|
try {
|
|
1562
1610
|
var msg = JSON.parse(e.data);
|
|
1563
1611
|
if (msg.type === 'data') {
|
|
1564
|
-
|
|
1612
|
+
hideLoading();
|
|
1613
|
+
if (!isCreatingNewSession && !isTransitioning) {
|
|
1565
1614
|
throttledWrite(msg.data);
|
|
1566
1615
|
}
|
|
1567
1616
|
}
|
|
@@ -1571,15 +1620,45 @@
|
|
|
1571
1620
|
throttledWrite('按 Enter 键重新启动 ' + currentMode + '...\r\n');
|
|
1572
1621
|
}
|
|
1573
1622
|
}
|
|
1623
|
+
else if (msg.type === 'state') {
|
|
1624
|
+
// 服务端还没启动进程时,查最近会话并自动恢复
|
|
1625
|
+
if (!msg.running && !mobileInitSent) {
|
|
1626
|
+
mobileInitSent = true;
|
|
1627
|
+
fetch('/api/last-sessions')
|
|
1628
|
+
.then(function(r) { return r.json(); })
|
|
1629
|
+
.then(function(data) {
|
|
1630
|
+
var oc = data.opencode;
|
|
1631
|
+
var cl = data.claude;
|
|
1632
|
+
// 取 mtime 最新的那个
|
|
1633
|
+
var useOc = oc && (!cl || oc.mtime > cl.mtime);
|
|
1634
|
+
var mode = useOc ? 'opencode' : 'claude';
|
|
1635
|
+
var sessionId = useOc ? (oc && oc.id) : (cl && cl.id);
|
|
1636
|
+
if (cl) {
|
|
1637
|
+
claudeSessionId = cl.id;
|
|
1638
|
+
claudeProject = cl.project;
|
|
1639
|
+
}
|
|
1640
|
+
currentMode = mode;
|
|
1641
|
+
ws.send(JSON.stringify({ type: 'init', mode: mode, sessionId: sessionId || null }));
|
|
1642
|
+
})
|
|
1643
|
+
.catch(function() {
|
|
1644
|
+
ws.send(JSON.stringify({ type: 'init', mode: 'claude' }));
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1574
1648
|
else if (msg.type === 'mode') {
|
|
1649
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1650
|
+
writeBuffer = '';
|
|
1651
|
+
term.reset();
|
|
1575
1652
|
endTransition(msg.mode);
|
|
1576
|
-
|
|
1653
|
+
if (msg.buffer) {
|
|
1654
|
+
term.write(msg.buffer);
|
|
1655
|
+
}
|
|
1577
1656
|
rebindTouchScroll();
|
|
1578
1657
|
}
|
|
1579
1658
|
else if (msg.type === 'switching') {
|
|
1580
|
-
|
|
1581
|
-
term.reset();
|
|
1659
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1582
1660
|
writeBuffer = '';
|
|
1661
|
+
term.clear();
|
|
1583
1662
|
}
|
|
1584
1663
|
else if (msg.type === 'state') {
|
|
1585
1664
|
if (msg.mode) {
|
|
@@ -1589,22 +1668,37 @@
|
|
|
1589
1668
|
}
|
|
1590
1669
|
}
|
|
1591
1670
|
else if (msg.type === 'restored') {
|
|
1592
|
-
//
|
|
1671
|
+
// 会话恢复成功,清除所有残留
|
|
1672
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1673
|
+
writeBuffer = '';
|
|
1593
1674
|
term.reset();
|
|
1675
|
+
if (msg.buffer) {
|
|
1676
|
+
term.write(msg.buffer);
|
|
1677
|
+
}
|
|
1678
|
+
isTransitioning = false;
|
|
1594
1679
|
}
|
|
1595
1680
|
else if (msg.type === 'restore-error') {
|
|
1596
|
-
|
|
1681
|
+
isTransitioning = false;
|
|
1597
1682
|
term.write('恢复失败: ' + msg.error + '\r\n');
|
|
1598
1683
|
}
|
|
1599
1684
|
else if (msg.type === 'started') {
|
|
1685
|
+
hideLoading();
|
|
1600
1686
|
rebindTouchScroll();
|
|
1687
|
+
preloadData();
|
|
1601
1688
|
}
|
|
1602
1689
|
else if (msg.type === 'new-session-ok') {
|
|
1603
|
-
|
|
1690
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1691
|
+
writeBuffer = '';
|
|
1604
1692
|
term.reset();
|
|
1693
|
+
if (msg.buffer) {
|
|
1694
|
+
term.write(msg.buffer);
|
|
1695
|
+
}
|
|
1696
|
+
isCreatingNewSession = false;
|
|
1697
|
+
isTransitioning = false;
|
|
1605
1698
|
}
|
|
1606
1699
|
else if (msg.type === 'new-session-error') {
|
|
1607
1700
|
isCreatingNewSession = false;
|
|
1701
|
+
isTransitioning = false;
|
|
1608
1702
|
term.write('新会话启动失败: ' + msg.error + '\r\n');
|
|
1609
1703
|
}
|
|
1610
1704
|
} catch(err) {}
|
|
@@ -1923,11 +2017,15 @@
|
|
|
1923
2017
|
// 关闭历史栏
|
|
1924
2018
|
toggleHistoryBar();
|
|
1925
2019
|
|
|
1926
|
-
//
|
|
2020
|
+
// 阻止旧数据写入
|
|
2021
|
+
isTransitioning = true;
|
|
2022
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
2023
|
+
writeBuffer = '';
|
|
1927
2024
|
term.reset();
|
|
1928
2025
|
|
|
1929
2026
|
if (ws && ws.readyState === 1) {
|
|
1930
2027
|
if (currentMode !== 'opencode') {
|
|
2028
|
+
isTransitioning = false;
|
|
1931
2029
|
term.write('错误: 请先切换到 OpenCode 模式\r\n');
|
|
1932
2030
|
return;
|
|
1933
2031
|
}
|
|
@@ -2022,7 +2120,7 @@
|
|
|
2022
2120
|
item.appendChild(deleteBtn);
|
|
2023
2121
|
|
|
2024
2122
|
item.addEventListener('click', function() {
|
|
2025
|
-
restoreClaudeSession(s.id);
|
|
2123
|
+
restoreClaudeSession(s.id, s.project);
|
|
2026
2124
|
});
|
|
2027
2125
|
|
|
2028
2126
|
sessionList.appendChild(item);
|
|
@@ -2113,12 +2211,18 @@
|
|
|
2113
2211
|
});
|
|
2114
2212
|
}
|
|
2115
2213
|
|
|
2116
|
-
function restoreClaudeSession(sessionId) {
|
|
2214
|
+
function restoreClaudeSession(sessionId, project) {
|
|
2117
2215
|
console.log('[restore] 恢复 Claude 会话:', sessionId);
|
|
2216
|
+
claudeSessionId = sessionId;
|
|
2217
|
+
if (project) claudeProject = project;
|
|
2118
2218
|
toggleHistoryBar();
|
|
2219
|
+
isTransitioning = true;
|
|
2220
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
2221
|
+
writeBuffer = '';
|
|
2119
2222
|
term.reset();
|
|
2120
2223
|
if (ws && ws.readyState === 1) {
|
|
2121
2224
|
if (currentMode !== 'claude') {
|
|
2225
|
+
isTransitioning = false;
|
|
2122
2226
|
term.write('错误: 请先切换到 Claude 模式\r\n');
|
|
2123
2227
|
return;
|
|
2124
2228
|
}
|
|
@@ -2140,9 +2244,12 @@
|
|
|
2140
2244
|
// 新会话按钮
|
|
2141
2245
|
var isCreatingNewSession = false;
|
|
2142
2246
|
document.getElementById('new-session-btn').addEventListener('click', function() {
|
|
2143
|
-
if (!ws || ws.readyState !== 1) return;
|
|
2247
|
+
if (!ws || ws.readyState !== 1 || isTransitioning) return;
|
|
2144
2248
|
isCreatingNewSession = true;
|
|
2145
|
-
|
|
2249
|
+
isTransitioning = true;
|
|
2250
|
+
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
2251
|
+
writeBuffer = '';
|
|
2252
|
+
term.reset();
|
|
2146
2253
|
ws.send(JSON.stringify({ type: 'new-session' }));
|
|
2147
2254
|
});
|
|
2148
2255
|
|
|
@@ -2227,23 +2334,81 @@
|
|
|
2227
2334
|
msgViewerContent.innerHTML = '<div class="msg-empty">加载中...</div>';
|
|
2228
2335
|
unbindTouchScroll();
|
|
2229
2336
|
|
|
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)
|
|
2337
|
+
if (currentMode === 'claude') {
|
|
2338
|
+
// Claude 模式:从 JSONL 文件读取消息
|
|
2339
|
+
var loadClaudeMessages = function(sid, proj) {
|
|
2340
|
+
fetch(basePath + '/api/claude-session-messages?id=' + encodeURIComponent(sid) + '&project=' + encodeURIComponent(proj))
|
|
2239
2341
|
.then(function(r) { return r.json(); })
|
|
2240
|
-
.then(function(
|
|
2342
|
+
.then(function(data) {
|
|
2343
|
+
if (data.error) {
|
|
2344
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">' + escapeHtml(data.error) + '</div>';
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
var messages = data.messages || [];
|
|
2241
2348
|
renderMessages(messages);
|
|
2349
|
+
})
|
|
2350
|
+
.catch(function(e) {
|
|
2351
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">加载失败: ' + e.message + '</div>';
|
|
2242
2352
|
});
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
}
|
|
2353
|
+
};
|
|
2354
|
+
if (claudeSessionId && claudeProject) {
|
|
2355
|
+
loadClaudeMessages(claudeSessionId, claudeProject);
|
|
2356
|
+
} else {
|
|
2357
|
+
// 尚未获取到 session 信息,先查
|
|
2358
|
+
fetch(basePath + '/api/last-sessions')
|
|
2359
|
+
.then(function(r) { return r.json(); })
|
|
2360
|
+
.then(function(data) {
|
|
2361
|
+
var cl = data.claude;
|
|
2362
|
+
if (cl && cl.id && cl.project) {
|
|
2363
|
+
claudeSessionId = cl.id;
|
|
2364
|
+
claudeProject = cl.project;
|
|
2365
|
+
loadClaudeMessages(cl.id, cl.project);
|
|
2366
|
+
} else {
|
|
2367
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">暂无活跃会话</div>';
|
|
2368
|
+
}
|
|
2369
|
+
})
|
|
2370
|
+
.catch(function(e) {
|
|
2371
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">加载失败: ' + e.message + '</div>';
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
} else {
|
|
2375
|
+
// OpenCode 模式:从 SQLite 读取消息
|
|
2376
|
+
fetch(basePath + '/api/current-session')
|
|
2377
|
+
.then(function(r) { return r.json(); })
|
|
2378
|
+
.then(function(data) {
|
|
2379
|
+
if (!data.sessionId) {
|
|
2380
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">暂无活跃会话</div>';
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
return fetch(basePath + '/api/session/' + data.sessionId)
|
|
2384
|
+
.then(function(r) { return r.json(); })
|
|
2385
|
+
.then(function(messages) {
|
|
2386
|
+
renderMessages(messages);
|
|
2387
|
+
});
|
|
2388
|
+
})
|
|
2389
|
+
.catch(function(e) {
|
|
2390
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">加载失败: ' + e.message + '</div>';
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
function formatMsgText(text) {
|
|
2396
|
+
// 将 markdown 代码块转为 <pre>,其余部分转义
|
|
2397
|
+
var parts = text.split(/(```[\s\S]*?```)/g);
|
|
2398
|
+
var result = '';
|
|
2399
|
+
for (var i = 0; i < parts.length; i++) {
|
|
2400
|
+
var p = parts[i];
|
|
2401
|
+
if (p.startsWith('```') && p.endsWith('```')) {
|
|
2402
|
+
// 去掉首尾 ```(可能带语言标记)
|
|
2403
|
+
var inner = p.slice(3, -3);
|
|
2404
|
+
var nlIdx = inner.indexOf('\n');
|
|
2405
|
+
if (nlIdx !== -1) inner = inner.slice(nlIdx + 1);
|
|
2406
|
+
result += '<pre>' + escapeHtml(inner) + '</pre>';
|
|
2407
|
+
} else {
|
|
2408
|
+
result += escapeHtml(p);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return result;
|
|
2247
2412
|
}
|
|
2248
2413
|
|
|
2249
2414
|
function renderMessages(messages) {
|
|
@@ -2259,7 +2424,7 @@
|
|
|
2259
2424
|
html += '<div class="msg-item ' + cls + '">';
|
|
2260
2425
|
html += '<div class="msg-role">' + roleLabel + '</div>';
|
|
2261
2426
|
if (msg.text) {
|
|
2262
|
-
html += '<div class="msg-text">' +
|
|
2427
|
+
html += '<div class="msg-text">' + formatMsgText(msg.text) + '</div>';
|
|
2263
2428
|
}
|
|
2264
2429
|
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
2265
2430
|
msg.toolCalls.forEach(function(tc) {
|
|
@@ -2269,6 +2434,7 @@
|
|
|
2269
2434
|
html += '</div>';
|
|
2270
2435
|
});
|
|
2271
2436
|
msgViewerContent.innerHTML = html;
|
|
2437
|
+
msgViewerContent.scrollTop = msgViewerContent.scrollHeight;
|
|
2272
2438
|
}
|
|
2273
2439
|
|
|
2274
2440
|
function escapeHtml(str) {
|
|
@@ -2302,6 +2468,16 @@
|
|
|
2302
2468
|
var diffChanges = [];
|
|
2303
2469
|
var diffSelectedFile = null;
|
|
2304
2470
|
|
|
2471
|
+
// 预加载缓存
|
|
2472
|
+
var cachedGitStatus = null;
|
|
2473
|
+
var cachedDocs = null;
|
|
2474
|
+
var gitStatusLoading = false;
|
|
2475
|
+
var docsLoading = false;
|
|
2476
|
+
|
|
2477
|
+
function preloadData() {
|
|
2478
|
+
// 预加载已移除,改为按需查询:首次打开面板时查询并缓存,刷新按钮才重新查
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2305
2481
|
var STATUS_COLORS = {
|
|
2306
2482
|
'M': '#e2c08d', 'A': '#73c991', 'D': '#f14c4c',
|
|
2307
2483
|
'R': '#73c991', 'C': '#73c991', 'U': '#e2c08d',
|
|
@@ -2313,27 +2489,44 @@
|
|
|
2313
2489
|
var bar = document.getElementById('git-diff-bar');
|
|
2314
2490
|
if (diffBarVisible) {
|
|
2315
2491
|
bar.classList.add('visible');
|
|
2316
|
-
loadGitStatus();
|
|
2492
|
+
loadGitStatus(false);
|
|
2317
2493
|
} else {
|
|
2318
2494
|
bar.classList.remove('visible');
|
|
2319
2495
|
diffSelectedFile = null;
|
|
2320
2496
|
}
|
|
2321
2497
|
}
|
|
2322
2498
|
|
|
2323
|
-
function loadGitStatus() {
|
|
2499
|
+
function loadGitStatus(forceRefresh) {
|
|
2324
2500
|
var fileList = document.getElementById('git-diff-file-list');
|
|
2325
|
-
|
|
2326
|
-
|
|
2501
|
+
|
|
2502
|
+
// 有缓存且非强制刷新,直接用缓存
|
|
2503
|
+
if (cachedGitStatus && !forceRefresh) {
|
|
2504
|
+
diffChanges = cachedGitStatus.changes || [];
|
|
2505
|
+
document.getElementById('git-diff-count').textContent = diffChanges.length;
|
|
2506
|
+
renderDiffFileList();
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
// 正在加载中或需要发起请求
|
|
2511
|
+
fileList.innerHTML = '<div class="git-diff-loading">' + (forceRefresh ? '正在刷新...' : '正在查询 git status...') + '</div>';
|
|
2512
|
+
document.getElementById('git-diff-count').textContent = '...';
|
|
2513
|
+
|
|
2514
|
+
if (gitStatusLoading && !forceRefresh) return; // 预加载进行中,等它完成
|
|
2515
|
+
gitStatusLoading = true;
|
|
2516
|
+
if (forceRefresh) cachedGitStatus = null;
|
|
2327
2517
|
|
|
2328
2518
|
fetch(basePath + '/api/git-status')
|
|
2329
2519
|
.then(function(r) { return r.json(); })
|
|
2330
2520
|
.then(function(data) {
|
|
2521
|
+
cachedGitStatus = data;
|
|
2522
|
+
gitStatusLoading = false;
|
|
2331
2523
|
diffChanges = data.changes || [];
|
|
2332
2524
|
document.getElementById('git-diff-count').textContent = diffChanges.length;
|
|
2333
|
-
renderDiffFileList();
|
|
2525
|
+
if (diffBarVisible) renderDiffFileList();
|
|
2334
2526
|
})
|
|
2335
2527
|
.catch(function() {
|
|
2336
|
-
|
|
2528
|
+
gitStatusLoading = false;
|
|
2529
|
+
if (diffBarVisible) fileList.innerHTML = '<div class="git-diff-error">无法加载 git status</div>';
|
|
2337
2530
|
});
|
|
2338
2531
|
}
|
|
2339
2532
|
|
|
@@ -2462,7 +2655,7 @@
|
|
|
2462
2655
|
document.getElementById('close-diff').addEventListener('click', toggleDiffBar);
|
|
2463
2656
|
document.getElementById('refresh-diff').addEventListener('click', function(e) {
|
|
2464
2657
|
e.stopPropagation();
|
|
2465
|
-
loadGitStatus();
|
|
2658
|
+
loadGitStatus(true);
|
|
2466
2659
|
// 重置 diff 内容区
|
|
2467
2660
|
diffSelectedFile = null;
|
|
2468
2661
|
document.getElementById('git-diff-content-area').innerHTML =
|
|
@@ -2484,49 +2677,68 @@
|
|
|
2484
2677
|
var bar = document.getElementById('docs-bar');
|
|
2485
2678
|
if (docsBarVisible) {
|
|
2486
2679
|
bar.classList.add('visible');
|
|
2487
|
-
loadDocs();
|
|
2680
|
+
loadDocs(false);
|
|
2488
2681
|
} else {
|
|
2489
2682
|
bar.classList.remove('visible');
|
|
2490
2683
|
docsSelectedFile = null;
|
|
2491
2684
|
}
|
|
2492
2685
|
}
|
|
2493
2686
|
|
|
2494
|
-
function
|
|
2687
|
+
function renderDocsList(data) {
|
|
2495
2688
|
var fileList = document.getElementById('docs-file-list');
|
|
2496
|
-
|
|
2497
|
-
document.getElementById('docs-count').textContent =
|
|
2689
|
+
var docs = data.docs || [];
|
|
2690
|
+
document.getElementById('docs-count').textContent = docs.length;
|
|
2691
|
+
if (!docs.length) {
|
|
2692
|
+
fileList.innerHTML = '<div class="docs-loading" style="color:#666;">无文档</div>';
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
var html = '<div style="padding:4px 12px;font-size:11px;color:#666;border-bottom:1px solid #2a2a2a;">📁 ' + escapeHtml(data.cwd || 'PROJECT_DIR') + '</div>';
|
|
2696
|
+
docs.forEach(function(doc) {
|
|
2697
|
+
var activeClass = docsSelectedFile === doc.name ? ' active' : '';
|
|
2698
|
+
var time = new Date(doc.mtime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
2699
|
+
html += '<div class="docs-file-item' + activeClass + '" data-file="' + escapeHtml(doc.name) + '">';
|
|
2700
|
+
html += '<span class="docs-file-name">' + escapeHtml(doc.name) + '</span>';
|
|
2701
|
+
html += '<span class="docs-file-time">' + time + '</span>';
|
|
2702
|
+
html += '</div>';
|
|
2703
|
+
});
|
|
2704
|
+
fileList.innerHTML = html;
|
|
2705
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(item) {
|
|
2706
|
+
item.addEventListener('click', function() {
|
|
2707
|
+
var file = this.getAttribute('data-file');
|
|
2708
|
+
docsSelectedFile = file;
|
|
2709
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(el) { el.classList.remove('active'); });
|
|
2710
|
+
this.classList.add('active');
|
|
2711
|
+
loadDocContent(file);
|
|
2712
|
+
});
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function loadDocs(forceRefresh) {
|
|
2717
|
+
var fileList = document.getElementById('docs-file-list');
|
|
2718
|
+
|
|
2719
|
+
// 有缓存且非强制刷新,直接用缓存
|
|
2720
|
+
if (cachedDocs && !forceRefresh) {
|
|
2721
|
+
renderDocsList(cachedDocs);
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
fileList.innerHTML = '<div class="docs-loading">' + (forceRefresh ? '正在刷新...' : '正在查询文档...') + '</div>';
|
|
2726
|
+
document.getElementById('docs-count').textContent = '...';
|
|
2727
|
+
|
|
2728
|
+
if (docsLoading && !forceRefresh) return;
|
|
2729
|
+
docsLoading = true;
|
|
2730
|
+
if (forceRefresh) cachedDocs = null;
|
|
2498
2731
|
|
|
2499
2732
|
fetch(basePath + '/api/docs')
|
|
2500
2733
|
.then(function(r) { return r.json(); })
|
|
2501
2734
|
.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
|
-
});
|
|
2735
|
+
cachedDocs = data;
|
|
2736
|
+
docsLoading = false;
|
|
2737
|
+
if (docsBarVisible) renderDocsList(data);
|
|
2527
2738
|
})
|
|
2528
2739
|
.catch(function() {
|
|
2529
|
-
|
|
2740
|
+
docsLoading = false;
|
|
2741
|
+
if (docsBarVisible) fileList.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
|
|
2530
2742
|
});
|
|
2531
2743
|
}
|
|
2532
2744
|
|
|
@@ -2551,7 +2763,7 @@
|
|
|
2551
2763
|
document.getElementById('close-docs').addEventListener('click', toggleDocsBar);
|
|
2552
2764
|
document.getElementById('refresh-docs').addEventListener('click', function(e) {
|
|
2553
2765
|
e.stopPropagation();
|
|
2554
|
-
loadDocs();
|
|
2766
|
+
loadDocs(true);
|
|
2555
2767
|
docsSelectedFile = null;
|
|
2556
2768
|
document.getElementById('docs-content-area').innerHTML =
|
|
2557
2769
|
'<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) => {
|