claude-opencode-viewer 2.6.10 → 2.6.12

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 CHANGED
@@ -87,170 +87,6 @@
87
87
  -webkit-overflow-scrolling: touch;
88
88
  }
89
89
 
90
- #session-detail-view {
91
- display: none;
92
- flex-direction: column;
93
- flex: 1;
94
- min-height: 0;
95
- }
96
-
97
- #session-detail-view.visible {
98
- display: flex;
99
- }
100
-
101
- #session-detail-header {
102
- padding: 12px 16px;
103
- background: #0d0d0d;
104
- border-bottom: 1px solid #222;
105
- flex-shrink: 0;
106
- }
107
-
108
- #session-detail-title {
109
- font-size: 14px;
110
- color: #ddd;
111
- font-weight: 500;
112
- margin-bottom: 4px;
113
- }
114
-
115
- #session-detail-meta {
116
- font-size: 11px;
117
- color: #888;
118
- display: flex;
119
- gap: 12px;
120
- }
121
-
122
- #session-detail-content {
123
- flex: 1;
124
- overflow-y: auto;
125
- padding: 16px;
126
- -webkit-overflow-scrolling: touch;
127
- }
128
-
129
- .message-item {
130
- margin-bottom: 20px;
131
- display: flex;
132
- gap: 12px;
133
- align-items: flex-start;
134
- }
135
-
136
- /* User 消息:左对齐 */
137
- .message-user {
138
- justify-content: flex-start;
139
- }
140
-
141
- /* Assistant 消息:右对齐 */
142
- .message-assistant {
143
- justify-content: flex-end;
144
- flex-direction: row-reverse;
145
- }
146
-
147
- .message-avatar {
148
- flex-shrink: 0;
149
- width: 32px;
150
- height: 32px;
151
- border-radius: 50%;
152
- background: #2a4a7c;
153
- display: flex;
154
- align-items: center;
155
- justify-content: center;
156
- font-size: 14px;
157
- }
158
-
159
- .message-assistant .message-avatar {
160
- background: #1a5a3a;
161
- }
162
-
163
- .message-content {
164
- flex: 1;
165
- min-width: 0;
166
- max-width: 80%;
167
- }
168
-
169
- .message-header {
170
- display: flex;
171
- align-items: center;
172
- gap: 8px;
173
- margin-bottom: 6px;
174
- }
175
-
176
- /* User 的标题左对齐 */
177
- .message-user .message-header {
178
- justify-content: flex-start;
179
- }
180
-
181
- /* Assistant 的标题右对齐 */
182
- .message-assistant .message-header {
183
- justify-content: flex-end;
184
- flex-direction: row-reverse;
185
- }
186
-
187
- .message-role {
188
- font-size: 11px;
189
- color: #888;
190
- font-weight: 600;
191
- text-transform: uppercase;
192
- letter-spacing: 0.5px;
193
- }
194
-
195
- .message-text {
196
- color: #ddd;
197
- font-size: 13px;
198
- line-height: 1.6;
199
- white-space: pre-wrap;
200
- word-break: break-word;
201
- background: #141414;
202
- padding: 12px;
203
- border-radius: 8px;
204
- border: 1px solid #222;
205
- }
206
-
207
- /* User 消息气泡 */
208
- .message-user .message-text {
209
- background: #1a2332;
210
- border-color: #2a4a7c;
211
- }
212
-
213
- /* Assistant 消息气泡 */
214
- .message-assistant .message-text {
215
- background: #1a2e1a;
216
- border-color: #2a5a3a;
217
- }
218
-
219
- .message-tool-call {
220
- margin-top: 8px;
221
- padding: 8px 12px;
222
- background: #1a1a0a;
223
- border: 1px solid #333;
224
- border-radius: 6px;
225
- font-size: 12px;
226
- }
227
-
228
- .message-tool-name {
229
- color: #f0ad4e;
230
- font-weight: 600;
231
- display: flex;
232
- align-items: center;
233
- gap: 6px;
234
- }
235
-
236
- .message-tool-result {
237
- margin-top: 6px;
238
- padding: 8px;
239
- background: #0a0a0a;
240
- border-radius: 4px;
241
- font-size: 11px;
242
- color: #999;
243
- max-height: 100px;
244
- overflow-y: auto;
245
- }
246
-
247
- .message-empty {
248
- text-align: center;
249
- padding: 40px 20px;
250
- color: #666;
251
- font-size: 13px;
252
- }
253
-
254
90
  #session-list {
255
91
  display: flex;
256
92
  flex-direction: column;
@@ -559,58 +395,6 @@
559
395
  display: block;
560
396
  }
561
397
 
