claude-opencode-viewer 2.6.1 → 2.6.2
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/.claude/settings.local.json +2 -1
- package/index.html +148 -39
- package/package.json +1 -1
- package/server.js +36 -0
package/index.html
CHANGED
|
@@ -373,6 +373,33 @@
|
|
|
373
373
|
font-size: 12px;
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
.session-delete-btn {
|
|
377
|
+
flex-shrink: 0;
|
|
378
|
+
width: 28px;
|
|
379
|
+
height: 28px;
|
|
380
|
+
border: none;
|
|
381
|
+
background: none;
|
|
382
|
+
color: #f85149;
|
|
383
|
+
font-size: 14px;
|
|
384
|
+
cursor: pointer;
|
|
385
|
+
border-radius: 50%;
|
|
386
|
+
display: flex;
|
|
387
|
+
align-items: center;
|
|
388
|
+
justify-content: center;
|
|
389
|
+
transition: all 0.15s;
|
|
390
|
+
-webkit-tap-highlight-color: transparent;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.session-delete-btn:hover {
|
|
394
|
+
background: rgba(248, 81, 73, 0.15);
|
|
395
|
+
color: #f85149;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.session-delete-btn:active {
|
|
399
|
+
background: rgba(248, 81, 73, 0.3);
|
|
400
|
+
color: #f85149;
|
|
401
|
+
}
|
|
402
|
+
|
|
376
403
|
.session-restore-hint {
|
|
377
404
|
margin-top: 8px;
|
|
378
405
|
padding: 8px 12px;
|
|
@@ -949,7 +976,6 @@
|
|
|
949
976
|
<div class="virtual-key" data-key="tab">Tab</div>
|
|
950
977
|
<div class="virtual-key" data-key="esc">Esc</div>
|
|
951
978
|
<div class="virtual-key" data-key="ctrlc">Ctrl+C</div>
|
|
952
|
-
<div class="virtual-key" id="btn-copy">复制</div>
|
|
953
979
|
</div>
|
|
954
980
|
</div>
|
|
955
981
|
</div>
|
|
@@ -984,6 +1010,32 @@
|
|
|
984
1010
|
|
|
985
1011
|
term.open(document.getElementById('terminal'));
|
|
986
1012
|
|
|
1013
|
+
// OSC 52 剪贴板支持:拦截应用发送的剪贴板设置请求
|
|
1014
|
+
term.parser.registerOscHandler(52, function(data) {
|
|
1015
|
+
var idx = data.indexOf(';');
|
|
1016
|
+
if (idx === -1) return false;
|
|
1017
|
+
var b64 = data.substring(idx + 1);
|
|
1018
|
+
if (!b64 || b64 === '?') return false;
|
|
1019
|
+
try {
|
|
1020
|
+
var text = atob(b64);
|
|
1021
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1022
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
1023
|
+
showCopyToast();
|
|
1024
|
+
});
|
|
1025
|
+
} else {
|
|
1026
|
+
var ta = document.createElement('textarea');
|
|
1027
|
+
ta.value = text;
|
|
1028
|
+
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
|
|
1029
|
+
document.body.appendChild(ta);
|
|
1030
|
+
ta.select();
|
|
1031
|
+
document.execCommand('copy');
|
|
1032
|
+
document.body.removeChild(ta);
|
|
1033
|
+
showCopyToast();
|
|
1034
|
+
}
|
|
1035
|
+
} catch (e) {}
|
|
1036
|
+
return true;
|
|
1037
|
+
});
|
|
1038
|
+
|
|
987
1039
|
var modeSelect = document.getElementById('mode-select');
|
|
988
1040
|
var terminalEl = document.getElementById('terminal');
|
|
989
1041
|
var ws = null;
|
|
@@ -1018,25 +1070,15 @@
|
|
|
1018
1070
|
}
|
|
1019
1071
|
|
|
1020
1072
|
function loadInputCache() {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
console.log('[cache] already restored, skipping');
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1073
|
+
if (cacheRestored) return;
|
|
1074
|
+
cacheRestored = true;
|
|
1026
1075
|
|
|
1027
1076
|
var cached = localStorage.getItem(CACHE_KEY);
|
|
1028
|
-
if (cached
|
|
1029
|
-
console.log('[cache] restoring:', cached);
|
|
1030
|
-
//
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
// 将缓存的输入发送到终端
|
|
1035
|
-
ws.send(JSON.stringify({ type: 'input', data: cached }));
|
|
1036
|
-
// 重要:恢复后清空 currentInputBuffer,防止再次保存
|
|
1037
|
-
currentInputBuffer = '';
|
|
1038
|
-
} else {
|
|
1039
|
-
console.log('[cache] no cache to restore');
|
|
1077
|
+
if (cached) {
|
|
1078
|
+
console.log('[cache] restoring buffer:', cached);
|
|
1079
|
+
// 只恢复跟踪变量,不重新发送到 pty
|
|
1080
|
+
// 因为 outputBuffer 回放已经包含了之前的回显
|
|
1081
|
+
currentInputBuffer = cached;
|
|
1040
1082
|
}
|
|
1041
1083
|
}
|
|
1042
1084
|
|
|
@@ -1159,16 +1201,39 @@
|
|
|
1159
1201
|
return term.buffer.active.type === 'alternate';
|
|
1160
1202
|
}
|
|
1161
1203
|
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1204
|
+
function emitWheelEvent(lines) {
|
|
1205
|
+
var screen = terminalEl.querySelector('.xterm-screen');
|
|
1206
|
+
if (!screen) return;
|
|
1207
|
+
var lh = getLineHeight();
|
|
1208
|
+
var rect = screen.getBoundingClientRect();
|
|
1209
|
+
var cx = rect.left + rect.width / 2;
|
|
1210
|
+
var cy = rect.top + rect.height / 2;
|
|
1211
|
+
var count = Math.abs(lines);
|
|
1212
|
+
var dy = lines < 0 ? -lh : lh;
|
|
1213
|
+
for (var i = 0; i < count; i++) {
|
|
1214
|
+
screen.dispatchEvent(new WheelEvent('wheel', {
|
|
1215
|
+
deltaY: dy,
|
|
1216
|
+
deltaMode: 0,
|
|
1217
|
+
clientX: cx,
|
|
1218
|
+
clientY: cy,
|
|
1219
|
+
bubbles: true,
|
|
1220
|
+
cancelable: true,
|
|
1221
|
+
}));
|
|
1222
|
+
}
|
|
1166
1223
|
}
|
|
1167
1224
|
|
|
1225
|
+
var altScrollAccum = 0;
|
|
1226
|
+
var ALT_SCROLL_THRESHOLD = 2;
|
|
1227
|
+
|
|
1168
1228
|
function doScroll(lines) {
|
|
1169
1229
|
if (lines === 0) return;
|
|
1170
1230
|
if (isAlternateBuffer()) {
|
|
1171
|
-
|
|
1231
|
+
altScrollAccum += lines;
|
|
1232
|
+
if (Math.abs(altScrollAccum) >= ALT_SCROLL_THRESHOLD) {
|
|
1233
|
+
var scrollLines = Math.trunc(altScrollAccum);
|
|
1234
|
+
emitWheelEvent(scrollLines);
|
|
1235
|
+
altScrollAccum -= scrollLines;
|
|
1236
|
+
}
|
|
1172
1237
|
} else {
|
|
1173
1238
|
term.scrollLines(lines);
|
|
1174
1239
|
}
|
|
@@ -1212,18 +1277,21 @@
|
|
|
1212
1277
|
// 长按检测
|
|
1213
1278
|
var longPressTimer = null;
|
|
1214
1279
|
var longPressTriggered = false;
|
|
1215
|
-
var LONG_PRESS_DELAY =
|
|
1280
|
+
var LONG_PRESS_DELAY = 550; // ms
|
|
1216
1281
|
|
|
1217
1282
|
function clearLongPress() {
|
|
1218
1283
|
if (longPressTimer) {
|
|
1219
1284
|
clearTimeout(longPressTimer);
|
|
1220
1285
|
longPressTimer = null;
|
|
1221
1286
|
}
|
|
1287
|
+
// 始终恢复 xterm textarea,防止 disabled 残留
|
|
1288
|
+
var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
|
|
1289
|
+
if (xtermTa) xtermTa.removeAttribute('disabled');
|
|
1222
1290
|
}
|
|
1223
1291
|
|
|
1224
1292
|
function handleTouchStart(e) {
|
|
1225
|
-
console.log('[scroll] touchstart');
|
|
1226
1293
|
stopMomentum();
|
|
1294
|
+
altScrollAccum = 0;
|
|
1227
1295
|
longPressTriggered = false;
|
|
1228
1296
|
clearLongPress();
|
|
1229
1297
|
if (e.touches.length !== 1) return;
|
|
@@ -1232,6 +1300,12 @@
|
|
|
1232
1300
|
velocitySamples = [];
|
|
1233
1301
|
|
|
1234
1302
|
// 启动长按计时器
|
|
1303
|
+
// 在长按检测期间阻止 xterm textarea 获取焦点,防止弹出键盘
|
|
1304
|
+
var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
|
|
1305
|
+
if (xtermTa) {
|
|
1306
|
+
xtermTa.setAttribute('disabled', 'true');
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1235
1309
|
longPressTimer = setTimeout(function() {
|
|
1236
1310
|
longPressTriggered = true;
|
|
1237
1311
|
longPressTimer = null;
|
|
@@ -1423,8 +1497,8 @@
|
|
|
1423
1497
|
} else if (d === '\x15') {
|
|
1424
1498
|
// Ctrl+U:清空整行
|
|
1425
1499
|
clearInputCache();
|
|
1426
|
-
} else if (d.
|
|
1427
|
-
//
|
|
1500
|
+
} else if (d.charCodeAt(0) >= 32 && d.charCodeAt(0) !== 127) {
|
|
1501
|
+
// 可打印字符(含中文等多字节):添加到缓冲区
|
|
1428
1502
|
currentInputBuffer += d;
|
|
1429
1503
|
saveInputCache();
|
|
1430
1504
|
}
|
|
@@ -1443,6 +1517,7 @@
|
|
|
1443
1517
|
|
|
1444
1518
|
ws.onopen = function() {
|
|
1445
1519
|
resize();
|
|
1520
|
+
rebindTouchScroll();
|
|
1446
1521
|
};
|
|
1447
1522
|
|
|
1448
1523
|
ws.onclose = function() {
|
|
@@ -1491,6 +1566,9 @@
|
|
|
1491
1566
|
// 恢复失败
|
|
1492
1567
|
term.write('\x1b[31m✗ 恢复失败: ' + msg.error + '\x1b[0m\r\n');
|
|
1493
1568
|
}
|
|
1569
|
+
else if (msg.type === 'started') {
|
|
1570
|
+
rebindTouchScroll();
|
|
1571
|
+
}
|
|
1494
1572
|
else if (msg.type === 'new-session-ok') {
|
|
1495
1573
|
isCreatingNewSession = false;
|
|
1496
1574
|
term.clear();
|
|
@@ -1507,7 +1585,6 @@
|
|
|
1507
1585
|
if (currentMode === 'opencode') {
|
|
1508
1586
|
loadInputCache();
|
|
1509
1587
|
} else {
|
|
1510
|
-
// 非 opencode 模式,标记为已恢复,避免后续缓存逻辑干扰
|
|
1511
1588
|
cacheRestored = true;
|
|
1512
1589
|
}
|
|
1513
1590
|
}, 800);
|
|
@@ -1743,8 +1820,18 @@
|
|
|
1743
1820
|
info.appendChild(title);
|
|
1744
1821
|
info.appendChild(meta);
|
|
1745
1822
|
|
|
1823
|
+
var deleteBtn = document.createElement('button');
|
|
1824
|
+
deleteBtn.className = 'session-delete-btn';
|
|
1825
|
+
deleteBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
|
|
1826
|
+
deleteBtn.title = '删除会话';
|
|
1827
|
+
deleteBtn.addEventListener('click', function(e) {
|
|
1828
|
+
e.stopPropagation();
|
|
1829
|
+
deleteSession(session.id, item);
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1746
1832
|
item.appendChild(icon);
|
|
1747
1833
|
item.appendChild(info);
|
|
1834
|
+
item.appendChild(deleteBtn);
|
|
1748
1835
|
|
|
1749
1836
|
item.addEventListener('click', function() {
|
|
1750
1837
|
loadSession(session);
|
|
@@ -1754,6 +1841,39 @@
|
|
|
1754
1841
|
});
|
|
1755
1842
|
}
|
|
1756
1843
|
|
|
1844
|
+
function deleteSession(sessionId, itemEl) {
|
|
1845
|
+
if (!confirm('确定要删除这个会话吗?')) return;
|
|
1846
|
+
|
|
1847
|
+
itemEl.style.opacity = '0.4';
|
|
1848
|
+
itemEl.style.pointerEvents = 'none';
|
|
1849
|
+
|
|
1850
|
+
fetch('/api/session/' + sessionId, { method: 'DELETE' })
|
|
1851
|
+
.then(function(r) { return r.json(); })
|
|
1852
|
+
.then(function(data) {
|
|
1853
|
+
if (data.ok) {
|
|
1854
|
+
itemEl.style.transition = 'all 0.2s';
|
|
1855
|
+
itemEl.style.maxHeight = '0';
|
|
1856
|
+
itemEl.style.overflow = 'hidden';
|
|
1857
|
+
itemEl.style.padding = '0 12px';
|
|
1858
|
+
itemEl.style.margin = '0';
|
|
1859
|
+
itemEl.style.opacity = '0';
|
|
1860
|
+
setTimeout(function() {
|
|
1861
|
+
sessions = sessions.filter(function(s) { return s.id !== sessionId; });
|
|
1862
|
+
renderSessions();
|
|
1863
|
+
}, 200);
|
|
1864
|
+
} else {
|
|
1865
|
+
itemEl.style.opacity = '';
|
|
1866
|
+
itemEl.style.pointerEvents = '';
|
|
1867
|
+
alert('删除失败: ' + (data.error || '未知错误'));
|
|
1868
|
+
}
|
|
1869
|
+
})
|
|
1870
|
+
.catch(function(err) {
|
|
1871
|
+
itemEl.style.opacity = '';
|
|
1872
|
+
itemEl.style.pointerEvents = '';
|
|
1873
|
+
alert('删除失败: ' + err.message);
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1757
1877
|
function showSessionDetail() {
|
|
1758
1878
|
document.getElementById('session-list-view').classList.add('hidden');
|
|
1759
1879
|
document.getElementById('session-detail-view').classList.add('visible');
|
|
@@ -2001,17 +2121,6 @@
|
|
|
2001
2121
|
setTimeout(function() { toast.classList.remove('show'); }, 1200);
|
|
2002
2122
|
}
|
|
2003
2123
|
|
|
2004
|
-
// "复制" 按钮
|
|
2005
|
-
document.getElementById('btn-copy').addEventListener('touchend', function(e) {
|
|
2006
|
-
e.preventDefault();
|
|
2007
|
-
var text = getTerminalText();
|
|
2008
|
-
if (text) copyToClipboard(text);
|
|
2009
|
-
});
|
|
2010
|
-
document.getElementById('btn-copy').addEventListener('click', function(e) {
|
|
2011
|
-
e.preventDefault();
|
|
2012
|
-
var text = getTerminalText();
|
|
2013
|
-
if (text) copyToClipboard(text);
|
|
2014
|
-
});
|
|
2015
2124
|
|
|
2016
2125
|
// 方案2: 长按进入选择模式 — 原位显示可选纯文本
|
|
2017
2126
|
var selectTextLayer = document.getElementById('select-text-layer');
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -501,6 +501,30 @@ const server = createServer(async (req, res) => {
|
|
|
501
501
|
return;
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
// API: 软删除会话(设置 time_archived)
|
|
505
|
+
if (req.method === 'DELETE' && req.url?.startsWith('/api/session/')) {
|
|
506
|
+
const sessionId = req.url.split('/').pop();
|
|
507
|
+
res.writeHead(200, {
|
|
508
|
+
'Content-Type': 'application/json',
|
|
509
|
+
'Access-Control-Allow-Origin': '*',
|
|
510
|
+
});
|
|
511
|
+
try {
|
|
512
|
+
if (!existsSync(OPENCODE_DB_PATH)) {
|
|
513
|
+
res.end(JSON.stringify({ ok: false, error: '数据库不存在' }));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const db = new Database(OPENCODE_DB_PATH);
|
|
517
|
+
const now = Date.now();
|
|
518
|
+
const result = db.prepare('UPDATE session SET time_archived = ? WHERE id = ? AND time_archived IS NULL').run(now, sessionId);
|
|
519
|
+
db.close();
|
|
520
|
+
res.end(JSON.stringify({ ok: true, changes: result.changes }));
|
|
521
|
+
} catch (err) {
|
|
522
|
+
console.error('[DB] 软删除会话失败:', err.message);
|
|
523
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
504
528
|
// API: 获取会话消息
|
|
505
529
|
if (req.url?.startsWith('/api/session/')) {
|
|
506
530
|
const sessionId = req.url.split('/').pop();
|
|
@@ -627,6 +651,18 @@ wss.on('connection', (ws, req) => {
|
|
|
627
651
|
ws.send(JSON.stringify({ type: 'restore-error', error: e.message }));
|
|
628
652
|
}
|
|
629
653
|
}
|
|
654
|
+
} else if (msg.type === 'start') {
|
|
655
|
+
// 前端启动指令:可选带 sessionId 恢复会话
|
|
656
|
+
const mode = currentMode;
|
|
657
|
+
console.log(`[start] 启动 ${mode}, sessionId=${msg.sessionId || '(新会话)'}`);
|
|
658
|
+
outputBuffer = '';
|
|
659
|
+
try {
|
|
660
|
+
await spawnProcess(mode, msg.sessionId || null);
|
|
661
|
+
ws.send(JSON.stringify({ type: 'started', sessionId: msg.sessionId || null }));
|
|
662
|
+
} catch (e) {
|
|
663
|
+
console.error('[start] 启动失败:', e.message);
|
|
664
|
+
ws.send(JSON.stringify({ type: 'start-error', error: e.message }));
|
|
665
|
+
}
|
|
630
666
|
} else if (msg.type === 'new-session') {
|
|
631
667
|
// 开启新会话:杀掉当前进程,重新启动不带 session 参数的 opencode
|
|
632
668
|
const mode = currentMode;
|