ui: Refactor models store, MCP service, and gate logs behind VITE_DEBUG (#23236)
* refactor: Scope console logs to `DEV` + `VITE_DEBUG` env vars * refactor: skip MCP proxy probe when no server requires it * refactor: suppress expected disconnect errors during MCP client shutdown * refactor: Deduplicate requests * refactor: deduplicate model fetching across ROUTER and MODEL modes * refactor: Clean up models logic * chore: Add `.env.example` file * refactor: replace client-side CORS proxy probe with server status flag * refactor: Post-review fixes * test: add vitest client setup with API fetch mocks
This commit is contained in:
@@ -392,7 +392,7 @@ export class MCPService {
|
||||
|
||||
const url = new URL(config.url);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||
console.log(`[MCPService] Creating WebSocket transport for ${url.href}`);
|
||||
}
|
||||
|
||||
@@ -413,12 +413,12 @@ export class MCPService {
|
||||
onLog
|
||||
);
|
||||
|
||||
if (useProxy && import.meta.env.DEV) {
|
||||
if (useProxy && import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||
console.log(`[MCPService] Using CORS proxy for ${config.url} -> ${url.href}`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (import.meta.env.DEV) {
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||
console.log(`[MCPService] Creating StreamableHTTP transport for ${url.href}`);
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ export class MCPService {
|
||||
)
|
||||
);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||
console.log(`[MCPService][${serverName}] Creating transport...`);
|
||||
}
|
||||
|
||||
@@ -560,6 +560,22 @@ export class MCPService {
|
||||
);
|
||||
|
||||
const runtimeErrorHandler = (error: Error) => {
|
||||
// Ignore errors that are expected when the SDK's transport is closed,
|
||||
// or when connecting to servers that don't support SSE (stateless-only
|
||||
// endpoints returning 405). The SDK wraps the original AbortError in
|
||||
// a new Error with the message "SSE stream disconnected: AbortError",
|
||||
// and also produces "Cannot cancel a stream locked by a reader".
|
||||
// DOMException is thrown by the browser when aborting fetch requests.
|
||||
const msg = error.message || String(error);
|
||||
if (
|
||||
error.name === 'AbortError' ||
|
||||
error instanceof DOMException ||
|
||||
msg.includes('SSE stream disconnected') ||
|
||||
msg.includes('stream locked by a reader') ||
|
||||
msg.includes('The operation was aborted')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
console.error(`[MCPService][${serverName}] Protocol error after initialize:`, error);
|
||||
};
|
||||
|
||||
@@ -658,7 +674,10 @@ export class MCPService {
|
||||
this.createLog(MCPConnectionPhase.LISTING_TOOLS, 'Listing available tools...')
|
||||
);
|
||||
|
||||
console.log(`[MCPService][${serverName}] Connected, listing tools...`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||
console.log(`[MCPService][${serverName}] Connected, listing tools...`);
|
||||
}
|
||||
|
||||
const tools = await this.listTools({
|
||||
client,
|
||||
transport,
|
||||
@@ -680,10 +699,11 @@ export class MCPService {
|
||||
`Connection established with ${tools.length} tools (${connectionTimeMs}ms)`
|
||||
)
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[MCPService][${serverName}] Initialization complete with ${tools.length} tools in ${connectionTimeMs}ms`
|
||||
);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||
console.log(
|
||||
`[MCPService][${serverName}] Initialization complete with ${tools.length} tools in ${connectionTimeMs}ms`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
client,
|
||||
@@ -709,9 +729,22 @@ export class MCPService {
|
||||
* @param connection - The active MCP connection to close
|
||||
*/
|
||||
static async disconnect(connection: MCPConnection): Promise<void> {
|
||||
console.log(`[MCPService][${connection.serverName}] Disconnecting...`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||
console.log(`[MCPService][${connection.serverName}] Disconnecting...`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Prevent reconnection on voluntary disconnect
|
||||
// Terminate the session first for streamable-http transports to cleanly
|
||||
// close streams, matching the inspector's disconnect flow.
|
||||
if (connection.transport instanceof StreamableHTTPClientTransport) {
|
||||
await connection.transport.terminateSession();
|
||||
}
|
||||
|
||||
// Clear error handlers before closing to prevent noise from expected
|
||||
// abort errors during shutdown. The inspector avoids this entirely
|
||||
// by not setting onerror, but since we use it for protocol logging,
|
||||
// we must clear it before disconnect.
|
||||
connection.client.onerror = undefined;
|
||||
if (connection.transport.onclose) {
|
||||
connection.transport.onclose = undefined;
|
||||
}
|
||||
@@ -1078,7 +1111,9 @@ export class MCPService {
|
||||
try {
|
||||
await connection.client.unsubscribeResource({ uri });
|
||||
|
||||
console.log(`[MCPService][${connection.serverName}] Unsubscribed from resource: ${uri}`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||
console.log(`[MCPService][${connection.serverName}] Unsubscribed from resource: ${uri}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[MCPService][${connection.serverName}] Failed to unsubscribe from resource:`,
|
||||
|
||||
@@ -119,7 +119,8 @@ const localStorageMigration: Migration = {
|
||||
// Only migrate if new key doesn't already exist
|
||||
const newValue = localStorage.getItem(newKey);
|
||||
if (newValue !== null) {
|
||||
console.log(`[Migration] localStorage: ${newKey} already exists, skipping`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log(`[Migration] localStorage: ${newKey} already exists, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -127,9 +128,11 @@ const localStorageMigration: Migration = {
|
||||
if (oldValue !== null) {
|
||||
localStorage.setItem(newKey, oldValue);
|
||||
// Keep old key for downgrade compatibility - DO NOT DELETE
|
||||
console.log(
|
||||
`[Migration] localStorage: copied ${deprecatedKey} → ${newKey} (preserved old)`
|
||||
);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||
console.log(
|
||||
`[Migration] localStorage: copied ${deprecatedKey} → ${newKey} (preserved old)`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +149,8 @@ const idxdbMigration: Migration = {
|
||||
async run(): Promise<void> {
|
||||
const oldDbNames = await Dexie.getDatabaseNames();
|
||||
if (!oldDbNames.includes(DB_APP_NAME_DEPRECATED)) {
|
||||
console.log('[Migration] IndexedDB: no old database found, skipping');
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log('[Migration] IndexedDB: no old database found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,11 +159,13 @@ const idxdbMigration: Migration = {
|
||||
newDb.version(1).stores(IDXDB_STORES);
|
||||
const existingConvs = await newDb.table(IDXDB_TABLES.conversations).count();
|
||||
if (existingConvs > 0) {
|
||||
console.log('[Migration] IndexedDB: new database already has data, skipping');
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log('[Migration] IndexedDB: new database already has data, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Migration] IndexedDB: copying from', DB_APP_NAME_DEPRECATED);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log('[Migration] IndexedDB: copying from', DB_APP_NAME_DEPRECATED);
|
||||
|
||||
const oldDb = new Dexie(DB_APP_NAME_DEPRECATED);
|
||||
oldDb.version(1).stores(IDXDB_STORES);
|
||||
@@ -169,15 +175,18 @@ const idxdbMigration: Migration = {
|
||||
|
||||
if (conversations.length > 0) {
|
||||
await newDb.table(IDXDB_TABLES.conversations).bulkAdd(conversations);
|
||||
console.log(`[Migration] IndexedDB: copied ${conversations.length} conversations`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log(`[Migration] IndexedDB: copied ${conversations.length} conversations`);
|
||||
}
|
||||
if (messages.length > 0) {
|
||||
await newDb.table(IDXDB_TABLES.messages).bulkAdd(messages);
|
||||
console.log(`[Migration] IndexedDB: copied ${messages.length} messages`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log(`[Migration] IndexedDB: copied ${messages.length} messages`);
|
||||
}
|
||||
|
||||
// Non-destructive: DO NOT delete old database - keep for downgrade compatibility
|
||||
console.log('[Migration] IndexedDB: preserved old database for downgrade compatibility');
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log('[Migration] IndexedDB: preserved old database for downgrade compatibility');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -419,7 +428,8 @@ const legacyMessageMigration: Migration = {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Migration] Legacy messages: migrated ${migratedCount} messages`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log(`[Migration] Legacy messages: migrated ${migratedCount} messages`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -434,7 +444,8 @@ const themeMigration: Migration = {
|
||||
async run(): Promise<void> {
|
||||
const legacyTheme = localStorage.getItem('theme');
|
||||
if (legacyTheme === null) {
|
||||
console.log('[Migration] Theme: no legacy theme key found, skipping');
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log('[Migration] Theme: no legacy theme key found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -443,7 +454,8 @@ const themeMigration: Migration = {
|
||||
const config = configRaw ? JSON.parse(configRaw) : {};
|
||||
|
||||
if (SETTINGS_KEYS.THEME in config) {
|
||||
console.log('[Migration] Theme: config already has theme, skipping');
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log('[Migration] Theme: config already has theme, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -451,7 +463,8 @@ const themeMigration: Migration = {
|
||||
localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(config));
|
||||
|
||||
// Non-destructive: DO NOT delete legacy theme key - keep for downgrade compatibility
|
||||
console.log(`[Migration] Theme: copied standalone theme to config (preserved old key)`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log(`[Migration] Theme: copied standalone theme to config (preserved old key)`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -491,7 +504,8 @@ export const MigrationService = {
|
||||
*/
|
||||
resetState(): void {
|
||||
localStorage.removeItem(MIGRATION_STATE_KEY);
|
||||
console.log('[Migration] State reset - all migrations will run again');
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log('[Migration] State reset - all migrations will run again');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -500,25 +514,30 @@ export const MigrationService = {
|
||||
*/
|
||||
async runAllMigrations(): Promise<void> {
|
||||
const state = getMigrationState();
|
||||
console.log('[Migration] Starting migration run, state:', state);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log('[Migration] Starting migration run, state:', state);
|
||||
|
||||
for (const migration of migrations) {
|
||||
if (isMigrationCompleted(migration.id)) {
|
||||
console.log(`[Migration] ${migration.id}: already completed, skipping`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log(`[Migration] ${migration.id}: already completed, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[Migration] ${migration.id}: running...`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log(`[Migration] ${migration.id}: running...`);
|
||||
await migration.run();
|
||||
markMigrationCompleted(migration.id);
|
||||
console.log(`[Migration] ${migration.id}: completed successfully`);
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log(`[Migration] ${migration.id}: completed successfully`);
|
||||
} catch (error) {
|
||||
console.error(`[Migration] ${migration.id}: failed`, error);
|
||||
markMigrationFailed(migration.id);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Migration] All migrations complete');
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
|
||||
console.log('[Migration] All migrations complete');
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user