562
- #copy-popup {
563
- display: none;
564
- position: fixed;
565
- top: 50%;
566
- left: 50%;
567
- transform: translate(-50%, -50%);
568
- background: #1a1a1a;
569
- border: 1px solid #444;
570
- border-radius: 8px;
571
- padding: 16px;
572
- z-index: 10000;
573
- max-width: 80vw;
574
- max-height: 60vh;
575
- }
576
- #copy-popup.show {
577
- display: flex;
578
- flex-direction: column;
579
- gap: 10px;
580
- }
581
- #copy-popup textarea {
582
- width: 500px;
583
- max-width: 70vw;
584
- height: 120px;
585
- background: #0a0a0a;
586
- color: #ccc;
587
- border: 1px solid #333;
588
- border-radius: 4px;
589
- padding: 8px;
590
- font-family: Menlo, Monaco, monospace;
591
- font-size: 12px;
592
- resize: vertical;
593
- }
594
- #copy-popup .popup-actions {
595
- display: flex;
596
- justify-content: flex-end;
597
- gap: 8px;
598
- }
599
- #copy-popup button {
600
- padding: 6px 16px;
601
- border: none;
602
- border-radius: 4px;
603
- cursor: pointer;
604
- font-size: 13px;
605
- }
606
- #copy-popup .btn-copy {
607
- background: #2563eb;
608
- color: #fff;
609
- }
610
- #copy-popup .btn-close {
611
- background: #333;
612
- color: #ccc;
613
- }
614
398
 
615
399
  /* Git Diff 面板 */
616
400
  #git-diff-bar {
@@ -894,38 +678,6 @@
894
678
  </div>
895
679
  </div>
896
680
 
897
- <!-- 会话详情视图 -->
898
- <div id="session-detail-view">
899
- <div id="session-history-header">
900
- <div id="session-history-title">会话详情</div>
901
- <div id="session-history-actions">
902
- <button class="history-toggle-btn" id="restore-session" style="background: #1a5a3a; border-color: #2a7a4a;">
903
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
904
- <polyline points="1 4 1 10 7 10"></polyline>
905
- <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
906
- </svg>
907
- <span>恢复会话</span>
908
- </button>
909
- <button class="history-toggle-btn" id="back-to-list">
910
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
911
- <line x1="19" y1="12" x2="5" y2="12"></line>
912
- <polyline points="12 19 5 12 12 5"></polyline>
913
- </svg>
914
- <span>返回列表</span>
915
- </button>
916
- </div>
917
- </div>
918
- <div id="session-detail-header">
919
- <div id="session-detail-title">会话标题</div>
920
- <div id="session-detail-meta">
921
- <span id="session-detail-time">时间</span>
922
- <span id="session-detail-dir">目录</span>
923
- </div>
924
- </div>
925
- <div id="session-detail-content">
926
- <div class="message-empty">加载中...</div>
927
- </div>
928
- </div>
929
681
  </div>
930
682
 
931
683
  <!-- Git Diff 面板 -->
@@ -982,14 +734,6 @@
982
734
  </div>
983
735
 
984
736
  <div id="copy-toast">已复制</div>
985
- <div id="copy-popup">
986
- <div style="color:#ccc;font-size:13px;">剪贴板写入需要 HTTPS,请手动复制:</div>
987
- <textarea id="copy-popup-text" readonly></textarea>
988
- <div class="popup-actions">
989
- <button class="btn-copy" onclick="document.getElementById('copy-popup-text').select();document.execCommand('copy');document.getElementById('copy-popup').classList.remove('show');">选中并复制</button>
990
- <button class="btn-close" onclick="document.getElementById('copy-popup').classList.remove('show');">关闭</button>
991
- </div>
992
- </div>
993
737
  <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
994
738
  <script>
995
739
  (function() {
@@ -1038,19 +782,15 @@
1038
782
  try {
1039
783
  var bytes = Uint8Array.from(atob(b64), function(c) { return c.charCodeAt(0); });
1040
784
  var text = new TextDecoder().decode(bytes);
1041
- if (navigator.clipboard && navigator.clipboard.writeText) {
1042
- navigator.clipboard.writeText(text).then(function() {
1043
- showCopyToast();
1044
- }).catch(function() {
1045
- showCopyPopup(text);
1046
- });
1047
- } else {
1048
- showCopyPopup(text);
1049
- }
785
+ copyToClipboard(text);
1050
786
  } catch (e) {}
1051
787
  return true;
1052
788
  });
1053
789
 
790
+ // 自动检测反向代理子路径,确保 API/WS 请求带正确前缀
791
+ var basePath = location.pathname.replace(/\/[^/]*$/, '');
792
+ if (basePath === '' || basePath === '/') basePath = '';
793
+
1054
794
  var modeSelect = document.getElementById('mode-select');
1055
795
  var terminalEl = document.getElementById('terminal');
1056
796
  var ws = null;
@@ -1466,7 +1206,7 @@
1466
1206
 
