Chat and Terminal as plugins
This commit is contained in:
@@ -3,13 +3,13 @@ import { useNavigate, useLocation } from 'react-router';
|
|||||||
import { useSessions } from '@/state/useSessions';
|
import { useSessions } from '@/state/useSessions';
|
||||||
import type { ModelOption } from '@/state/useModels';
|
import type { ModelOption } from '@/state/useModels';
|
||||||
import { useOpenCodeSessions } from '@/state/useOpenCodeSessions';
|
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 { useSlashCommands } from '@/state/useSlashCommands';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { SessionBar } from './SessionBar';
|
import { SessionBar } from './SessionBar';
|
||||||
import { EmbeddableChat } from '../EmbeddableChat';
|
|
||||||
|
|
||||||
export type { Attachment } from '../EmbeddableChat';
|
export type { Attachment };
|
||||||
|
|
||||||
type ChatPanelProps = {
|
type ChatPanelProps = {
|
||||||
chat: ReturnType<typeof useClaude>;
|
chat: ReturnType<typeof useClaude>;
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useClaude, useOpenCode, type SessionEntry } from 'plugins/Chat/client';
|
||||||
import { DashboardLayout } from '../Layout';
|
import { DashboardLayout } from '../Layout';
|
||||||
import { useClaude } from './useClaude';
|
|
||||||
import { useOpenCode } from './useOpenCode';
|
|
||||||
import { ChatPanel } from './ChatPanel';
|
import { ChatPanel } from './ChatPanel';
|
||||||
import { SessionList } from './SessionList';
|
import { SessionList } from './SessionList';
|
||||||
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
||||||
import type { SessionEntry } from './types';
|
|
||||||
|
|
||||||
export const ClaudeSessions = () => {
|
export const ClaudeSessions = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useSettings } from '@/state/useSettings';
|
import { useSettings } from '@/state/useSettings';
|
||||||
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
||||||
import type { Attachment } from '../Chat/ChatPanel';
|
import type { Attachment } from 'plugins/Chat/client';
|
||||||
|
|
||||||
export const ChatLauncher = () => {
|
export const ChatLauncher = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -10,10 +10,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } f
|
|||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { useVisibleClaudeModels } from '@/state/useModels';
|
import { useVisibleClaudeModels } from '@/state/useModels';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
|
import { useClaude, EmbeddableChat, type ChatMessage } from 'plugins/Chat/client';
|
||||||
import { DashboardLayout } from './Layout';
|
import { DashboardLayout } from './Layout';
|
||||||
import { useClaude } from './Chat/useClaude';
|
|
||||||
import { EmbeddableChat } from './Chat/EmbeddableChat';
|
|
||||||
import type { ChatMessage } from './Chat/types';
|
|
||||||
|
|
||||||
type ResourceSummary = {
|
type ResourceSummary = {
|
||||||
dirName: string;
|
dirName: string;
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { Search, AlertCircle, CheckCircle2, Clock, ArrowLeft } from 'lucide-reac
|
|||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { DashboardLayout } from '../Layout';
|
import { DashboardLayout } from '../Layout';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { MessageBubble } from '../Chat/MessageBubble';
|
import { MessageBubble, type ChatMessage } from 'plugins/Chat/client';
|
||||||
import type { ChatMessage } from '../Chat/types';
|
|
||||||
|
|
||||||
type LogMetadata = {
|
type LogMetadata = {
|
||||||
filename: string;
|
filename: string;
|
||||||
|
|||||||
@@ -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 { useAuth } from 'hooks/useAuth';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|||||||
@@ -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 type { SlashCommandResult } from './useSlashCommands';
|
||||||
import { useAuth } from 'hooks/useAuth';
|
import { useAuth } from 'hooks/useAuth';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@ import { toast } from 'sonner';
|
|||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import type { ModelOption } from '@/state/useModels';
|
import type { ModelOption } from '@/state/useModels';
|
||||||
import type { useClaude } from './useClaude';
|
import type { useClaude } from './useClaude';
|
||||||
import { MessageList } from './ChatPanel/MessageList';
|
import { MessageList } from './MessageList';
|
||||||
import { InputArea } from './ChatPanel/InputArea';
|
import { InputArea } from './InputArea';
|
||||||
|
|
||||||
export type Attachment =
|
export type Attachment =
|
||||||
| { type: 'webpage'; url: string; title: string; content: string; attachmentId: string; loading?: boolean }
|
| { type: 'webpage'; url: string; title: string; content: string; attachmentId: string; loading?: boolean }
|
||||||
+2
-2
@@ -10,8 +10,8 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import type { ModelOption } from '@/state/useModels';
|
import type { ModelOption } from '@/state/useModels';
|
||||||
import type { ChatMessage } from '../types';
|
import type { ChatMessage } from './types';
|
||||||
import type { Attachment } from './index';
|
import type { Attachment } from './EmbeddableChat';
|
||||||
import { Settings } from './Settings';
|
import { Settings } from './Settings';
|
||||||
|
|
||||||
type InputAreaProps = {
|
type InputAreaProps = {
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import { ArrowDown } from 'lucide-react';
|
import { ArrowDown } from 'lucide-react';
|
||||||
import type { ChatMessage } from '../types';
|
import type { ChatMessage } from './types';
|
||||||
import { MessageBubble, StreamingBubble } from '../MessageBubble';
|
import { MessageBubble, StreamingBubble } from './MessageBubble';
|
||||||
|
|
||||||
type MessageListProps = {
|
type MessageListProps = {
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { useAuth } from 'hooks/useAuth';
|
import { useAuth } from 'hooks/useAuth';
|
||||||
import type { ModelOption } from '@/state/useModels';
|
import type { ModelOption } from '@/state/useModels';
|
||||||
import type { ChatMessage } from '../types';
|
import type { ChatMessage } from './types';
|
||||||
import { OpenCodeModelPicker } from '../OpenCodeModelPicker';
|
import { OpenCodeModelPicker } from './OpenCodeModelPicker';
|
||||||
|
|
||||||
type SettingsProps = {
|
type SettingsProps = {
|
||||||
provider: 'claude' | 'opencode';
|
provider: 'claude' | 'opencode';
|
||||||
@@ -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';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './client';
|
||||||
|
|
||||||
|
export const plugin = {
|
||||||
|
id: 'Chat',
|
||||||
|
name: 'Chat',
|
||||||
|
description: 'Embeddable chat UI',
|
||||||
|
};
|
||||||
@@ -3,12 +3,9 @@ import { X } from 'lucide-react';
|
|||||||
import { Dialog, DialogOverlay, DialogPortal } from '@/components/ui/dialog';
|
import { Dialog, DialogOverlay, DialogPortal } from '@/components/ui/dialog';
|
||||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { cardStyle } from '@/components/Card';
|
import { cardStyle } from '@/components/Card';
|
||||||
import { useClaude } from '@/Screens/Dashboard/Chat/useClaude';
|
import { useClaude, useOpenCode, EmbeddableChat, type TaskInfo } from 'plugins/Chat/client';
|
||||||
import { useOpenCode } from '@/Screens/Dashboard/Chat/useOpenCode';
|
|
||||||
import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat';
|
|
||||||
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
||||||
import { useSettings } from '@/state/useSettings';
|
import { useSettings } from '@/state/useSettings';
|
||||||
import type { TaskInfo } from '@/Screens/Dashboard/Chat/types';
|
|
||||||
import type { TaskSummary } from '../state/useTasks';
|
import type { TaskSummary } from '../state/useTasks';
|
||||||
|
|
||||||
const playDing = () => {
|
const playDing = () => {
|
||||||
|
|||||||
@@ -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 { DashboardLayout } from '@/Screens/Dashboard/Layout';
|
||||||
import { useMounted } from 'hooks/useMounted';
|
import { TerminalView } from '../TerminalView';
|
||||||
|
|
||||||
export const Terminal = () => {
|
export const Terminal = () => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const termRef = useRef<XTerm | null>(null);
|
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
|
||||||
const fitAddonRef = useRef<FitAddon | null>(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 (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div ref={containerRef} className="h-full w-full p-2" style={{ backgroundColor: '#1a1a2e' }} />
|
<TerminalView className="h-full w-full p-2" />
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<TerminalTheme> = {
|
||||||
|
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<HTMLDivElement>(null);
|
||||||
|
const termRef = useRef<XTerm | null>(null);
|
||||||
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||||
|
const isMounted = useMounted();
|
||||||
|
const onReadyRef = useRef<TerminalViewProps['onReady']>(onReady);
|
||||||
|
const onExitRef = useRef<TerminalViewProps['onExit']>(onExit);
|
||||||
|
const onDisconnectRef = useRef<TerminalViewProps['onDisconnect']>(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 (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={className}
|
||||||
|
style={{ backgroundColor: background, ...style }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1 +1,2 @@
|
|||||||
export { Terminal as Screen } from './Screen';
|
export { Terminal as Screen } from './Screen';
|
||||||
|
export { TerminalView } from './TerminalView';
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
"./FileBrowser/server": "./FileBrowser/server/index.ts",
|
"./FileBrowser/server": "./FileBrowser/server/index.ts",
|
||||||
"./Terminal": "./Terminal/index.ts",
|
"./Terminal": "./Terminal/index.ts",
|
||||||
"./Terminal/client": "./Terminal/client/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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user