codex-lens 0.1.26 → 0.1.28
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/dist/aggregator.js +0 -1
- package/dist/public/assets/{main-BO4694Xj.js → main-BRIK-RhC.js} +39 -37
- package/dist/public/index.html +1 -1
- package/package.json +1 -1
- package/src/aggregator.js +0 -1
- package/src/components/App.jsx +61 -1
- package/src/components/CodeViewer.jsx +2 -2
- package/src/components/ErrorBoundary.jsx +172 -0
- package/src/components/TerminalPanel.jsx +154 -163
- package/src/main.jsx +10 -2
- package/tsconfig.json +24 -0
package/dist/public/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Codex Lens</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/main-
|
|
7
|
+
<script type="module" crossorigin src="./assets/main-BRIK-RhC.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="./assets/main-DNXrKVO-.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
package/src/aggregator.js
CHANGED
package/src/components/App.jsx
CHANGED
|
@@ -237,6 +237,54 @@ export function App() {
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
async function saveAllFiles() {
|
|
241
|
+
const modifiedTabs = tabs.filter(t => t.modified);
|
|
242
|
+
if (modifiedTabs.length === 0) return;
|
|
243
|
+
|
|
244
|
+
setSaving(true);
|
|
245
|
+
const port = window.location.port === '5173' ? '5174' : window.location.port;
|
|
246
|
+
const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
|
|
247
|
+
|
|
248
|
+
let savedCount = 0;
|
|
249
|
+
let failedFiles = [];
|
|
250
|
+
|
|
251
|
+
for (const tab of modifiedTabs) {
|
|
252
|
+
try {
|
|
253
|
+
const response = await fetch(`${protocol}//${window.location.hostname}:${port}/api/save-file`, {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: { 'Content-Type': 'application/json' },
|
|
256
|
+
body: JSON.stringify({ path: tab.path, content: tab.content })
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (response.ok) {
|
|
260
|
+
savedCount++;
|
|
261
|
+
} else {
|
|
262
|
+
const error = await response.json();
|
|
263
|
+
failedFiles.push(tab.name);
|
|
264
|
+
console.error('Failed to save file:', tab.name, error.message);
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
failedFiles.push(tab.name);
|
|
268
|
+
console.error('Failed to save file:', tab.name, error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (savedCount > 0) {
|
|
273
|
+
setTabs(prevTabs => prevTabs.map(t => {
|
|
274
|
+
if (t.modified && !failedFiles.includes(t.name)) {
|
|
275
|
+
return { ...t, originalContent: t.content, modified: false };
|
|
276
|
+
}
|
|
277
|
+
return t;
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (failedFiles.length > 0) {
|
|
282
|
+
alert(`以下文件保存失败: ${failedFiles.join(', ')}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
setSaving(false);
|
|
286
|
+
}
|
|
287
|
+
|
|
240
288
|
function handleFileClick(path) {
|
|
241
289
|
const existingTab = tabs.find(t => t.path === path);
|
|
242
290
|
if (existingTab) {
|
|
@@ -382,6 +430,7 @@ export function App() {
|
|
|
382
430
|
x={contextMenu.x}
|
|
383
431
|
y={contextMenu.y}
|
|
384
432
|
tab={tabs.find(t => t.id === contextMenu.tabId)}
|
|
433
|
+
tabs={tabs}
|
|
385
434
|
saving={saving}
|
|
386
435
|
onClose={() => setContextMenu(null)}
|
|
387
436
|
onCloseTab={() => {
|
|
@@ -400,6 +449,10 @@ export function App() {
|
|
|
400
449
|
saveFile(contextMenu.tabId);
|
|
401
450
|
setContextMenu(null);
|
|
402
451
|
}}
|
|
452
|
+
onSaveAll={() => {
|
|
453
|
+
saveAllFiles();
|
|
454
|
+
setContextMenu(null);
|
|
455
|
+
}}
|
|
403
456
|
/>
|
|
404
457
|
)}
|
|
405
458
|
<div className="panel right-panel">
|
|
@@ -441,7 +494,9 @@ function TabBar({ tabs, activeTabId, onTabClick, onTabClose, onContextMenu }) {
|
|
|
441
494
|
);
|
|
442
495
|
}
|
|
443
496
|
|
|
444
|
-
function ContextMenu({ x, y, tab, saving, onClose, onCloseTab, onCloseOtherTabs, onCloseAllTabs, onSave }) {
|
|
497
|
+
function ContextMenu({ x, y, tab, tabs, saving, onClose, onCloseTab, onCloseOtherTabs, onCloseAllTabs, onSave, onSaveAll }) {
|
|
498
|
+
const hasModified = tabs?.some(t => t.modified);
|
|
499
|
+
|
|
445
500
|
return (
|
|
446
501
|
<div className="context-menu" style={{ left: x, top: y }} onClick={(e) => e.stopPropagation()}>
|
|
447
502
|
{tab?.modified && (
|
|
@@ -449,6 +504,11 @@ function ContextMenu({ x, y, tab, saving, onClose, onCloseTab, onCloseOtherTabs,
|
|
|
449
504
|
{saving ? '保存中...' : '保存'}
|
|
450
505
|
</div>
|
|
451
506
|
)}
|
|
507
|
+
{hasModified && (
|
|
508
|
+
<div className="context-menu-item" onClick={onSaveAll} style={{ color: '#4ade80' }}>
|
|
509
|
+
{saving ? '保存中...' : '全部保存'}
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
452
512
|
<div className="context-menu-item" onClick={onCloseTab}>关闭</div>
|
|
453
513
|
<div className="context-menu-item" onClick={onCloseOtherTabs}>关闭其他</div>
|
|
454
514
|
<div className="context-menu-item" onClick={onCloseAllTabs}>关闭所有</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useMemo, useRef } from 'react';
|
|
2
2
|
import CodeMirror from '@uiw/react-codemirror';
|
|
3
|
-
import { EditorView, Decoration, ViewPlugin
|
|
3
|
+
import { EditorView, Decoration, ViewPlugin } from '@codemirror/view';
|
|
4
4
|
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
5
5
|
import { tags as t } from '@lezer/highlight';
|
|
6
6
|
import { RangeSetBuilder } from '@codemirror/state';
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
|
|
3
|
+
class DefaultErrorFallback extends Component {
|
|
4
|
+
render() {
|
|
5
|
+
const { error, onReload, onReset } = this.props;
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div style={{
|
|
9
|
+
height: '100vh',
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexDirection: 'column',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
justifyContent: 'center',
|
|
14
|
+
background: '#1e1e1e',
|
|
15
|
+
color: '#e0e0e0',
|
|
16
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
17
|
+
padding: '20px',
|
|
18
|
+
}}>
|
|
19
|
+
<div style={{
|
|
20
|
+
maxWidth: '600px',
|
|
21
|
+
padding: '32px',
|
|
22
|
+
background: '#252526',
|
|
23
|
+
borderRadius: '8px',
|
|
24
|
+
border: '1px solid #3c3c3c',
|
|
25
|
+
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.4)',
|
|
26
|
+
textAlign: 'center',
|
|
27
|
+
}}>
|
|
28
|
+
<div style={{
|
|
29
|
+
fontSize: '48px',
|
|
30
|
+
marginBottom: '16px',
|
|
31
|
+
}}>
|
|
32
|
+
<span style={{ color: '#f85149' }}>!</span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<h1 style={{
|
|
36
|
+
fontSize: '24px',
|
|
37
|
+
fontWeight: 600,
|
|
38
|
+
marginBottom: '8px',
|
|
39
|
+
color: '#ffffff',
|
|
40
|
+
}}>
|
|
41
|
+
应用程序出错
|
|
42
|
+
</h1>
|
|
43
|
+
|
|
44
|
+
<p style={{
|
|
45
|
+
fontSize: '14px',
|
|
46
|
+
color: '#858585',
|
|
47
|
+
marginBottom: '24px',
|
|
48
|
+
}}>
|
|
49
|
+
抱歉,应用程序遇到了一个错误。请尝试刷新页面或重置应用状态。
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
{error && (
|
|
53
|
+
<details style={{
|
|
54
|
+
marginBottom: '24px',
|
|
55
|
+
padding: '12px',
|
|
56
|
+
background: '#1e1e1e',
|
|
57
|
+
borderRadius: '4px',
|
|
58
|
+
textAlign: 'left',
|
|
59
|
+
}}>
|
|
60
|
+
<summary style={{
|
|
61
|
+
cursor: 'pointer',
|
|
62
|
+
fontSize: '13px',
|
|
63
|
+
color: '#e0e0e0',
|
|
64
|
+
fontWeight: 500,
|
|
65
|
+
}}>
|
|
66
|
+
错误详情
|
|
67
|
+
</summary>
|
|
68
|
+
<pre style={{
|
|
69
|
+
marginTop: '12px',
|
|
70
|
+
padding: '12px',
|
|
71
|
+
background: '#0a0a0a',
|
|
72
|
+
borderRadius: '4px',
|
|
73
|
+
fontSize: '12px',
|
|
74
|
+
color: '#f48771',
|
|
75
|
+
overflow: 'auto',
|
|
76
|
+
maxHeight: '200px',
|
|
77
|
+
fontFamily: '"SF Mono", Consolas, monospace',
|
|
78
|
+
}}>
|
|
79
|
+
{error.toString()}
|
|
80
|
+
{error.stack && `\n\n${error.stack}`}
|
|
81
|
+
</pre>
|
|
82
|
+
</details>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<div style={{
|
|
86
|
+
display: 'flex',
|
|
87
|
+
gap: '12px',
|
|
88
|
+
justifyContent: 'center',
|
|
89
|
+
}}>
|
|
90
|
+
<button
|
|
91
|
+
onClick={onReset}
|
|
92
|
+
style={{
|
|
93
|
+
padding: '10px 20px',
|
|
94
|
+
background: '#3c3c3c',
|
|
95
|
+
border: '1px solid #4c4c4c',
|
|
96
|
+
borderRadius: '4px',
|
|
97
|
+
color: '#cccccc',
|
|
98
|
+
fontSize: '14px',
|
|
99
|
+
cursor: 'pointer',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
重置应用
|
|
103
|
+
</button>
|
|
104
|
+
<button
|
|
105
|
+
onClick={onReload}
|
|
106
|
+
style={{
|
|
107
|
+
padding: '10px 20px',
|
|
108
|
+
background: '#0078d4',
|
|
109
|
+
border: 'none',
|
|
110
|
+
borderRadius: '4px',
|
|
111
|
+
color: '#ffffff',
|
|
112
|
+
fontSize: '14px',
|
|
113
|
+
cursor: 'pointer',
|
|
114
|
+
fontWeight: 500,
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
刷新页面
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class ErrorBoundary extends Component {
|
|
127
|
+
constructor(props) {
|
|
128
|
+
super(props);
|
|
129
|
+
this.state = {
|
|
130
|
+
hasError: false,
|
|
131
|
+
error: null,
|
|
132
|
+
errorInfo: null,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static getDerivedStateFromError(error) {
|
|
137
|
+
return { hasError: true, error };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
componentDidCatch(error, errorInfo) {
|
|
141
|
+
this.setState({ errorInfo });
|
|
142
|
+
this.props.onError?.(error, errorInfo);
|
|
143
|
+
console.error('[ErrorBoundary]', error, errorInfo);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
handleReload = () => {
|
|
147
|
+
this.setState({ hasError: false, error: null, errorInfo: null });
|
|
148
|
+
window.location.reload();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
handleReset = () => {
|
|
152
|
+
this.setState({ hasError: false, error: null, errorInfo: null });
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
render() {
|
|
156
|
+
if (this.state.hasError) {
|
|
157
|
+
if (this.props.fallback) {
|
|
158
|
+
return this.props.fallback;
|
|
159
|
+
}
|
|
160
|
+
return (
|
|
161
|
+
<DefaultErrorFallback
|
|
162
|
+
error={this.state.error}
|
|
163
|
+
errorInfo={this.state.errorInfo}
|
|
164
|
+
onReload={this.handleReload}
|
|
165
|
+
onReset={this.handleReset}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return this.props.children;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
import { Terminal } from '@xterm/xterm';
|
|
3
3
|
import { FitAddon } from '@xterm/addon-fit';
|
|
4
4
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
@@ -15,42 +15,66 @@ const VIRTUAL_KEYS = [
|
|
|
15
15
|
{ label: 'Ctrl+C', seq: '\x03' },
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
componentWillUnmount() {
|
|
37
|
-
if (this._writeTimer) {
|
|
38
|
-
cancelAnimationFrame(this._writeTimer);
|
|
18
|
+
export function TerminalPanel() {
|
|
19
|
+
const containerRef = useRef(null);
|
|
20
|
+
const terminalRef = useRef(null);
|
|
21
|
+
const fitAddonRef = useRef(null);
|
|
22
|
+
const wsRef = useRef(null);
|
|
23
|
+
const resizeObserverRef = useRef(null);
|
|
24
|
+
const writeBufferRef = useRef('');
|
|
25
|
+
const writeTimerRef = useRef(null);
|
|
26
|
+
const resizeTimerRef = useRef(null);
|
|
27
|
+
|
|
28
|
+
const sendResize = useCallback(() => {
|
|
29
|
+
if (wsRef.current?.readyState === WebSocket.OPEN && terminalRef.current) {
|
|
30
|
+
wsRef.current.send(JSON.stringify({
|
|
31
|
+
type: 'resize',
|
|
32
|
+
cols: terminalRef.current.cols,
|
|
33
|
+
rows: terminalRef.current.rows,
|
|
34
|
+
}));
|
|
39
35
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const flushWrite = useCallback(() => {
|
|
39
|
+
if (writeTimerRef.current) {
|
|
40
|
+
cancelAnimationFrame(writeTimerRef.current);
|
|
41
|
+
writeTimerRef.current = null;
|
|
43
42
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
43
|
+
if (!writeBufferRef.current || !terminalRef.current) return;
|
|
44
|
+
|
|
45
|
+
const CHUNK_SIZE = 32768;
|
|
46
|
+
if (writeBufferRef.current.length <= CHUNK_SIZE) {
|
|
47
|
+
const buf = writeBufferRef.current;
|
|
48
|
+
writeBufferRef.current = '';
|
|
49
|
+
terminalRef.current.write(buf);
|
|
50
|
+
} else {
|
|
51
|
+
const chunk = writeBufferRef.current.slice(0, CHUNK_SIZE);
|
|
52
|
+
writeBufferRef.current = writeBufferRef.current.slice(CHUNK_SIZE);
|
|
53
|
+
terminalRef.current.write(chunk);
|
|
54
|
+
writeTimerRef.current = requestAnimationFrame(() => {
|
|
55
|
+
flushWrite();
|
|
56
|
+
});
|
|
46
57
|
}
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const throttledWrite = useCallback((data) => {
|
|
61
|
+
writeBufferRef.current += data;
|
|
62
|
+
if (!writeTimerRef.current) {
|
|
63
|
+
writeTimerRef.current = requestAnimationFrame(() => {
|
|
64
|
+
flushWrite();
|
|
65
|
+
});
|
|
49
66
|
}
|
|
50
|
-
}
|
|
67
|
+
}, [flushWrite]);
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
const handleVirtualKey = useCallback((seq) => {
|
|
70
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
71
|
+
wsRef.current.send(JSON.stringify({ type: 'input', data: seq }));
|
|
72
|
+
}
|
|
73
|
+
terminalRef.current?.focus();
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const terminal = new Terminal({
|
|
54
78
|
cursorBlink: true,
|
|
55
79
|
cursorStyle: 'bar',
|
|
56
80
|
fontSize: 13,
|
|
@@ -65,52 +89,54 @@ export class TerminalPanel extends React.Component {
|
|
|
65
89
|
allowProposedApi: true,
|
|
66
90
|
});
|
|
67
91
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
92
|
+
const fitAddon = new FitAddon();
|
|
93
|
+
terminal.loadAddon(fitAddon);
|
|
94
|
+
terminal.loadAddon(new WebLinksAddon());
|
|
71
95
|
|
|
72
|
-
|
|
96
|
+
terminal.open(containerRef.current);
|
|
97
|
+
terminalRef.current = terminal;
|
|
98
|
+
fitAddonRef.current = fitAddon;
|
|
73
99
|
|
|
74
100
|
requestAnimationFrame(() => {
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
101
|
+
if (fitAddon) {
|
|
102
|
+
fitAddon.fit();
|
|
103
|
+
terminal.focus();
|
|
78
104
|
}
|
|
79
105
|
});
|
|
80
106
|
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
107
|
+
terminal.onData((data) => {
|
|
108
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
109
|
+
wsRef.current.send(JSON.stringify({ type: 'input', data }));
|
|
84
110
|
}
|
|
85
111
|
});
|
|
86
|
-
}
|
|
87
112
|
|
|
88
|
-
|
|
113
|
+
// WebSocket connection
|
|
89
114
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
90
115
|
const host = window.location.hostname;
|
|
91
116
|
const port = window.location.port === '5173' ? '5174' : window.location.port;
|
|
92
117
|
const wsUrl = `${protocol}//${host}:${port}/ws/terminal`;
|
|
93
118
|
|
|
94
|
-
|
|
119
|
+
const ws = new WebSocket(wsUrl);
|
|
120
|
+
wsRef.current = ws;
|
|
95
121
|
|
|
96
|
-
|
|
122
|
+
ws.onopen = () => {
|
|
97
123
|
console.log('[Terminal] Connected to PTY service');
|
|
98
|
-
|
|
124
|
+
sendResize();
|
|
99
125
|
};
|
|
100
126
|
|
|
101
|
-
|
|
127
|
+
ws.onmessage = (event) => {
|
|
102
128
|
try {
|
|
103
129
|
const msg = JSON.parse(event.data);
|
|
104
130
|
if (msg.type === 'data') {
|
|
105
|
-
|
|
131
|
+
throttledWrite(msg.data);
|
|
106
132
|
} else if (msg.type === 'exit') {
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
133
|
+
flushWrite();
|
|
134
|
+
if (terminal) {
|
|
135
|
+
terminal.write(`\r\n[Process exited with code ${msg.exitCode ?? '?'}]\r\n`);
|
|
110
136
|
}
|
|
111
137
|
} else if (msg.type === 'state') {
|
|
112
|
-
if (!msg.running &&
|
|
113
|
-
|
|
138
|
+
if (!msg.running && terminal) {
|
|
139
|
+
flushWrite();
|
|
114
140
|
}
|
|
115
141
|
}
|
|
116
142
|
} catch (e) {
|
|
@@ -118,132 +144,97 @@ export class TerminalPanel extends React.Component {
|
|
|
118
144
|
}
|
|
119
145
|
};
|
|
120
146
|
|
|
121
|
-
|
|
147
|
+
ws.onclose = () => {
|
|
122
148
|
console.log('[Terminal] Disconnected, reconnecting in 3s...');
|
|
123
149
|
setTimeout(() => {
|
|
124
|
-
if (
|
|
125
|
-
|
|
150
|
+
if (containerRef.current) {
|
|
151
|
+
// Reconnect handled by re-mounting effect
|
|
126
152
|
}
|
|
127
153
|
}, 3000);
|
|
128
154
|
};
|
|
129
155
|
|
|
130
|
-
|
|
156
|
+
ws.onerror = (error) => {
|
|
131
157
|
console.error('[Terminal] WebSocket error:', error);
|
|
132
158
|
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
sendResize() {
|
|
136
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.terminal) {
|
|
137
|
-
this.ws.send(JSON.stringify({
|
|
138
|
-
type: 'resize',
|
|
139
|
-
cols: this.terminal.cols,
|
|
140
|
-
rows: this.terminal.rows,
|
|
141
|
-
}));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
159
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this._resizeTimer = setTimeout(() => {
|
|
151
|
-
if (this.fitAddon && this.containerRef.current) {
|
|
160
|
+
// ResizeObserver setup
|
|
161
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
162
|
+
if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
|
|
163
|
+
resizeTimerRef.current = setTimeout(() => {
|
|
164
|
+
if (fitAddonRef.current && containerRef.current) {
|
|
152
165
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
fitAddonRef.current.fit();
|
|
167
|
+
sendResize();
|
|
155
168
|
} catch {}
|
|
156
169
|
}
|
|
157
170
|
}, 150);
|
|
158
171
|
});
|
|
159
172
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
_throttledWrite(data) {
|
|
164
|
-
this._writeBuffer += data;
|
|
165
|
-
if (!this._writeTimer) {
|
|
166
|
-
this._writeTimer = requestAnimationFrame(() => {
|
|
167
|
-
this._flushWrite();
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
_flushWrite() {
|
|
173
|
-
if (this._writeTimer) {
|
|
174
|
-
cancelAnimationFrame(this._writeTimer);
|
|
175
|
-
this._writeTimer = null;
|
|
176
|
-
}
|
|
177
|
-
if (!this._writeBuffer || !this.terminal) return;
|
|
178
|
-
|
|
179
|
-
const CHUNK_SIZE = 32768;
|
|
180
|
-
if (this._writeBuffer.length <= CHUNK_SIZE) {
|
|
181
|
-
const buf = this._writeBuffer;
|
|
182
|
-
this._writeBuffer = '';
|
|
183
|
-
this.terminal.write(buf);
|
|
184
|
-
} else {
|
|
185
|
-
const chunk = this._writeBuffer.slice(0, CHUNK_SIZE);
|
|
186
|
-
this._writeBuffer = this._writeBuffer.slice(CHUNK_SIZE);
|
|
187
|
-
this.terminal.write(chunk);
|
|
188
|
-
this._writeTimer = requestAnimationFrame(() => {
|
|
189
|
-
this._flushWrite();
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
173
|
+
resizeObserver.observe(containerRef.current);
|
|
174
|
+
resizeObserverRef.current = resizeObserver;
|
|
193
175
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
176
|
+
// Cleanup on unmount
|
|
177
|
+
return () => {
|
|
178
|
+
if (writeTimerRef.current) {
|
|
179
|
+
cancelAnimationFrame(writeTimerRef.current);
|
|
180
|
+
}
|
|
181
|
+
if (ws) {
|
|
182
|
+
ws.close();
|
|
183
|
+
wsRef.current = null;
|
|
184
|
+
}
|
|
185
|
+
if (resizeObserver) {
|
|
186
|
+
resizeObserver.disconnect();
|
|
187
|
+
}
|
|
188
|
+
if (terminal) {
|
|
189
|
+
terminal.dispose();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}, [sendResize, throttledWrite, flushWrite]);
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div style={{
|
|
196
|
+
height: '100%',
|
|
197
|
+
display: 'flex',
|
|
198
|
+
flexDirection: 'column',
|
|
199
|
+
background: '#0a0a0a',
|
|
200
|
+
}}>
|
|
201
|
+
<div
|
|
202
|
+
ref={containerRef}
|
|
203
|
+
style={{
|
|
204
|
+
flex: 1,
|
|
205
|
+
overflow: 'hidden',
|
|
206
|
+
padding: '4px 8px',
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
203
209
|
<div style={{
|
|
204
|
-
height: '100%',
|
|
205
210
|
display: 'flex',
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
gap: '4px',
|
|
212
|
+
padding: '8px',
|
|
213
|
+
background: '#111',
|
|
214
|
+
borderTop: '1px solid #222',
|
|
215
|
+
flexWrap: 'wrap',
|
|
208
216
|
}}>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
style={{
|
|
230
|
-
padding: '8px 12px',
|
|
231
|
-
border: '1px solid #333',
|
|
232
|
-
borderRadius: '4px',
|
|
233
|
-
background: '#1a1a1a',
|
|
234
|
-
color: '#ccc',
|
|
235
|
-
fontSize: '13px',
|
|
236
|
-
fontFamily: 'Menlo, Monaco, monospace',
|
|
237
|
-
cursor: 'pointer',
|
|
238
|
-
minWidth: '44px',
|
|
239
|
-
minHeight: '44px',
|
|
240
|
-
}}
|
|
241
|
-
>
|
|
242
|
-
{key.label}
|
|
243
|
-
</button>
|
|
244
|
-
))}
|
|
245
|
-
</div>
|
|
217
|
+
{VIRTUAL_KEYS.map((key) => (
|
|
218
|
+
<button
|
|
219
|
+
key={key.label}
|
|
220
|
+
onClick={() => handleVirtualKey(key.seq)}
|
|
221
|
+
style={{
|
|
222
|
+
padding: '8px 12px',
|
|
223
|
+
border: '1px solid #333',
|
|
224
|
+
borderRadius: '4px',
|
|
225
|
+
background: '#1a1a1a',
|
|
226
|
+
color: '#ccc',
|
|
227
|
+
fontSize: '13px',
|
|
228
|
+
fontFamily: 'Menlo, Monaco, monospace',
|
|
229
|
+
cursor: 'pointer',
|
|
230
|
+
minWidth: '44px',
|
|
231
|
+
minHeight: '44px',
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
{key.label}
|
|
235
|
+
</button>
|
|
236
|
+
))}
|
|
246
237
|
</div>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|