From 9953112ffb5762be580e6f7b1049eae77e3ebf28 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Mon, 21 Oct 2024 19:47:28 -0400 Subject: [PATCH] Refurbish bottom drawer (#400) * feat: rename statusbar to bottom drawer * feat: add preview:log event to send logs from vite to the client * feat: add new addLog method to useLogs to append log messages in the new format * feat: add button to open / close bototm drawer from header * Committing progress * Add tooltip, reuse modal from notebooks for installing apps * Format * Fix up styles for layout + log table. Adjust copy on packages panel * Ability to click on 'Logs' to toggle the pane * Fix deps array --------- Co-authored-by: Nicholas Charriere --- packages/api/server/channels/app.mts | 14 ++ packages/shared/src/schemas/websockets.mts | 7 + packages/shared/src/types/websockets.mts | 2 + packages/web/src/clients/websocket/index.ts | 2 + .../web/src/components/apps/bottom-drawer.tsx | 132 ++++++++++++++++++ packages/web/src/components/apps/header.tsx | 30 ++-- .../src/components/apps/panels/settings.tsx | 98 +++++++------ .../web/src/components/apps/statusbar.tsx | 110 --------------- packages/web/src/components/apps/use-logs.tsx | 75 +++++++--- .../src/components/apps/use-package-json.tsx | 25 +++- .../web/src/components/apps/use-preview.tsx | 5 + .../src/components/apps/workspace/editor.tsx | 2 +- .../src/components/install-package-modal.tsx | 11 +- packages/web/src/routes/apps.tsx | 24 +++- packages/web/src/routes/session.tsx | 31 +++- 15 files changed, 365 insertions(+), 203 deletions(-) create mode 100644 packages/web/src/components/apps/bottom-drawer.tsx delete mode 100644 packages/web/src/components/apps/statusbar.tsx diff --git a/packages/api/server/channels/app.mts b/packages/api/server/channels/app.mts index afc27142..dd3da7bc 100644 --- a/packages/api/server/channels/app.mts +++ b/packages/api/server/channels/app.mts @@ -82,6 +82,13 @@ async function previewStart( console.log(encodedData); bufferedLogs.push(encodedData); + conn.reply(`app:${app.externalId}`, 'preview:log', { + log: { + type: 'stdout', + data: encodedData, + }, + }); + const potentialPortMatch = VITE_PORT_REGEX.exec(encodedData); if (potentialPortMatch) { const portString = potentialPortMatch[1]!; @@ -93,6 +100,13 @@ async function previewStart( const encodedData = data.toString('utf8'); console.error(encodedData); bufferedLogs.push(encodedData); + + conn.reply(`app:${app.externalId}`, 'preview:log', { + log: { + type: 'stderr', + data: encodedData, + }, + }); }, onExit: (code) => { processMetadata.delete(app.externalId); diff --git a/packages/shared/src/schemas/websockets.mts b/packages/shared/src/schemas/websockets.mts index d3cf8c97..c5f64157 100644 --- a/packages/shared/src/schemas/websockets.mts +++ b/packages/shared/src/schemas/websockets.mts @@ -176,6 +176,13 @@ export const PreviewStatusPayloadSchema = z.union([ export const PreviewStartPayloadSchema = z.object({}); export const PreviewStopPayloadSchema = z.object({}); +export const PreviewLogPayloadSchema = z.object({ + log: z.union([ + z.object({ type: z.literal('stdout'), data: z.string() }), + z.object({ type: z.literal('stderr'), data: z.string() }), + ]), +}); + export const DepsInstallLogPayloadSchema = z.object({ log: z.union([ z.object({ type: z.literal('stdout'), data: z.string() }), diff --git a/packages/shared/src/types/websockets.mts b/packages/shared/src/types/websockets.mts index b3eb2b44..aa0fcb45 100644 --- a/packages/shared/src/types/websockets.mts +++ b/packages/shared/src/types/websockets.mts @@ -42,6 +42,7 @@ import { DepsClearPayloadSchema, DepsStatusPayloadSchema, DepsStatusResponsePayloadSchema, + PreviewLogPayloadSchema, } from '../schemas/websockets.mjs'; export type CellExecPayloadType = z.infer; @@ -109,5 +110,6 @@ export type FileDeletedPayloadType = z.infer; export type PreviewStatusPayloadType = z.infer; export type PreviewStartPayloadType = z.infer; export type PreviewStopPayloadType = z.infer; +export type PreviewLogPayloadType = z.infer; export type DepsInstallLogPayloadType = z.infer; export type DepsInstallStatusPayloadType = z.infer; diff --git a/packages/web/src/clients/websocket/index.ts b/packages/web/src/clients/websocket/index.ts index 49de3536..749731c0 100644 --- a/packages/web/src/clients/websocket/index.ts +++ b/packages/web/src/clients/websocket/index.ts @@ -40,6 +40,7 @@ import { DepsClearPayloadSchema, DepsStatusResponsePayloadSchema, DepsStatusPayloadSchema, + PreviewLogPayloadSchema, } from '@srcbook/shared'; import Channel from '@/clients/websocket/channel'; import WebSocketClient from '@/clients/websocket/client'; @@ -99,6 +100,7 @@ export class SessionChannel extends Channel< const IncomingAppEvents = { file: FilePayloadSchema, 'preview:status': PreviewStatusPayloadSchema, + 'preview:log': PreviewLogPayloadSchema, 'deps:install:log': DepsInstallLogPayloadSchema, 'deps:install:status': DepsInstallStatusPayloadSchema, 'deps:status:response': DepsStatusResponsePayloadSchema, diff --git a/packages/web/src/components/apps/bottom-drawer.tsx b/packages/web/src/components/apps/bottom-drawer.tsx new file mode 100644 index 00000000..9ad42e09 --- /dev/null +++ b/packages/web/src/components/apps/bottom-drawer.tsx @@ -0,0 +1,132 @@ +import { BanIcon, XIcon } from 'lucide-react'; +import { useHotkeys } from 'react-hotkeys-hook'; + +import { Button } from '@srcbook/components/src/components/ui/button'; +import { cn } from '@/lib/utils.ts'; +import { useLogs } from './use-logs'; +import { useEffect, useRef } from 'react'; + +const maxHeightInPx = 320; + +export default function BottomDrawer() { + const { logs, clearLogs, open, togglePane, closePane } = useLogs(); + + useHotkeys('mod+shift+y', () => { + togglePane(); + }); + + const scrollWrapperRef = useRef(null); + + // Scroll to the bottom of the logs panel when the user opens the panel fresh + useEffect(() => { + if (!scrollWrapperRef.current) { + return; + } + scrollWrapperRef.current.scrollTop = scrollWrapperRef.current.scrollHeight; + }, [open]); + + // Determine if the user has scrolled all the way to the bottom of the div + const scrollPinnedToBottomRef = useRef(false); + useEffect(() => { + if (!scrollWrapperRef.current) { + return; + } + const element = scrollWrapperRef.current; + + const onScroll = () => { + scrollPinnedToBottomRef.current = + element.scrollTop === element.scrollHeight - element.clientHeight; + }; + + element.addEventListener('scroll', onScroll); + return () => element.removeEventListener('scroll', onScroll); + }, []); + + // If the user has scrolled all the way to the bottom, then keep the bottom scroll pinned as new + // logs come in. + useEffect(() => { + if (!scrollWrapperRef.current) { + return; + } + + if (scrollPinnedToBottomRef.current) { + scrollWrapperRef.current.scrollTop = scrollWrapperRef.current.scrollHeight; + } + }, [logs]); + + return ( +
+
+ togglePane()} + className="text-sm font-medium ml-2 select-none cursor-pointer" + > + Logs + + +
+ {open && logs.length > 0 && ( + + )} + +
+
+ + {open && ( +
+ + + {logs.map((log, index) => ( + + + + + + ))} + +
+ + {log.timestamp.toISOString()} + + + {log.source} + +
+                      {log.message}
+                    
+
+ {logs.length === 0 && ( +
+ No logs +
+ )} +
+ )} +
+ ); +} diff --git a/packages/web/src/components/apps/header.tsx b/packages/web/src/components/apps/header.tsx index 6d0c55c6..ac8cd698 100644 --- a/packages/web/src/components/apps/header.tsx +++ b/packages/web/src/components/apps/header.tsx @@ -2,10 +2,12 @@ import { ShareIcon, PlayIcon, StopCircleIcon, - EllipsisIcon, PlayCircleIcon, Code2Icon, Loader2Icon, + CircleAlertIcon, + PanelBottomOpenIcon, + PanelBottomCloseIcon, } from 'lucide-react'; import { Link } from 'react-router-dom'; import { SrcbookLogo } from '@/components/logos'; @@ -32,6 +34,7 @@ import { useState } from 'react'; import { usePreview } from './use-preview'; import { exportApp } from '@/clients/http/apps'; import { toast } from 'sonner'; +import { useLogs } from './use-logs'; export type EditorHeaderTab = 'code' | 'preview'; @@ -47,6 +50,7 @@ export default function EditorHeader(props: PropsType) { const { start: startPreview, stop: stopPreview, status: previewStatus } = usePreview(); const { status: npmInstallStatus, nodeModulesExists } = usePackageJson(); const [isExporting, setIsExporting] = useState(false); + const { open, togglePane, panelIcon } = useLogs(); const [nameChangeDialogOpen, setNameChangeDialogOpen] = useState(false); @@ -198,17 +202,17 @@ export default function EditorHeader(props: PropsType) { - Export app + Open logs @@ -217,13 +221,17 @@ export default function EditorHeader(props: PropsType) { - More options + Export app diff --git a/packages/web/src/components/apps/panels/settings.tsx b/packages/web/src/components/apps/panels/settings.tsx index 87e4be1d..dc5954ea 100644 --- a/packages/web/src/components/apps/panels/settings.tsx +++ b/packages/web/src/components/apps/panels/settings.tsx @@ -1,57 +1,65 @@ import { Button } from '@srcbook/components/src/components/ui/button'; import { usePackageJson } from '../use-package-json'; +import { PackagePlus } from 'lucide-react'; +import Shortcut from '@srcbook/components/src/components/keyboard-shortcut'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@srcbook/components/src/components/ui/tooltip'; export default function PackagesPanel() { - const { status, output, npmInstall, clearNodeModules, nodeModulesExists } = usePackageJson(); + const { setShowInstallModal, npmInstall, clearNodeModules, nodeModulesExists, status } = + usePackageJson(); return ( -
-

- Clear your node_modules, re-install packages and inspect the output logs from{' '} -

npm install
-

-
- +
+
+

+ To add packages, you can simply ask the AI in chat, or use the button below. +

+ +
+ + + + + + + Install packages + + + +
- {status !== 'idle' ? ( - <> -

Logs

-
-            {/* FIXME: disambiguate between stdout and stderr in here using n.type! */}
-            {output.map((n) => n.data).join('\n')}
-          
- - ) : null} +
+

+ If you suspect your node_modules are corrupted, you can clear them and reinstall all + packages. +

+
+ +
+
- {process.env.NODE_ENV !== 'production' && ( - <> - - Status: {status} - -
- -
-
- exists={JSON.stringify(nodeModulesExists)} - -
- - )} +
+

+ Re-run npm install. This will run against the package.json + from the project root. +

+
+ +
+
); } diff --git a/packages/web/src/components/apps/statusbar.tsx b/packages/web/src/components/apps/statusbar.tsx deleted file mode 100644 index 7bc88126..00000000 --- a/packages/web/src/components/apps/statusbar.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState } from 'react'; -import { BugIcon, ChevronDownIcon, ChevronRightIcon, TrashIcon } from 'lucide-react'; -import { useHotkeys } from 'react-hotkeys-hook'; - -import { Button } from '@srcbook/components/src/components/ui/button'; -import { cn } from '@/lib/utils.ts'; -import { LogMessage, useLogs } from './use-logs'; - -function getLabelForError(error: LogMessage) { - switch (error.type) { - case 'vite_error': - return 'Error running vite preview server'; - case 'npm_install_error': - return 'Error running npm install'; - } -} - -type CollapsibleErrorMessageProps = { - error: LogMessage; -}; - -function CollapsibleErrorMessage({ error }: CollapsibleErrorMessageProps) { - const [open, setOpen] = useState(false); - return ( - <> - - {open ? ( -
-          {error.contents}
-        
- ) : null} - - ); -} - -export default function Statusbar() { - const { logs, clearLogs, unreadLogsCount, open, togglePane } = useLogs(); - - useHotkeys('mod+shift+y', () => { - togglePane(); - }); - - return ( - <> -
- - {open && logs.length > 0 ? ( - - ) : null} -
- -
- {logs.map((error) => ( - // FIXME: add a better explicit key, maybe a uuid in each log message? - - ))} - - {logs.length === 0 ? ( -
- No errors -
- ) : null} -
- - ); -} diff --git a/packages/web/src/components/apps/use-logs.tsx b/packages/web/src/components/apps/use-logs.tsx index e42a8fe8..457d87db 100644 --- a/packages/web/src/components/apps/use-logs.tsx +++ b/packages/web/src/components/apps/use-logs.tsx @@ -1,22 +1,26 @@ import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { AppChannel } from '@/clients/websocket'; -import { PreviewStatusPayloadType } from '@srcbook/shared'; +import { DepsInstallLogPayloadType, PreviewLogPayloadType } from '@srcbook/shared'; export type LogMessage = { - type: 'npm_install_error' | 'vite_error'; // TODO: add more types like "warning" or "problem" + type: 'stderr' | 'stdout' | 'info'; + source: 'vite' | 'npm install'; timestamp: Date; - contents: string; + message: string; }; export interface LogsContextValue { logs: Array; clearLogs: () => void; - addError: (message: Omit) => void; unreadLogsCount: number; + panelIcon: 'default' | 'error'; + + addLog: (type: LogMessage['type'], source: LogMessage['source'], message: string) => void; open: boolean; togglePane: () => void; + closePane: () => void; } const LogsContext = createContext(undefined); @@ -29,42 +33,77 @@ type ProviderPropsType = { export function LogsProvider({ channel, children }: ProviderPropsType) { const [logs, setLogs] = useState>([]); const [unreadLogsCount, setUnreadLogsCount] = useState(0); + const [panelIcon, setPanelIcon] = useState('default'); const [open, setOpen] = useState(false); function clearLogs() { setLogs([]); + setPanelIcon('default'); setUnreadLogsCount(0); } - const addError = useCallback((error: Omit) => { - setLogs((logs) => [{ ...error, timestamp: new Date() }, ...logs]); - setUnreadLogsCount((n) => n + 1); - }, []); + const addLog = useCallback( + (type: LogMessage['type'], source: LogMessage['source'], message: LogMessage['message']) => { + setLogs((logs) => [...logs, { type, message, source, timestamp: new Date() }]); + if (type === 'stderr') { + setPanelIcon('error'); + } + setUnreadLogsCount((n) => n + 1); + }, + [], + ); function togglePane() { setOpen((n) => !n); + setPanelIcon('default'); + setUnreadLogsCount(0); + } + + function closePane() { + setOpen(false); + setPanelIcon('default'); setUnreadLogsCount(0); } - // If vite crashes, then create an error log + // As the server generates logs, show them in the logs panel useEffect(() => { - function onViteError(payload: PreviewStatusPayloadType) { - if (payload.status !== 'stopped' || payload.stoppedSuccessfully) { - return; + function onPreviewLog(payload: PreviewLogPayloadType) { + for (const row of payload.log.data.split('\n')) { + addLog(payload.log.type, 'vite', row); } - addError({ type: 'vite_error', contents: payload.logs ?? '' }); } - channel.on('preview:status', onViteError); + channel.on('preview:log', onPreviewLog); - return () => channel.off('preview:status', onViteError); - }, [channel, addError]); + function onDepsInstallLog(payload: DepsInstallLogPayloadType) { + for (const row of payload.log.data.split('\n')) { + addLog(payload.log.type, 'npm install', row); + } + } + channel.on('deps:install:log', onDepsInstallLog); - // TODO: if npm install fails, add an error log + return () => { + channel.off('preview:log', onPreviewLog); + channel.off('deps:install:log', onDepsInstallLog); + }; + }, [channel, addLog]); return ( - + {children} ); diff --git a/packages/web/src/components/apps/use-package-json.tsx b/packages/web/src/components/apps/use-package-json.tsx index 4bdbe84b..bcaddc00 100644 --- a/packages/web/src/components/apps/use-package-json.tsx +++ b/packages/web/src/components/apps/use-package-json.tsx @@ -19,6 +19,8 @@ export interface PackageJsonContextValue { installing: boolean; failed: boolean; output: Array; + showInstallModal: boolean; + setShowInstallModal: (value: boolean) => void; } const PackageJsonContext = createContext(undefined); @@ -32,8 +34,8 @@ export function PackageJsonProvider({ channel, children }: ProviderPropsType) { const [status, setStatus] = useState('idle'); const [output, setOutput] = useState>([]); const [nodeModulesExists, setNodeModulesExists] = useState(null); - - const { addError } = useLogs(); + const [showInstallModal, setShowInstallModal] = useState(false); + const { addLog } = useLogs(); useEffect(() => { channel.push('deps:status', {}); @@ -52,6 +54,12 @@ export function PackageJsonProvider({ channel, children }: ProviderPropsType) { const npmInstall = useCallback( async (packages?: Array) => { + addLog( + 'info', + 'npm install', + `Running ${!packages ? 'npm install' : `npm install ${packages.join(' ')}`}`, + ); + // NOTE: caching of the log output is required here because socket events that call callback // functions in here hold on to old scope values let contents = ''; @@ -63,15 +71,20 @@ export function PackageJsonProvider({ channel, children }: ProviderPropsType) { }; channel.on('deps:install:log', logCallback); - const statusCallback = ({ status }: DepsInstallStatusPayloadType) => { + const statusCallback = ({ status, code }: DepsInstallStatusPayloadType) => { channel.off('deps:install:log', logCallback); channel.off('deps:install:status', statusCallback); setStatus(status); + addLog( + 'info', + 'npm install', + `${!packages ? 'npm install' : `npm install ${packages.join(' ')}`} exited with status code ${code}`, + ); + if (status === 'complete') { resolve(); } else { - addError({ type: 'npm_install_error', contents }); reject(new Error(`Error running npm install: ${contents}`)); } }; @@ -82,7 +95,7 @@ export function PackageJsonProvider({ channel, children }: ProviderPropsType) { channel.push('deps:install', { packages }); }); }, - [channel, addError], + [channel, addLog], ); const clearNodeModules = useCallback(() => { @@ -98,6 +111,8 @@ export function PackageJsonProvider({ channel, children }: ProviderPropsType) { installing: status === 'installing', failed: status === 'failed', output, + showInstallModal, + setShowInstallModal, }; return {children}; diff --git a/packages/web/src/components/apps/use-preview.tsx b/packages/web/src/components/apps/use-preview.tsx index 9ca83fe5..959d6fc1 100644 --- a/packages/web/src/components/apps/use-preview.tsx +++ b/packages/web/src/components/apps/use-preview.tsx @@ -4,6 +4,7 @@ import { AppChannel } from '@/clients/websocket'; import { PreviewStatusPayloadType } from '@srcbook/shared'; import useEffectOnce from '@/components/use-effect-once'; import { usePackageJson } from './use-package-json'; +import { useLogs } from './use-logs'; export type PreviewStatusType = 'booting' | 'connecting' | 'running' | 'stopped'; @@ -28,6 +29,7 @@ export function PreviewProvider({ channel, children }: ProviderPropsType) { const [lastStoppedError, setLastStoppedError] = useState(null); const { npmInstall, nodeModulesExists } = usePackageJson(); + const { addLog } = useLogs(); useEffect(() => { function onStatusUpdate(payload: PreviewStatusPayloadType) { @@ -35,6 +37,8 @@ export function PreviewProvider({ channel, children }: ProviderPropsType) { setStatus(payload.status); if (payload.status === 'stopped' && !payload.stoppedSuccessfully) { + // FIXME: add log here + // addLog('info', 'npm install', `Running vite ${payload.XXX}`); setLastStoppedError(payload.logs ?? ''); } else { setLastStoppedError(null); @@ -50,6 +54,7 @@ export function PreviewProvider({ channel, children }: ProviderPropsType) { if (nodeModulesExists === false) { await npmInstall(); } + addLog('info', 'npm install', `Running vite`); channel.push('preview:start', {}); } diff --git a/packages/web/src/components/apps/workspace/editor.tsx b/packages/web/src/components/apps/workspace/editor.tsx index 2564633b..a442fc1e 100644 --- a/packages/web/src/components/apps/workspace/editor.tsx +++ b/packages/web/src/components/apps/workspace/editor.tsx @@ -23,7 +23,7 @@ export function Editor({ tab, onChangeTab, onShowPackagesPanel }: EditorProps) { }, [openedFile, onChangeTab]); return ( -
+
{tab === 'code' ? ( diff --git a/packages/web/src/components/install-package-modal.tsx b/packages/web/src/components/install-package-modal.tsx index bc4d3c66..20f384d6 100644 --- a/packages/web/src/components/install-package-modal.tsx +++ b/packages/web/src/components/install-package-modal.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { OutputType } from '@srcbook/components/src/types'; import { Loader2 } from 'lucide-react'; import { useDebounce } from 'use-debounce'; import { cn } from '@/lib/utils'; @@ -12,8 +13,6 @@ import { DialogTitle, } from '@srcbook/components/src/components/ui/dialog'; -import { usePackageJson } from './use-package-json'; - type NPMPackageType = { name: string; version: string; @@ -38,9 +37,15 @@ function getSelected(results: NPMPackageType[], selectedName: string, type: 'nex export default function InstallPackageModal({ open, setOpen, + npmInstall, + installing, + output, }: { open: boolean; setOpen: (val: boolean) => void; + npmInstall: (packages?: string[]) => void; + installing: boolean; + output: OutputType[]; }) { const [mode, setMode] = useState<'search' | 'loading' | 'success' | 'error'>('search'); const [query, setQuery] = useState(''); @@ -48,8 +53,6 @@ export default function InstallPackageModal({ const [pkg, setPkg] = useState(''); const [selectedName, setSelectedName] = useState(null); - const { npmInstall, installing, output } = usePackageJson(); - const [value] = useDebounce(query, 300); useEffect(() => { diff --git a/packages/web/src/routes/apps.tsx b/packages/web/src/routes/apps.tsx index 7609324b..fadf8370 100644 --- a/packages/web/src/routes/apps.tsx +++ b/packages/web/src/routes/apps.tsx @@ -5,18 +5,20 @@ import type { AppType, DirEntryType } from '@srcbook/shared'; import { loadApp, loadDirectory } from '@/clients/http/apps'; import Sidebar, { PanelType } from '@/components/apps/sidebar'; import { useEffect, useRef, useState } from 'react'; -import Statusbar from '@/components/apps/statusbar'; +import BottomDrawer from '@/components/apps/bottom-drawer'; import { AppChannel } from '@/clients/websocket'; import { FilesProvider, useFiles } from '@/components/apps/use-files'; import { Editor } from '@/components/apps/workspace/editor'; import { PreviewProvider } from '@/components/apps/use-preview'; import { LogsProvider } from '@/components/apps/use-logs'; -import { PackageJsonProvider } from '@/components/apps/use-package-json'; +import { PackageJsonProvider, usePackageJson } from '@/components/apps/use-package-json'; import { ChatPanel } from '@/components/chat'; import DiffModal from '@/components/apps/diff-modal'; import { FileDiffType } from '@srcbook/shared'; import EditorHeader, { EditorHeaderTab } from '@/components/apps/header'; import { AppProvider } from '@/components/apps/use-app'; +import InstallPackageModal from '@/components/install-package-modal'; +import { useHotkeys } from 'react-hotkeys-hook'; async function loader({ params }: LoaderFunctionArgs) { const [{ data: app }, { data: rootDirEntries }] = await Promise.all([ @@ -72,15 +74,29 @@ function Apps() { const { openedFile } = useFiles(); const [panel, setPanel] = useState(openedFile === null ? 'explorer' : null); + const { installing, npmInstall, output, showInstallModal, setShowInstallModal } = + usePackageJson(); const [diffModalProps, triggerDiffModal] = useState<{ files: FileDiffType[]; onUndoAll: () => void; } | null>(null); + useHotkeys('mod+i', () => { + setShowInstallModal(true); + }); + return ( <> {diffModalProps && triggerDiffModal(null)} />} + +
-
+
setPanel('packages')} /> - +
diff --git a/packages/web/src/routes/session.tsx b/packages/web/src/routes/session.tsx index 2d6aa39f..9df79995 100644 --- a/packages/web/src/routes/session.tsx +++ b/packages/web/src/routes/session.tsx @@ -14,7 +14,7 @@ import { } from '@srcbook/shared'; import { loadSession, loadSessions, getConfig } from '@/lib/server'; import type { SessionType, SettingsType } from '@/types'; -import { GenerateAICellType } from '@srcbook/components/src/types'; +import { GenerateAICellType, OutputType } from '@srcbook/components/src/types'; import { TitleCell, MarkdownCell } from '@srcbook/components'; import ControlledCodeCell from '@/components/cells/code'; import GenerateAiCell from '@/components/cells/generate-ai'; @@ -139,6 +139,7 @@ function Session(props: SessionProps) { failed: dependencyInstallFailed, outdated: outdatedDependencies, installing: installingDependencies, + output: dependencyInstallOutput, } = usePackageJson(); const [depsInstallModalOpen, setDepsInstallModalOpen] = useState(false); @@ -350,7 +351,13 @@ function Session(props: SessionProps) {
{!readOnly ? ( - + ) : null} {readOnly ? ( void }) { - const { open, onOpenChange } = props; +function PackageInstallModal(props: { + open: boolean; + onOpenChange: (value: boolean) => void; + installing: boolean; + npmInstall: (packages?: string[]) => void; + output: OutputType[]; +}) { + const { open, onOpenChange, installing, npmInstall, output } = props; useHotkeys('mod+i', () => { if (!open) { @@ -503,7 +516,15 @@ function PackageInstallModal(props: { open: boolean; onOpenChange: (value: boole } }); - return ; + return ( + + ); } SessionPage.loader = loader;