1467
1207
  function connect() {
1468
1208
  var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1469
- ws = new WebSocket(proto + '//' + location.host + '/ws');
1209
+ ws = new WebSocket(proto + '//' + location.host + basePath + '/ws');
1470
1210
 
1471
1211
  ws.onopen = function() {
1472
1212
  isBufferReplay = true;
@@ -1612,7 +1352,7 @@
1612
1352
  var sessionList = document.getElementById('session-list');
1613
1353
  sessionList.innerHTML = '<div class="session-loading">加载历史会话...</div>';
1614
1354
 
1615
- fetch('/api/sessions')
1355
+ fetch(basePath + '/api/sessions')
1616
1356
  .then(function(response) { return response.json(); })
1617
1357
  .then(function(data) {
1618
1358
  sessions = data;
@@ -1695,7 +1435,7 @@
1695
1435
  itemEl.style.opacity = '0.4';
1696
1436
  itemEl.style.pointerEvents = 'none';
1697
1437
 
1698
- fetch('/api/session/' + sessionId, { method: 'DELETE' })
1438
+ fetch(basePath + '/api/session/' + sessionId, { method: 'DELETE' })
1699
1439
  .then(function(r) { return r.json(); })
1700
1440
  .then(function(data) {
1701
1441
  if (data.ok) {
@@ -1722,110 +1462,35 @@
1722
1462
  });
1723
1463
  }
1724
1464
 
1725
- function showSessionDetail() {
1726
- document.getElementById('session-list-view').classList.add('hidden');
1727
- document.getElementById('session-detail-view').classList.add('visible');
1728
- }
1729
1465
 
1730
- function showSessionList() {
1731
- document.getElementById('session-list-view').classList.remove('hidden');
1732
- document.getElementById('session-detail-view').classList.remove('visible');
1733
- }
1734
1466
 
1735
1467
  function loadSession(session) {
1736
- console.log('[session] 加载会话:', session.title);
1737
-
1738
- // 保存当前会话数据
1468
+ console.log('[restore] 直接恢复会话:', session.title);
1739
1469
  currentSessionData = session;
1740
1470
 
1741
- // 切换到详情视图
1742
- showSessionDetail();
1743
-
1744
- // 更新详情页标题和元信息
1745
- document.getElementById('session-detail-title').textContent = session.preview || session.title;
1746
- document.getElementById('session-detail-time').textContent = formatTime(session.time_updated);
1747
- document.getElementById('session-detail-dir').textContent = session.directory.replace(/^\/Users\/[^\/]+/, '~');
1748
-
1749
- var contentDiv = document.getElementById('session-detail-content');
1750
- contentDiv.innerHTML = '<div class="message-empty">加载消息中...</div>';
1751
-
1752
- // 获取会话的所有消息
1753
- fetch('/api/session/' + session.id)
1754
- .then(function(response) { return response.json(); })
1755
- .then(function(messages) {
1756
- console.log('[session] 收到', messages.length, '条消息');
1757
-
1758
- if (messages.length === 0) {
1759
- contentDiv.innerHTML = '<div class="message-empty">该会话暂无消息</div>';
1760
- return;
1761
- }
1762
-
1763
- // 渲染消息列表
1764
- var html = '';
1765
- messages.forEach(function(msg) {
1766
- console.log('[render] 消息:', msg);
1767
-
1768
- var roleClass = msg.role === 'assistant' ? 'message-assistant' : 'message-user';
1769
- var avatar = msg.role === 'user' ? '👤' : '🤖';
1770
- var roleName = msg.role === 'user' ? 'User' : 'Assistant';
1771
-
1772
- html += '<div class="message-item ' + roleClass + '">';
1773
- html += '<div class="message-avatar">' + avatar + '</div>';
1774
- html += '<div class="message-content">';
1775
- html += '<div class="message-header">';
1776
- html += '<div class="message-role">' + roleName + '</div>';
1777
- html += '</div>';
1778
-
1779
- // 显示推理过程(仅 assistant)
1780
- if (msg.reasoning) {
1781
- html += '<div class="message-text" style="background: #1a1a0a; border-color: #333;">';
1782
- html += '<div style="color: #f0ad4e; font-size: 11px; font-weight: 600; margin-bottom: 6px;">💭 思考过程</div>';
1783
- html += '<div style="color: #bbb;">' + escapeHtml(msg.reasoning) + '</div>';
1784
- html += '</div>';
1785
- }
1786
-
1787
- // 显示文本内容
1788
- if (msg.text) {
1789
- html += '<div class="message-text">' + escapeHtml(msg.text) + '</div>';
1790
- } else if (!msg.reasoning && !msg.toolCalls && !msg.toolResults) {
1791
- html += '<div class="message-text" style="color: #666; font-style: italic;">(无文本内容)</div>';
1792
- }
1793
-
1794
- // 显示工具调用
1795
- if (msg.toolCalls && msg.toolCalls.length > 0) {
1796
- msg.toolCalls.forEach(function(tool) {
1797
- html += '<div class="message-tool-call">';
1798
- html += '<div class="message-tool-name">🔧 工具调用: ' + escapeHtml(tool.name || '未知工具') + '</div>';
1799
- html += '</div>';
1800
- });
1801
- }
1802
-
1803
- // 显示工具结果
1804
- if (msg.toolResults && msg.toolResults.length > 0) {
1805
- msg.toolResults.forEach(function(result) {
1806
- if (result.text) {
1807
- var resultText = result.text.length > 200 ? result.text.substring(0, 200) + '...' : result.text;
1808
- html += '<div class="message-tool-call">';
1809
- html += '<div class="message-tool-name">📝 工具结果</div>';
1810
- html += '<div class="message-tool-result">' + escapeHtml(resultText) + '</div>';
1811
- html += '</div>';
1812
- }
1813
- });
1814
- }
1471
+ // 关闭历史栏
1472
+ toggleHistoryBar();
1815
1473
 
1816
- html += '</div>';
1817
- html += '</div>';
1818
- });
1474
+ // 清空终端
1475
+ term.clear();
1819
1476
 
1820
- contentDiv.innerHTML = html;
1477
+ // 显示正在恢复的提示
1478
+ term.write('\x1b[1;36m╔══════════════════════════════════════════════════════════════╗\x1b[0m\r\n');
1479
+ term.write('\x1b[1;36m║ \x1b[1;37m正在恢复会话... \x1b[1;36m║\x1b[0m\r\n');
1480
+ term.write('\x1b[1;36m╚══════════════════════════════════════════════════════════════╝\x1b[0m\r\n');
1481
+ term.write('\r\n');
1821
1482
 
1822
- // 滚动到底部
1823
- contentDiv.scrollTop = contentDiv.scrollHeight;
1824
- })
1825
- .catch(function(err) {
1826
- console.error('[session] 加载消息失败:', err);
1827
- contentDiv.innerHTML = '<div class="message-empty">加载失败: ' + escapeHtml(err.message) + '</div>';
1828
- });
1483
+ if (ws && ws.readyState === 1) {
1484
+ if (currentMode !== 'opencode') {
1485
+ term.write('\x1b[31m错误: 请先切换到 OpenCode 模式\x1b[0m\r\n');
1486
+ return;
1487
+ }
1488
+ term.write('\x1b[33m正在重启 OpenCode 并恢复会话: ' + session.id + '\x1b[0m\r\n');
1489
+ term.write('\r\n');
1490
+ ws.send(JSON.stringify({ type: 'restore', sessionId: session.id }));
1491
+ } else {
1492
+ term.write('\x1b[31m错误: WebSocket 未连接\x1b[0m\r\n');
1493
+ }
1829
1494
  }
1830
1495
 
1831
1496
  function escapeHtml(text) {
@@ -1878,53 +1543,6 @@
1878
1543
  toggleHistoryBar();
1879
1544
  });
1880
1545
 
1881
- // 返回到会话列表
1882
- document.getElementById('back-to-list').addEventListener('click', function(e) {
1883
- e.stopPropagation();
1884
- showSessionList();
1885
- });
1886
-
1887
- // 恢复会话
1888
- document.getElementById('restore-session').addEventListener('click', function(e) {
1889
- e.stopPropagation();
1890
- if (!currentSessionData) {
1891
- console.error('[restore] 没有当前会话数据');
1892
- return;
1893
- }
1894
-
1895
- console.log('[restore] 恢复会话:', currentSessionData.id);
1896
-
1897
- // 关闭历史栏
1898
- toggleHistoryBar();
1899
-
1900
- // 清空终端
1901
- term.clear();
1902
-
1903
- // 显示正在恢复的提示
1904
- term.write('\x1b[1;36m╔══════════════════════════════════════════════════════════════╗\x1b[0m\r\n');
1905
- term.write('\x1b[1;36m║ \x1b[1;37m正在恢复会话... \x1b[1;36m║\x1b[0m\r\n');
1906
- term.write('\x1b[1;36m╚══════════════════════════════════════════════════════════════╝\x1b[0m\r\n');
1907
- term.write('\r\n');
1908
-
1909
- // 发送恢复会话的请求到服务端
1910
- if (ws && ws.readyState === 1) {
1911
- if (currentMode !== 'opencode') {
1912
- term.write('\x1b[31m错误: 请先切换到 OpenCode 模式\x1b[0m\r\n');
1913
- return;
1914
- }
1915
-
1916
- term.write('\x1b[33m正在重启 OpenCode 并恢复会话: ' + currentSessionData.id + '\x1b[0m\r\n');
1917
- term.write('\r\n');
1918
-
1919
- // 发送恢复请求
1920
- ws.send(JSON.stringify({
1921
- type: 'restore',
1922
- sessionId: currentSessionData.id
1923
- }));
1924
- } else {
1925
- term.write('\x1b[31m错误: WebSocket 未连接\x1b[0m\r\n');
1926
- }
1927
- });
1928
1546
 
1929
1547
  // 提取终端缓冲区文本
1930
1548
  function getTerminalText() {
@@ -1963,15 +1581,6 @@
1963
1581
  showCopyToast();
1964
1582
  }
1965
1583
 
1966
- function showCopyPopup(text) {
1967
- var popup = document.getElementById('copy-popup');
1968
- var ta = document.getElementById('copy-popup-text');
1969
- ta.value = text;
1970
- popup.classList.add('show');
1971
- ta.focus();
1972
- ta.select();
1973
- }
1974
-
1975
1584
  function showCopyToast() {
1976
1585
  var toast = document.getElementById('copy-toast');
1977
1586
  toast.classList.add('show');
@@ -2050,7 +1659,7 @@
2050
1659
  fileList.innerHTML = '<div class="git-diff-loading">加载中...</div>';
2051
1660
  document.getElementById('git-diff-count').textContent = '0';
2052
1661
 
2053
- fetch('/api/git-status')
1662
+ fetch(basePath + '/api/git-status')
2054
1663
  .then(function(r) { return r.json(); })
2055
1664
  .then(function(data) {
2056
1665
  diffChanges = data.changes || [];
@@ -2098,7 +1707,7 @@
2098
1707
  var area = document.getElementById('git-diff-content-area');
2099
1708
  area.innerHTML = '<div class="git-diff-loading">加载 diff...</div>';
2100
1709
 
2101
- fetch('/api/git-diff?files=' + encodeURIComponent(file))
1710
+ fetch(basePath + '/api/git-diff?files=' + encodeURIComponent(file))
2102
1711
  .then(function(r) { return r.json(); })
2103
1712
  .then(function(data) {
2104
1713
  if (!data.diffs || !data.diffs[0]) {
package/index.html CHANGED
@@ -87,170 +87,6 @@
87
87
  -webkit-overflow-scrolling: touch;
88
88
  }
89
89
 
90
- #session-detail-view {
91
- display: none;
92
- flex-direction: column;
93
- flex: 1;
94
- min-height: 0;
95
- }
96
-
97
- #session-detail-view.visible {
98
- display: flex;
99
- }
100
-
101
- #session-detail-header {
102
- padding: 12px 16px;
103
- background: #0d0d0d;
104
- border-bottom: 1px solid #222;
105
- flex-shrink: 0;
106
- }
107
-
108
- #session-detail-title {
109
- font-size: 14px;
110
- color: #ddd;
111
- font-weight: 500;
112
- margin-bottom: 4px;
113
- }
114
-
115
- #session-detail-meta {
116
- font-size: 11px;
117
- color: #888;
118
- display: flex;
119
- gap: 12px;
120
- }
121
-
122
- #session-detail-content {
123
- flex: 1;
124
- overflow-y: auto;
125
- padding: 16px;
126
- -webkit-overflow-scrolling: touch;
127
- }
128
-
129
- .message-item {
130
- margin-bottom: 20px;
131
- display: flex;
132
- gap: 12px;
133
- align-items: flex-start;
134
- }
135
-
136
- /* User 消息:左对齐 */
137
- .message-user {
138
- justify-content: flex-start;
139
- }
140
-
141
- /* Assistant 消息:右对齐 */
142
- .message-assistant {
143
- justify-content: flex-end;
144
- flex-direction: row-reverse;
145
- }
146
-
147
- .message-avatar {
148
- flex-shrink: 0;
149
- width: 32px;
150
- height: 32px;
151
- border-radius: 50%;
152
- background: #2a4a7c;
153
- display: flex;
154
- align-items: center;
155
- justify-content: center;
156
- font-size: 14px;
157
- }
158
-
159
- .message-assistant .message-avatar {
160
- background: #1a5a3a;
161
- }
162
-
163
- .message-content {
164
- flex: 1;
165
- min-width: 0;
166
- max-width: 80%;
167
- }
168
-
169
- .message-header {
170
- display: flex;
171
- align-items: center;
172
- gap: 8px;
173
- margin-bottom: 6px;
174
- }
175
-
176
- /* User 的标题左对齐 */
177
- .message-user .message-header {
178
- justify-content: flex-start;
179
- }
180
-
181
- /* Assistant 的标题右对齐 */
182
- .message-assistant .message-header {
183
- justify-content: flex-end;
184
- flex-direction: row-reverse;
185
- }
186
-
187
- .message-role {
188
- font-size: 11px;
189
- color: #888;
190
- font-weight: 600;
191
- text-transform: uppercase;
192
- letter-spacing: 0.5px;
193
- }
194
-
195
- .message-text {
196
- color: #ddd;
197
- font-size: 13px;
198
- line-height: 1.6;
199
- white-space: pre-wrap;
200
- word-break: break-word;
201
- background: #141414;
202
- padding: 12px;
203
- border-radius: 8px;
204
- border: 1px solid #222;
205
- }
206
-
207
- /* User 消息气泡 */
208
- .message-user .message-text {
209
- background: #1a2332;
210
- border-color: #2a4a7c;
211
- }
212
-
213
- /* Assistant 消息气泡 */
214
- .message-assistant .message-text {
215
- background: #1a2e1a;
216
- border-color: #2a5a3a;
217
- }
218
-
219
- .message-tool-call {
220
- margin-top: 8px;
221
- padding: 8px 12px;
222
- background: #1a1a0a;
223
- border: 1px solid #333;
224
- border-radius: 6px;
225
- font-size: 12px;
226
- }
227
-
228
- .message-tool-name {
229
- color: #f0ad4e;
230
- font-weight: 600;
231
- display: flex;
232
- align-items: center;
233
- gap: 6px;
234
- }
235
-
236
- .message-tool-result {
237
- margin-top: 6px;
238
- padding: 8px;
239
- background: #0a0a0a;
240
- border-radius: 4px;
241
- font-size: 11px;
242
- color: #999;
243
- max-height: 100px;
244
- overflow-y: auto;
245
- }
246
-
247
- .message-empty {
248
- text-align: center;
249
- padding: 40px 20px;
250
- color: #666;
251
- font-size: 13px;
252
- }
253
-
254
90
  #session-list {
255
91
  display: flex;
256
92
  flex-direction: column;
@@ -884,38 +720,6 @@
884
720
  </div>
885
721
  </div>
886
722
 
887
- <!-- 会话详情视图 -->
888
- <div id="session-detail-view">
889
- <div id="session-history-header">
890
- <div id="session-history-title">会话详情</div>
891
- <div id="session-history-actions">
892
- <button class="history-toggle-btn" id="restore-session" style="background: #1a5a3a; border-color: #2a7a4a;">
893
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
894
- <polyline points="1 4 1 10 7 10"></polyline>
895
- <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
896
- </svg>
897
- <span>恢复会话</span>
898
- </button>
899
- <button class="history-toggle-btn" id="back-to-list">
900
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
901
- <line x1="19" y1="12" x2="5" y2="12"></line>
902
- <polyline points="12 19 5 12 12 5"></polyline>
903
- </svg>
904
- <span>返回列表</span>
905
- </button>
906
- </div>
907
- </div>
908
- <div id="session-detail-header">
909
- <div id="session-detail-title">会话标题</div>
910
- <div id="session-detail-meta">
911
- <span id="session-detail-time">时间</span>
912
- <span id="session-detail-dir">目录</span>
913
- </div>
914
- </div>
915
- <div id="session-detail-content">
916
- <div class="message-empty">加载中...</div>
917
- </div>
918
- </div>
919
723
  </div>
920
724
 
921
725
  <!-- Git Diff 面板 -->
@@ -1046,6 +850,10 @@
1046
850
  return true;
1047
851
  });
