This commit is contained in:
2025-04-22 17:21:44 +02:00
parent f3b508c9f7
commit 9309509858
7 changed files with 814 additions and 198 deletions

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy, createEventDispatcher } from 'svelte'; import { onMount, onDestroy, createEventDispatcher } from 'svelte';
import { MudConnection } from '$lib/connection/MudConnection'; import { MudConnection } from '$lib/connection/MudConnection';
import { ConnectionManager, activeConnections } from '$lib/connection/ConnectionManager';
import { GmcpHandler } from '$lib/gmcp/GmcpHandler'; import { GmcpHandler } from '$lib/gmcp/GmcpHandler';
import { TriggerSystem } from '$lib/triggers/TriggerSystem'; import { TriggerSystem } from '$lib/triggers/TriggerSystem';
import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager'; import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager';
@@ -8,11 +9,13 @@
connections, connections,
connectionStatus, connectionStatus,
activeProfileId, activeProfileId,
activeProfile, activeProfile,
profiles,
addToOutputHistory, addToOutputHistory,
updateGmcpData, updateGmcpData,
accessibilitySettings, accessibilitySettings,
uiSettings uiSettings,
outputHistory
} from '$lib/stores/mudStore'; } from '$lib/stores/mudStore';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
@@ -24,6 +27,7 @@
// Local state // Local state
let connection: MudConnection | null = null; let connection: MudConnection | null = null;
let connectionManager: ConnectionManager | null = null;
let gmcpHandler: GmcpHandler | null = null; let gmcpHandler: GmcpHandler | null = null;
let triggerSystem: TriggerSystem | null = null; let triggerSystem: TriggerSystem | null = null;
let accessibilityManager: AccessibilityManager | null = null; let accessibilityManager: AccessibilityManager | null = null;
@@ -52,47 +56,95 @@
} }
} }
let unsubscribeFunctions = [];
onMount(() => { onMount(() => {
console.log(`MudConnection component mounted for profile: ${profileId}`);
// Initialize components // Initialize components
initializeComponents(); initializeComponents();
// Set up keyboard listener for speech control - use capture phase and make it top-level // Set up keyboard listener for speech control - use capture phase and make it top-level
document.addEventListener('keydown', handleKeyDown, true); // Using document instead of window document.addEventListener('keydown', handleKeyDown, true); // Using document instead of window
// Auto-connect if enabled // Get the connection status from the store
if (autoConnect) { const currentStatus = get(connectionStatus)[profileId];
console.log('Auto-connecting profile:', profileId);
connect(); // Check if we already have a connection in the ConnectionManager
if (currentStatus === 'connected' || currentStatus === 'connecting') {
console.log(`Connection exists for ${profileId}, getting from manager`);
const connectionManager = ConnectionManager.getInstance();
const existingConnection = connectionManager.getExistingConnection(profileId);
if (existingConnection) {
console.log(`Found existing connection for ${profileId}, updating local ref`);
// Update the connections store
connections.update(conns => ({
...conns,
[profileId]: existingConnection
}));
// Update local reference
connection = existingConnection;
// Re-attach listeners
setupConnectionListeners(existingConnection);
}
} else {
// Register this connection in the connections store with component instance
connections.update(conns => {
console.log(`Registering connection component for profile: ${profileId}`);
return {
...conns,
[profileId]: this
};
});
// Update connection status - ensure UI shows status as 'disconnected' at start
if (!currentStatus) {
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'disconnected'`);
return {
...statuses,
[profileId]: 'disconnected'
};
});
}
} }
// Update connection status - ensure UI shows status as 'disconnected' at start // Dump the current connection status for debugging
connectionStatus.update(statuses => ({ setTimeout(() => {
...statuses, console.log('Current connection status at initialization:', get(connectionStatus));
[profileId]: 'disconnected' }, 100);
}));
// Auto-connect if enabled and not already connected - with a slight delay
if (autoConnect && currentStatus !== 'connected') {
console.log('Auto-connecting profile:', profileId);
setTimeout(() => connect(), 100);
}
}); });
onDestroy(() => { onDestroy(() => {
console.log(`MudConnection component being destroyed for profile: ${profileId}`);
// Remove keyboard listener // Remove keyboard listener
document.removeEventListener('keydown', handleKeyDown, true); // Match document and capture phase document.removeEventListener('keydown', handleKeyDown, true); // Match document and capture phase
// Clean up connection // Don't disconnect the connection - it should persist even when the component is unmounted
if (connection) { // Only remove this component instance from the local connections store (not the ConnectionManager)
connection.disconnect();
}
// Remove from connections store
connections.update(conns => { connections.update(conns => {
const newConns = { ...conns }; const newConns = { ...conns };
delete newConns[profileId]; delete newConns[profileId];
return newConns; return newConns;
}); });
// Update connection status // Clean up subscriptions
connectionStatus.update(statuses => { unsubscribeFunctions.forEach(unsubFn => {
const newStatuses = { ...statuses }; if (typeof unsubFn === 'function') {
delete newStatuses[profileId]; try {
return newStatuses; unsubFn();
} catch (error) {
console.error('Error during unsubscribe:', error);
}
}
}); });
}); });
@@ -100,6 +152,9 @@
* Initialize connection components * Initialize connection components
*/ */
function initializeComponents() { function initializeComponents() {
// Get singleton instance of ConnectionManager
connectionManager = ConnectionManager.getInstance();
// Create GMCP handler // Create GMCP handler
gmcpHandler = new GmcpHandler(); gmcpHandler = new GmcpHandler();
@@ -111,6 +166,47 @@
// Set up event listeners // Set up event listeners
setupEventListeners(); setupEventListeners();
// Get or create connection from ConnectionManager
updateConnectionFromStore();
}
/**
* Update local connection reference from store
*/
function updateConnectionFromStore() {
// Subscribe to activeConnections to get updates
const unsubscribe = activeConnections.subscribe(connections => {
connection = connections[profileId] || null;
if (connection) {
// Re-attach event listeners when connection changes
setupConnectionListeners(connection);
}
});
// Add to unsubscribe functions
unsubscribeFunctions.push(unsubscribe);
}
/**
* Set up listeners for a specific connection
*/
function setupConnectionListeners(conn: MudConnection) {
// First remove ALL event listeners to prevent duplicates
console.log('Setting up connection listeners and removing old ones');
conn.off('received');
conn.off('sent');
conn.off('connected');
conn.off('disconnected');
conn.off('error');
// Add new listeners
conn.on('received', handleReceived);
conn.on('sent', handleSent);
conn.on('connected', handleConnected);
conn.on('disconnected', handleDisconnected);
conn.on('error', handleError);
} }
/** /**
@@ -164,6 +260,7 @@
console.error('Error updating TTS setting:', error); console.error('Error updating TTS setting:', error);
} }
}); });
unsubscribeFunctions.push(unsubscribeTts);
// Set up only GMCP debugging subscription // Set up only GMCP debugging subscription
let previousGmcpDebug = $uiSettings.debugGmcp; let previousGmcpDebug = $uiSettings.debugGmcp;
@@ -172,70 +269,82 @@
previousGmcpDebug = settings.debugGmcp; previousGmcpDebug = settings.debugGmcp;
} }
}); });
unsubscribeFunctions.push(unsubscribeUiSettings);
// Clean up subscriptions on component destruction
onDestroy(() => {
unsubscribeUiSettings();
unsubscribeTts();
});
} }
/** /**
* Connect to the MUD server * Connect to the MUD server
*/ */
export function connect() { export function connect() {
const profile = get(activeProfile);
console.log('Connecting to profile:', profile);
if (!profile) {
addToOutputHistory('Error: No active profile selected.');
console.error('No active profile selected');
return;
}
// Update connection status
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'connecting'
}));
try { try {
addToOutputHistory(`Connecting to ${profile.host}:${profile.port}...`); // First check if we are already connected
const currentStatus = get(connectionStatus)[profileId];
if (currentStatus === 'connected' || currentStatus === 'connecting') {
console.log(`Already ${currentStatus} for profile ${profileId}, not connecting again`);
return;
}
// Create connection // Find the profile by ID - don't rely on activeProfile
connection = new MudConnection({ const allProfiles = get(profiles);
const profile = allProfiles.find(p => p.id === profileId);
console.log(`Connecting to profile ${profileId}:`, profile);
if (!profile) {
addToOutputHistory(`Error: Profile ${profileId} not found.`);
console.error(`Profile ${profileId} not found`);
return;
}
// Update connection status
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'connecting'`);
return {
...statuses,
[profileId]: 'connecting'
};
});
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) {
addToOutputHistory(`Connecting to ${profile.host}:${profile.port}...`);
}
// Use the connection manager to get or create a connection
if (!connectionManager) {
connectionManager = ConnectionManager.getInstance();
}
// Connect using the connection manager
connectionManager.connect(profileId, {
host: profile.host, host: profile.host,
port: profile.port, port: profile.port,
useSSL: profile.useSSL, useSSL: profile.useSSL,
gmcpHandler: gmcpHandler || undefined gmcpHandler: gmcpHandler || undefined
}); });
console.log('Connection object created:', connection); // Update local connection reference
setTimeout(() => {
updateConnectionFromStore();
}, 100);
// Set up connection event listeners
connection.on('connected', handleConnected);
connection.on('disconnected', handleDisconnected);
connection.on('error', handleError);
connection.on('received', handleReceived);
connection.on('sent', handleSent);
// Add to connections store
connections.update(conns => ({
...conns,
[profileId]: connection as MudConnection
}));
// Connect
connection.connect();
} catch (error) { } catch (error) {
console.error('Failed to connect:', error); console.error(`Failed to connect to ${profileId}:`, error);
connectionStatus.update(statuses => ({ connectionStatus.update(statuses => {
...statuses, return {
[profileId]: 'error' ...statuses,
})); [profileId]: 'error'
};
});
addToOutputHistory(`Error connecting to ${profile.host}:${profile.port} - ${error}`); const errorMsg = `Error connecting to profile ${profileId}: ${error}`;
console.error(errorMsg);
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) {
addToOutputHistory(errorMsg);
}
} }
} }
@@ -243,23 +352,46 @@
* Disconnect from the MUD server * Disconnect from the MUD server
*/ */
export function disconnect() { export function disconnect() {
if (connection) { if (!connectionManager) {
connection.disconnect(); connectionManager = ConnectionManager.getInstance();
} }
connectionManager.disconnect(profileId);
console.log(`Disconnected profile ${profileId}`);
} }
/** /**
* Handle connection established * Handle connection established
*/ */
function handleConnected() { function handleConnected() {
const profile = get(activeProfile); try {
// Find the profile by ID
connectionStatus.update(statuses => ({ const allProfiles = get(profiles);
...statuses, const profile = allProfiles.find(p => p.id === profileId);
[profileId]: 'connected'
})); console.log(`Profile ${profileId} connected:`, profile);
addToOutputHistory(`Connected to ${profile?.host}:${profile?.port}`); // Update connection status GLOBALLY, not just for this profile
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'connected'`);
console.log('Previous connection statuses:', statuses);
const newStatuses = {
...statuses,
[profileId]: 'connected'
};
console.log('New connection statuses:', newStatuses);
return newStatuses;
});
// Dump the current connection status for debugging
setTimeout(() => {
console.log('Current connection status after connect:', get(connectionStatus));
}, 100);
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) {
addToOutputHistory(`Connected to ${profile?.host}:${profile?.port}`);
}
// Handle auto-login if enabled // Handle auto-login if enabled
if (profile?.autoLogin?.enabled) { if (profile?.autoLogin?.enabled) {
@@ -290,18 +422,30 @@
} }
dispatch('connected'); dispatch('connected');
} catch (error) {
console.error(`Error handling connection for profile ${profileId}:`, error);
}
} }
/** /**
* Handle connection closed * Handle connection closed
*/ */
function handleDisconnected() { function handleDisconnected() {
connectionStatus.update(statuses => ({ console.log(`Profile ${profileId} disconnected`);
...statuses,
[profileId]: 'disconnected' connectionStatus.update(statuses => {
})); console.log(`Setting connection status for ${profileId} to 'disconnected'`);
return {
...statuses,
[profileId]: 'disconnected'
};
});
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) {
addToOutputHistory('Disconnected from server.');
}
addToOutputHistory('Disconnected from server.');
dispatch('disconnected'); dispatch('disconnected');
} }
@@ -309,19 +453,27 @@
* Handle connection error * Handle connection error
*/ */
function handleError(error: any) { function handleError(error: any) {
connectionStatus.update(statuses => ({ console.log(`Profile ${profileId} connection error:`, error);
...statuses,
[profileId]: 'error' connectionStatus.update(statuses => {
})); console.log(`Setting connection status for ${profileId} to 'error'`);
return {
...statuses,
[profileId]: 'error'
};
});
// Format the error message for display // Format the error message for display
const errorMessage = typeof error === 'object' ? const errorMessage = typeof error === 'object' ?
(error.message || JSON.stringify(error)) : (error.message || JSON.stringify(error)) :
String(error); String(error);
addToOutputHistory(`Connection error: ${errorMessage}`, false, [ // Only add to output history if this is the active profile
{ pattern: 'Connection error', color: '#ff5555', isRegex: false } if (get(activeProfileId) === profileId) {
]); addToOutputHistory(`Connection error: ${errorMessage}`, false, [
{ pattern: 'Connection error', color: '#ff5555', isRegex: false }
]);
}
dispatch('error', { error }); dispatch('error', { error });
} }
@@ -330,31 +482,69 @@
* Handle received data * Handle received data
*/ */
function handleReceived(text: string) { function handleReceived(text: string) {
// Add to output history first console.log(`Profile ${profileId} received data, active profile: ${get(activeProfileId)}`);
addToOutputHistory(text);
// Process triggers with safe error handling try {
if (triggerSystem) { // First check if this is the active profile - only handle messages for the active profile
try { if (get(activeProfileId) !== profileId) {
triggerSystem.processTriggers(text); console.log(`Ignoring received data for inactive profile ${profileId}`);
} catch (error) { return;
console.error('Error processing triggers:', error);
} }
}
// First, ensure this profile has an output history entry
// Try to use text-to-speech if enabled const history = get(outputHistory);
if (accessibilityManager && $accessibilitySettings.textToSpeech) { if (!history[profileId] || !Array.isArray(history[profileId])) {
// Use a small timeout to avoid UI blocking console.log(`Creating fresh output history for profile ${profileId}`);
setTimeout(() => { outputHistory.update(h => ({
...h,
[profileId]: []
}));
}
// Create the output item
const outputItem = {
id: `output-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
text,
timestamp: Date.now(),
isInput: false,
highlights: []
};
// Add directly to this profile's history
outputHistory.update(history => {
console.log(`Adding text to history for profile ${profileId}`);
const profileHistory = history[profileId] || [];
return {
...history,
[profileId]: [...profileHistory, outputItem]
};
});
// Process triggers with safe error handling
if (triggerSystem) {
try { try {
accessibilityManager.speak(text); triggerSystem.processTriggers(text);
} catch (error) { } catch (error) {
console.error('Error using text-to-speech:', error); console.error(`Error processing triggers for ${profileId}:`, error);
} }
}, 10); }
}
dispatch('received', { text }); // Try to use text-to-speech if enabled
if (accessibilityManager && $accessibilitySettings.textToSpeech && get(activeProfileId) === profileId) {
// Use a small timeout to avoid UI blocking
setTimeout(() => {
try {
accessibilityManager.speak(text);
} catch (error) {
console.error('Error using text-to-speech:', error);
}
}, 10);
}
dispatch('received', { text });
} catch (error) {
console.error(`Error handling received data for profile ${profileId}:`, error);
}
} }
/** /**

View File

@@ -2,96 +2,232 @@
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import MudTerminal from './MudTerminal.svelte'; import MudTerminal from './MudTerminal.svelte';
import MudConnection from './MudConnection.svelte'; import MudConnection from './MudConnection.svelte';
import { activeProfileId, profiles, connectionStatus, addToOutputHistory } from '$lib/stores/mudStore'; import { ConnectionManager } from '$lib/connection/ConnectionManager';
import { activeProfileId, profiles, connectionStatus, addToOutputHistory, outputHistory, connections } from '$lib/stores/mudStore';
import { get } from 'svelte/store';
import type { MudProfile } from '$lib/profiles/ProfileManager'; import type { MudProfile } from '$lib/profiles/ProfileManager';
// Make sure tabs are defined to prevent errors
$: safeTabs = tabs || [];
$: safeConnectionStatus = $connectionStatus || {};
// Local state // Local state
let tabs: { id: string; profile: MudProfile }[] = []; let tabs: { id: string; profile: MudProfile }[] = [];
let activeTab: string | null = null; let activeTab: string | null = null;
let autoConnectOnStart = true; // Auto-connect to the active profile on start let autoConnectOnStart = true; // Auto-connect to the active profile on start
// Component references // Handle keyboard navigation in the tab bar
let connections: { [key: string]: any } = {}; function handleTabsKeydown(event: KeyboardEvent) {
if (!safeTabs || safeTabs.length === 0) return;
// Get current tab index
const currentIndex = safeTabs.findIndex(tab => tab.id === activeTab);
switch (event.key) {
case 'ArrowRight':
case 'ArrowDown':
// Move to next tab
event.preventDefault();
const nextIndex = (currentIndex + 1) % safeTabs.length;
changeTab(safeTabs[nextIndex].id);
document.getElementById(`tab-${safeTabs[nextIndex].id}`)?.focus();
break;
case 'ArrowLeft':
case 'ArrowUp':
// Move to previous tab
event.preventDefault();
const prevIndex = (currentIndex - 1 + safeTabs.length) % safeTabs.length;
changeTab(safeTabs[prevIndex].id);
document.getElementById(`tab-${safeTabs[prevIndex].id}`)?.focus();
break;
case 'Home':
// Move to first tab
event.preventDefault();
changeTab(safeTabs[0].id);
document.getElementById(`tab-${safeTabs[0].id}`)?.focus();
break;
case 'End':
// Move to last tab
event.preventDefault();
const lastIndex = safeTabs.length - 1;
changeTab(safeTabs[lastIndex].id);
document.getElementById(`tab-${safeTabs[lastIndex].id}`)?.focus();
break;
default:
// Let other keys function normally
break;
}
}
// Initialize tabs from profiles // Initialize tabs from profiles
function initializeTabs() { function initializeTabs() {
// Get the profiles from the store try {
const allProfiles = $profiles || []; // Get the profiles from the store
console.log('Initializing tabs with profiles:', allProfiles); const allProfiles = $profiles || [];
console.log('Initializing tabs with profiles:', allProfiles);
if (allProfiles.length === 0) {
console.warn('No profiles available to create tabs');
tabs = [];
// If no tabs are available, we should show a message to create a profile
addToOutputHistory('No profiles available. Please create a profile in the sidebar.');
return;
}
tabs = allProfiles.map(profile => ({
id: profile.id,
profile
}));
console.log('Created tabs:', tabs);
// Set the active tab from the store or default to the first tab
activeTab = $activeProfileId || (tabs.length > 0 ? tabs[0].id : null);
console.log('Active tab set to:', activeTab);
// Update the active profile ID in the store
if (activeTab) {
activeProfileId.set(activeTab);
// Auto-connect if enabled if (allProfiles.length === 0) {
if (autoConnectOnStart && tabs.length > 0 && !$connectionStatus[activeTab]) { console.warn('No profiles available to create tabs');
console.log(`Auto-connecting to tab: ${activeTab}`); tabs = [];
setTimeout(() => connectToMud(activeTab), 500); // If no tabs are available, we should show a message to create a profile
addToOutputHistory('No profiles available. Please create a profile in the sidebar.');
return;
} }
// Create new tabs array with all profile information
const newTabs = allProfiles.map(profile => ({
id: profile.id,
profile
}));
// Update tabs array
tabs = newTabs;
console.log('Created tabs:', tabs);
// Set the active tab from the store or default to the first tab
const newActiveTab = $activeProfileId || (tabs.length > 0 ? tabs[0].id : null);
if (newActiveTab !== activeTab) {
console.log(`Changing active tab from ${activeTab} to ${newActiveTab}`);
activeTab = newActiveTab;
}
console.log('Active tab set to:', activeTab);
// Update the active profile ID in the store
if (activeTab) {
console.log(`Setting active profile ID to ${activeTab}`);
activeProfileId.set(activeTab);
// Auto-connect if enabled
if (autoConnectOnStart && tabs.length > 0 && !$connectionStatus[activeTab]) {
console.log(`Auto-connecting to tab: ${activeTab}`);
setTimeout(() => connectToMud(activeTab), 1000);
}
}
// Force a rerender of the tabs
setTimeout(() => {
console.log('Forcing tab rerender');
tabs = [...tabs];
}, 0);
} catch (error) {
console.error('Error initializing tabs:', error);
} }
// Force a rerender of the tabs
setTimeout(() => {
console.log('Forcing tab rerender');
tabs = [...tabs];
}, 0);
} }
// Handle tab changes // Handle tab changes
function changeTab(tabId: string) { function changeTab(tabId: string) {
activeTab = tabId; try {
activeProfileId.set(tabId); console.log(`Changing tab to: ${tabId}`);
// First, update activeProfileId in the store to trigger reactive updates
activeProfileId.set(tabId);
// Then update the local state
activeTab = tabId;
// Log the connections state for debugging
console.log('Current connection status:', $connectionStatus);
// Make sure the connection store is updated with the active profile
const connectionManager = ConnectionManager.getInstance();
const existingConnection = connectionManager.getExistingConnection(tabId);
// Update the connections store - this ensures the connection is associated with the profile ID
if (existingConnection) {
// Check if we need to ensure event listeners are properly set up
const isReused = get(connections)[tabId] === existingConnection;
console.log(`Connection is ${isReused ? 'being reused' : 'new'} for tab ${tabId}`);
connections.update(conns => ({
...conns,
[tabId]: existingConnection
}));
}
// Since we're only rendering the active tab now, we don't need to force updates
// to other components, the component itself will be created/destroyed
console.log(`Tab change complete - active tab is now: ${activeTab}`);
} catch (error) {
console.error(`Error changing tab to ${tabId}:`, error);
}
} }
// Connect to a MUD server // Connect to a MUD server
function connectToMud(profileId: string) { function connectToMud(profileId: string) {
const connectionComponent = connections[profileId]; try {
if (connectionComponent) { console.log(`Attempting to connect to MUD for profile: ${profileId}`);
connectionComponent.connect();
// Use the ConnectionManager instead of the local connections object
const connectionManager = ConnectionManager.getInstance();
// Find the profile
const profile = $profiles.find(p => p.id === profileId);
if (!profile) {
console.error(`No profile found for ID: ${profileId}`);
return;
}
// Connect using the connection manager
connectionManager.connect(profileId, {
host: profile.host,
port: profile.port,
useSSL: profile.useSSL
});
} catch (error) {
console.error(`Error connecting to MUD for profile ${profileId}:`, error);
} }
} }
// Disconnect from a MUD server // Disconnect from a MUD server
function disconnectFromMud(profileId: string) { function disconnectFromMud(profileId: string) {
const connectionComponent = connections[profileId]; try {
if (connectionComponent) { console.log(`Attempting to disconnect from MUD for profile: ${profileId}`);
connectionComponent.disconnect();
// Use the ConnectionManager
const connectionManager = ConnectionManager.getInstance();
connectionManager.disconnect(profileId);
} catch (error) {
console.error(`Error disconnecting from MUD for profile ${profileId}:`, error);
} }
} }
// Close a tab // Close a tab
function closeTab(profileId: string) { function closeTab(profileId: string) {
// Disconnect if connected try {
if ($connectionStatus[profileId] === 'connected') { console.log(`Attempting to close tab for profile: ${profileId}`);
disconnectFromMud(profileId);
} // Safely check the connection status
const status = $connectionStatus[profileId];
// Remove the tab console.log(`Current connection status for ${profileId}: ${status}`);
tabs = tabs.filter(tab => tab.id !== profileId);
// Disconnect if connected
// If the closed tab was active, activate another tab if (status === 'connected') {
if (activeTab === profileId) { console.log(`Disconnecting ${profileId} before closing tab`);
activeTab = tabs.length > 0 ? tabs[0].id : null; disconnectFromMud(profileId);
activeProfileId.set(activeTab); }
// Remove the tab
console.log(`Removing tab for profile ${profileId}`);
tabs = tabs.filter(tab => tab.id !== profileId);
// If the closed tab was active, activate another tab
if (activeTab === profileId) {
console.log(`Closed tab was active, finding new active tab`);
const newActiveTab = tabs.length > 0 ? tabs[0].id : null;
console.log(`Setting new active tab to: ${newActiveTab}`);
activeTab = newActiveTab;
activeProfileId.set(newActiveTab);
}
} catch (error) {
console.error(`Error closing tab for profile ${profileId}:`, error);
} }
} }
@@ -119,13 +255,30 @@
} }
} }
// Update when profiles change // Update when profiles change or active profile changes
$: if ($profiles) { $: if ($profiles) {
console.log('Profiles updated in store, reinitializing tabs:', $profiles); console.log('Profiles updated in store, reinitializing tabs:', $profiles);
initializeTabs(); initializeTabs();
} }
// When activeProfileId changes externally, ensure activeTab is in sync
$: if ($activeProfileId && $activeProfileId !== activeTab) {
console.log(`activeProfileId changed to ${$activeProfileId}, updating activeTab to match`);
activeTab = $activeProfileId;
// Force a UI update
setTimeout(() => {
tabs = [...tabs];
}, 0);
}
onMount(() => { onMount(() => {
console.log('MudMdi component mounted');
// Make sure connectionStatus store is initialized
if (!$connectionStatus) {
connectionStatus.set({});
}
// Initial setup of tabs // Initial setup of tabs
initializeTabs(); initializeTabs();
@@ -138,8 +291,8 @@
</script> </script>
<div class="mud-mdi"> <div class="mud-mdi">
<div class="mud-mdi-tabs" role="tablist"> <div class="mud-mdi-tabs" role="tablist" aria-label="MUD connections" tabindex="0" on:keydown={handleTabsKeydown}>
{#each tabs as tab} {#each safeTabs as tab}
<button <button
class="mud-mdi-tab" class="mud-mdi-tab"
class:active={activeTab === tab.id} class:active={activeTab === tab.id}
@@ -179,13 +332,15 @@
</div> </div>
</div> </div>
{:else} {:else}
{#each tabs as tab (tab.id)} <!-- Only render the active tab -->
{#each safeTabs.filter(tab => tab.id === activeTab) as tab (tab.id)}
<div <div
class="mud-mdi-pane" class="mud-mdi-pane"
style="display: {activeTab === tab.id ? 'flex' : 'none'}"
role="tabpanel" role="tabpanel"
id={`panel-${tab.id}`} id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`} aria-labelledby={`tab-${tab.id}`}
data-tab-id={tab.id}
data-active-tab={activeTab}
> >
<div class="mud-mdi-pane-header"> <div class="mud-mdi-pane-header">
<div class="mud-mdi-pane-title"> <div class="mud-mdi-pane-title">
@@ -226,7 +381,6 @@
<MudConnection <MudConnection
profileId={tab.id} profileId={tab.id}
bind:this={connections[tab.id]}
/> />
<MudTerminal <MudTerminal
autofocus={activeTab === tab.id} autofocus={activeTab === tab.id}
@@ -391,12 +545,7 @@
} }
.mud-mdi-pane { .mud-mdi-pane {
position: absolute; display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex !important; /* Force display flex */
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
width: 100%; width: 100%;

