chore(webui): remove dead monaco-terminal component and monaco deps

monaco-terminal.tsx had no importers; drop it together with the now-unused
monaco-editor and @monaco-editor/react dependencies (~3 MB). The component is
preserved on branch feature/frontend-monaco-terminal for later work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-06-12 21:44:17 +07:00
co-authored by Claude Opus 4.8
parent 5a7bdca4ac
commit 82b82c5f49
3 changed files with 0 additions and 611 deletions
-2
View File
@@ -20,7 +20,6 @@
"dependencies": {
"@apollo/client": "^4.1.9",
"@hookform/resolvers": "^5.2.2",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
@@ -71,7 +70,6 @@
"lru-cache": "^11.3.6",
"lucide-react": "^1.16.0",
"marked": "^18.0.3",
"monaco-editor": "^0.55.1",
"motion": "^12.39.0",
"react": "^19.2.6",
"react-day-picker": "^10.0.1",
-60
View File
@@ -14,9 +14,6 @@ importers:
'@hookform/resolvers':
specifier: ^5.2.2
version: 5.2.2(react-hook-form@7.76.0(react@19.2.6))
'@monaco-editor/react':
specifier: ^4.7.0
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@radix-ui/react-accordion':
specifier: ^1.2.12
version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -167,9 +164,6 @@ importers:
marked:
specifier: ^18.0.3
version: 18.0.3
monaco-editor:
specifier: ^0.55.1
version: 0.55.1
motion:
specifier: ^12.39.0
version: 12.39.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -1344,16 +1338,6 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@monaco-editor/loader@1.7.0':
resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==}
'@monaco-editor/react@4.7.0':
resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==}
peerDependencies:
monaco-editor: '>= 0.25.0 < 1'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@napi-rs/wasm-runtime@1.1.4':
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
peerDependencies:
@@ -2607,9 +2591,6 @@ packages:
'@types/react@19.2.14':
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@@ -3391,9 +3372,6 @@ packages:
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
engines: {node: '>= 4'}
dompurify@3.2.7:
resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
@@ -4476,11 +4454,6 @@ packages:
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
marked@14.0.0:
resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==}
engines: {node: '>= 18'}
hasBin: true
marked@18.0.3:
resolution: {integrity: sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==}
engines: {node: '>= 20'}
@@ -4679,9 +4652,6 @@ packages:
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
engines: {node: '>=10'}
monaco-editor@0.55.1:
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
motion-dom@12.39.0:
resolution: {integrity: sha512-Xn7aAcGDhco/JZTXOub64UmaYn73C6J1Po7Fk+8EvkJsNGTqfhon6UJY53vJKXW5v5Zl8HrYsVxv6oPXeGoGLQ==}
@@ -5409,9 +5379,6 @@ packages:
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
state-local@1.0.7:
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
std-env@4.1.0:
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
@@ -7172,17 +7139,6 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@monaco-editor/loader@1.7.0':
dependencies:
state-local: 1.0.7
'@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@monaco-editor/loader': 1.7.0
monaco-editor: 0.55.1
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.10.0
@@ -8415,9 +8371,6 @@ snapshots:
dependencies:
csstype: 3.2.3
'@types/trusted-types@2.0.7':
optional: true
'@types/unist@2.0.11': {}
'@types/unist@3.0.3': {}
@@ -9259,10 +9212,6 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.2.7:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@2.8.0:
dependencies:
dom-serializer: 1.4.1
@@ -10513,8 +10462,6 @@ snapshots:
markdown-table@3.0.4: {}
marked@14.0.0: {}
marked@18.0.3: {}
math-intrinsics@1.1.0: {}
@@ -10906,11 +10853,6 @@ snapshots:
dependencies:
brace-expansion: 2.1.0
monaco-editor@0.55.1:
dependencies:
dompurify: 3.2.7
marked: 14.0.0
motion-dom@12.39.0:
dependencies:
motion-utils: 12.39.0
@@ -11706,8 +11648,6 @@ snapshots:
stackback@0.0.2: {}
state-local@1.0.7: {}
std-env@4.1.0: {}
stop-iteration-iterator@1.1.0:
@@ -1,549 +0,0 @@
/**
* Monaco Terminal Component
*
* High-performance terminal log viewer based on Monaco Editor with ANSI color support.
* Uses the `anser` library for robust ANSI escape sequence parsing.
*
* Supported ANSI features (via anser):
* - SGR codes: Reset, Bold, Dim, Italic, Underline, Blink, Reverse, Hidden, Strikethrough
* - Standard and bright foreground/background colors (30-37, 40-47, 90-97, 100-107)
* - 256-color palette (38;5;N, 48;5;N)
* - True color / 24-bit RGB (38;2;R;G;B, 48;2;R;G;B)
* - Reverse video (proper fg/bg color swap handled by anser)
*
* Key optimizations:
* - Dynamic CSS class injection with deduplication for color support
* - IEditorDecorationsCollection for efficient decoration management
* - Module-level WeakMap cache for parsed logs (avoids re-parsing same logs array)
* - Memoized content extraction for better React performance
* - Incremental decoration updates (only decorates new lines)
* - Stable useCallback for mount handler
*
* @see https://microsoft.github.io/monaco-editor/docs.html for Monaco API reference
* @see https://github.com/IonicaBizau/anser for ANSI parser documentation
*/
import type * as monaco from 'monaco-editor';
import Editor, { type Monaco, type OnMount } from '@monaco-editor/react';
import Anser from 'anser';
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { useTheme } from '@/hooks/use-theme';
import { Log } from '@/lib/log';
import { cn } from '@/lib/utils';
interface MonacoTerminalProps {
className?: string;
logs: string[];
searchValue?: string;
}
interface MonacoTerminalRef {
findNext: () => void;
findPrevious: () => void;
}
interface ParsedLine {
lineNumber: number;
segments: ParsedSegment[];
text: string;
}
interface ParsedSegment {
className: string;
endColumn: number;
startColumn: number;
}
/**
* Set of already injected CSS class names to prevent duplicate style injection.
*/
const injectedColorClasses = new Set<string>();
/**
* Converts an RGB string like "187, 0, 0" to a hex string "bb0000".
*/
const rgbStringToHex = (rgb: string): string =>
rgb
.split(',')
.map((part) =>
Math.min(255, Math.max(0, parseInt(part.trim(), 10)))
.toString(16)
.padStart(2, '0'),
)
.join('');
/**
* Returns the shared style element for dynamic color injection, creating it if needed.
*/
const getDynamicStyleElement = (): HTMLStyleElement => {
const styleId = 'monaco-terminal-dynamic-colors';
let element = document.getElementById(styleId) as HTMLStyleElement | null;
if (!element) {
element = document.createElement('style');
element.id = styleId;
document.head.appendChild(element);
}
return element;
};
/**
* Ensures a CSS class for the given RGB color exists, injecting it if needed.
* Returns the class name.
*/
const ensureColorClass = (prefix: string, rgb: string, cssProperty: string): string => {
const hex = rgbStringToHex(rgb);
const className = `${prefix}-${hex}`;
if (!injectedColorClasses.has(className)) {
getDynamicStyleElement().textContent += `.${className} { ${cssProperty}: rgb(${rgb}) !important; }\n`;
injectedColorClasses.add(className);
}
return className;
};
/**
* Maps anser decoration names to CSS class names.
*/
const DECORATION_CLASS_MAP: Record<string, string> = {
blink: 'ansi-blink',
bold: 'ansi-bold',
dim: 'ansi-dim',
hidden: 'ansi-hidden',
italic: 'ansi-italic',
strikethrough: 'ansi-strikethrough',
underline: 'ansi-underline',
};
/**
* Static CSS for text decoration and font style classes.
*/
const DECORATION_STYLES = [
'.ansi-bold { font-weight: bold !important; }',
'.ansi-dim { opacity: 0.7 !important; }',
'.ansi-italic { font-style: italic !important; }',
'.ansi-underline { text-decoration: underline !important; }',
'.ansi-strikethrough { text-decoration: line-through !important; }',
'.ansi-underline.ansi-strikethrough { text-decoration: underline line-through !important; }',
'.ansi-hidden { visibility: hidden !important; }',
'.ansi-blink { animation: ansi-blink 1s step-end infinite !important; }',
'@keyframes ansi-blink { 50% { opacity: 0; } }',
].join('\n');
/**
* Regex matching ANSI sequences and control characters not handled by anser.
* Anser only processes CSI SGR (ESC[...m). This regex matches everything else:
* - OSC sequences: ESC ] ... (BEL | ESC \)
* - DCS sequences: ESC P ... (ESC \)
* - Character set designations: ESC ( X, ESC ) X
* - DEC private sequences: ESC # N
* - Single-character escape codes: ESC followed by 7,8,D,M,E,H,c,N,O,Z,=,>,<
* - Lone ESC not followed by [ (catch-all for any remaining non-CSI escape)
* - Non-printable control characters (except \t and \n)
*/
const UNSUPPORTED_SEQUENCES_REGEX = new RegExp(
[
'\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\)', // OSC: ESC ] ... (BEL | ST)
'\\x1bP[\\s\\S]*?\\x1b\\\\', // DCS: ESC P ... ST
'\\x1b[()][A-Z0-9]', // Character set: ESC ( X or ESC ) X
'\\x1b#[0-9]', // DEC screen alignment: ESC # N
'\\x1b[78DMEHcNOZ=>]', // Single-char escape sequences
'\\x1b(?!\\[)', // Lone ESC not starting a CSI sequence
'[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1a\\x1c-\\x1f\\x7f]', // Control chars (keep \t=0x09, \n=0x0a, \r=0x0d; skip ESC=0x1b)
].join('|'),
'g',
);
/**
* Sanitizes a single line before passing to anser:
* 1. Handles carriage returns (\r) — simulates terminal overwrite behavior
* by keeping only content after the last \r on the line
* 2. Strips non-SGR escape sequences and control characters
*/
const sanitizeTerminalLine = (line: string): string => {
// Handle carriage returns: keep only content after the last lone \r
// This simulates terminal overwrite (e.g. progress bars)
const lastCarriageReturn = line.lastIndexOf('\r');
const withoutCarriageReturns = lastCarriageReturn !== -1 ? line.substring(lastCarriageReturn + 1) : line;
return withoutCarriageReturns.replace(UNSUPPORTED_SEQUENCES_REGEX, '');
};
/**
* Parses a single line using anser and returns structured data for Monaco decorations.
* Anser handles all ANSI SGR codes including 256-color, true color, and reverse video.
* Non-SGR sequences and control characters are stripped before parsing.
*/
const parseAnsiLine = (line: string, lineNumber: number): ParsedLine => {
if (!line) {
return { lineNumber, segments: [], text: '' };
}
const sanitizedLine = sanitizeTerminalLine(line);
if (!sanitizedLine) {
return { lineNumber, segments: [], text: '' };
}
const entries = Anser.ansiToJson(sanitizedLine, { remove_empty: true });
const segments: ParsedSegment[] = [];
let column = 1;
for (const entry of entries) {
if (!entry.content) {
continue;
}
const startColumn = column;
const endColumn = column + entry.content.length;
column = endColumn;
const classNames: string[] = [];
if (entry.fg) {
classNames.push(ensureColorClass('ansi-fg', entry.fg, 'color'));
}
if (entry.bg) {
classNames.push(ensureColorClass('ansi-bg', entry.bg, 'background-color'));
}
for (const decoration of entry.decorations) {
const decorationClass = DECORATION_CLASS_MAP[decoration];
if (decorationClass) {
classNames.push(decorationClass);
}
}
if (classNames.length) {
segments.push({
className: classNames.join(' '),
endColumn,
startColumn,
});
}
}
const text = entries.map((entry) => entry.content).join('');
return { lineNumber, segments, text };
};
/**
* Module-level cache for parsed lines.
* Uses WeakMap to avoid memory leaks - when logs array is garbage collected, cache is cleaned.
*/
const parsedLogsCache = new WeakMap<readonly string[], ParsedLine[]>();
/**
* Parse logs array with caching.
* Returns cached result if the same logs array reference is passed.
*/
const parseLogsWithCache = (logs: string[]): ParsedLine[] => {
const cached = parsedLogsCache.get(logs);
if (cached) {
return cached;
}
const allLogsText = logs.join('\n');
const lines = allLogsText.split('\n');
const parsedLines = lines.map((line, index) => parseAnsiLine(line, index + 1));
parsedLogsCache.set(logs, parsedLines);
return parsedLines;
};
/**
* Terminal component based on Monaco Editor with ANSI color support.
* Provides a read-only code viewer with search functionality, theme support, and ANSI color rendering.
* Compatible with the existing Terminal component API.
*/
function MonacoTerminal({
className,
logs,
ref,
searchValue,
}: MonacoTerminalProps & { ref?: React.RefObject<MonacoTerminalRef | null> }) {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<Monaco | null>(null);
const { theme } = useTheme();
const [isEditorReady, setIsEditorReady] = useState(false);
const prevLogsLengthRef = useRef<number>(0);
const searchWidgetRef = useRef<null | { findNext: () => void; findPrevious: () => void }>(null);
const decorationsCollectionRef = useRef<monaco.editor.IEditorDecorationsCollection | null>(null);
const parsedContent = useMemo(() => parseLogsWithCache(logs), [logs]);
const content = useMemo(() => parsedContent.map((line) => line.text).join('\n'), [parsedContent]);
const handleEditorDidMount: OnMount = useCallback((editor, monacoInstance) => {
editorRef.current = editor;
monacoRef.current = monacoInstance;
setIsEditorReady(true);
editor.updateOptions({
automaticLayout: true,
folding: false,
glyphMargin: false,
lineDecorationsWidth: 0,
lineNumbers: 'on',
lineNumbersMinChars: 5,
minimap: { enabled: false },
overviewRulerLanes: 0, // Disable overview ruler for better performance
padding: { top: 4 },
readOnly: true,
renderLineHighlight: 'none',
renderWhitespace: 'none',
scrollbar: {
alwaysConsumeMouseWheel: false,
horizontal: 'hidden', // Hide horizontal scrollbar completely
useShadows: false,
vertical: 'visible',
verticalScrollbarSize: 10,
},
scrollBeyondLastLine: false,
selectOnLineNumbers: false,
wordWrap: 'on', // Enable word wrap (simple 'on' works better than 'bounded')
wrappingIndent: 'same',
wrappingStrategy: 'simple', // Use simple strategy for better performance
});
const ansiStyleId = 'monaco-terminal-ansi-styles';
if (!document.getElementById(ansiStyleId)) {
const ansiStyleElement = document.createElement('style');
ansiStyleElement.id = ansiStyleId;
ansiStyleElement.textContent = DECORATION_STYLES;
document.head.appendChild(ansiStyleElement);
}
const terminalStyleId = 'monaco-terminal-custom-styles';
if (!document.getElementById(terminalStyleId)) {
const terminalStyleElement = document.createElement('style');
terminalStyleElement.id = terminalStyleId;
terminalStyleElement.textContent = `
.monaco-editor .line-numbers {
padding-right: 12px !important;
}
.monaco-editor,
.monaco-editor .monaco-editor-background,
.monaco-editor .margin {
background-color: var(--background) !important;
}
`;
document.head.appendChild(terminalStyleElement);
}
searchWidgetRef.current = {
findNext: () => {
try {
editor.trigger('monaco-terminal', 'actions.find', {});
editor.trigger('monaco-terminal', 'editor.action.nextMatchFindAction', {});
} catch (error: unknown) {
Log.error('Monaco findNext failed:', error);
}
},
findPrevious: () => {
try {
editor.trigger('monaco-terminal', 'actions.find', {});
editor.trigger('monaco-terminal', 'editor.action.previousMatchFindAction', {});
} catch (error: unknown) {
Log.error('Monaco findPrevious failed:', error);
}
},
};
}, []);
useImperativeHandle(
ref,
() => ({
findNext: () => {
if (searchWidgetRef.current && editorRef.current) {
searchWidgetRef.current.findNext();
}
},
findPrevious: () => {
if (searchWidgetRef.current && editorRef.current) {
searchWidgetRef.current.findPrevious();
}
},
}),
[],
);
const decoratedLinesCountRef = useRef<number>(0);
useEffect(() => {
if (!isEditorReady || !editorRef.current || !monacoRef.current) {
return;
}
const editor = editorRef.current;
const monacoInstance = monacoRef.current;
try {
const decoratedCount = decoratedLinesCountRef.current;
if (parsedContent.length < decoratedCount) {
if (decorationsCollectionRef.current) {
decorationsCollectionRef.current.clear();
}
decoratedLinesCountRef.current = 0;
}
if (parsedContent.length <= decoratedLinesCountRef.current) {
return;
}
// Incremental: only decorate the tail of `parsedContent` past `decoratedLinesCountRef`.
// Re-applying the full set on every log append turns large terminals into a stutter
// because Monaco re-runs the decoration delta over every line.
const newDecorations: monaco.editor.IModelDeltaDecoration[] = [];
for (let index = decoratedLinesCountRef.current; index < parsedContent.length; index++) {
const line = parsedContent[index];
if (!line) {
continue;
}
for (const segment of line.segments) {
newDecorations.push({
options: {
inlineClassName: segment.className,
},
range: new monacoInstance.Range(
line.lineNumber,
segment.startColumn,
line.lineNumber,
segment.endColumn,
),
});
}
}
if (newDecorations.length) {
if (decorationsCollectionRef.current) {
decorationsCollectionRef.current.append(newDecorations);
} else {
decorationsCollectionRef.current = editor.createDecorationsCollection(newDecorations);
}
}
decoratedLinesCountRef.current = parsedContent.length;
} catch (error: unknown) {
Log.error('Monaco apply decorations failed:', error);
}
}, [parsedContent, isEditorReady]);
useEffect(() => {
if (!isEditorReady || !editorRef.current) {
return;
}
const editor = editorRef.current;
// Auto-scroll only on appends — not on the initial mount (`prev === 0`) and not when logs
// shrink (cleared), so we don't fight a user who scrolled up before more output arrives.
if (logs.length > prevLogsLengthRef.current && prevLogsLengthRef.current > 0) {
try {
const lineCount = editor.getModel()?.getLineCount() ?? 0;
if (lineCount > 0) {
editor.revealLine(lineCount, 1);
}
} catch (error: unknown) {
Log.error('Monaco scroll failed:', error);
}
}
prevLogsLengthRef.current = logs.length;
}, [logs, isEditorReady]);
useEffect(() => {
if (!isEditorReady || !editorRef.current || !monacoRef.current) {
return;
}
const editor = editorRef.current;
if (searchValue?.trim()) {
try {
const searchText = searchValue.trim();
const findOptions = {
isRegex: false,
matchCase: false,
preserveCase: false,
searchString: searchText,
wholeWord: false,
};
editor.trigger('monaco-terminal', 'actions.find', findOptions);
// rAF (instead of setTimeout) lets Monaco finish mounting the find widget before
// we issue the jump-to-first-match — fewer flaky races at log-bursty moments.
requestAnimationFrame(() => {
editor.trigger('monaco-terminal', 'editor.action.nextMatchFindAction', {});
});
} catch (error: unknown) {
Log.error('Monaco search failed:', error);
}
} else {
try {
editor.trigger('monaco-terminal', 'closeFindWidget', {});
} catch (error: unknown) {
Log.error('Monaco close find widget failed:', error);
}
}
}, [searchValue, isEditorReady]);
useEffect(() => {
return () => {
if (decorationsCollectionRef.current) {
decorationsCollectionRef.current.clear();
decorationsCollectionRef.current = null;
}
decoratedLinesCountRef.current = 0;
// Shared <style> tags are deliberately left attached — multiple instances share them
// and the WeakMap-backed parseLogsWithCache cleans itself up.
};
}, []);
return (
<div className={cn('relative size-full overflow-hidden', className)}>
<Editor
defaultLanguage="plaintext"
height="100%"
language="plaintext"
onMount={handleEditorDidMount}
options={{
domReadOnly: true,
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
fontSize: 12,
fontWeight: '600',
}}
theme={theme === 'dark' ? 'vs-dark' : 'vs-light'}
value={content}
width="100%"
/>
</div>
);
}
export default MonacoTerminal;