BugFix: Terminal render too many times causing performance freeze
Browse files
app/components/workbench/EditorPanel.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import { useStore } from '@nanostores/react';
|
2 |
-
import { memo,
|
3 |
-
import { Panel, PanelGroup, PanelResizeHandle
|
4 |
import {
|
5 |
CodeMirrorEditor,
|
6 |
type EditorDocument,
|
@@ -9,21 +9,17 @@ import {
|
|
9 |
type OnSaveCallback as OnEditorSave,
|
10 |
type OnScrollCallback as OnEditorScroll,
|
11 |
} from '~/components/editor/codemirror/CodeMirrorEditor';
|
12 |
-
import { IconButton } from '~/components/ui/IconButton';
|
13 |
import { PanelHeader } from '~/components/ui/PanelHeader';
|
14 |
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
|
15 |
-
import { shortcutEventEmitter } from '~/lib/hooks';
|
16 |
import type { FileMap } from '~/lib/stores/files';
|
17 |
import { themeStore } from '~/lib/stores/theme';
|
18 |
-
import { workbenchStore } from '~/lib/stores/workbench';
|
19 |
-
import { classNames } from '~/utils/classNames';
|
20 |
import { WORK_DIR } from '~/utils/constants';
|
21 |
-
import {
|
22 |
import { isMobile } from '~/utils/mobile';
|
23 |
import { FileBreadcrumb } from './FileBreadcrumb';
|
24 |
import { FileTree } from './FileTree';
|
25 |
-
import {
|
26 |
-
import
|
27 |
|
28 |
interface EditorPanelProps {
|
29 |
files?: FileMap;
|
@@ -38,8 +34,6 @@ interface EditorPanelProps {
|
|
38 |
onFileReset?: () => void;
|
39 |
}
|
40 |
|
41 |
-
const MAX_TERMINALS = 3;
|
42 |
-
const DEFAULT_TERMINAL_SIZE = 25;
|
43 |
const DEFAULT_EDITOR_SIZE = 100 - DEFAULT_TERMINAL_SIZE;
|
44 |
|
45 |
const editorSettings: EditorSettings = { tabSize: 2 };
|
@@ -62,13 +56,6 @@ export const EditorPanel = memo(
|
|
62 |
const theme = useStore(themeStore);
|
63 |
const showTerminal = useStore(workbenchStore.showTerminal);
|
64 |
|
65 |
-
const terminalRefs = useRef<Array<TerminalRef | null>>([]);
|
66 |
-
const terminalPanelRef = useRef<ImperativePanelHandle>(null);
|
67 |
-
const terminalToggledByShortcut = useRef(false);
|
68 |
-
|
69 |
-
const [activeTerminal, setActiveTerminal] = useState(0);
|
70 |
-
const [terminalCount, setTerminalCount] = useState(1);
|
71 |
-
|
72 |
const activeFileSegments = useMemo(() => {
|
73 |
if (!editorDocument) {
|
74 |
return undefined;
|
@@ -81,48 +68,6 @@ export const EditorPanel = memo(
|
|
81 |
return editorDocument !== undefined && unsavedFiles?.has(editorDocument.filePath);
|
82 |
}, [editorDocument, unsavedFiles]);
|
83 |
|
84 |
-
useEffect(() => {
|
85 |
-
const unsubscribeFromEventEmitter = shortcutEventEmitter.on('toggleTerminal', () => {
|
86 |
-
terminalToggledByShortcut.current = true;
|
87 |
-
});
|
88 |
-
|
89 |
-
const unsubscribeFromThemeStore = themeStore.subscribe(() => {
|
90 |
-
for (const ref of Object.values(terminalRefs.current)) {
|
91 |
-
ref?.reloadStyles();
|
92 |
-
}
|
93 |
-
});
|
94 |
-
|
95 |
-
return () => {
|
96 |
-
unsubscribeFromEventEmitter();
|
97 |
-
unsubscribeFromThemeStore();
|
98 |
-
};
|
99 |
-
}, []);
|
100 |
-
|
101 |
-
useEffect(() => {
|
102 |
-
const { current: terminal } = terminalPanelRef;
|
103 |
-
|
104 |
-
if (!terminal) {
|
105 |
-
return;
|
106 |
-
}
|
107 |
-
|
108 |
-
const isCollapsed = terminal.isCollapsed();
|
109 |
-
|
110 |
-
if (!showTerminal && !isCollapsed) {
|
111 |
-
terminal.collapse();
|
112 |
-
} else if (showTerminal && isCollapsed) {
|
113 |
-
terminal.resize(DEFAULT_TERMINAL_SIZE);
|
114 |
-
}
|
115 |
-
|
116 |
-
terminalToggledByShortcut.current = false;
|
117 |
-
}, [showTerminal]);
|
118 |
-
|
119 |
-
const addTerminal = () => {
|
120 |
-
if (terminalCount < MAX_TERMINALS) {
|
121 |
-
setTerminalCount(terminalCount + 1);
|
122 |
-
setActiveTerminal(terminalCount);
|
123 |
-
}
|
124 |
-
};
|
125 |
-
|
126 |
return (
|
127 |
<PanelGroup direction="vertical">
|
128 |
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
|
@@ -181,118 +126,7 @@ export const EditorPanel = memo(
|
|
181 |
</PanelGroup>
|
182 |
</Panel>
|
183 |
<PanelResizeHandle />
|
184 |
-
<
|
185 |
-
ref={terminalPanelRef}
|
186 |
-
defaultSize={showTerminal ? DEFAULT_TERMINAL_SIZE : 0}
|
187 |
-
minSize={10}
|
188 |
-
collapsible
|
189 |
-
onExpand={() => {
|
190 |
-
if (!terminalToggledByShortcut.current) {
|
191 |
-
workbenchStore.toggleTerminal(true);
|
192 |
-
}
|
193 |
-
}}
|
194 |
-
onCollapse={() => {
|
195 |
-
if (!terminalToggledByShortcut.current) {
|
196 |
-
workbenchStore.toggleTerminal(false);
|
197 |
-
}
|
198 |
-
}}
|
199 |
-
>
|
200 |
-
<div className="h-full">
|
201 |
-
<div className="bg-bolt-elements-terminals-background h-full flex flex-col">
|
202 |
-
<div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
|
203 |
-
{Array.from({ length: terminalCount + 1 }, (_, index) => {
|
204 |
-
const isActive = activeTerminal === index;
|
205 |
-
|
206 |
-
return (
|
207 |
-
<React.Fragment key={index}>
|
208 |
-
{index == 0 ? (
|
209 |
-
<button
|
210 |
-
key={index}
|
211 |
-
className={classNames(
|
212 |
-
'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
|
213 |
-
{
|
214 |
-
'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary':
|
215 |
-
isActive,
|
216 |
-
'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
|
217 |
-
!isActive,
|
218 |
-
},
|
219 |
-
)}
|
220 |
-
onClick={() => setActiveTerminal(index)}
|
221 |
-
>
|
222 |
-
<div className="i-ph:terminal-window-duotone text-lg" />
|
223 |
-
Bolt Terminal
|
224 |
-
</button>
|
225 |
-
) : (
|
226 |
-
<React.Fragment>
|
227 |
-
<button
|
228 |
-
key={index}
|
229 |
-
className={classNames(
|
230 |
-
'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
|
231 |
-
{
|
232 |
-
'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
|
233 |
-
'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
|
234 |
-
!isActive,
|
235 |
-
},
|
236 |
-
)}
|
237 |
-
onClick={() => setActiveTerminal(index)}
|
238 |
-
>
|
239 |
-
<div className="i-ph:terminal-window-duotone text-lg" />
|
240 |
-
Terminal {terminalCount > 1 && index}
|
241 |
-
</button>
|
242 |
-
</React.Fragment>
|
243 |
-
)}
|
244 |
-
</React.Fragment>
|
245 |
-
);
|
246 |
-
})}
|
247 |
-
{terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
|
248 |
-
<IconButton
|
249 |
-
className="ml-auto"
|
250 |
-
icon="i-ph:caret-down"
|
251 |
-
title="Close"
|
252 |
-
size="md"
|
253 |
-
onClick={() => workbenchStore.toggleTerminal(false)}
|
254 |
-
/>
|
255 |
-
</div>
|
256 |
-
{Array.from({ length: terminalCount + 1 }, (_, index) => {
|
257 |
-
const isActive = activeTerminal === index;
|
258 |
-
|
259 |
-
if (index == 0) {
|
260 |
-
logger.info('Starting bolt terminal');
|
261 |
-
|
262 |
-
return (
|
263 |
-
<Terminal
|
264 |
-
key={index}
|
265 |
-
className={classNames('h-full overflow-hidden', {
|
266 |
-
hidden: !isActive,
|
267 |
-
})}
|
268 |
-
ref={(ref) => {
|
269 |
-
terminalRefs.current.push(ref);
|
270 |
-
}}
|
271 |
-
onTerminalReady={(terminal) => workbenchStore.attachBoltTerminal(terminal)}
|
272 |
-
onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
|
273 |
-
theme={theme}
|
274 |
-
/>
|
275 |
-
);
|
276 |
-
}
|
277 |
-
|
278 |
-
return (
|
279 |
-
<Terminal
|
280 |
-
key={index}
|
281 |
-
className={classNames('h-full overflow-hidden', {
|
282 |
-
hidden: !isActive,
|
283 |
-
})}
|
284 |
-
ref={(ref) => {
|
285 |
-
terminalRefs.current.push(ref);
|
286 |
-
}}
|
287 |
-
onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)}
|
288 |
-
onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
|
289 |
-
theme={theme}
|
290 |
-
/>
|
291 |
-
);
|
292 |
-
})}
|
293 |
-
</div>
|
294 |
-
</div>
|
295 |
-
</Panel>
|
296 |
</PanelGroup>
|
297 |
);
|
298 |
},
|
|
|
1 |
import { useStore } from '@nanostores/react';
|
2 |
+
import { memo, useMemo } from 'react';
|
3 |
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
4 |
import {
|
5 |
CodeMirrorEditor,
|
6 |
type EditorDocument,
|
|
|
9 |
type OnSaveCallback as OnEditorSave,
|
10 |
type OnScrollCallback as OnEditorScroll,
|
11 |
} from '~/components/editor/codemirror/CodeMirrorEditor';
|
|
|
12 |
import { PanelHeader } from '~/components/ui/PanelHeader';
|
13 |
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
|
|
|
14 |
import type { FileMap } from '~/lib/stores/files';
|
15 |
import { themeStore } from '~/lib/stores/theme';
|
|
|
|
|
16 |
import { WORK_DIR } from '~/utils/constants';
|
17 |
+
import { renderLogger } from '~/utils/logger';
|
18 |
import { isMobile } from '~/utils/mobile';
|
19 |
import { FileBreadcrumb } from './FileBreadcrumb';
|
20 |
import { FileTree } from './FileTree';
|
21 |
+
import { DEFAULT_TERMINAL_SIZE, TerminalTabs } from './terminal/TerminalTabs';
|
22 |
+
import { workbenchStore } from '~/lib/stores/workbench';
|
23 |
|
24 |
interface EditorPanelProps {
|
25 |
files?: FileMap;
|
|
|
34 |
onFileReset?: () => void;
|
35 |
}
|
36 |
|
|
|
|
|
37 |
const DEFAULT_EDITOR_SIZE = 100 - DEFAULT_TERMINAL_SIZE;
|
38 |
|
39 |
const editorSettings: EditorSettings = { tabSize: 2 };
|
|
|
56 |
const theme = useStore(themeStore);
|
57 |
const showTerminal = useStore(workbenchStore.showTerminal);
|
58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
const activeFileSegments = useMemo(() => {
|
60 |
if (!editorDocument) {
|
61 |
return undefined;
|
|
|
68 |
return editorDocument !== undefined && unsavedFiles?.has(editorDocument.filePath);
|
69 |
}, [editorDocument, unsavedFiles]);
|
70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
return (
|
72 |
<PanelGroup direction="vertical">
|
73 |
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
|
|
|
126 |
</PanelGroup>
|
127 |
</Panel>
|
128 |
<PanelResizeHandle />
|
129 |
+
<TerminalTabs />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
</PanelGroup>
|
131 |
);
|
132 |
},
|
app/components/workbench/terminal/Terminal.tsx
CHANGED
@@ -16,71 +16,74 @@ export interface TerminalProps {
|
|
16 |
className?: string;
|
17 |
theme: Theme;
|
18 |
readonly?: boolean;
|
|
|
19 |
onTerminalReady?: (terminal: XTerm) => void;
|
20 |
onTerminalResize?: (cols: number, rows: number) => void;
|
21 |
}
|
22 |
|
23 |
export const Terminal = memo(
|
24 |
-
forwardRef<TerminalRef, TerminalProps>(
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
|
|
|
|
86 |
);
|
|
|
16 |
className?: string;
|
17 |
theme: Theme;
|
18 |
readonly?: boolean;
|
19 |
+
id: string;
|
20 |
onTerminalReady?: (terminal: XTerm) => void;
|
21 |
onTerminalResize?: (cols: number, rows: number) => void;
|
22 |
}
|
23 |
|
24 |
export const Terminal = memo(
|
25 |
+
forwardRef<TerminalRef, TerminalProps>(
|
26 |
+
({ className, theme, readonly, id, onTerminalReady, onTerminalResize }, ref) => {
|
27 |
+
const terminalElementRef = useRef<HTMLDivElement>(null);
|
28 |
+
const terminalRef = useRef<XTerm>();
|
29 |
+
|
30 |
+
useEffect(() => {
|
31 |
+
const element = terminalElementRef.current!;
|
32 |
+
|
33 |
+
const fitAddon = new FitAddon();
|
34 |
+
const webLinksAddon = new WebLinksAddon();
|
35 |
+
|
36 |
+
const terminal = new XTerm({
|
37 |
+
cursorBlink: true,
|
38 |
+
convertEol: true,
|
39 |
+
disableStdin: readonly,
|
40 |
+
theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}),
|
41 |
+
fontSize: 12,
|
42 |
+
fontFamily: 'Menlo, courier-new, courier, monospace',
|
43 |
+
});
|
44 |
+
|
45 |
+
terminalRef.current = terminal;
|
46 |
+
|
47 |
+
terminal.loadAddon(fitAddon);
|
48 |
+
terminal.loadAddon(webLinksAddon);
|
49 |
+
terminal.open(element);
|
50 |
+
|
51 |
+
const resizeObserver = new ResizeObserver(() => {
|
52 |
+
fitAddon.fit();
|
53 |
+
onTerminalResize?.(terminal.cols, terminal.rows);
|
54 |
+
});
|
55 |
+
|
56 |
+
resizeObserver.observe(element);
|
57 |
+
|
58 |
+
logger.debug(`Attach [${id}]`);
|
59 |
+
|
60 |
+
onTerminalReady?.(terminal);
|
61 |
+
|
62 |
+
return () => {
|
63 |
+
resizeObserver.disconnect();
|
64 |
+
terminal.dispose();
|
65 |
+
};
|
66 |
+
}, []);
|
67 |
+
|
68 |
+
useEffect(() => {
|
69 |
+
const terminal = terminalRef.current!;
|
70 |
+
|
71 |
+
// we render a transparent cursor in case the terminal is readonly
|
72 |
+
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
|
73 |
+
|
74 |
+
terminal.options.disableStdin = readonly;
|
75 |
+
}, [theme, readonly]);
|
76 |
+
|
77 |
+
useImperativeHandle(ref, () => {
|
78 |
+
return {
|
79 |
+
reloadStyles: () => {
|
80 |
+
const terminal = terminalRef.current!;
|
81 |
+
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
|
82 |
+
},
|
83 |
+
};
|
84 |
+
}, []);
|
85 |
+
|
86 |
+
return <div className={className} ref={terminalElementRef} />;
|
87 |
+
},
|
88 |
+
),
|
89 |
);
|
app/components/workbench/terminal/TerminalTabs.tsx
ADDED
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useStore } from '@nanostores/react';
|
2 |
+
import React, { memo, useEffect, useRef, useState } from 'react';
|
3 |
+
import { Panel, type ImperativePanelHandle } from 'react-resizable-panels';
|
4 |
+
import { IconButton } from '~/components/ui/IconButton';
|
5 |
+
import { shortcutEventEmitter } from '~/lib/hooks';
|
6 |
+
import { themeStore } from '~/lib/stores/theme';
|
7 |
+
import { workbenchStore } from '~/lib/stores/workbench';
|
8 |
+
import { classNames } from '~/utils/classNames';
|
9 |
+
import { Terminal, type TerminalRef } from './Terminal';
|
10 |
+
import { createScopedLogger } from '~/utils/logger';
|
11 |
+
|
12 |
+
const logger = createScopedLogger('Terminal');
|
13 |
+
|
14 |
+
const MAX_TERMINALS = 3;
|
15 |
+
export const DEFAULT_TERMINAL_SIZE = 25;
|
16 |
+
|
17 |
+
export const TerminalTabs = memo(() => {
|
18 |
+
const showTerminal = useStore(workbenchStore.showTerminal);
|
19 |
+
const theme = useStore(themeStore);
|
20 |
+
|
21 |
+
const terminalRefs = useRef<Array<TerminalRef | null>>([]);
|
22 |
+
const terminalPanelRef = useRef<ImperativePanelHandle>(null);
|
23 |
+
const terminalToggledByShortcut = useRef(false);
|
24 |
+
|
25 |
+
const [activeTerminal, setActiveTerminal] = useState(0);
|
26 |
+
const [terminalCount, setTerminalCount] = useState(1);
|
27 |
+
|
28 |
+
const addTerminal = () => {
|
29 |
+
if (terminalCount < MAX_TERMINALS) {
|
30 |
+
setTerminalCount(terminalCount + 1);
|
31 |
+
setActiveTerminal(terminalCount);
|
32 |
+
}
|
33 |
+
};
|
34 |
+
|
35 |
+
useEffect(() => {
|
36 |
+
const { current: terminal } = terminalPanelRef;
|
37 |
+
|
38 |
+
if (!terminal) {
|
39 |
+
return;
|
40 |
+
}
|
41 |
+
|
42 |
+
const isCollapsed = terminal.isCollapsed();
|
43 |
+
|
44 |
+
if (!showTerminal && !isCollapsed) {
|
45 |
+
terminal.collapse();
|
46 |
+
} else if (showTerminal && isCollapsed) {
|
47 |
+
terminal.resize(DEFAULT_TERMINAL_SIZE);
|
48 |
+
}
|
49 |
+
|
50 |
+
terminalToggledByShortcut.current = false;
|
51 |
+
}, [showTerminal]);
|
52 |
+
|
53 |
+
useEffect(() => {
|
54 |
+
const unsubscribeFromEventEmitter = shortcutEventEmitter.on('toggleTerminal', () => {
|
55 |
+
terminalToggledByShortcut.current = true;
|
56 |
+
});
|
57 |
+
|
58 |
+
const unsubscribeFromThemeStore = themeStore.subscribe(() => {
|
59 |
+
for (const ref of Object.values(terminalRefs.current)) {
|
60 |
+
ref?.reloadStyles();
|
61 |
+
}
|
62 |
+
});
|
63 |
+
|
64 |
+
return () => {
|
65 |
+
unsubscribeFromEventEmitter();
|
66 |
+
unsubscribeFromThemeStore();
|
67 |
+
};
|
68 |
+
}, []);
|
69 |
+
|
70 |
+
return (
|
71 |
+
<Panel
|
72 |
+
ref={terminalPanelRef}
|
73 |
+
defaultSize={showTerminal ? DEFAULT_TERMINAL_SIZE : 0}
|
74 |
+
minSize={10}
|
75 |
+
collapsible
|
76 |
+
onExpand={() => {
|
77 |
+
if (!terminalToggledByShortcut.current) {
|
78 |
+
workbenchStore.toggleTerminal(true);
|
79 |
+
}
|
80 |
+
}}
|
81 |
+
onCollapse={() => {
|
82 |
+
if (!terminalToggledByShortcut.current) {
|
83 |
+
workbenchStore.toggleTerminal(false);
|
84 |
+
}
|
85 |
+
}}
|
86 |
+
>
|
87 |
+
<div className="h-full">
|
88 |
+
<div className="bg-bolt-elements-terminals-background h-full flex flex-col">
|
89 |
+
<div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
|
90 |
+
{Array.from({ length: terminalCount + 1 }, (_, index) => {
|
91 |
+
const isActive = activeTerminal === index;
|
92 |
+
|
93 |
+
return (
|
94 |
+
<React.Fragment key={index}>
|
95 |
+
{index == 0 ? (
|
96 |
+
<button
|
97 |
+
key={index}
|
98 |
+
className={classNames(
|
99 |
+
'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
|
100 |
+
{
|
101 |
+
'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary':
|
102 |
+
isActive,
|
103 |
+
'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
|
104 |
+
!isActive,
|
105 |
+
},
|
106 |
+
)}
|
107 |
+
onClick={() => setActiveTerminal(index)}
|
108 |
+
>
|
109 |
+
<div className="i-ph:terminal-window-duotone text-lg" />
|
110 |
+
Bolt Terminal
|
111 |
+
</button>
|
112 |
+
) : (
|
113 |
+
<React.Fragment>
|
114 |
+
<button
|
115 |
+
key={index}
|
116 |
+
className={classNames(
|
117 |
+
'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
|
118 |
+
{
|
119 |
+
'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
|
120 |
+
'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
|
121 |
+
!isActive,
|
122 |
+
},
|
123 |
+
)}
|
124 |
+
onClick={() => setActiveTerminal(index)}
|
125 |
+
>
|
126 |
+
<div className="i-ph:terminal-window-duotone text-lg" />
|
127 |
+
Terminal {terminalCount > 1 && index}
|
128 |
+
</button>
|
129 |
+
</React.Fragment>
|
130 |
+
)}
|
131 |
+
</React.Fragment>
|
132 |
+
);
|
133 |
+
})}
|
134 |
+
{terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
|
135 |
+
<IconButton
|
136 |
+
className="ml-auto"
|
137 |
+
icon="i-ph:caret-down"
|
138 |
+
title="Close"
|
139 |
+
size="md"
|
140 |
+
onClick={() => workbenchStore.toggleTerminal(false)}
|
141 |
+
/>
|
142 |
+
</div>
|
143 |
+
{Array.from({ length: terminalCount + 1 }, (_, index) => {
|
144 |
+
const isActive = activeTerminal === index;
|
145 |
+
|
146 |
+
logger.debug(`Starting bolt terminal [${index}]`);
|
147 |
+
|
148 |
+
if (index == 0) {
|
149 |
+
return (
|
150 |
+
<Terminal
|
151 |
+
key={index}
|
152 |
+
id={`terminal_${index}`}
|
153 |
+
className={classNames('h-full overflow-hidden', {
|
154 |
+
hidden: !isActive,
|
155 |
+
})}
|
156 |
+
ref={(ref) => {
|
157 |
+
terminalRefs.current.push(ref);
|
158 |
+
}}
|
159 |
+
onTerminalReady={(terminal) => workbenchStore.attachBoltTerminal(terminal)}
|
160 |
+
onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
|
161 |
+
theme={theme}
|
162 |
+
/>
|
163 |
+
);
|
164 |
+
} else {
|
165 |
+
return (
|
166 |
+
<Terminal
|
167 |
+
key={index}
|
168 |
+
id={`terminal_${index}`}
|
169 |
+
className={classNames('h-full overflow-hidden', {
|
170 |
+
hidden: !isActive,
|
171 |
+
})}
|
172 |
+
ref={(ref) => {
|
173 |
+
terminalRefs.current.push(ref);
|
174 |
+
}}
|
175 |
+
onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)}
|
176 |
+
onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
|
177 |
+
theme={theme}
|
178 |
+
/>
|
179 |
+
);
|
180 |
+
}
|
181 |
+
})}
|
182 |
+
</div>
|
183 |
+
</div>
|
184 |
+
</Panel>
|
185 |
+
);
|
186 |
+
});
|