Skip to content

Commit 55ef89e

Browse files
feat(terminal): integrate xterm.js for enhanced terminal functionality and add related addons
1 parent 001c4dd commit 55ef89e

File tree

4 files changed

+285
-125
lines changed

4 files changed

+285
-125
lines changed

bun.lock

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@
112112
"@tiptap/suggestion": "^3.0.1",
113113
"@typefox/monaco-editor-react": "^6.9.0",
114114
"@vitejs/plugin-react": "^4.6.0",
115+
"@xterm/addon-fit": "^0.10.0",
116+
"@xterm/addon-unicode11": "^0.8.0",
117+
"@xterm/addon-web-links": "^0.11.0",
118+
"@xterm/addon-webgl": "^0.18.0",
115119
"ai": "^4.3.17",
116120
"babel-plugin-react-compiler": "^19.0.0-beta-e993439-20250328",
117121
"chromadb": "^3.0.7",
@@ -145,6 +149,7 @@
145149
"tailwindcss-animate": "^1.0.7",
146150
"tippy.js": "^6.3.7",
147151
"vaul": "^1.1.2",
152+
"xterm": "^5.3.0",
148153
"zod": "3",
149154
"zustand": "^5.0.6"
150155
}

src/pages/workspace/components/terminal/terminal-panel.tsx

Lines changed: 69 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, { useEffect, useRef, useState } from 'react';
1+
import React, { useState, useCallback, memo } from 'react';
22
import { useTerminalStore } from '@/stores/terminal';
33
import { useProjectStore } from '@/stores/project';
44
import { Button } from '@/components/ui/button';
55
import { X, Plus, Split, Trash2 } from 'lucide-react';
66
import { cn } from '@/utils/tailwind';
7+
import { XTerminal } from './xterm-component';
78

