feat: add terminal detachment functionality

implement terminal cleanup when closing tabs or unmounting component

remove unused actionRunner prop across components

delete unused file-watcher utility
This commit is contained in:
KevIsDev
2025-07-01 10:53:09 +01:00
parent a3fa024686
commit 7ce263e0f5
8 changed files with 78 additions and 245 deletions

View File

@@ -25,7 +25,6 @@ import ChatAlert from './ChatAlert';
import type { ModelInfo } from '~/lib/modules/llm/types';
import ProgressCompilation from './ProgressCompilation';
import type { ProgressAnnotation } from '~/types/context';
import type { ActionRunner } from '~/lib/runtime/action-runner';
import { SupabaseChatAlert } from '~/components/chat/SupabaseAlert';
import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
import { useStore } from '@nanostores/react';
@@ -71,7 +70,6 @@ interface BaseChatProps {
deployAlert?: DeployAlert;
clearDeployAlert?: () => void;
data?: JSONValue[] | undefined;
actionRunner?: ActionRunner;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
append?: (message: Message) => void;
@@ -116,7 +114,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
supabaseAlert,
clearSupabaseAlert,
data,
actionRunner,
chatMode,
setChatMode,
append,
@@ -483,12 +480,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</div>
<ClientOnly>
{() => (
<Workbench
actionRunner={actionRunner ?? ({} as ActionRunner)}
chatStarted={chatStarted}
isStreaming={isStreaming}
setSelectedElement={setSelectedElement}
/>
<Workbench chatStarted={chatStarted} isStreaming={isStreaming} setSelectedElement={setSelectedElement} />
)}
</ClientOnly>
</div>

View File

@@ -7,7 +7,6 @@ import { diffLines, type Change } from 'diff';
import { getHighlighter } from 'shiki';
import '~/styles/diff-view.css';
import { diffFiles, extractRelativePath } from '~/utils/diff';
import { ActionRunner } from '~/lib/runtime/action-runner';
import type { FileHistory } from '~/types/actions';
import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension';
import { themeStore } from '~/lib/stores/theme';
@@ -664,7 +663,6 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }
interface DiffViewProps {
fileHistory: Record<string, FileHistory>;
setFileHistory: React.Dispatch<React.SetStateAction<Record<string, FileHistory>>>;
actionRunner: ActionRunner;
}
export const DiffView = memo(({ fileHistory, setFileHistory }: DiffViewProps) => {

View File

@@ -5,7 +5,6 @@ import { memo, useCallback, useEffect, useState, useMemo } from 'react';
import { toast } from 'react-toastify';
import { Popover, Transition } from '@headlessui/react';
import { diffLines, type Change } from 'diff';
import { ActionRunner } from '~/lib/runtime/action-runner';
import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension';
import type { FileHistory } from '~/types/actions';
import { DiffView } from './DiffView';
@@ -32,7 +31,6 @@ import type { ElementInfo } from './Inspector';
interface WorkspaceProps {
chatStarted?: boolean;
isStreaming?: boolean;
actionRunner: ActionRunner;
metadata?: {
gitUrl?: string;
};
@@ -281,7 +279,7 @@ const FileModifiedDropdown = memo(
);
export const Workbench = memo(
({ chatStarted, isStreaming, actionRunner, metadata, updateChatMestaData, setSelectedElement }: WorkspaceProps) => {
({ chatStarted, isStreaming, metadata, updateChatMestaData, setSelectedElement }: WorkspaceProps) => {
renderLogger.trace('Workbench');
const [isSyncing, setIsSyncing] = useState(false);
@@ -486,7 +484,7 @@ export const Workbench = memo(
initial={{ x: '100%' }}
animate={{ x: selectedView === 'diff' ? '0%' : selectedView === 'code' ? '100%' : '-100%' }}
>
<DiffView fileHistory={fileHistory} setFileHistory={setFileHistory} actionRunner={actionRunner} />
<DiffView fileHistory={fileHistory} setFileHistory={setFileHistory} />
</View>
<View initial={{ x: '100%' }} animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}>
<Preview setSelectedElement={setSelectedElement} />

View File

@@ -10,6 +10,7 @@ const logger = createScopedLogger('Terminal');
export interface TerminalRef {
reloadStyles: () => void;
getTerminal: () => XTerm | undefined;
}
export interface TerminalProps {
@@ -80,6 +81,9 @@ export const Terminal = memo(
const terminal = terminalRef.current!;
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
},
getTerminal: () => {
return terminalRef.current;
},
};
}, []);

View File

@@ -23,7 +23,7 @@ export const TerminalTabs = memo(() => {
const terminalToggledByShortcut = useRef(false);
const [activeTerminal, setActiveTerminal] = useState(0);
const [terminalCount, setTerminalCount] = useState(1);
const [terminalCount, setTerminalCount] = useState(0);
const addTerminal = () => {
if (terminalCount < MAX_TERMINALS) {
@@ -32,6 +32,48 @@ export const TerminalTabs = memo(() => {
}
};
const closeTerminal = (index: number) => {
if (index === 0) {
return;
} // Can't close bolt terminal
const terminalRef = terminalRefs.current[index];
if (terminalRef?.getTerminal) {
const terminal = terminalRef.getTerminal();
if (terminal) {
workbenchStore.detachTerminal(terminal);
}
}
// Remove the terminal from refs
terminalRefs.current.splice(index, 1);
// Adjust terminal count and active terminal
setTerminalCount(terminalCount - 1);
if (activeTerminal === index) {
setActiveTerminal(Math.max(0, index - 1));
} else if (activeTerminal > index) {
setActiveTerminal(activeTerminal - 1);
}
};
useEffect(() => {
return () => {
terminalRefs.current.forEach((ref, index) => {
if (index > 0 && ref?.getTerminal) {
const terminal = ref.getTerminal();
if (terminal) {
workbenchStore.detachTerminal(terminal);
}
}
});
};
}, []);
useEffect(() => {
const { current: terminal } = terminalPanelRef;
@@ -125,6 +167,15 @@ export const TerminalTabs = memo(() => {
>
<div className="i-ph:terminal-window-duotone text-lg" />
Terminal {terminalCount > 1 && index}
<button
className="bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary hover:bg-transparent rounded"
onClick={(e) => {
e.stopPropagation();
closeTerminal(index);
}}
>
<div className="i-ph:x text-xs" />
</button>
</button>
</React.Fragment>
)}