Mayank2027 commited on
Commit
a2da7e4
·
verified ·
1 Parent(s): da6d1d4

Update frontend/src/components/Terminal.tsx

Browse files
Files changed (1) hide show
  1. frontend/src/components/Terminal.tsx +180 -57
frontend/src/components/Terminal.tsx CHANGED
@@ -1,22 +1,25 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { Terminal as XTerm } from 'xterm';
 
 
3
  import { FitAddon } from 'xterm-addon-fit';
4
  import 'xterm/css/xterm.css';
5
- import { FaPlus, FaTimes } from 'react-icons/fa';
6
 
7
  interface Tab {
8
  id: number;
9
  terminal: XTerm;
10
  socket: WebSocket;
 
11
  }
12
 
13
  const TerminalTabs: React.FC = () => {
14
  const [tabs, setTabs] = useState<Tab[]>([]);
15
- const [activeTab, setActiveTab] = useState(0);
16
  const termRefs = useRef<Map<number, HTMLDivElement>>(new Map());
17
  const nextId = useRef(1);
 
18
 
19
- const createTerminal = () => {
20
  const id = nextId.current++;
21
  const term = new XTerm({
22
  cursorBlink: true,
@@ -35,87 +38,207 @@ const TerminalTabs: React.FC = () => {
35
  },
36
  fontSize: 13,
37
  fontFamily: 'monospace',
 
 
38
  });
39
- const fit = new FitAddon();
40
- term.loadAddon(fit);
 
 
41
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
42
  const socket = new WebSocket(`${protocol}//${window.location.host}/ws/terminal`);
43
- term.onData(data => socket.send(`exec:${data}`));
44
- socket.onmessage = (event) => term.write(event.data);
45
- const newTab: Tab = { id, terminal: term, socket };
46
- setTabs(prev => [...prev, newTab]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  setActiveTab(id);
48
- return { term, socket, fit };
49
- };
50
 
 
51
  useEffect(() => {
52
- // Create initial terminal
53
- createTerminal();
 
 
54
  }, []);
55
 
 
56
  useEffect(() => {
57
- const tab = tabs.find(t => t.id === activeTab);
58
- if (tab) {
59
- const container = termRefs.current.get(tab.id);
60
- if (container && !tab.terminal.element) {
61
- tab.terminal.open(container);
62
- const fitAddon = new FitAddon();
63
- tab.terminal.loadAddon(fitAddon);
64
- fitAddon.fit();
65
- window.addEventListener('resize', () => fitAddon.fit());
66
- }
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
  }, [activeTab, tabs]);
69
 
70
  const removeTab = (id: number) => {
71
- const tab = tabs.find(t => t.id === id);
72
- if (tab) {
 
 
 
73
  tab.socket.close();
74
- tab.terminal.dispose();
75
- setTabs(prev => prev.filter(t => t.id !== id));
76
- if (activeTab === id) {
77
- const remaining = tabs.filter(t => t.id !== id);
78
- if (remaining.length > 0) setActiveTab(remaining[0].id);
 
 
 
 
79
  }
80
  }
81
  };
82
 
 
 
83
  return (
84
- <div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#0d1117' }}>
85
- <div style={{ display: 'flex', background: 'var(--bg-tertiary)', borderBottom: '1px solid var(--border)' }}>
86
- {tabs.map(tab => (
87
- <div key={tab.id}
88
- onClick={() => setActiveTab(tab.id)}
89
- style={{
90
- padding: '4px 12px',
91
- cursor: 'pointer',
92
- background: activeTab === tab.id ? 'var(--bg-primary)' : 'transparent',
93
- borderRight: '1px solid var(--border)',
94
- display: 'flex',
95
- alignItems: 'center',
96
- gap: 6,
97
- fontSize: 12,
98
- color: activeTab === tab.id ? 'var(--text-primary)' : 'var(--text-secondary)',
99
- }}>
100
- <span>Terminal {tab.id}</span>
101
- <FaTimes size={10} onClick={(e) => { e.stopPropagation(); removeTab(tab.id); }} style={{ opacity: 0.6 }} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  </div>
103
  ))}
104
- <button onClick={() => createTerminal()} style={{ padding: '4px 8px', color: 'var(--text-secondary)' }}>
105
- <FaPlus size={12} />
 
 
 
 
 
 
 
 
 
 
 
 
106
  </button>
107
  </div>
108
- <div style={{ flex: 1, position: 'relative' }}>
109
- {tabs.map(tab => (
 
 
110
  <div
111
  key={tab.id}
112
- ref={el => { if (el) termRefs.current.set(tab.id, el); }}
113
- style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: activeTab === tab.id ? 'block' : 'none' }}
 
 
 
 
 
 
 
 
 
 
114
  />
115
  ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  </div>
117
  </div>
118
  );
119
  };
120
 
121
- export default TerminalTabs;
 
 
 
 
 
 
1
+
2
+ # Fixed Terminal.tsx
3
+ terminal_tsx = '''import React, { useState, useEffect, useRef, useCallback } from 'react';
4
+ import { XTerm } from 'xterm';
5
  import { FitAddon } from 'xterm-addon-fit';
6
  import 'xterm/css/xterm.css';
 
7
 
8
  interface Tab {
9
  id: number;
10
  terminal: XTerm;
11
  socket: WebSocket;
12
+ fitAddon: FitAddon;
13
  }
14
 
15
  const TerminalTabs: React.FC = () => {
16
  const [tabs, setTabs] = useState<Tab[]>([]);
17
+ const [activeTab, setActiveTab] = useState<number>(0);
18
  const termRefs = useRef<Map<number, HTMLDivElement>>(new Map());
19
  const nextId = useRef(1);
20
+ const resizeHandlerRef = useRef<(() => void) | null>(null);
21
 
22
+ const createTerminal = useCallback(() => {
23
  const id = nextId.current++;
24
  const term = new XTerm({
25
  cursorBlink: true,
 
38
  },
39
  fontSize: 13,
40
  fontFamily: 'monospace',
41
+ rows: 24,
42
+ cols: 80,
43
  });
44
+
45
+ const fitAddon = new FitAddon();
46
+ term.loadAddon(fitAddon);
47
+
48
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
49
  const socket = new WebSocket(`${protocol}//${window.location.host}/ws/terminal`);
50
+
51
+ socket.onopen = () => {
52
+ term.writeln('\\r\\n\\x1b[32mConnected to terminal\\x1b[0m\\r\\n');
53
+ };
54
+
55
+ socket.onmessage = (event) => {
56
+ term.write(event.data);
57
+ };
58
+
59
+ socket.onerror = (e) => {
60
+ term.writeln('\\r\\n\\x1b[31mConnection error\\x1b[0m\\r\\n');
61
+ };
62
+
63
+ socket.onclose = () => {
64
+ term.writeln('\\r\\n\\x1b[33mDisconnected\\x1b[0m\\r\\n');
65
+ };
66
+
67
+ term.onData((data) => {
68
+ if (socket.readyState === WebSocket.OPEN) {
69
+ socket.send(`exec:${data}`);
70
+ }
71
+ });
72
+
73
+ const newTab: Tab = { id, terminal: term, socket, fitAddon };
74
+ setTabs((prev) => [...prev, newTab]);
75
  setActiveTab(id);
76
+ return newTab;
77
+ }, []);
78
 
79
+ // Create initial terminal
80
  useEffect(() => {
81
+ if (tabs.length === 0) {
82
+ createTerminal();
83
+ }
84
+ // eslint-disable-next-line react-hooks/exhaustive-deps
85
  }, []);
86
 
87
+ // Mount terminal to DOM when active tab changes
88
  useEffect(() => {
89
+ const tab = tabs.find((t) => t.id === activeTab);
90
+ if (!tab) return;
91
+
92
+ const container = termRefs.current.get(tab.id);
93
+ if (!container) return;
94
+
95
+ // Only open if not already open
96
+ if (!tab.terminal.element) {
97
+ tab.terminal.open(container);
98
+ tab.fitAddon.fit();
99
+
100
+ // Handle resize
101
+ const handleResize = () => {
102
+ tab.fitAddon.fit();
103
+ };
104
+ resizeHandlerRef.current = handleResize;
105
+ window.addEventListener('resize', handleResize);
106
+
107
+ return () => {
108
+ window.removeEventListener('resize', handleResize);
109
+ };
110
  }
111
  }, [activeTab, tabs]);
112
 
113
  const removeTab = (id: number) => {
114
+ const tab = tabs.find((t) => t.id === id);
115
+ if (!tab) return;
116
+
117
+ // Clean up
118
+ if (tab.socket.readyState === WebSocket.OPEN || tab.socket.readyState === WebSocket.CONNECTING) {
119
  tab.socket.close();
120
+ }
121
+ tab.terminal.dispose();
122
+
123
+ setTabs((prev) => prev.filter((t) => t.id !== id));
124
+
125
+ if (activeTab === id) {
126
+ const remaining = tabs.filter((t) => t.id !== id);
127
+ if (remaining.length > 0) {
128
+ setActiveTab(remaining[0].id);
129
  }
130
  }
131
  };
132
 
133
+ const activeTabIndex = tabs.findIndex((t) => t.id === activeTab);
134
+
135
  return (
136
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
137
+ {/* Tab Bar */}
138
+ <div
139
+ style={{
140
+ display: 'flex',
141
+ alignItems: 'center',
142
+ background: '#161b22',
143
+ borderBottom: '1px solid #30363d',
144
+ overflowX: 'auto',
145
+ }}
146
+ >
147
+ {tabs.map((tab, idx) => (
148
+ <div
149
+ key={tab.id}
150
+ onClick={() => setActiveTab(tab.id)}
151
+ style={{
152
+ padding: '6px 14px',
153
+ cursor: 'pointer',
154
+ background: activeTab === tab.id ? '#0d1117' : 'transparent',
155
+ borderRight: '1px solid #30363d',
156
+ display: 'flex',
157
+ alignItems: 'center',
158
+ gap: 8,
159
+ fontSize: 12,
160
+ color: activeTab === tab.id ? '#c9d1d9' : '#8b949e',
161
+ whiteSpace: 'nowrap',
162
+ userSelect: 'none',
163
+ }}
164
+ >
165
+ <span>Terminal {idx + 1}</span>
166
+ <span
167
+ onClick={(e) => {
168
+ e.stopPropagation();
169
+ removeTab(tab.id);
170
+ }}
171
+ style={{
172
+ opacity: 0.5,
173
+ cursor: 'pointer',
174
+ fontSize: 14,
175
+ lineHeight: 1,
176
+ }}
177
+ onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
178
+ onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.5')}
179
+ >
180
+ ×
181
+ </span>
182
  </div>
183
  ))}
184
+ <button
185
+ onClick={() => createTerminal()}
186
+ style={{
187
+ padding: '6px 12px',
188
+ background: 'transparent',
189
+ border: 'none',
190
+ color: '#8b949e',
191
+ cursor: 'pointer',
192
+ fontSize: 16,
193
+ lineHeight: 1,
194
+ }}
195
+ title="New Terminal"
196
+ >
197
+ +
198
  </button>
199
  </div>
200
+
201
+ {/* Terminal Containers */}
202
+ <div style={{ flex: 1, position: 'relative', background: '#0d1117' }}>
203
+ {tabs.map((tab) => (
204
  <div
205
  key={tab.id}
206
+ ref={(el) => {
207
+ if (el) termRefs.current.set(tab.id, el);
208
+ }}
209
+ style={{
210
+ position: 'absolute',
211
+ top: 0,
212
+ left: 0,
213
+ right: 0,
214
+ bottom: 0,
215
+ display: activeTab === tab.id ? 'block' : 'none',
216
+ padding: 4,
217
+ }}
218
  />
219
  ))}
220
+ {tabs.length === 0 && (
221
+ <div
222
+ style={{
223
+ display: 'flex',
224
+ alignItems: 'center',
225
+ justifyContent: 'center',
226
+ height: '100%',
227
+ color: '#8b949e',
228
+ fontSize: 14,
229
+ }}
230
+ >
231
+ No terminals open. Click + to create one.
232
+ </div>
233
+ )}
234
  </div>
235
  </div>
236
  );
237
  };
238
 
239
+ export default TerminalTabs;
240
+ '''
241
+
242
+ with open('/mnt/agents/output/Terminal.tsx', 'w') as f:
243
+ f.write(terminal_tsx)
244
+ print("Terminal.tsx written successfully")