View File

@@ -1,10 +1,14 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy, createEventDispatcher } from 'svelte'; import { onMount, onDestroy, createEventDispatcher } from 'svelte';
import { activeOutputHistory, addToOutputHistory, addToInputHistory, navigateInputHistory, activeInputHistoryIndex, activeConnection, uiSettings, accessibilitySettings, activeInputHistory } from '$lib/stores/mudStore'; import { activeOutputHistory, addToOutputHistory, addToInputHistory, navigateInputHistory, activeInputHistoryIndex, activeConnection, uiSettings, accessibilitySettings, activeInputHistory, activeProfileId, connectionStatus } from '$lib/stores/mudStore';
import { tick } from 'svelte'; import { tick } from 'svelte';
import AnsiToHtml from 'ansi-to-html'; import AnsiToHtml from 'ansi-to-html';
import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager'; import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager';
// Create safe defaults for reactivity
$: safeOutputHistory = $activeOutputHistory || [];
$: safeActiveProfileId = $activeProfileId || null;
// Create event dispatcher // Create event dispatcher
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -92,7 +96,7 @@
} }
// Handle input submission // Handle input submission
function handleSubmit(event: Event) { async function handleSubmit(event: Event) {
event.preventDefault(); event.preventDefault();
if (!currentInput.trim()) return; if (!currentInput.trim()) return;
@@ -112,17 +116,35 @@
} else { } else {
addToOutputHistory(`> ********`, true); addToOutputHistory(`> ********`, true);
} }
// Get the currently active profile id
const profileId = $activeProfileId;
// Send the command if connected // Send the command if connected
if ($activeConnection) { if (profileId) {
try { // Get connection status for this profile
$activeConnection.send(currentInput); const status = $connectionStatus[profileId];
} catch (error) {
console.error('Error sending command:', error); if (status === 'connected') {
addToOutputHistory(`Error sending command: ${error}`, false, [{ pattern: 'Error', color: '#ff5555', isRegex: false }]); try {
// Try using the activeConnection first
if ($activeConnection) {
$activeConnection.send(currentInput);
} else {
// If not available, use the ConnectionManager directly
const { ConnectionManager } = await import('$lib/connection/ConnectionManager');
const connectionManager = ConnectionManager.getInstance();
connectionManager.send(profileId, currentInput);
}
} catch (error) {
console.error('Error sending command:', error);
addToOutputHistory(`Error sending command: ${error}`, false, [{ pattern: 'Error', color: '#ff5555', isRegex: false }]);
}
} else {
addToOutputHistory(`Not connected to any MUD server. Status: ${status || 'disconnected'}`, false, [{ pattern: 'Not connected', color: '#ff5555', isRegex: false }]);
} }
} else { } else {
addToOutputHistory('Not connected to any MUD server.', false, [{ pattern: 'Not connected', color: '#ff5555', isRegex: false }]); addToOutputHistory('No profile selected.', false, [{ pattern: 'No profile', color: '#ff5555', isRegex: false }]);
} }
// Clear the input // Clear the input
@@ -266,16 +288,35 @@
} }
} }
// Watch output history changes to scroll to bottom // Watch output history and profile changes to update display
$: { $: {
if ($activeOutputHistory) { if (safeOutputHistory.length > 0) {
console.log('Active output history changed, updating terminal display');
scrollToBottom(); scrollToBottom();
// Update message elements when output history changes // Update message elements when output history changes
setTimeout(updateMessageElements, 0); setTimeout(updateMessageElements, 0);
} }
} }
$: {
// Every time the active profile changes, update the terminal content
if ($activeProfileId) {
console.log(`Active profile is now: ${$activeProfileId}, updating output display`);
updateOutputDisplay();
}
}
// Function to update the displayed output based on the active profile
async function updateOutputDisplay() {
console.log('Updating output display...');
await tick(); // Wait for Svelte to update
scrollToBottom();
updateMessageElements();
}
onMount(() => { onMount(() => {
console.log('MudTerminal component mounted for profile:', $activeProfileId);
// Initialize accessibility manager // Initialize accessibility manager
accessibilityManager = new AccessibilityManager(); accessibilityManager = new AccessibilityManager();
@@ -318,7 +359,7 @@
tabindex="0" tabindex="0"
on:keydown={handleOutputKeyDown} on:keydown={handleOutputKeyDown}
style="font-family: {$uiSettings.font}; font-size: {$accessibilitySettings.fontSize}px; line-height: {$accessibilitySettings.lineSpacing};"> style="font-family: {$uiSettings.font}; font-size: {$accessibilitySettings.fontSize}px; line-height: {$accessibilitySettings.lineSpacing};">
{#each $activeOutputHistory as item (item.id)} {#each safeOutputHistory as item (item.id)}
<!-- For input lines, keep them as a single block --> <!-- For input lines, keep them as a single block -->
{#if item.isInput} {#if item.isInput}
<div class="mud-terminal-line mud-input-line" tabindex="-1"> <div class="mud-terminal-line mud-input-line" tabindex="-1">

View File

@@ -0,0 +1,191 @@
import { writable, get } from 'svelte/store';
import { MudConnection } from './MudConnection';
import type { GmcpHandler } from '$lib/gmcp/GmcpHandler';
import { connectionStatus } from '$lib/stores/mudStore';
// Store for active MUD connections (persistent across tab switches)
export const activeConnections = writable<{ [key: string]: MudConnection }>({});
/**
* ConnectionManager - Singleton service to manage MUD connections
* This ensures connections stay alive even when components are unmounted during tab switches
*/
export class ConnectionManager {
private static instance: ConnectionManager;
// Private constructor for singleton pattern
private constructor() {
console.log('ConnectionManager initialized');
}
/**
* Get the singleton instance of ConnectionManager
*/
public static getInstance(): ConnectionManager {
if (!ConnectionManager.instance) {
ConnectionManager.instance = new ConnectionManager();
}
return ConnectionManager.instance;
}
/**
* Get an existing connection without creating a new one
*/
public getExistingConnection(profileId: string): MudConnection | null {
const connections = get(activeConnections);
return connections[profileId] || null;
}
/**
* Create a new connection or return an existing one
*/
public getConnection(profileId: string, options: {
host: string;
port: number;
useSSL?: boolean;
gmcpHandler?: GmcpHandler;
}): MudConnection {
// Get current connections
const connections = get(activeConnections);
// Check if a connection already exists for this profile
if (connections[profileId]) {
console.log(`Returning existing connection for profile ${profileId}`);
return connections[profileId];
}
// Create a new connection
console.log(`Creating new connection for profile ${profileId}`);
const connection = new MudConnection({
host: options.host,
port: options.port,
useSSL: options.useSSL,
gmcpHandler: options.gmcpHandler
});
// Set up event handlers
this.setupConnectionEvents(connection, profileId);
// Store the connection
activeConnections.update(connections => ({
...connections,
[profileId]: connection
}));
// Return the connection
return connection;
}
/**
* Connect to a MUD server
*/
public connect(profileId: string, options: {
host: string;
port: number;
useSSL?: boolean;
gmcpHandler?: GmcpHandler;
}): void {
const connection = this.getConnection(profileId, options);
// Update connection status
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'connecting'
}));
// Connect
connection.connect();
}
/**
* Disconnect from a MUD server
*/
public disconnect(profileId: string): void {
const connections = get(activeConnections);
if (connections[profileId]) {
connections[profileId].disconnect();
// Update connection status
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'disconnected'
}));
}
}
/**
* Send text to a MUD server
*/
public send(profileId: string, text: string): void {
const connections = get(activeConnections);
if (connections[profileId]) {
connections[profileId].send(text);
}
}
/**
* Close a connection and remove it
*/
public closeConnection(profileId: string): void {
const connections = get(activeConnections);
if (connections[profileId]) {
// Disconnect first
connections[profileId].disconnect();
// Then remove from store
activeConnections.update(connections => {
const newConnections = { ...connections };
delete newConnections[profileId];
return newConnections;
});
// Update connection status
connectionStatus.update(statuses => {
const newStatuses = { ...statuses };
delete newStatuses[profileId];
return newStatuses;
});
}
}
/**
* Set up event handlers for a connection
*/
private setupConnectionEvents(connection: MudConnection, profileId: string): void {
// Handle connection established
connection.on('connected', () => {
console.log(`ConnectionManager: Connection established for profile ${profileId}`);
// Update connection status
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'connected'
}));
});
// Handle connection closed
connection.on('disconnected', () => {
console.log(`ConnectionManager: Connection closed for profile ${profileId}`);
// Update connection status
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'disconnected'
}));
});
// Handle connection error
connection.on('error', (error) => {
console.error(`ConnectionManager: Connection error for profile ${profileId}:`, error);
// Update connection status
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'error'
}));
});
}
}