89
// Simple terminal component using pre-styled div (will be replaced with xterm.js later)
910
interface TerminalViewProps {
@@ -12,81 +13,17 @@ interface TerminalViewProps {
1213
onWrite: (data: string) => void;
1314
}
1415

15-
function TerminalView({ terminalId, isActive, onWrite }: TerminalViewProps) {
16-
const [output, setOutput] = useState<string>('');
17-
const [input, setInput] = useState<string>('');
18-
const outputRef = useRef<HTMLDivElement>(null);
19-
const inputRef = useRef<HTMLInputElement>(null);
20-
21-
useEffect(() => {
22-
if (isActive && inputRef.current) {
23-
inputRef.current.focus();
24-
}
25-
}, [isActive]);
26-
27-
useEffect(() => {
28-
if (outputRef.current) {
29-
outputRef.current.scrollTop = outputRef.current.scrollHeight;
30-
}
31-
}, [output]);
32-
33-
useEffect(() => {
34-
const unsubscribeData = window.terminalApi.onData((data) => {
35-
if (data.terminalId === terminalId) {
36-
setOutput(prev => prev + data.data);
37-
}
38-
});
39-
40-
const unsubscribeExit = window.terminalApi.onExit((data) => {
41-
if (data.terminalId === terminalId) {
42-
setOutput(prev => prev + `\n[Process exited with code ${data.exitCode}]\n`);
43-
}
44-
});
45-
46-
return () => {
47-
unsubscribeData();
48-
unsubscribeExit();
49-
};
50-
}, [terminalId]);
51-
52-
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
53-
if (e.key === 'Enter') {
54-
const command = input + '\n';
55-
onWrite(command);
56-
setInput('');
57-
} else if (e.key === 'Tab') {
58-
e.preventDefault();
59-
// Handle tab completion later
60-
}
61-
};
62-
16+
const TerminalView = memo(function TerminalView({ terminalId, isActive, onWrite }: TerminalViewProps) {
6317
return (
64-
<div className={cn(
65-
"flex flex-col h-full bg-black text-white font-mono text-sm",
66-
!isActive && "hidden"
67-
)}>
68-
<div
69-
ref={outputRef}
70-
className="flex-1 p-2 overflow-y-auto whitespace-pre-wrap"
71-
style={{ fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace' }}
72-
>
73-
{output}
74-
</div>
75-
<div className="flex items-center p-2 border-t border-gray-700">
76-
<span className="text-blue-400 mr-2">$</span>
77-
<input
78-
ref={inputRef}
79-
type="text"
80-
value={input}
81-
onChange={(e) => setInput(e.target.value)}
82-
onKeyDown={handleKeyDown}
83-
className="flex-1 bg-transparent border-none outline-none text-green-400"
84-
placeholder="Type your command..."
85-
/>
86-
</div>
18+
<div className="h-full w-full">
19+
<XTerminal
20+
terminalId={terminalId}
21+
isActive={isActive}
22+
onWrite={onWrite}
23+
/>
8724
</div>
8825
);
89-
}
26+
});
9027

9128
export function TerminalPanel() {
9229
const { currentProject } = useProjectStore();
@@ -167,13 +104,13 @@ export function TerminalPanel() {
167104
}
168105
};
169106

170-
const handleWrite = async (terminalId: string, data: string) => {
107+
const handleWrite = useCallback(async (terminalId: string, data: string) => {
171108
try {
172109
await window.terminalApi.write(terminalId, data);
173110
} catch (error) {
174111
console.error('Failed to write to terminal:', error);
175112
}
176-
};
113+
}, []);
177114

178115
if (!isVisible) {
179116
return null;
@@ -182,18 +119,18 @@ export function TerminalPanel() {
182119
const activeSplits = getActiveSplits();
183120

184121
return (
185-
<div className="flex flex-col bg-gray-900 border-t border-gray-700" style={{ height }}>
122+
<div className="flex flex-col bg-background border-t border-gray-800" style={{ height }}>
186123
{/* Tab Bar */}
187-
<div className="flex items-center justify-between bg-gray-800 border-b border-gray-700 px-2 py-1">
124+
<div className="flex items-center justify-between bg-muted/30 border-b px-2 py-1">
188125
<div className="flex items-center space-x-1">
189126
{tabs.map((tab) => (
190127
<div
191128
key={tab.id}
192129
className={cn(
193130
"group flex items-center px-3 py-1 rounded-t-md cursor-pointer text-sm",
194131
tab.isActive
195-
? "bg-gray-900 text-white border-b-2 border-blue-500"
196-
: "bg-gray-700 text-gray-300 hover:bg-gray-600"
132+
? "bg-background text-white border-b-2 border-emerald-500"
133+
: "bg-muted/70 text-gray-300 hover:bg-[#3e3e42]"
197134
)}
198135
onClick={() => setActiveTab(tab.id)}
199136
>
@@ -275,52 +212,60 @@ export function TerminalPanel() {
275212
</div>
276213

277214
{/* Terminal Content */}
278-
<div className="flex-1 relative">
279-
{tabs.map((tab) => (
280-
<div
281-
key={tab.id}
282-
className={cn(
283-
"absolute inset-0",
284-
!tab.isActive && "hidden"
285-
)}
286-
>
287-
{activeSplits.length === 0 ? (
288-
<TerminalView
289-
terminalId={tab.id}
290-
isActive={tab.isActive}
291-
onWrite={(data) => handleWrite(tab.id, data)}
292-
/>
293-
) : (
294-
<div className="flex h-full">
295-
<div className="flex-1">
296-
<TerminalView
297-
terminalId={tab.id}
298-
isActive={tab.isActive}
299-
onWrite={(data) => handleWrite(tab.id, data)}
300-
/>
301-
</div>
302-
<div className="w-px bg-gray-600" />
303-
<div className="flex-1">
304-
{activeSplits.map((split) => (
305-
<div
306-
key={split.id}
307-
className={cn(
308-
"h-full",
309-
split.id !== activeSplitId && "hidden"
310-
)}
311-
>
312-
<TerminalView
313-
terminalId={split.terminalId}
314-
isActive={split.id === activeSplitId}
315-
onWrite={(data) => handleWrite(split.terminalId, data)}
316-
/>
317-
</div>
318-
))}
215+
<div className="flex-1 relative overflow-hidden">
216+
{tabs.map((tab) => {
217+
const handleTabWrite = useCallback((data: string) => handleWrite(tab.id, data), [tab.id, handleWrite]);
218+
219+
return (
220+
<div
221+
key={tab.id}
222+
className={cn(
223+
"absolute inset-0",
224+
!tab.isActive && "hidden"
225+
)}
226+
>
227+
{activeSplits.length === 0 ? (
228+
<TerminalView
229+
terminalId={tab.id}
230+
isActive={tab.isActive}
231+
onWrite={handleTabWrite}
232+
/>
233+
) : (
234+
<div className="flex h-full">
235+
<div className="flex-1">
236+
<TerminalView
237+
terminalId={tab.id}
238+
isActive={tab.isActive}
239+
onWrite={handleTabWrite}
240+
/>
241+
</div>
242+
<div className="w-px bg-[#3e3e42]" />
243+
<div className="flex-1">
244+
{activeSplits.map((split) => {
245+
const handleSplitWrite = useCallback((data: string) => handleWrite(split.terminalId, data), [split.terminalId, handleWrite]);
246+
247+
return (
248+
<div
249+
key={split.id}
250+
className={cn(
251+
"h-full",
252+
split.id !== activeSplitId && "hidden"
253+
)}
254+
>
255+
<TerminalView
256+
terminalId={split.terminalId}
257+
isActive={split.id === activeSplitId}
258+
onWrite={handleSplitWrite}
259+
/>
260+
</div>
261+
);
262+
})}
263+
</div>
319264
</div>
320-
</div>
321-
)}
322-
</div>
323-
))}
265+
)}
266+
</div>
267+
);
268+
})}
324269

325270
{tabs.length === 0 && (
326271
<div className="flex items-center justify-center h-full text-gray-500">

0 commit comments

Comments
 (0)