mirror of
https://github.com/coleam00/bolt.new-any-llm
synced 2024-12-27 22:33:03 +00:00
feat: add terminal and simple shortcut system (#16)
This commit is contained in:
parent
d35f64eb1d
commit
8486d85f64
@ -3,7 +3,7 @@ import { useChat } from 'ai/react';
|
||||
import { useAnimate } from 'framer-motion';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
||||
import { useMessageParser, usePromptEnhancer, useSnapScroll } from '~/lib/hooks';
|
||||
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
|
||||
import { useChatHistory } from '~/lib/persistence';
|
||||
import { chatStore } from '~/lib/stores/chat';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
@ -25,7 +25,7 @@ export function Chat() {
|
||||
return (
|
||||
<>
|
||||
{ready && <ChatImpl initialMessages={initialMessages} storeMessageHistory={storeMessageHistory} />}
|
||||
<ToastContainer position="bottom-right" stacked pauseOnFocusLoss transition={toastAnimation} />;
|
||||
<ToastContainer position="bottom-right" stacked pauseOnFocusLoss transition={toastAnimation} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -36,6 +36,8 @@ interface ChatProps {
|
||||
}
|
||||
|
||||
export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
|
||||
useShortcuts();
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
||||
|
15
packages/bolt/app/components/ui/PanelHeader.tsx
Normal file
15
packages/bolt/app/components/ui/PanelHeader.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { memo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
|
||||
interface PanelHeaderProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const PanelHeader = memo(({ className, children }: PanelHeaderProps) => {
|
||||
return (
|
||||
<div className={classNames('flex items-center gap-2 bg-gray-50 border-b px-4 py-1 min-h-[34px]', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels';
|
||||
import {
|
||||
CodeMirrorEditor,
|
||||
type EditorDocument,
|
||||
@ -9,12 +9,16 @@ import {
|
||||
type OnSaveCallback as OnEditorSave,
|
||||
type OnScrollCallback as OnEditorScroll,
|
||||
} from '~/components/editor/codemirror/CodeMirrorEditor';
|
||||
import { PanelHeader } from '~/components/ui/PanelHeader';
|
||||
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
|
||||
import { shortcutEventEmitter } from '~/lib/hooks';
|
||||
import type { FileMap } from '~/lib/stores/files';
|
||||
import { themeStore } from '~/lib/stores/theme';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { renderLogger } from '~/utils/logger';
|
||||
import { isMobile } from '~/utils/mobile';
|
||||
import { FileTreePanel } from './FileTreePanel';
|
||||
import { Terminal, type TerminalRef } from './terminal/Terminal';
|
||||
|
||||
interface EditorPanelProps {
|
||||
files?: FileMap;
|
||||
@ -29,6 +33,9 @@ interface EditorPanelProps {
|
||||
onFileReset?: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_TERMINAL_SIZE = 25;
|
||||
const DEFAULT_EDITOR_SIZE = 100 - DEFAULT_TERMINAL_SIZE;
|
||||
|
||||
const editorSettings: EditorSettings = { tabSize: 2 };
|
||||
|
||||
export const EditorPanel = memo(
|
||||
@ -47,6 +54,13 @@ export const EditorPanel = memo(
|
||||
renderLogger.trace('EditorPanel');
|
||||
|
||||
const theme = useStore(themeStore);
|
||||
const showTerminal = useStore(workbenchStore.showTerminal);
|
||||
|
||||
const terminalRefs = useRef<Array<TerminalRef | null>>([]);
|
||||
const terminalPanelRef = useRef<ImperativePanelHandle>(null);
|
||||
const terminalToggledByShortcut = useRef(false);
|
||||
|
||||
const [terminalCount] = useState(1);
|
||||
|
||||
const activeFile = useMemo(() => {
|
||||
if (!editorDocument) {
|
||||
@ -60,54 +74,133 @@ export const EditorPanel = memo(
|
||||
return editorDocument !== undefined && unsavedFiles?.has(editorDocument.filePath);
|
||||
}, [editorDocument, unsavedFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeFromEventEmitter = shortcutEventEmitter.on('toggleTerminal', () => {
|
||||
terminalToggledByShortcut.current = true;
|
||||
});
|
||||
|
||||
const unsubscribeFromThemeStore = themeStore.subscribe(() => {
|
||||
for (const ref of Object.values(terminalRefs.current)) {
|
||||
ref?.reloadStyles();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEventEmitter();
|
||||
unsubscribeFromThemeStore();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { current: terminal } = terminalPanelRef;
|
||||
|
||||
if (!terminal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCollapsed = terminal.isCollapsed();
|
||||
|
||||
if (!showTerminal && !isCollapsed) {
|
||||
terminal.collapse();
|
||||
} else if (showTerminal && isCollapsed) {
|
||||
terminal.resize(DEFAULT_TERMINAL_SIZE);
|
||||
}
|
||||
|
||||
terminalToggledByShortcut.current = false;
|
||||
}, [showTerminal]);
|
||||
|
||||
return (
|
||||
<PanelGroup direction="horizontal">
|
||||
<Panel defaultSize={25} minSize={10} collapsible>
|
||||
<div className="flex flex-col border-r h-full">
|
||||
<div className="flex items-center gap-2 bg-gray-50 border-b px-4 py-1 min-h-[34px]">
|
||||
<div className="i-ph:tree-structure-duotone shrink-0" />
|
||||
Files
|
||||
</div>
|
||||
<FileTreePanel
|
||||
files={files}
|
||||
unsavedFiles={unsavedFiles}
|
||||
selectedFile={selectedFile}
|
||||
onFileSelect={onFileSelect}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel className="flex flex-col" defaultSize={75} minSize={20}>
|
||||
<div className="flex items-center gap-2 bg-gray-50 border-b px-4 py-1 min-h-[34px] text-sm">
|
||||
{activeFile && (
|
||||
<div className="flex items-center flex-1">
|
||||
{activeFile} {isStreaming && <span className="text-xs ml-1 font-semibold">(read-only)</span>}
|
||||
{activeFileUnsaved && (
|
||||
<div className="flex gap-1 ml-auto -mr-1.5">
|
||||
<PanelHeaderButton onClick={onFileSave}>
|
||||
<div className="i-ph:floppy-disk-duotone" />
|
||||
Save
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton onClick={onFileReset}>
|
||||
<div className="i-ph:clock-counter-clockwise-duotone" />
|
||||
Reset
|
||||
</PanelHeaderButton>
|
||||
<PanelGroup direction="vertical">
|
||||
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
|
||||
<PanelGroup direction="horizontal">
|
||||
<Panel defaultSize={25} minSize={10} collapsible>
|
||||
<div className="flex flex-col border-r h-full">
|
||||
<PanelHeader>
|
||||
<div className="i-ph:tree-structure-duotone shrink-0" />
|
||||
Files
|
||||
</PanelHeader>
|
||||
<FileTreePanel
|
||||
files={files}
|
||||
unsavedFiles={unsavedFiles}
|
||||
selectedFile={selectedFile}
|
||||
onFileSelect={onFileSelect}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel className="flex flex-col" defaultSize={75} minSize={20}>
|
||||
<PanelHeader>
|
||||
{activeFile && (
|
||||
<div className="flex items-center flex-1 text-sm">
|
||||
{activeFile} {isStreaming && <span className="text-xs ml-1 font-semibold">(read-only)</span>}
|
||||
{activeFileUnsaved && (
|
||||
<div className="flex gap-1 ml-auto -mr-1.5">
|
||||
<PanelHeaderButton onClick={onFileSave}>
|
||||
<div className="i-ph:floppy-disk-duotone" />
|
||||
Save
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton onClick={onFileReset}>
|
||||
<div className="i-ph:clock-counter-clockwise-duotone" />
|
||||
Reset
|
||||
</PanelHeaderButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PanelHeader>
|
||||
<div className="h-full flex-1 overflow-hidden">
|
||||
<CodeMirrorEditor
|
||||
theme={theme}
|
||||
editable={!isStreaming && editorDocument !== undefined}
|
||||
settings={editorSettings}
|
||||
doc={editorDocument}
|
||||
autoFocusOnDocumentChange={!isMobile()}
|
||||
onScroll={onEditorScroll}
|
||||
onChange={onEditorChange}
|
||||
onSave={onFileSave}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-full flex-1 overflow-hidden">
|
||||
<CodeMirrorEditor
|
||||
theme={theme}
|
||||
editable={!isStreaming && editorDocument !== undefined}
|
||||
settings={editorSettings}
|
||||
doc={editorDocument}
|
||||
autoFocusOnDocumentChange={!isMobile()}
|
||||
onScroll={onEditorScroll}
|
||||
onChange={onEditorChange}
|
||||
onSave={onFileSave}
|
||||
/>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel
|
||||
ref={terminalPanelRef}
|
||||
defaultSize={showTerminal ? DEFAULT_TERMINAL_SIZE : 0}
|
||||
minSize={10}
|
||||
collapsible
|
||||
onExpand={() => {
|
||||
if (!terminalToggledByShortcut.current) {
|
||||
workbenchStore.toggleTerminal(true);
|
||||
}
|
||||
}}
|
||||
onCollapse={() => {
|
||||
if (!terminalToggledByShortcut.current) {
|
||||
workbenchStore.toggleTerminal(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="border-t h-full">
|
||||
<PanelHeader>
|
||||
<span className="i-ph:terminal-window-duotone shrink-0" /> Terminal
|
||||
</PanelHeader>
|
||||
<div className="p-3.5">
|
||||
{Array.from({ length: terminalCount }, (_, index) => {
|
||||
return (
|
||||
<div key={index} className="h-full">
|
||||
<Terminal
|
||||
ref={(ref) => {
|
||||
terminalRefs.current.push(ref);
|
||||
}}
|
||||
onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)}
|
||||
onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
|
||||
className="h-full"
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { AnimatePresence, motion, type HTMLMotionProps, type Variants } from 'framer-motion';
|
||||
import { motion, type HTMLMotionProps, type Variants } from 'framer-motion';
|
||||
import { computed } from 'nanostores';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
@ -98,52 +98,48 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
|
||||
return (
|
||||
chatStarted && (
|
||||
<AnimatePresence>
|
||||
{showWorkbench && (
|
||||
<motion.div initial="closed" animate="open" exit="closed" variants={workbenchVariants}>
|
||||
<div className="fixed top-[calc(var(--header-height)+1.5rem)] bottom-[calc(1.5rem-1px)] w-[50vw] mr-4 z-0">
|
||||
<div className="flex flex-col bg-white border border-gray-200 shadow-sm rounded-lg overflow-hidden absolute inset-0 right-8">
|
||||
<div className="flex items-center px-3 py-2 border-b border-gray-200">
|
||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||
<IconButton
|
||||
icon="i-ph:x-circle"
|
||||
className="ml-auto -mr-1"
|
||||
size="xxl"
|
||||
onClick={() => {
|
||||
workbenchStore.showWorkbench.set(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<View
|
||||
initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||
animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||
>
|
||||
<EditorPanel
|
||||
editorDocument={currentDocument}
|
||||
isStreaming={isStreaming}
|
||||
selectedFile={selectedFile}
|
||||
files={files}
|
||||
unsavedFiles={unsavedFiles}
|
||||
onFileSelect={onFileSelect}
|
||||
onEditorScroll={onEditorScroll}
|
||||
onEditorChange={onEditorChange}
|
||||
onFileSave={onFileSave}
|
||||
onFileReset={onFileReset}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
||||
animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
||||
>
|
||||
<Preview />
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
<motion.div initial="closed" animate={showWorkbench ? 'open' : 'closed'} variants={workbenchVariants}>
|
||||
<div className="fixed top-[calc(var(--header-height)+1.5rem)] bottom-[calc(1.5rem-1px)] w-[50vw] mr-4 z-0">
|
||||
<div className="flex flex-col bg-white border border-gray-200 shadow-sm rounded-lg overflow-hidden absolute inset-0 right-8">
|
||||
<div className="flex items-center px-3 py-2 border-b border-gray-200">
|
||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||
<IconButton
|
||||
icon="i-ph:x-circle"
|
||||
className="ml-auto -mr-1"
|
||||
size="xxl"
|
||||
onClick={() => {
|
||||
workbenchStore.showWorkbench.set(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<View
|
||||
initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||
animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||
>
|
||||
<EditorPanel
|
||||
editorDocument={currentDocument}
|
||||
isStreaming={isStreaming}
|
||||
selectedFile={selectedFile}
|
||||
files={files}
|
||||
unsavedFiles={unsavedFiles}
|
||||
onFileSelect={onFileSelect}
|
||||
onEditorScroll={onEditorScroll}
|
||||
onEditorChange={onEditorChange}
|
||||
onFileSave={onFileSave}
|
||||
onFileReset={onFileReset}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
||||
animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
||||
>
|
||||
<Preview />
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
83
packages/bolt/app/components/workbench/terminal/Terminal.tsx
Normal file
83
packages/bolt/app/components/workbench/terminal/Terminal.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import { Terminal as XTerm } from '@xterm/xterm';
|
||||
import { forwardRef, memo, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import type { Theme } from '~/lib/stores/theme';
|
||||
import { getTerminalTheme } from './theme';
|
||||
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
|
||||
export interface TerminalRef {
|
||||
reloadStyles: () => void;
|
||||
}
|
||||
|
||||
export interface TerminalProps {
|
||||
className?: string;
|
||||
theme: Theme;
|
||||
readonly?: boolean;
|
||||
onTerminalReady?: (terminal: XTerm) => void;
|
||||
onTerminalResize?: (cols: number, rows: number) => void;
|
||||
}
|
||||
|
||||
export const Terminal = memo(
|
||||
forwardRef<TerminalRef, TerminalProps>(({ className, theme, readonly, onTerminalReady, onTerminalResize }, ref) => {
|
||||
const terminalElementRef = useRef<HTMLDivElement>(null);
|
||||
const terminalRef = useRef<XTerm>();
|
||||
|
||||
useEffect(() => {
|
||||
const element = terminalElementRef.current!;
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
const webLinksAddon = new WebLinksAddon();
|
||||
|
||||
const terminal = new XTerm({
|
||||
cursorBlink: true,
|
||||
convertEol: true,
|
||||
disableStdin: readonly,
|
||||
theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}),
|
||||
fontSize: 13,
|
||||
fontFamily: 'Menlo, courier-new, courier, monospace',
|
||||
});
|
||||
|
||||
terminalRef.current = terminal;
|
||||
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(webLinksAddon);
|
||||
terminal.open(element);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.fit();
|
||||
onTerminalResize?.(terminal.cols, terminal.rows);
|
||||
});
|
||||
|
||||
resizeObserver.observe(element);
|
||||
|
||||
onTerminalReady?.(terminal);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
terminal.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const terminal = terminalRef.current!;
|
||||
|
||||
// we render a transparent cursor in case the terminal is readonly
|
||||
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
|
||||
|
||||
terminal.options.disableStdin = readonly;
|
||||
}, [theme, readonly]);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
reloadStyles: () => {
|
||||
const terminal = terminalRef.current!;
|
||||
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div className={className} ref={terminalElementRef} />;
|
||||
}),
|
||||
);
|
36
packages/bolt/app/components/workbench/terminal/theme.ts
Normal file
36
packages/bolt/app/components/workbench/terminal/theme.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { ITheme } from '@xterm/xterm';
|
||||
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
const cssVar = (token: string) => style.getPropertyValue(token) || undefined;
|
||||
|
||||
export function getTerminalTheme(overrides?: ITheme): ITheme {
|
||||
return {
|
||||
cursor: cssVar('--bolt-elements-terminal-cursorColor'),
|
||||
cursorAccent: cssVar('--bolt-elements-terminal-cursorColorAccent'),
|
||||
foreground: cssVar('--bolt-elements-terminal-textColor'),
|
||||
background: cssVar('--bolt-elements-terminal-backgroundColor'),
|
||||
selectionBackground: cssVar('--bolt-elements-terminal-selection-backgroundColor'),
|
||||
selectionForeground: cssVar('--bolt-elements-terminal-selection-textColor'),
|
||||
selectionInactiveBackground: cssVar('--bolt-elements-terminal-selection-backgroundColorInactive'),
|
||||
|
||||
// ansi escape code colors
|
||||
black: cssVar('--bolt-elements-terminal-color-black'),
|
||||
red: cssVar('--bolt-elements-terminal-color-red'),
|
||||
green: cssVar('--bolt-elements-terminal-color-green'),
|
||||
yellow: cssVar('--bolt-elements-terminal-color-yellow'),
|
||||
blue: cssVar('--bolt-elements-terminal-color-blue'),
|
||||
magenta: cssVar('--bolt-elements-terminal-color-magenta'),
|
||||
cyan: cssVar('--bolt-elements-terminal-color-cyan'),
|
||||
white: cssVar('--bolt-elements-terminal-color-white'),
|
||||
brightBlack: cssVar('--bolt-elements-terminal-color-brightBlack'),
|
||||
brightRed: cssVar('--bolt-elements-terminal-color-brightRed'),
|
||||
brightGreen: cssVar('--bolt-elements-terminal-color-brightGreen'),
|
||||
brightYellow: cssVar('--bolt-elements-terminal-color-brightYellow'),
|
||||
brightBlue: cssVar('--bolt-elements-terminal-color-brightBlue'),
|
||||
brightMagenta: cssVar('--bolt-elements-terminal-color-brightMagenta'),
|
||||
brightCyan: cssVar('--bolt-elements-terminal-color-brightCyan'),
|
||||
brightWhite: cssVar('--bolt-elements-terminal-color-brightWhite'),
|
||||
|
||||
...overrides,
|
||||
};
|
||||
}
|
@ -1,12 +1,7 @@
|
||||
import { RemixBrowser } from '@remix-run/react';
|
||||
import { startTransition, StrictMode } from 'react';
|
||||
import { startTransition } from 'react';
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<RemixBrowser />
|
||||
</StrictMode>,
|
||||
);
|
||||
hydrateRoot(document, <RemixBrowser />);
|
||||
});
|
||||
|
@ -13,6 +13,10 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
||||
|
||||
IMPORTANT: Git is NOT available.
|
||||
|
||||
IMPORTANT: Prefer writing Node.js scripts instead of shell scripts. The environment doesn't fully support shell scripts, so use Node.js for scripting tasks whenever possible!
|
||||
|
||||
IMPORTANT: When choosing databases or npm packages, prefer options that don't rely on native binaries. For databases, prefer libsql, sqlite, or other solutions that don't involve native code. WebContainer CANNOT execute arbitrary native binaries.
|
||||
|
||||
Available shell commands: cat, chmod, cp, echo, hostname, kill, ln, ls, mkdir, mv, ps, pwd, rm, rmdir, xxd, alias, cd, clear, curl, env, false, getconf, head, sort, tail, touch, true, uptime, which, code, jq, loadenv, node, python3, wasm, xdg-open, command, exit, export, source
|
||||
</system_constraints>
|
||||
|
||||
@ -92,7 +96,7 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
||||
|
||||
- When Using \`npx\`, ALWAYS provide the \`--yes\` flag.
|
||||
- When running multiple shell commands, use \`&&\` to run them sequentially.
|
||||
- IMPORTANT: Do NOT re-run a dev command if there is one that starts a dev server and new dependencies were installed or files updated! If a dev server has started already, assume that installing dependencies will be executed in a different process and will be picked up by the dev server.
|
||||
- ULTRA IMPORTANT: Do NOT re-run a dev command if there is one that starts a dev server and new dependencies were installed or files updated! If a dev server has started already, assume that installing dependencies will be executed in a different process and will be picked up by the dev server.
|
||||
|
||||
- file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`<boltAction>\` tag to specify the file path. The content of the file artifact is the file contents. All file paths MUST BE relative to the current working directory.
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './useMessageParser';
|
||||
export * from './usePromptEnhancer';
|
||||
export * from './useShortcuts';
|
||||
export * from './useSnapScroll';
|
||||
|
56
packages/bolt/app/lib/hooks/useShortcuts.ts
Normal file
56
packages/bolt/app/lib/hooks/useShortcuts.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect } from 'react';
|
||||
import { shortcutsStore, type Shortcuts } from '~/lib/stores/settings';
|
||||
|
||||
class ShortcutEventEmitter {
|
||||
#emitter = new EventTarget();
|
||||
|
||||
dispatch(type: keyof Shortcuts) {
|
||||
this.#emitter.dispatchEvent(new Event(type));
|
||||
}
|
||||
|
||||
on(type: keyof Shortcuts, cb: VoidFunction) {
|
||||
this.#emitter.addEventListener(type, cb);
|
||||
|
||||
return () => {
|
||||
this.#emitter.removeEventListener(type, cb);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const shortcutEventEmitter = new ShortcutEventEmitter();
|
||||
|
||||
export function useShortcuts(): void {
|
||||
const shortcuts = useStore(shortcutsStore);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
const { key, ctrlKey, shiftKey, altKey, metaKey } = event;
|
||||
|
||||
for (const name in shortcuts) {
|
||||
const shortcut = shortcuts[name as keyof Shortcuts];
|
||||
|
||||
if (
|
||||
shortcut.key.toLowerCase() === key.toLowerCase() &&
|
||||
(shortcut.ctrlOrMetaKey
|
||||
? ctrlKey || metaKey
|
||||
: (shortcut.ctrlKey === undefined || shortcut.ctrlKey === ctrlKey) &&
|
||||
(shortcut.metaKey === undefined || shortcut.metaKey === metaKey)) &&
|
||||
(shortcut.shiftKey === undefined || shortcut.shiftKey === shiftKey) &&
|
||||
(shortcut.altKey === undefined || shortcut.altKey === altKey)
|
||||
) {
|
||||
shortcutEventEmitter.dispatch(name as keyof Shortcuts);
|
||||
event.preventDefault();
|
||||
shortcut.action();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [shortcuts]);
|
||||
}
|
39
packages/bolt/app/lib/stores/settings.ts
Normal file
39
packages/bolt/app/lib/stores/settings.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { map } from 'nanostores';
|
||||
import { workbenchStore } from './workbench';
|
||||
|
||||
export interface Shortcut {
|
||||
key: string;
|
||||
ctrlKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
altKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
ctrlOrMetaKey?: boolean;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
export interface Shortcuts {
|
||||
toggleTerminal: Shortcut;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
shortcuts: Shortcuts;
|
||||
}
|
||||
|
||||
export const shortcutsStore = map<Shortcuts>({
|
||||
toggleTerminal: {
|
||||
key: 'j',
|
||||
ctrlOrMetaKey: true,
|
||||
action: () => workbenchStore.toggleTerminal(),
|
||||
},
|
||||
});
|
||||
|
||||
export const settingsStore = map<Settings>({
|
||||
shortcuts: shortcutsStore.get(),
|
||||
});
|
||||
|
||||
shortcutsStore.subscribe((shortcuts) => {
|
||||
settingsStore.set({
|
||||
...settingsStore.get(),
|
||||
shortcuts,
|
||||
});
|
||||
});
|
40
packages/bolt/app/lib/stores/terminal.ts
Normal file
40
packages/bolt/app/lib/stores/terminal.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
|
||||
import { atom, type WritableAtom } from 'nanostores';
|
||||
import type { ITerminal } from '~/types/terminal';
|
||||
import { newShellProcess } from '~/utils/shell';
|
||||
import { coloredText } from '~/utils/terminal';
|
||||
|
||||
export class TerminalStore {
|
||||
#webcontainer: Promise<WebContainer>;
|
||||
#terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];
|
||||
|
||||
showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(false);
|
||||
|
||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.showTerminal = this.showTerminal;
|
||||
}
|
||||
}
|
||||
|
||||
toggleTerminal(value?: boolean) {
|
||||
this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());
|
||||
}
|
||||
|
||||
async attachTerminal(terminal: ITerminal) {
|
||||
try {
|
||||
const shellProcess = await newShellProcess(await this.#webcontainer, terminal);
|
||||
this.#terminals.push({ terminal, process: shellProcess });
|
||||
} catch (error: any) {
|
||||
terminal.write(coloredText.red('Failed to spawn shell\n\n') + error.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onTerminalResize(cols: number, rows: number) {
|
||||
for (const { process } of this.#terminals) {
|
||||
process.resize({ cols, rows });
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ export function themeIsDark() {
|
||||
return themeStore.get() === 'dark';
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME = 'light';
|
||||
|
||||
export const themeStore = atom<Theme>(initStore());
|
||||
|
||||
function initStore() {
|
||||
@ -15,10 +17,10 @@ function initStore() {
|
||||
const persistedTheme = localStorage.getItem(kTheme) as Theme | undefined;
|
||||
const themeAttribute = document.querySelector('html')?.getAttribute('data-theme');
|
||||
|
||||
return persistedTheme ?? (themeAttribute as Theme) ?? 'light';
|
||||
return persistedTheme ?? (themeAttribute as Theme) ?? DEFAULT_THEME;
|
||||
}
|
||||
|
||||
return 'light';
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
|
||||
export function toggleTheme() {
|
||||
|
@ -3,10 +3,12 @@ import type { EditorDocument, ScrollPosition } from '~/components/editor/codemir
|
||||
import { ActionRunner } from '~/lib/runtime/action-runner';
|
||||
import type { ActionCallbackData, ArtifactCallbackData } from '~/lib/runtime/message-parser';
|
||||
import { webcontainer } from '~/lib/webcontainer';
|
||||
import type { ITerminal } from '~/types/terminal';
|
||||
import { unreachable } from '~/utils/unreachable';
|
||||
import { EditorStore } from './editor';
|
||||
import { FilesStore, type FileMap } from './files';
|
||||
import { PreviewsStore } from './previews';
|
||||
import { TerminalStore } from './terminal';
|
||||
|
||||
export interface ArtifactState {
|
||||
title: string;
|
||||
@ -22,6 +24,7 @@ export class WorkbenchStore {
|
||||
#previewsStore = new PreviewsStore(webcontainer);
|
||||
#filesStore = new FilesStore(webcontainer);
|
||||
#editorStore = new EditorStore(this.#filesStore);
|
||||
#terminalStore = new TerminalStore(webcontainer);
|
||||
|
||||
artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({});
|
||||
showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
|
||||
@ -53,6 +56,22 @@ export class WorkbenchStore {
|
||||
return this.#editorStore.selectedFile;
|
||||
}
|
||||
|
||||
get showTerminal() {
|
||||
return this.#terminalStore.showTerminal;
|
||||
}
|
||||
|
||||
toggleTerminal(value?: boolean) {
|
||||
this.#terminalStore.toggleTerminal(value);
|
||||
}
|
||||
|
||||
attachTerminal(terminal: ITerminal) {
|
||||
this.#terminalStore.attachTerminal(terminal);
|
||||
}
|
||||
|
||||
onTerminalResize(cols: number, rows: number) {
|
||||
this.#terminalStore.onTerminalResize(cols, rows);
|
||||
}
|
||||
|
||||
setDocuments(files: FileMap) {
|
||||
this.#editorStore.setDocuments(files);
|
||||
|
||||
|
28
packages/bolt/app/styles/components/resize-handle.scss
Normal file
28
packages/bolt/app/styles/components/resize-handle.scss
Normal file
@ -0,0 +1,28 @@
|
||||
[data-resize-handle] {
|
||||
position: relative;
|
||||
|
||||
&[data-panel-group-direction='horizontal']:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -6px;
|
||||
right: -5px;
|
||||
z-index: $zIndexMax;
|
||||
}
|
||||
|
||||
&[data-panel-group-direction='vertical']:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -5px;
|
||||
bottom: -6px;
|
||||
z-index: $zIndexMax;
|
||||
}
|
||||
|
||||
&[data-resize-handle-state='hover']:after,
|
||||
&[data-resize-handle-state='drag']:after {
|
||||
background-color: #8882;
|
||||
}
|
||||
}
|
3
packages/bolt/app/styles/components/terminal.scss
Normal file
3
packages/bolt/app/styles/components/terminal.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.xterm {
|
||||
height: 100%;
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
@import './variables.scss';
|
||||
@import './z-index.scss';
|
||||
@import './animations.scss';
|
||||
@import './components/terminal.scss';
|
||||
@import './components/resize-handle.scss';
|
||||
|
||||
body {
|
||||
--at-apply: bg-bolt-elements-app-backgroundColor;
|
||||
|
@ -20,12 +20,72 @@
|
||||
|
||||
--bolt-border-primary: theme('colors.gray.200');
|
||||
--bolt-border-accent: theme('colors.accent.600');
|
||||
|
||||
/* Terminal Colors */
|
||||
--bolt-terminal-background: var(--bolt-background-primary);
|
||||
--bolt-terminal-foreground: #333333;
|
||||
--bolt-terminal-selection-background: #00000040;
|
||||
--bolt-terminal-black: #000000;
|
||||
--bolt-terminal-red: #cd3131;
|
||||
--bolt-terminal-green: #00bc00;
|
||||
--bolt-terminal-yellow: #949800;
|
||||
--bolt-terminal-blue: #0451a5;
|
||||
--bolt-terminal-magenta: #bc05bc;
|
||||
--bolt-terminal-cyan: #0598bc;
|
||||
--bolt-terminal-white: #555555;
|
||||
--bolt-terminal-brightBlack: #686868;
|
||||
--bolt-terminal-brightRed: #cd3131;
|
||||
--bolt-terminal-brightGreen: #00bc00;
|
||||
--bolt-terminal-brightYellow: #949800;
|
||||
--bolt-terminal-brightBlue: #0451a5;
|
||||
--bolt-terminal-brightMagenta: #bc05bc;
|
||||
--bolt-terminal-brightCyan: #0598bc;
|
||||
--bolt-terminal-brightWhite: #a5a5a5;
|
||||
}
|
||||
|
||||
/* Color Tokens Dark Theme */
|
||||
:root,
|
||||
:root[data-theme='dark'] {
|
||||
--bolt-background-primary: theme('colors.gray.50');
|
||||
--bolt-background-primary: theme('colors.gray.0');
|
||||
--bolt-background-secondary: theme('colors.gray.50');
|
||||
--bolt-background-active: theme('colors.gray.200');
|
||||
--bolt-background-accent: theme('colors.accent.600');
|
||||
--bolt-background-accent-secondary: theme('colors.accent.600');
|
||||
--bolt-background-accent-active: theme('colors.accent.500');
|
||||
|
||||
--bolt-text-primary: theme('colors.gray.800');
|
||||
--bolt-text-primary-inverted: theme('colors.gray.0');
|
||||
--bolt-text-secondary: theme('colors.gray.600');
|
||||
--bolt-text-secondary-inverted: theme('colors.gray.200');
|
||||
--bolt-text-disabled: theme('colors.gray.400');
|
||||
--bolt-text-accent: theme('colors.accent.600');
|
||||
--bolt-text-positive: theme('colors.positive.700');
|
||||
--bolt-text-warning: theme('colors.warning.600');
|
||||
--bolt-text-negative: theme('colors.negative.600');
|
||||
|
||||
--bolt-border-primary: theme('colors.gray.200');
|
||||
--bolt-border-accent: theme('colors.accent.600');
|
||||
|
||||
/* Terminal Colors */
|
||||
--bolt-terminal-background: #16181d;
|
||||
--bolt-terminal-foreground: #eff0eb;
|
||||
--bolt-terminal-selection-background: #97979b33;
|
||||
--bolt-terminal-black: #000000;
|
||||
--bolt-terminal-red: #ff5c57;
|
||||
--bolt-terminal-green: #5af78e;
|
||||
--bolt-terminal-yellow: #f3f99d;
|
||||
--bolt-terminal-blue: #57c7ff;
|
||||
--bolt-terminal-magenta: #ff6ac1;
|
||||
--bolt-terminal-cyan: #9aedfe;
|
||||
--bolt-terminal-white: #f1f1f0;
|
||||
--bolt-terminal-brightBlack: #686868;
|
||||
--bolt-terminal-brightRed: #ff5c57;
|
||||
--bolt-terminal-brightGreen: #5af78e;
|
||||
--bolt-terminal-brightYellow: #f3f99d;
|
||||
--bolt-terminal-brightBlue: #57c7ff;
|
||||
--bolt-terminal-brightMagenta: #ff6ac1;
|
||||
--bolt-terminal-brightCyan: #9aedfe;
|
||||
--bolt-terminal-brightWhite: #f1f1f0;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -36,9 +96,33 @@
|
||||
:root {
|
||||
--header-height: 65px;
|
||||
|
||||
--z-index-max: 999;
|
||||
|
||||
/* App */
|
||||
--bolt-elements-app-backgroundColor: var(--bolt-background-primary);
|
||||
--bolt-elements-app-borderColor: var(--bolt-border-primary);
|
||||
--bolt-elements-app-textColor: var(--bolt-text-primary);
|
||||
--bolt-elements-app-linkColor: var(--bolt-text-accent);
|
||||
|
||||
/* Terminal */
|
||||
--bolt-elements-terminal-backgroundColor: var(--bolt-terminal-background);
|
||||
--bolt-elements-terminal-textColor: var(--bolt-terminal-foreground);
|
||||
--bolt-elements-terminal-cursorColor: var(--bolt-terminal-foreground);
|
||||
--bolt-elements-terminal-selection-backgroundColor: var(--bolt-terminal-selection-background);
|
||||
--bolt-elements-terminal-color-black: var(--bolt-terminal-black);
|
||||
--bolt-elements-terminal-color-red: var(--bolt-terminal-red);
|
||||
--bolt-elements-terminal-color-green: var(--bolt-terminal-green);
|
||||
--bolt-elements-terminal-color-yellow: var(--bolt-terminal-yellow);
|
||||
--bolt-elements-terminal-color-blue: var(--bolt-terminal-blue);
|
||||
--bolt-elements-terminal-color-magenta: var(--bolt-terminal-magenta);
|
||||
--bolt-elements-terminal-color-cyan: var(--bolt-terminal-cyan);
|
||||
--bolt-elements-terminal-color-white: var(--bolt-terminal-white);
|
||||
--bolt-elements-terminal-color-brightBlack: var(--bolt-terminal-brightBlack);
|
||||
--bolt-elements-terminal-color-brightRed: var(--bolt-terminal-brightRed);
|
||||
--bolt-elements-terminal-color-brightGreen: var(--bolt-terminal-brightGreen);
|
||||
--bolt-elements-terminal-color-brightYellow: var(--bolt-terminal-brightYellow);
|
||||
--bolt-elements-terminal-color-brightBlue: var(--bolt-terminal-brightBlue);
|
||||
--bolt-elements-terminal-color-brightMagenta: var(--bolt-terminal-brightMagenta);
|
||||
--bolt-elements-terminal-color-brightCyan: var(--bolt-terminal-brightCyan);
|
||||
--bolt-elements-terminal-color-brightWhite: var(--bolt-terminal-brightWhite);
|
||||
}
|
||||
|
1
packages/bolt/app/styles/z-index.scss
Normal file
1
packages/bolt/app/styles/z-index.scss
Normal file
@ -0,0 +1 @@
|
||||
$zIndexMax: 999;
|
8
packages/bolt/app/types/terminal.ts
Normal file
8
packages/bolt/app/types/terminal.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface ITerminal {
|
||||
readonly cols?: number;
|
||||
readonly rows?: number;
|
||||
|
||||
reset: () => void;
|
||||
write: (data: string) => void;
|
||||
onData: (cb: (data: string) => void) => void;
|
||||
}
|
51
packages/bolt/app/utils/shell.ts
Normal file
51
packages/bolt/app/utils/shell.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type { WebContainer } from '@webcontainer/api';
|
||||
import type { ITerminal } from '~/types/terminal';
|
||||
import { withResolvers } from './promises';
|
||||
|
||||
export async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
|
||||
const args: string[] = [];
|
||||
|
||||
// we spawn a JSH process with a fallback cols and rows in case the process is not attached yet to a visible terminal
|
||||
const process = await webcontainer.spawn('/bin/jsh', ['--osc', ...args], {
|
||||
terminal: {
|
||||
cols: terminal.cols ?? 80,
|
||||
rows: terminal.rows ?? 15,
|
||||
},
|
||||
});
|
||||
|
||||
const input = process.input.getWriter();
|
||||
const output = process.output;
|
||||
|
||||
const jshReady = withResolvers<void>();
|
||||
|
||||
let isInteractive = false;
|
||||
|
||||
output.pipeTo(
|
||||
new WritableStream({
|
||||
write(data) {
|
||||
if (!isInteractive) {
|
||||
const [, osc] = data.match(/\x1b\]654;([^\x07]+)\x07/) || [];
|
||||
|
||||
if (osc === 'interactive') {
|
||||
// wait until we see the interactive OSC
|
||||
isInteractive = true;
|
||||
|
||||
jshReady.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
terminal.write(data);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
terminal.onData((data) => {
|
||||
if (isInteractive) {
|
||||
input.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
await jshReady.promise;
|
||||
|
||||
return process;
|
||||
}
|
11
packages/bolt/app/utils/terminal.ts
Normal file
11
packages/bolt/app/utils/terminal.ts
Normal file
@ -0,0 +1,11 @@
|
||||
const reset = '\x1b[0m';
|
||||
|
||||
export const escapeCodes = {
|
||||
reset,
|
||||
clear: '\x1b[g',
|
||||
red: '\x1b[1;31m',
|
||||
};
|
||||
|
||||
export const coloredText = {
|
||||
red: (text: string) => `${escapeCodes.red}${text}${reset}`,
|
||||
};
|
@ -39,7 +39,7 @@
|
||||
"@remix-run/react": "^2.10.2",
|
||||
"@stackblitz/sdk": "^1.11.0",
|
||||
"@unocss/reset": "^0.61.0",
|
||||
"@webcontainer/api": "^1.3.0-internal.1",
|
||||
"@webcontainer/api": "^1.3.0-internal.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
@ -51,6 +51,7 @@
|
||||
"nanostores": "^0.10.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.0.20",
|
||||
"react-toastify": "^10.0.5",
|
||||
|
@ -105,8 +105,8 @@ importers:
|
||||
specifier: ^0.61.0
|
||||
version: 0.61.0
|
||||
'@webcontainer/api':
|
||||
specifier: ^1.3.0-internal.1
|
||||
version: 1.3.0-internal.1
|
||||
specifier: ^1.3.0-internal.2
|
||||
version: 1.3.0-internal.2
|
||||
'@xterm/addon-fit':
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0(@xterm/xterm@5.5.0)
|
||||
@ -140,6 +140,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-hotkeys-hook:
|
||||
specifier: ^4.5.0
|
||||
version: 4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-markdown:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
@ -1722,8 +1725,8 @@ packages:
|
||||
'@web3-storage/multipart-parser@1.0.0':
|
||||
resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
|
||||
|
||||
'@webcontainer/api@1.3.0-internal.1':
|
||||
resolution: {integrity: sha512-XHveAaZgZItLWieict8xTteBbPLeAwCJLuc80Zq6Mmk0LEWTw8yYZep0dTKbet6bd9MPTQ1+vjPAsEtD0H1fOA==}
|
||||
'@webcontainer/api@1.3.0-internal.2':
|
||||
resolution: {integrity: sha512-lLSlSehbuYc9E7ecK+tMRX4BbWETNX1OgRlS+NerQh3X3sHNbxLD86eScEMAiA5VBnUeSnLtLe7eC/ftM8fR3Q==}
|
||||
|
||||
'@xterm/addon-fit@0.10.0':
|
||||
resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
|
||||
@ -4090,6 +4093,12 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
|
||||
react-hotkeys-hook@4.5.0:
|
||||
resolution: {integrity: sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.1'
|
||||
react-dom: '>=16.8.1'
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
@ -6814,7 +6823,7 @@ snapshots:
|
||||
|
||||
'@web3-storage/multipart-parser@1.0.0': {}
|
||||
|
||||
'@webcontainer/api@1.3.0-internal.1': {}
|
||||
'@webcontainer/api@1.3.0-internal.2': {}
|
||||
|
||||
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
|
||||
dependencies:
|
||||
@ -9748,6 +9757,11 @@ snapshots:
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
react-hotkeys-hook@4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
react-markdown@9.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||
|
Loading…
Reference in New Issue
Block a user