View File

@@ -49,10 +49,30 @@ export const activeProfile = derived(
// Derived store for active connection // Derived store for active connection
export const activeConnection = derived( export const activeConnection = derived(
[connections, activeProfileId], [connections, activeProfileId, connectionStatus],
([$connections, $activeProfileId]) => { ([$connections, $activeProfileId, $connectionStatus]) => {
// First check if there's an active profile ID
if (!$activeProfileId) return null; if (!$activeProfileId) return null;
return $connections[$activeProfileId] || null;
// Check for the connection in the local connections store
const localConnection = $connections[$activeProfileId];
if (localConnection) return localConnection;
// If not in local store but the connection status shows connected/connecting,
// use the ConnectionManager's connection instead
if ($connectionStatus[$activeProfileId] === 'connected' || $connectionStatus[$activeProfileId] === 'connecting') {
try {
// We can't do a dynamic import in a derived store, so we'll return null
// and let the components handle getting the connection from the manager directly
console.log('Connection not in local store but status indicates it exists in manager');
return null;
} catch(e) {
console.error('Error getting connection from manager:', e);
return null;
}
}
return null;
} }
); );

View File

@@ -18,9 +18,14 @@ export class EventEmitter {
return this.on(event, onceWrapper); return this.on(event, onceWrapper);
} }
off(event: string, listener: Function): this { off(event: string, listener?: Function): this {
if (this.events[event]) { if (this.events[event]) {
this.events[event] = this.events[event].filter(l => l !== listener); if (listener) {
this.events[event] = this.events[event].filter(l => l !== listener);
} else {
// Remove all listeners for this event if no specific listener is provided
this.events[event] = [];
}
} }
return this; return this;
} }