1048
852
 
853
+ // 自动检测反向代理子路径,确保 API/WS 请求带正确前缀
854
+ var basePath = location.pathname.replace(/\/[^/]*$/, '');
855
+ if (basePath === '' || basePath === '/') basePath = '';
856
+
1049
857
  var modeSelect = document.getElementById('mode-select');
1050
858
  var terminalEl = document.getElementById('terminal');
1051
859
  var ws = null;
@@ -1523,7 +1331,7 @@
1523
1331
 
1524
1332
  function connect() {
1525
1333
  var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1526
- ws = new WebSocket(proto + '//' + location.host + '/ws');
1334
+ ws = new WebSocket(proto + '//' + location.host + basePath + '/ws');
1527
1335
 
1528
1336
  ws.onopen = function() {
1529
1337
  resize();
@@ -1774,7 +1582,7 @@
1774
1582
  var sessionList = document.getElementById('session-list');
1775
1583
  sessionList.innerHTML = '<div class="session-loading">加载历史会话...</div>';
1776
1584
 
1777
- fetch('/api/sessions')
1585
+ fetch(basePath + '/api/sessions')
1778
1586
  .then(function(response) { return response.json(); })
1779
1587
  .then(function(data) {
1780
1588
  sessions = data;
@@ -1857,7 +1665,7 @@
1857
1665
  itemEl.style.opacity = '0.4';
1858
1666
  itemEl.style.pointerEvents = 'none';
1859
1667
 
1860
- fetch('/api/session/' + sessionId, { method: 'DELETE' })
1668
+ fetch(basePath + '/api/session/' + sessionId, { method: 'DELETE' })
1861
1669
  .then(function(r) { return r.json(); })
1862
1670
  .then(function(data) {
1863
1671
  if (data.ok) {
@@ -1884,110 +1692,35 @@
1884
1692
  });
1885
1693
  }
1886
1694
 
1887
- function showSessionDetail() {
1888
- document.getElementById('session-list-view').classList.add('hidden');
1889
- document.getElementById('session-detail-view').classList.add('visible');
1890
- }
1891
1695
 
1892
- function showSessionList() {
1893
- document.getElementById('session-list-view').classList.remove('hidden');
1894
- document.getElementById('session-detail-view').classList.remove('visible');
1895
- }
1896
1696
 
1897
1697
  function loadSession(session) {
1898
- console.log('[session] 加载会话:', session.title);
1899
-
1900
- // 保存当前会话数据
1698
+ console.log('[restore] 直接恢复会话:', session.title);
1901
1699
  currentSessionData = session;
1902
1700
 
1903
- // 切换到详情视图
1904
- showSessionDetail();
1905
-
1906
- // 更新详情页标题和元信息
1907
- document.getElementById('session-detail-title').textContent = session.preview || session.title;
1908
- document.getElementById('session-detail-time').textContent = formatTime(session.time_updated);
1909
- document.getElementById('session-detail-dir').textContent = session.directory.replace(/^\/Users\/[^\/]+/, '~');
1910
-
1911
- var contentDiv = document.getElementById('session-detail-content');
1912
- contentDiv.innerHTML = '<div class="message-empty">加载消息中...</div>';
1913
-
1914
- // 获取会话的所有消息
1915
- fetch('/api/session/' + session.id)
1916
- .then(function(response) { return response.json(); })
1917
- .then(function(messages) {
1918
- console.log('[session] 收到', messages.length, '条消息');
1919
-
1920
- if (messages.length === 0) {
1921
- contentDiv.innerHTML = '<div class="message-empty">该会话暂无消息</div>';
1922
- return;
1923
- }
1924
-
1925
- // 渲染消息列表
1926
- var html = '';
1927
- messages.forEach(function(msg) {
1928
- console.log('[render] 消息:', msg);
1929
-
1930
- var roleClass = msg.role === 'assistant' ? 'message-assistant' : 'message-user';
1931
- var avatar = msg.role === 'user' ? '👤' : '🤖';
1932
- var roleName = msg.role === 'user' ? 'User' : 'Assistant';
1933
-
1934
- html += '<div class="message-item ' + roleClass + '">';
1935
- html += '<div class="message-avatar">' + avatar + '</div>';
1936
- html += '<div class="message-content">';
1937
- html += '<div class="message-header">';
1938
- html += '<div class="message-role">' + roleName + '</div>';
1939
- html += '</div>';
1940
-
1941
- // 显示推理过程(仅 assistant)
1942
- if (msg.reasoning) {
1943
- html += '<div class="message-text" style="background: #1a1a0a; border-color: #333;">';
1944
- html += '<div style="color: #f0ad4e; font-size: 11px; font-weight: 600; margin-bottom: 6px;">💭 思考过程</div>';
1945
- html += '<div style="color: #bbb;">' + escapeHtml(msg.reasoning) + '</div>';
1946
- html += '</div>';
1947
- }
1948
-
1949
- // 显示文本内容
1950
- if (msg.text) {
1951
- html += '<div class="message-text">' + escapeHtml(msg.text) + '</div>';
1952
- } else if (!msg.reasoning && !msg.toolCalls && !msg.toolResults) {
1953
- html += '<div class="message-text" style="color: #666; font-style: italic;">(无文本内容)</div>';
1954
- }
1955
-
1956
- // 显示工具调用
1957
- if (msg.toolCalls && msg.toolCalls.length > 0) {
1958
- msg.toolCalls.forEach(function(tool) {
1959
- html += '<div class="message-tool-call">';
1960
- html += '<div class="message-tool-name">🔧 工具调用: ' + escapeHtml(tool.name || '未知工具') + '</div>';
1961
- html += '</div>';
1962
- });
1963
- }
1964
-
1965
- // 显示工具结果
1966
- if (msg.toolResults && msg.toolResults.length > 0) {
1967
- msg.toolResults.forEach(function(result) {
1968
- if (result.text) {
1969
- var resultText = result.text.length > 200 ? result.text.substring(0, 200) + '...' : result.text;
1970
- html += '<div class="message-tool-call">';
1971
- html += '<div class="message-tool-name">📝 工具结果</div>';
1972
- html += '<div class="message-tool-result">' + escapeHtml(resultText) + '</div>';
1973
- html += '</div>';
1974
- }
1975
- });
1976
- }
1701
+ // 关闭历史栏
1702
+ toggleHistoryBar();
1977
1703
 
1978
- html += '</div>';
1979
- html += '</div>';
1980
- });
1704
+ // 清空终端
1705
+ term.clear();
1981
1706
 
1982
- contentDiv.innerHTML = html;
1707
+ // 显示正在恢复的提示
1708
+ term.write('\x1b[1;36m╔══════════════════════════════════════════════════════════════╗\x1b[0m\r\n');
1709
+ term.write('\x1b[1;36m║ \x1b[1;37m正在恢复会话... \x1b[1;36m║\x1b[0m\r\n');
1710
+ term.write('\x1b[1;36m╚══════════════════════════════════════════════════════════════╝\x1b[0m\r\n');
1711
+ term.write('\r\n');
1983
1712
 
1984
- // 滚动到底部
1985
- contentDiv.scrollTop = contentDiv.scrollHeight;
1986
- })
1987
- .catch(function(err) {
1988
- console.error('[session] 加载消息失败:', err);
1989
- contentDiv.innerHTML = '<div class="message-empty">加载失败: ' + escapeHtml(err.message) + '</div>';
1990
- });
1713
+ if (ws && ws.readyState === 1) {
1714
+ if (currentMode !== 'opencode') {
1715
+ term.write('\x1b[31m错误: 请先切换到 OpenCode 模式\x1b[0m\r\n');
1716
+ return;
1717
+ }
1718
+ term.write('\x1b[33m正在重启 OpenCode 并恢复会话: ' + session.id + '\x1b[0m\r\n');
1719
+ term.write('\r\n');
1720
+ ws.send(JSON.stringify({ type: 'restore', sessionId: session.id }));
1721
+ } else {
1722
+ term.write('\x1b[31m错误: WebSocket 未连接\x1b[0m\r\n');
1723
+ }
1991
1724
  }
1992
1725
 
1993
1726
  function escapeHtml(text) {
@@ -2040,53 +1773,6 @@
2040
1773
  toggleHistoryBar();
2041
1774
  });
2042
1775
 
2043
- // 返回到会话列表
2044
- document.getElementById('back-to-list').addEventListener('click', function(e) {
2045
- e.stopPropagation();
2046
- showSessionList();
2047
- });
2048
-
2049
- // 恢复会话
2050
- document.getElementById('restore-session').addEventListener('click', function(e) {
2051
- e.stopPropagation();
2052
- if (!currentSessionData) {
2053
- console.error('[restore] 没有当前会话数据');
2054
- return;
2055
- }
2056
-
2057
- console.log('[restore] 恢复会话:', currentSessionData.id);
2058
-
2059
- // 关闭历史栏
2060
- toggleHistoryBar();
2061
-
2062
- // 清空终端
2063
- term.clear();
2064
-
2065
- // 显示正在恢复的提示
2066
- term.write('\x1b[1;36m╔══════════════════════════════════════════════════════════════╗\x1b[0m\r\n');
2067
- term.write('\x1b[1;36m║ \x1b[1;37m正在恢复会话... \x1b[1;36m║\x1b[0m\r\n');
2068
- term.write('\x1b[1;36m╚══════════════════════════════════════════════════════════════╝\x1b[0m\r\n');
2069
- term.write('\r\n');
2070
-
2071
- // 发送恢复会话的请求到服务端
2072
- if (ws && ws.readyState === 1) {
2073
- if (currentMode !== 'opencode') {
2074
- term.write('\x1b[31m错误: 请先切换到 OpenCode 模式\x1b[0m\r\n');
2075
- return;
2076
- }
2077
-
2078
- term.write('\x1b[33m正在重启 OpenCode 并恢复会话: ' + currentSessionData.id + '\x1b[0m\r\n');
2079
- term.write('\r\n');
2080
-
2081
- // 发送恢复请求
2082
- ws.send(JSON.stringify({
2083
- type: 'restore',
2084
- sessionId: currentSessionData.id
2085
- }));
2086
- } else {
2087
- term.write('\x1b[31m错误: WebSocket 未连接\x1b[0m\r\n');
2088
- }
2089
- });
2090
1776
 
2091
1777
  // 提取终端缓冲区文本
2092
1778
  function getTerminalText() {
@@ -2203,7 +1889,7 @@
2203
1889
  fileList.innerHTML = '<div class="git-diff-loading">加载中...</div>';
2204
1890
  document.getElementById('git-diff-count').textContent = '0';
2205
1891
 
2206
- fetch('/api/git-status')
1892
+ fetch(basePath + '/api/git-status')
2207
1893
  .then(function(r) { return r.json(); })
2208
1894
  .then(function(data) {
2209
1895
  diffChanges = data.changes || [];
@@ -2251,7 +1937,7 @@
2251
1937
  var area = document.getElementById('git-diff-content-area');
2252
1938
  area.innerHTML = '<div class="git-diff-loading">加载 diff...</div>';
2253
1939
 
2254
- fetch('/api/git-diff?files=' + encodeURIComponent(file))
1940
+ fetch(basePath + '/api/git-diff?files=' + encodeURIComponent(file))
2255
1941
  .then(function(r) { return r.json(); })
2256
1942
  .then(function(data) {
2257
1943
  if (!data.diffs || !data.diffs[0]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.6.10",
3
+ "version": "2.6.12",
4
4
  "description": "A unified terminal viewer for Claude Code and OpenCode with seamless switching",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -30,7 +30,7 @@ async function getOrCreateCert() {
30
30
  return { key: readFileSync(keyPath), cert: readFileSync(certPath) };
31
31
  }
32
32
 
33
- console.log('🔐 首次使用 HTTPS,生成自签名证书...');
33
+ if (!JSON_OUTPUT) console.log('🔐 首次使用 HTTPS,生成自签名证书...');
34
34
  mkdirSync(certDir, { recursive: true });
35
35
 
36
36
  const { default: selfsigned } = await import('selfsigned');
@@ -48,7 +48,7 @@ async function getOrCreateCert() {
48
48
 
49
49
  writeFileSync(keyPath, pems.private);
50
50
  writeFileSync(certPath, pems.cert);
51
- console.log(`📁 证书已保存到 ${certDir}`);
51
+ if (!JSON_OUTPUT) console.log(`📁 证书已保存到 ${certDir}`);
52
52
  return { key: pems.private, cert: pems.cert };
53
53
  }
54
54