diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/index.tsx b/src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/index.tsx index fbeaa11a..7e4435c7 100644 --- a/src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/index.tsx @@ -3,13 +3,13 @@ import { useNavigate, useLocation } from 'react-router'; import { useSessions } from '@/state/useSessions'; import type { ModelOption } from '@/state/useModels'; import { useOpenCodeSessions } from '@/state/useOpenCodeSessions'; -import type { useClaude } from '../useClaude'; +import type { useClaude, Attachment } from 'plugins/Chat/client'; +import { EmbeddableChat } from 'plugins/Chat/client'; import { useSlashCommands } from '@/state/useSlashCommands'; import { Card } from '@/components/Card'; import { SessionBar } from './SessionBar'; -import { EmbeddableChat } from '../EmbeddableChat'; -export type { Attachment } from '../EmbeddableChat'; +export type { Attachment }; type ChatPanelProps = { chat: ReturnType; diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/index.tsx b/src/apps/officer-web/Screens/Dashboard/Chat/index.tsx index 5e632755..dfbbc2fd 100644 --- a/src/apps/officer-web/Screens/Dashboard/Chat/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Chat/index.tsx @@ -1,13 +1,11 @@ import { useState } from 'react'; import { useParams } from 'react-router'; import { useQueryClient } from '@tanstack/react-query'; +import { useClaude, useOpenCode, type SessionEntry } from 'plugins/Chat/client'; import { DashboardLayout } from '../Layout'; -import { useClaude } from './useClaude'; -import { useOpenCode } from './useOpenCode'; import { ChatPanel } from './ChatPanel'; import { SessionList } from './SessionList'; import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels'; -import type { SessionEntry } from './types'; export const ClaudeSessions = () => { return ( diff --git a/src/apps/officer-web/Screens/Dashboard/Home/ChatLauncher.tsx b/src/apps/officer-web/Screens/Dashboard/Home/ChatLauncher.tsx index 559ad055..b117a486 100644 --- a/src/apps/officer-web/Screens/Dashboard/Home/ChatLauncher.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Home/ChatLauncher.tsx @@ -26,7 +26,7 @@ import { } from '@/components/ui/dropdown-menu'; import { useSettings } from '@/state/useSettings'; import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels'; -import type { Attachment } from '../Chat/ChatPanel'; +import type { Attachment } from 'plugins/Chat/client'; export const ChatLauncher = () => { const navigate = useNavigate(); diff --git a/src/apps/officer-web/Screens/Dashboard/ResourcePage.tsx b/src/apps/officer-web/Screens/Dashboard/ResourcePage.tsx index 515bae0b..fa4ad370 100644 --- a/src/apps/officer-web/Screens/Dashboard/ResourcePage.tsx +++ b/src/apps/officer-web/Screens/Dashboard/ResourcePage.tsx @@ -10,10 +10,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } f import { useClient } from 'hooks/useClient'; import { useVisibleClaudeModels } from '@/state/useModels'; import { Card } from '@/components/Card'; +import { useClaude, EmbeddableChat, type ChatMessage } from 'plugins/Chat/client'; import { DashboardLayout } from './Layout'; -import { useClaude } from './Chat/useClaude'; -import { EmbeddableChat } from './Chat/EmbeddableChat'; -import type { ChatMessage } from './Chat/types'; type ResourceSummary = { dirName: string; diff --git a/src/apps/officer-web/Screens/Dashboard/TaskLogs/index.tsx b/src/apps/officer-web/Screens/Dashboard/TaskLogs/index.tsx index 55e23b1e..ec2bf640 100644 --- a/src/apps/officer-web/Screens/Dashboard/TaskLogs/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/TaskLogs/index.tsx @@ -3,8 +3,7 @@ import { Search, AlertCircle, CheckCircle2, Clock, ArrowLeft } from 'lucide-reac import { useClient } from 'hooks/useClient'; import { DashboardLayout } from '../Layout'; import { Card } from '@/components/Card'; -import { MessageBubble } from '../Chat/MessageBubble'; -import type { ChatMessage } from '../Chat/types'; +import { MessageBubble, type ChatMessage } from 'plugins/Chat/client'; type LogMetadata = { filename: string; diff --git a/src/apps/officer-web/state/useOpenCodeSessions.ts b/src/apps/officer-web/state/useOpenCodeSessions.ts index c82e4583..40b9cfc8 100644 --- a/src/apps/officer-web/state/useOpenCodeSessions.ts +++ b/src/apps/officer-web/state/useOpenCodeSessions.ts @@ -1,4 +1,4 @@ -import type { SessionEntry, ChatMessage } from '@/Screens/Dashboard/Chat/types'; +import type { SessionEntry, ChatMessage } from 'plugins/Chat/client'; import { useAuth } from 'hooks/useAuth'; import { useClient } from 'hooks/useClient'; import { useQuery, useQueryClient } from '@tanstack/react-query'; diff --git a/src/apps/officer-web/state/useSessions.ts b/src/apps/officer-web/state/useSessions.ts index f65883a5..c3266700 100644 --- a/src/apps/officer-web/state/useSessions.ts +++ b/src/apps/officer-web/state/useSessions.ts @@ -1,4 +1,4 @@ -import type { SessionEntry, ChatMessage } from '@/Screens/Dashboard/Chat/types'; +import type { SessionEntry, ChatMessage } from 'plugins/Chat/client'; import type { SlashCommandResult } from './useSlashCommands'; import { useAuth } from 'hooks/useAuth'; import { useClient } from 'hooks/useClient'; diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/EmbeddableChat.tsx b/src/workspaces/plugins/Chat/client/EmbeddableChat.tsx similarity index 98% rename from src/apps/officer-web/Screens/Dashboard/Chat/EmbeddableChat.tsx rename to src/workspaces/plugins/Chat/client/EmbeddableChat.tsx index e4804a13..ad5c2b25 100644 --- a/src/apps/officer-web/Screens/Dashboard/Chat/EmbeddableChat.tsx +++ b/src/workspaces/plugins/Chat/client/EmbeddableChat.tsx @@ -4,8 +4,8 @@ import { toast } from 'sonner'; import { useClient } from 'hooks/useClient'; import type { ModelOption } from '@/state/useModels'; import type { useClaude } from './useClaude'; -import { MessageList } from './ChatPanel/MessageList'; -import { InputArea } from './ChatPanel/InputArea'; +import { MessageList } from './MessageList'; +import { InputArea } from './InputArea'; export type Attachment = | { type: 'webpage'; url: string; title: string; content: string; attachmentId: string; loading?: boolean } diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/InputArea.tsx b/src/workspaces/plugins/Chat/client/InputArea.tsx similarity index 98% rename from src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/InputArea.tsx rename to src/workspaces/plugins/Chat/client/InputArea.tsx index cc2c24e9..703b5847 100644 --- a/src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/InputArea.tsx +++ b/src/workspaces/plugins/Chat/client/InputArea.tsx @@ -10,8 +10,8 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import type { ModelOption } from '@/state/useModels'; -import type { ChatMessage } from '../types'; -import type { Attachment } from './index'; +import type { ChatMessage } from './types'; +import type { Attachment } from './EmbeddableChat'; import { Settings } from './Settings'; type InputAreaProps = { diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/MessageBubble.tsx b/src/workspaces/plugins/Chat/client/MessageBubble.tsx similarity index 100% rename from src/apps/officer-web/Screens/Dashboard/Chat/MessageBubble.tsx rename to src/workspaces/plugins/Chat/client/MessageBubble.tsx diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/MessageList.tsx b/src/workspaces/plugins/Chat/client/MessageList.tsx similarity index 93% rename from src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/MessageList.tsx rename to src/workspaces/plugins/Chat/client/MessageList.tsx index 752eef39..dfce8c38 100644 --- a/src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/MessageList.tsx +++ b/src/workspaces/plugins/Chat/client/MessageList.tsx @@ -1,7 +1,7 @@ import type { RefObject } from 'react'; import { ArrowDown } from 'lucide-react'; -import type { ChatMessage } from '../types'; -import { MessageBubble, StreamingBubble } from '../MessageBubble'; +import type { ChatMessage } from './types'; +import { MessageBubble, StreamingBubble } from './MessageBubble'; type MessageListProps = { messages: ChatMessage[]; diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/OpenCodeModelPicker.tsx b/src/workspaces/plugins/Chat/client/OpenCodeModelPicker.tsx similarity index 100% rename from src/apps/officer-web/Screens/Dashboard/Chat/OpenCodeModelPicker.tsx rename to src/workspaces/plugins/Chat/client/OpenCodeModelPicker.tsx diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/QuestionActivity.tsx b/src/workspaces/plugins/Chat/client/QuestionActivity.tsx similarity index 100% rename from src/apps/officer-web/Screens/Dashboard/Chat/QuestionActivity.tsx rename to src/workspaces/plugins/Chat/client/QuestionActivity.tsx diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/Settings.tsx b/src/workspaces/plugins/Chat/client/Settings.tsx similarity index 96% rename from src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/Settings.tsx rename to src/workspaces/plugins/Chat/client/Settings.tsx index 4e5a992a..a211b3d2 100644 --- a/src/apps/officer-web/Screens/Dashboard/Chat/ChatPanel/Settings.tsx +++ b/src/workspaces/plugins/Chat/client/Settings.tsx @@ -1,8 +1,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { useAuth } from 'hooks/useAuth'; import type { ModelOption } from '@/state/useModels'; -import type { ChatMessage } from '../types'; -import { OpenCodeModelPicker } from '../OpenCodeModelPicker'; +import type { ChatMessage } from './types'; +import { OpenCodeModelPicker } from './OpenCodeModelPicker'; type SettingsProps = { provider: 'claude' | 'opencode'; diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/ToolActivity.tsx b/src/workspaces/plugins/Chat/client/ToolActivity.tsx similarity index 100% rename from src/apps/officer-web/Screens/Dashboard/Chat/ToolActivity.tsx rename to src/workspaces/plugins/Chat/client/ToolActivity.tsx diff --git a/src/workspaces/plugins/Chat/client/index.ts b/src/workspaces/plugins/Chat/client/index.ts new file mode 100644 index 00000000..2cc184b9 --- /dev/null +++ b/src/workspaces/plugins/Chat/client/index.ts @@ -0,0 +1,11 @@ +export { EmbeddableChat, type Attachment } from './EmbeddableChat'; +export { MessageList } from './MessageList'; +export { InputArea } from './InputArea'; +export { Settings } from './Settings'; +export { MessageBubble, StreamingBubble } from './MessageBubble'; +export { ToolActivity } from './ToolActivity'; +export { QuestionActivity } from './QuestionActivity'; +export { OpenCodeModelPicker } from './OpenCodeModelPicker'; +export { useClaude } from './useClaude'; +export { useOpenCode } from './useOpenCode'; +export type { ChatMessage, SessionEntry, ServerMessage, TaskInfo } from './types'; diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/types.ts b/src/workspaces/plugins/Chat/client/types.ts similarity index 100% rename from src/apps/officer-web/Screens/Dashboard/Chat/types.ts rename to src/workspaces/plugins/Chat/client/types.ts diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/useClaude.ts b/src/workspaces/plugins/Chat/client/useClaude.ts similarity index 100% rename from src/apps/officer-web/Screens/Dashboard/Chat/useClaude.ts rename to src/workspaces/plugins/Chat/client/useClaude.ts diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/useOpenCode.ts b/src/workspaces/plugins/Chat/client/useOpenCode.ts similarity index 100% rename from src/apps/officer-web/Screens/Dashboard/Chat/useOpenCode.ts rename to src/workspaces/plugins/Chat/client/useOpenCode.ts diff --git a/src/workspaces/plugins/Chat/index.ts b/src/workspaces/plugins/Chat/index.ts new file mode 100644 index 00000000..dda14b28 --- /dev/null +++ b/src/workspaces/plugins/Chat/index.ts @@ -0,0 +1,7 @@ +export * from './client'; + +export const plugin = { + id: 'Chat', + name: 'Chat', + description: 'Embeddable chat UI', +}; diff --git a/src/workspaces/plugins/FileBrowser/client/Screen/TaskRunnerModal.tsx b/src/workspaces/plugins/FileBrowser/client/Screen/TaskRunnerModal.tsx index 74e86f89..b5fa8979 100644 --- a/src/workspaces/plugins/FileBrowser/client/Screen/TaskRunnerModal.tsx +++ b/src/workspaces/plugins/FileBrowser/client/Screen/TaskRunnerModal.tsx @@ -3,12 +3,9 @@ import { X } from 'lucide-react'; import { Dialog, DialogOverlay, DialogPortal } from '@/components/ui/dialog'; import * as DialogPrimitive from '@radix-ui/react-dialog'; import { cardStyle } from '@/components/Card'; -import { useClaude } from '@/Screens/Dashboard/Chat/useClaude'; -import { useOpenCode } from '@/Screens/Dashboard/Chat/useOpenCode'; -import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat'; +import { useClaude, useOpenCode, EmbeddableChat, type TaskInfo } from 'plugins/Chat/client'; import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels'; import { useSettings } from '@/state/useSettings'; -import type { TaskInfo } from '@/Screens/Dashboard/Chat/types'; import type { TaskSummary } from '../state/useTasks'; const playDing = () => { diff --git a/src/workspaces/plugins/Terminal/client/Screen/index.tsx b/src/workspaces/plugins/Terminal/client/Screen/index.tsx index 9462aa86..6cb89dff 100644 --- a/src/workspaces/plugins/Terminal/client/Screen/index.tsx +++ b/src/workspaces/plugins/Terminal/client/Screen/index.tsx @@ -1,108 +1,10 @@ -import { useEffect, useRef } from 'react'; -import { Terminal as XTerm } from '@xterm/xterm'; -import { FitAddon } from '@xterm/addon-fit'; -import '@xterm/xterm/css/xterm.css'; import { DashboardLayout } from '@/Screens/Dashboard/Layout'; -import { useMounted } from 'hooks/useMounted'; +import { TerminalView } from '../TerminalView'; export const Terminal = () => { - const containerRef = useRef(null); - const termRef = useRef(null); - const wsRef = useRef(null); - const fitAddonRef = useRef(null); - const isMounted = useMounted(); - - useEffect(() => { - if (!isMounted) return; - const container = containerRef.current; - if (!container) return; - - // Deferred init for StrictMode compatibility - const initTimeout = setTimeout(() => { - const term = new XTerm({ - cursorBlink: true, - fontSize: 14, - fontFamily: 'Menlo, Monaco, "Courier New", monospace', - theme: { - background: '#1a1a2e', - foreground: '#e0e0e0', - cursor: '#e0e0e0', - selectionBackground: '#3a3a5e', - }, - }); - - const fitAddon = new FitAddon(); - term.loadAddon(fitAddon); - term.open(container); - fitAddon.fit(); - - termRef.current = term; - fitAddonRef.current = fitAddon; - - // WebSocket connection - const token = localStorage.getItem('BEARER_TOKEN'); - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const ws = new WebSocket(`${protocol}//${window.location.host}/api/terminal/ws?token=${token}`); - wsRef.current = ws; - - ws.addEventListener('open', () => { - // Send initial size - ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); - }); - - ws.addEventListener('message', (ev) => { - try { - const msg = JSON.parse(ev.data); - if (msg.type === 'output') { - term.write(msg.data); - } else if (msg.type === 'exit') { - term.write('\r\n[Process exited]\r\n'); - } - } catch { - // ignore - } - }); - - ws.addEventListener('close', () => { - term.write('\r\n[Disconnected]\r\n'); - }); - - // Send terminal input to server - term.onData((data) => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'input', data })); - } - }); - - // ResizeObserver for auto-fitting - const resizeObserver = new ResizeObserver(() => { - fitAddon.fit(); - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); - } - }); - resizeObserver.observe(container); - - // Store observer for cleanup - (container as any).__resizeObserver = resizeObserver; - }, 0); - - return () => { - clearTimeout(initTimeout); - const resizeObserver = (container as any).__resizeObserver as ResizeObserver | undefined; - resizeObserver?.disconnect(); - delete (container as any).__resizeObserver; - wsRef.current?.close(); - wsRef.current = null; - termRef.current?.dispose(); - termRef.current = null; - fitAddonRef.current = null; - }; - }, [isMounted]); - return ( -
+ ); }; diff --git a/src/workspaces/plugins/Terminal/client/TerminalView.tsx b/src/workspaces/plugins/Terminal/client/TerminalView.tsx new file mode 100644 index 00000000..dae6f18e --- /dev/null +++ b/src/workspaces/plugins/Terminal/client/TerminalView.tsx @@ -0,0 +1,188 @@ +import type { CSSProperties } from 'react'; +import { useEffect, useRef } from 'react'; +import { Terminal as XTerm } from '@xterm/xterm'; +import { FitAddon } from '@xterm/addon-fit'; +import '@xterm/xterm/css/xterm.css'; +import { useMounted } from 'hooks/useMounted'; + +type TerminalTheme = { + background?: string; + foreground?: string; + cursor?: string; + selectionBackground?: string; +}; + +export type TerminalViewProps = { + className?: string; + style?: CSSProperties; + wsPath?: string; + fontSize?: number; + fontFamily?: string; + theme?: TerminalTheme; + autoFocus?: boolean; + onReady?: (term: XTerm) => void; + onExit?: () => void; + onDisconnect?: () => void; +}; + +const DEFAULT_THEME: Required = { + background: '#1a1a2e', + foreground: '#e0e0e0', + cursor: '#e0e0e0', + selectionBackground: '#3a3a5e', +}; + +const buildWsUrl = (wsPath: string) => { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const token = localStorage.getItem('BEARER_TOKEN') ?? ''; + const separator = wsPath.includes('?') ? '&' : '?'; + return `${protocol}//${window.location.host}${wsPath}${separator}token=${encodeURIComponent(token)}`; +}; + +export const TerminalView = ({ + className, + style, + wsPath = '/api/terminal/ws', + fontSize = 14, + fontFamily = 'Menlo, Monaco, "Courier New", monospace', + theme, + autoFocus = true, + onReady, + onExit, + onDisconnect, +}: TerminalViewProps) => { + const containerRef = useRef(null); + const termRef = useRef(null); + const wsRef = useRef(null); + const fitAddonRef = useRef(null); + const isMounted = useMounted(); + const onReadyRef = useRef(onReady); + const onExitRef = useRef(onExit); + const onDisconnectRef = useRef(onDisconnect); + + onReadyRef.current = onReady; + onExitRef.current = onExit; + onDisconnectRef.current = onDisconnect; + + const background = theme?.background ?? DEFAULT_THEME.background; + const foreground = theme?.foreground ?? DEFAULT_THEME.foreground; + const cursor = theme?.cursor ?? DEFAULT_THEME.cursor; + const selectionBackground = theme?.selectionBackground ?? DEFAULT_THEME.selectionBackground; + + useEffect(() => { + if (!isMounted) return; + const container = containerRef.current; + if (!container) return; + + let disposed = false; + const initTimeout = setTimeout(() => { + if (disposed) return; + const term = new XTerm({ + cursorBlink: true, + fontSize, + fontFamily, + theme: { + background, + foreground, + cursor, + selectionBackground, + }, + }); + + const fitAddon = new FitAddon(); + term.loadAddon(fitAddon); + term.open(container); + fitAddon.fit(); + if (autoFocus) term.focus(); + + termRef.current = term; + fitAddonRef.current = fitAddon; + onReadyRef.current?.(term); + + const ws = new WebSocket(buildWsUrl(wsPath)); + wsRef.current = ws; + + const handleOpen = () => { + ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); + }; + + const handleMessage = (ev: MessageEvent) => { + try { + const msg = JSON.parse(ev.data as string); + if (msg.type === 'output') { + term.write(msg.data); + } else if (msg.type === 'exit') { + term.write('\r\n[Process exited]\r\n'); + onExitRef.current?.(); + } + } catch { + // ignore + } + }; + + const handleClose = () => { + term.write('\r\n[Disconnected]\r\n'); + onDisconnectRef.current?.(); + }; + + ws.addEventListener('open', handleOpen); + ws.addEventListener('message', handleMessage); + ws.addEventListener('close', handleClose); + + const dataDisposable = term.onData((data) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'input', data })); + } + }); + + const resizeObserver = new ResizeObserver(() => { + fitAddon.fit(); + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); + } + }); + resizeObserver.observe(container); + + const cleanup = () => { + dataDisposable.dispose(); + resizeObserver.disconnect(); + ws.removeEventListener('open', handleOpen); + ws.removeEventListener('message', handleMessage); + ws.removeEventListener('close', handleClose); + }; + + (container as any).__terminalCleanup = cleanup; + }, 0); + + return () => { + disposed = true; + clearTimeout(initTimeout); + const cleanup = (container as any).__terminalCleanup as (() => void) | undefined; + cleanup?.(); + delete (container as any).__terminalCleanup; + wsRef.current?.close(); + wsRef.current = null; + termRef.current?.dispose(); + termRef.current = null; + fitAddonRef.current = null; + }; + }, [ + isMounted, + wsPath, + fontSize, + fontFamily, + background, + foreground, + cursor, + selectionBackground, + autoFocus, + ]); + + return ( +
+ ); +}; diff --git a/src/workspaces/plugins/Terminal/client/index.ts b/src/workspaces/plugins/Terminal/client/index.ts index 9cf0fcbc..21942f1b 100644 --- a/src/workspaces/plugins/Terminal/client/index.ts +++ b/src/workspaces/plugins/Terminal/client/index.ts @@ -1 +1,2 @@ export { Terminal as Screen } from './Screen'; +export { TerminalView } from './TerminalView'; diff --git a/src/workspaces/plugins/package.json b/src/workspaces/plugins/package.json index 84f51582..b03f904f 100644 --- a/src/workspaces/plugins/package.json +++ b/src/workspaces/plugins/package.json @@ -9,6 +9,8 @@ "./FileBrowser/server": "./FileBrowser/server/index.ts", "./Terminal": "./Terminal/index.ts", "./Terminal/client": "./Terminal/client/index.ts", - "./Terminal/server": "./Terminal/server/index.ts" + "./Terminal/server": "./Terminal/server/index.ts", + "./Chat": "./Chat/index.ts", + "./Chat/client": "./Chat/client/index.ts" } }