View File

@@ -18,7 +18,10 @@
triggers, triggers,
uiSettings, uiSettings,
accessibilitySettings, accessibilitySettings,
addToOutputHistory addToOutputHistory,
connectionStatus,
connections,
outputHistory
} from '$lib/stores/mudStore'; } from '$lib/stores/mudStore';
import { ProfileManager } from '$lib/profiles/ProfileManager'; import { ProfileManager } from '$lib/profiles/ProfileManager';
import type { MudProfile } from '$lib/profiles/ProfileManager'; import type { MudProfile } from '$lib/profiles/ProfileManager';
@@ -78,6 +81,14 @@
triggerSystem = new TriggerSystem(); triggerSystem = new TriggerSystem();
console.log('Trigger system initialized'); console.log('Trigger system initialized');
// Initialize the connection status store
connectionStatus.set({});
console.log('Connection status store initialized');
// Clear connections store to start fresh
connections.set({});
console.log('Connections store cleared');
// Load profiles first to ensure we have them // Load profiles first to ensure we have them
console.log('Loading profiles...'); console.log('Loading profiles...');
loadProfiles(); loadProfiles();
@@ -114,6 +125,15 @@
console.log('No active profile selected, setting first profile as active'); console.log('No active profile selected, setting first profile as active');
activeProfileId.set($profiles[0].id); activeProfileId.set($profiles[0].id);
} }
// Initialize output history for all profiles
const outputHistoryObject = {};
$profiles.forEach(profile => {
outputHistoryObject[profile.id] = [];
});
outputHistory.set(outputHistoryObject);
console.log('Output history initialized for all profiles');
} catch (error) { } catch (error) {
console.error('Error during page initialization:', error); console.error('Error during page initialization:', error);
console.error('Error details:', error.message, error.stack); console.error('Error details:', error.message, error.stack);