claude-opencode-viewer 2.4.0 → 2.4.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/index.html +49 -12
- package/package.json +1 -1
- package/server.js +38 -3
package/index.html
CHANGED
|
@@ -493,14 +493,22 @@
|
|
|
493
493
|
<!-- 参考 cc-viewer 的 App.jsx 行 1315-1607: 完整的移动端布局结构 -->
|
|
494
494
|
<div id="layout">
|
|
495
495
|
<div id="header">
|
|
496
|
-
<div style="
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
496
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
497
|
+
<button class="history-toggle-btn" id="new-session-btn">
|
|
498
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
499
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
500
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
501
|
+
</svg>
|
|
502
|
+
<span>新会话</span>
|
|
503
|
+
</button>
|
|
504
|
+
<button class="history-toggle-btn" id="history-toggle">
|
|
505
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
506
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
507
|
+
<polyline points="12 6 12 12 16 14"></polyline>
|
|
508
|
+
</svg>
|
|
509
|
+
<span>历史</span>
|
|
510
|
+
</button>
|
|
511
|
+
</div>
|
|
504
512
|
<div id="mode-switcher">
|
|
505
513
|
<span id="mode-label">Mode:</span>
|
|
506
514
|
<select id="mode-select">
|
|
@@ -1048,10 +1056,16 @@
|
|
|
1048
1056
|
ws.onmessage = function(e) {
|
|
1049
1057
|
try {
|
|
1050
1058
|
var msg = JSON.parse(e.data);
|
|
1051
|
-
if (msg.type === 'data')
|
|
1059
|
+
if (msg.type === 'data') {
|
|
1060
|
+
if (!isCreatingNewSession) {
|
|
1061
|
+
throttledWrite(msg.data);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1052
1064
|
else if (msg.type === 'exit') {
|
|
1053
|
-
|
|
1054
|
-
|
|
1065
|
+
if (!isCreatingNewSession) {
|
|
1066
|
+
throttledWrite('\r\n\x1b[33m[进程已退出: ' + msg.exitCode + ']\x1b[0m\r\n');
|
|
1067
|
+
throttledWrite('\x1b[90m按 Enter 键重新启动 ' + currentMode + '...\x1b[0m\r\n');
|
|
1068
|
+
}
|
|
1055
1069
|
}
|
|
1056
1070
|
else if (msg.type === 'mode') {
|
|
1057
1071
|
endTransition(msg.mode);
|
|
@@ -1080,6 +1094,14 @@
|
|
|
1080
1094
|
// 恢复失败
|
|
1081
1095
|
term.write('\x1b[31m✗ 恢复失败: ' + msg.error + '\x1b[0m\r\n');
|
|
1082
1096
|
}
|
|
1097
|
+
else if (msg.type === 'new-session-ok') {
|
|
1098
|
+
isCreatingNewSession = false;
|
|
1099
|
+
term.clear();
|
|
1100
|
+
}
|
|
1101
|
+
else if (msg.type === 'new-session-error') {
|
|
1102
|
+
isCreatingNewSession = false;
|
|
1103
|
+
term.write('\x1b[31m✗ 新会话启动失败: ' + msg.error + '\x1b[0m\r\n');
|
|
1104
|
+
}
|
|
1083
1105
|
} catch(err) {}
|
|
1084
1106
|
};
|
|
1085
1107
|
|
|
@@ -1455,12 +1477,27 @@
|
|
|
1455
1477
|
|
|
1456
1478
|
if (historyBarVisible) {
|
|
1457
1479
|
historyBar.classList.add('visible');
|
|
1458
|
-
|
|
1480
|
+
if (currentMode === 'opencode') {
|
|
1481
|
+
loadSessions();
|
|
1482
|
+
} else {
|
|
1483
|
+
// claude 模式暂无历史会话功能
|
|
1484
|
+
var sessionList = document.getElementById('session-list');
|
|
1485
|
+
sessionList.innerHTML = '<div class="session-empty">Claude Code 暂不支持历史会话</div>';
|
|
1486
|
+
}
|
|
1459
1487
|
} else {
|
|
1460
1488
|
historyBar.classList.remove('visible');
|
|
1461
1489
|
}
|
|
1462
1490
|
}
|
|
1463
1491
|
|
|
1492
|
+
// 新会话按钮
|
|
1493
|
+
var isCreatingNewSession = false;
|
|
1494
|
+
document.getElementById('new-session-btn').addEventListener('click', function() {
|
|
1495
|
+
if (!ws || ws.readyState !== 1) return;
|
|
1496
|
+
isCreatingNewSession = true;
|
|
1497
|
+
term.clear();
|
|
1498
|
+
ws.send(JSON.stringify({ type: 'new-session' }));
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1464
1501
|
// 绑定历史按钮
|
|
1465
1502
|
document.getElementById('history-toggle').addEventListener('click', function() {
|
|
1466
1503
|
toggleHistoryBar();
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -15,8 +15,11 @@ process.title = 'claude-opencode-viewer';
|
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const PORT = 7008;
|
|
17
17
|
|
|
18
|
-
// OpenCode
|
|
19
|
-
const OPENCODE_DB_PATH = join(
|
|
18
|
+
// OpenCode 数据库路径(支持环境变量覆盖,自动检测 /halo 环境)
|
|
19
|
+
const OPENCODE_DB_PATH = process.env.OPENCODE_DB_PATH || join(
|
|
20
|
+
existsSync('/halo') ? '/halo/.local/share' : (process.env.XDG_DATA_HOME || join(homedir(), '.local/share')),
|
|
21
|
+
'opencode/opencode.db'
|
|
22
|
+
);
|
|
20
23
|
|
|
21
24
|
const MAX_BUFFER = 200000;
|
|
22
25
|
|
|
@@ -119,12 +122,18 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
const spawnEnv = { ...process.env };
|
|
126
|
+
// 如果 /halo 存在,将 opencode 数据持久化到 NAS
|
|
127
|
+
if (mode === 'opencode' && existsSync('/halo')) {
|
|
128
|
+
spawnEnv.XDG_DATA_HOME = '/halo/.local/share';
|
|
129
|
+
}
|
|
130
|
+
|
|
122
131
|
const proc = pty.spawn(command, args, {
|
|
123
132
|
name: 'xterm-256color',
|
|
124
133
|
cols: lastPtyCols,
|
|
125
134
|
rows: lastPtyRows,
|
|
126
135
|
cwd: process.cwd(),
|
|
127
|
-
env:
|
|
136
|
+
env: spawnEnv,
|
|
128
137
|
});
|
|
129
138
|
|
|
130
139
|
proc.onData((data) => {
|
|
@@ -526,6 +535,32 @@ wss.on('connection', (ws, req) => {
|
|
|
526
535
|
ws.send(JSON.stringify({ type: 'restore-error', error: e.message }));
|
|
527
536
|
}
|
|
528
537
|
}
|
|
538
|
+
} else if (msg.type === 'new-session') {
|
|
539
|
+
// 开启新会话:杀掉当前进程,重新启动不带 session 参数的 opencode
|
|
540
|
+
const mode = currentMode;
|
|
541
|
+
console.log(`[new-session] 开启新会话, mode=${mode}`);
|
|
542
|
+
|
|
543
|
+
if (mode === 'opencode' && opencodeProcess) {
|
|
544
|
+
try { opencodeProcess.kill(); } catch {}
|
|
545
|
+
opencodeProcess = null;
|
|
546
|
+
currentProcess = null;
|
|
547
|
+
} else if (mode === 'claude' && claudeProcess) {
|
|
548
|
+
try { claudeProcess.kill(); } catch {}
|
|
549
|
+
claudeProcess = null;
|
|
550
|
+
currentProcess = null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
outputBuffer = '';
|
|
554
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
555
|
+
|
|
556
|
+
// 先通知前端准备好,再启动新进程
|
|
557
|
+
ws.send(JSON.stringify({ type: 'new-session-ok', mode }));
|
|
558
|
+
try {
|
|
559
|
+
await spawnProcess(mode);
|
|
560
|
+
} catch (e) {
|
|
561
|
+
console.error('[new-session] 启动失败:', e.message);
|
|
562
|
+
ws.send(JSON.stringify({ type: 'new-session-error', error: e.message }));
|
|
563
|
+
}
|
|
529
564
|
}
|
|
530
565
|
} catch (err) {
|
|
531
566
|
console.error('[WS] Error:', err.message);
|