diff --git a/package-lock.json b/package-lock.json index bd6ace567ca..d470e84ce8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47653,6 +47653,7 @@ "@mongodb-js/compass-logging": "^1.7.25", "@mongodb-js/compass-telemetry": "^1.19.5", "@mongodb-js/connection-info": "^0.24.0", + "@mongodb-js/workspace-info": "^1.0.0", "ai": "^5.0.26", "compass-preferences-model": "^2.66.3", "mongodb-connection-string-url": "^3.0.1", @@ -52028,6 +52029,7 @@ "dependencies": { "@mongodb-js/compass-app-registry": "^9.4.29", "@mongodb-js/compass-app-stores": "^7.75.0", + "@mongodb-js/compass-assistant": "^1.20.0", "@mongodb-js/compass-components": "^1.59.2", "@mongodb-js/compass-connections": "^1.89.0", "@mongodb-js/compass-logging": "^1.7.25", @@ -61215,6 +61217,7 @@ "@mongodb-js/prettier-config-compass": "^1.2.9", "@mongodb-js/testing-library-compass": "^1.4.0", "@mongodb-js/tsconfig-compass": "^1.2.12", + "@mongodb-js/workspace-info": "^1.0.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", "@types/mocha": "^9.0.0", @@ -64579,6 +64582,7 @@ "requires": { "@mongodb-js/compass-app-registry": "^9.4.29", "@mongodb-js/compass-app-stores": "^7.75.0", + "@mongodb-js/compass-assistant": "^1.20.0", "@mongodb-js/compass-components": "^1.59.2", "@mongodb-js/compass-connections": "^1.89.0", "@mongodb-js/compass-logging": "^1.7.25", diff --git a/packages/compass-assistant/package.json b/packages/compass-assistant/package.json index b070d221711..51754c29ea5 100644 --- a/packages/compass-assistant/package.json +++ b/packages/compass-assistant/package.json @@ -61,8 +61,10 @@ "@mongodb-js/compass-logging": "^1.7.25", "@mongodb-js/compass-telemetry": "^1.19.5", "@mongodb-js/connection-info": "^0.24.0", + "@mongodb-js/workspace-info": "^1.0.0", "ai": "^5.0.26", "compass-preferences-model": "^2.66.3", + "mongodb-collection-model": "^5.37.0", "mongodb-connection-string-url": "^3.0.1", "react": "^17.0.2", "throttleit": "^2.1.0", diff --git a/packages/compass-assistant/src/assistant-global-state.tsx b/packages/compass-assistant/src/assistant-global-state.tsx new file mode 100644 index 00000000000..e69f0e181a4 --- /dev/null +++ b/packages/compass-assistant/src/assistant-global-state.tsx @@ -0,0 +1,77 @@ +import type { ConnectionInfo } from '@mongodb-js/connection-info'; +import type { + WorkspaceTab, + CollectionSubtab, +} from '@mongodb-js/workspace-info'; +import type { CollectionMetadata } from 'mongodb-collection-model'; +import React, { useEffect } from 'react'; + +export type GlobalState = { + activeConnections: ConnectionInfo[]; + activeWorkspace: WorkspaceTab | null; + activeCollectionMetadata: CollectionMetadata | null; + currentQuery: object | null; + currentAggregation: object | null; + activeCollectionSubTab: CollectionSubtab | null; +}; + +const INITIAL_STATE: GlobalState = { + activeConnections: [], + activeWorkspace: null, + activeCollectionMetadata: null, + currentQuery: null, + currentAggregation: null, + activeCollectionSubTab: null, +}; + +const AssistantGlobalStateContext = React.createContext({ + ...INITIAL_STATE, +}); + +const AssistantGlobalSetStateContext = React.createContext< + React.Dispatch> +>(() => undefined); + +export const AssistantGlobalStateProvider: React.FunctionComponent = ({ + children, +}) => { + const [globalState, setGlobalState] = React.useState({ ...INITIAL_STATE }); + return ( + + + {children} + + + ); +}; + +export function useSyncAssistantGlobalState( + stateKey: T, + newState: GlobalState[T] +) { + const setState = React.useContext(AssistantGlobalSetStateContext); + useEffect(() => { + setState((prevState) => { + const state = { + ...prevState, + [stateKey]: newState, + }; + + // Get rid of some non-sensical states incase the user switches away from + // a collection tab to something that is not a collection tab. + // activeConnections and activeWorkspace will get updated no matter + // how/where the user navigates because those concepts are always + // "present" in a way that an active collection is not. + if (state.activeWorkspace?.type !== 'Collection') { + state.activeCollectionMetadata = null; + state.activeCollectionSubTab = null; + } + + return state; + }); + }, [newState, setState, stateKey]); +} + +export function useAssistantGlobalState() { + return React.useContext(AssistantGlobalStateContext); +} diff --git a/packages/compass-assistant/src/compass-assistant-provider.spec.tsx b/packages/compass-assistant/src/compass-assistant-provider.spec.tsx index ac4f8e935ab..7fd0199192b 100644 --- a/packages/compass-assistant/src/compass-assistant-provider.spec.tsx +++ b/packages/compass-assistant/src/compass-assistant-provider.spec.tsx @@ -414,20 +414,48 @@ describe('CompassAssistantProvider', function () { await renderOpenAssistantDrawer({ chat: mockChat }); - userEvent.type( - screen.getByPlaceholderText('Ask a question'), - 'Hello assistant!' - ); - userEvent.click(screen.getByLabelText('Send message')); + for (let i = 0; i < 2; i++) { + userEvent.type( + screen.getByPlaceholderText('Ask a question'), + `Hello assistant! (${i})` + ); + userEvent.click(screen.getByLabelText('Send message')); - await waitFor(() => { - expect(sendMessageSpy.calledOnce).to.be.true; - expect(sendMessageSpy.firstCall.args[0]).to.deep.include({ - text: 'Hello assistant!', + await waitFor(() => { + expect(sendMessageSpy.callCount).to.equal(i + 1); + expect(sendMessageSpy.getCall(i).args[0]).to.deep.include({ + text: `Hello assistant! (${i})`, + }); + + expect(screen.getByText(`Hello assistant! (${i})`)).to.exist; }); + } - expect(screen.getByText('Hello assistant!')).to.exist; - }); + const contextMessages = mockChat.messages.filter( + (message) => message.metadata?.isSystemContext + ); + + for (const contextMessage of contextMessages) { + // just clear it up so we can deep compare + contextMessage.id = 'system-context'; + } + + // it only sent one + expect(contextMessages).to.deep.equal([ + { + id: 'system-context', + role: 'system', + metadata: { + isSystemContext: true, + }, + parts: [ + { + type: 'text', + text: 'The user does not have any tabs open.', + }, + ], + }, + ]); }); it('will not send new messages if the user does not opt in', async function () { diff --git a/packages/compass-assistant/src/compass-assistant-provider.tsx b/packages/compass-assistant/src/compass-assistant-provider.tsx index 105c7b0144b..6898f6d2cd9 100644 --- a/packages/compass-assistant/src/compass-assistant-provider.tsx +++ b/packages/compass-assistant/src/compass-assistant-provider.tsx @@ -13,11 +13,13 @@ import { } from '@mongodb-js/atlas-service/provider'; import { DocsProviderTransport } from './docs-provider-transport'; import { + useCurrentValueRef, useDrawerActions, useInitialValue, } from '@mongodb-js/compass-components'; import { buildConnectionErrorPrompt, + buildContextPrompt, buildExplainPlanPrompt, buildProactiveInsightsPrompt, type EntryPointMessage, @@ -31,7 +33,7 @@ import { createLoggerLocator, type Logger, } from '@mongodb-js/compass-logging/provider'; -import type { ConnectionInfo } from '@mongodb-js/connection-info'; +import { type ConnectionInfo } from '@mongodb-js/connection-info'; import { telemetryLocator, type TrackFunction, @@ -41,10 +43,15 @@ import type { AtlasAiService } from '@mongodb-js/compass-generative-ai/provider' import { atlasAiServiceLocator } from '@mongodb-js/compass-generative-ai/provider'; import { buildConversationInstructionsPrompt } from './prompts'; import { createOpenAI } from '@ai-sdk/openai'; +import { + AssistantGlobalStateProvider, + useAssistantGlobalState, +} from './assistant-global-state'; export const ASSISTANT_DRAWER_ID = 'compass-assistant-drawer'; export type AssistantMessage = UIMessage & { + role?: 'user' | 'assistant' | 'system'; metadata?: { /** The text to display instead of the message text. */ displayText?: string; @@ -63,6 +70,11 @@ export type AssistantMessage = UIMessage & { instructions?: string; /** Excludes history if this message is the last message being sent */ sendWithoutHistory?: boolean; + /** Whether to send the current context along with the message if the context changed */ + sendContext?: boolean; + + /** Whether this is a message to the model that we don't want to display to the user*/ + isSystemContext?: boolean; }; }; @@ -173,6 +185,16 @@ export type CompassAssistantService = { getIsAssistantEnabled: () => boolean; }; +// Type guard to check if activeWorkspace has a connectionId property +function hasConnectionId(obj: unknown): obj is { connectionId: string } { + return ( + typeof obj === 'object' && + obj !== null && + 'connectionId' in obj && + typeof (obj as any).connectionId === 'string' + ); +} + export const AssistantProvider: React.FunctionComponent< PropsWithChildren<{ appNameForPrompt: string; @@ -183,12 +205,23 @@ export const AssistantProvider: React.FunctionComponent< const { openDrawer } = useDrawerActions(); const track = useTelemetry(); + const assistantGlobalStateRef = useCurrentValueRef(useAssistantGlobalState()); + + const lastContextPromptRef = useRef(null); + const ensureOptInAndSend = useInitialValue(() => { return async function ( message: SendMessage, options: SendOptions, callback: () => void ) { + const { + activeWorkspace, + activeConnections, + activeCollectionMetadata, + activeCollectionSubTab, + } = assistantGlobalStateRef.current; + try { await atlasAiService.ensureAiFeatureAccess(); } catch { @@ -204,6 +237,36 @@ export const AssistantProvider: React.FunctionComponent< await chat.stop(); } + const activeConnection = + activeConnections.find((connInfo) => { + return ( + hasConnectionId(activeWorkspace) && + connInfo.id === activeWorkspace.connectionId + ); + }) ?? null; + + const contextPrompt = buildContextPrompt({ + activeWorkspace, + activeConnection, + activeCollectionMetadata, + activeCollectionSubTab, + }); + + // use just the text so we have a stable reference to compare against + const contextPromptText = + contextPrompt.parts[0].type === 'text' + ? contextPrompt.parts[0].text + : ''; + + const shouldSendContextPrompt = + message?.metadata?.sendContext && + (!lastContextPromptRef.current || + lastContextPromptRef.current !== contextPromptText); + if (shouldSendContextPrompt) { + lastContextPromptRef.current = contextPromptText; + chat.messages = [...chat.messages, contextPrompt]; + } + await chat.sendMessage(message, options); }; }); @@ -224,6 +287,7 @@ export const AssistantProvider: React.FunctionComponent< metadata: { ...metadata, source: entryPointName, + sendContext: true, }, }, {}, @@ -285,13 +349,15 @@ export const CompassAssistantProvider = registerCompassPlugin( throw new Error('atlasAiService was not provided by the state'); } return ( - - {children} - + + + {children} + + ); }, activate: ( diff --git a/packages/compass-assistant/src/components/assistant-chat.tsx b/packages/compass-assistant/src/components/assistant-chat.tsx index 1af46a9072f..cac05c5ef6b 100644 --- a/packages/compass-assistant/src/components/assistant-chat.tsx +++ b/packages/compass-assistant/src/components/assistant-chat.tsx @@ -263,11 +263,15 @@ export const AssistantChat: React.FunctionComponent = ({ const trimmedMessageBody = messageBody.trim(); if (trimmedMessageBody) { await chat.stop(); - void ensureOptInAndSend?.({ text: trimmedMessageBody }, {}, () => { - track('Assistant Prompt Submitted', { - user_input_length: trimmedMessageBody.length, - }); - }); + void ensureOptInAndSend?.( + { text: trimmedMessageBody, metadata: { sendContext: true } }, + {}, + () => { + track('Assistant Prompt Submitted', { + user_input_length: trimmedMessageBody.length, + }); + } + ); } }, [track, ensureOptInAndSend, chat] @@ -357,6 +361,10 @@ export const AssistantChat: React.FunctionComponent = ({ [ensureOptInAndSend, setMessages, track] ); + const visibleMessages = messages.filter( + (message) => !message.metadata?.isSystemContext + ); + return (
= ({ ref={messagesContainerRef} >
- {messages.map((message, index) => { + {visibleMessages.map((message, index) => { const { id, role, metadata, parts } = message; const seenTitles = new Set(); const sources = []; @@ -395,7 +403,7 @@ export const AssistantChat: React.FunctionComponent = ({ } if (metadata?.confirmation) { const { description, state } = metadata.confirmation; - const isLastMessage = index === messages.length - 1; + const isLastMessage = index === visibleMessages.length - 1; return ( [0]; + expected: string; + }[] = [ + // No active tab + { + context: { + activeWorkspace: null, + activeConnection: null, + activeCollectionMetadata: null, + activeCollectionSubTab: null, + }, + expected: 'The user does not have any tabs open.', + }, + // Welcome + { + context: { + activeWorkspace: { + id: 'welcome-tab-1', + type: 'Welcome', + }, + activeConnection: null, + activeCollectionMetadata: null, + activeCollectionSubTab: null, + }, + expected: 'The user is on the "Welcome" tab.', + }, + // My Queries + { + context: { + activeWorkspace: { + id: 'my-queries-tab-1', + type: 'My Queries', + }, + activeConnection: null, + activeCollectionMetadata: null, + activeCollectionSubTab: null, + }, + expected: 'The user is on the "My Queries" tab.', + }, + // Data Modeling + { + context: { + activeWorkspace: { + id: 'data-modelling-tab-1', + type: 'Data Modeling', + }, + activeConnection: null, + activeCollectionMetadata: null, + activeCollectionSubTab: null, + }, + expected: 'The user is on the "Data Modeling" tab.', + }, + // Databases + { + context: { + activeWorkspace: { + id: 'databases-tab-1', + type: 'Databases', + connectionId: 'conn-1', + }, + activeConnection: { + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + }, + activeCollectionMetadata: null, + activeCollectionSubTab: null, + }, + expected: + 'The connection is named "localhost:27017". The redacted connection string is "mongodb://localhost:27017/".\n\nThe user is on the "Databases" tab.', + }, + // Performance + { + context: { + activeWorkspace: { + id: 'performance-tab-1', + type: 'Performance', + connectionId: 'conn-1', + }, + activeConnection: { + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + }, + activeCollectionMetadata: null, + activeCollectionSubTab: null, + }, + expected: + 'The connection is named "localhost:27017". The redacted connection string is "mongodb://localhost:27017/".\n\nThe user is on the "Performance" tab.', + }, + // Shell + { + context: { + activeWorkspace: { + id: 'shell-tab-1', + type: 'Shell', + connectionId: 'conn-1', + }, + activeConnection: { + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + }, + activeCollectionMetadata: null, + activeCollectionSubTab: null, + }, + expected: + 'The connection is named "localhost:27017". The redacted connection string is "mongodb://localhost:27017/".\n\nThe user is on the "Shell" tab.', + }, + // Collections + { + context: { + activeWorkspace: { + id: 'collections-tab-1', + type: 'Collections', + connectionId: 'conn-1', + namespace: 'test', + }, + activeConnection: { + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + }, + activeCollectionMetadata: null, + activeCollectionSubTab: null, + }, + expected: + 'The connection is named "localhost:27017". The redacted connection string is "mongodb://localhost:27017/".\n\nThe user is on the "Collections" tab for the "test" namespace.', + }, + // Normal Collection + { + context: { + activeWorkspace: { + id: 'collection-tab-1', + type: 'Collection', + connectionId: 'conn-1', + namespace: 'test.normal', + subTab: 'Schema', + }, + activeConnection: { + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + }, + activeCollectionMetadata: { + isTimeSeries: false, + }, + activeCollectionSubTab: 'Schema', + }, + expected: + 'The connection is named "localhost:27017". The redacted connection string is "mongodb://localhost:27017/".\n\nThe user is on the "Schema" tab for the "test.normal" namespace.', + }, + // Timeseries Collection + { + context: { + activeWorkspace: { + id: 'collection-tab-1', + type: 'Collection', + connectionId: 'conn-1', + namespace: 'test.timeseries', + subTab: 'Aggregations', + }, + activeConnection: { + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + }, + activeCollectionMetadata: { + isTimeSeries: true, + }, + activeCollectionSubTab: 'Aggregations', + }, + expected: + 'The connection is named "localhost:27017". The redacted connection string is "mongodb://localhost:27017/".\n\nThe user is on the "Aggregations" tab for the "test.timeseries" namespace. "test.timeseries" is a time-series collection.', + }, + // View Collection + { + context: { + activeWorkspace: { + id: 'collection-tab-1', + type: 'Collection', + connectionId: 'conn-1', + namespace: 'test.view', + subTab: 'Documents', + }, + activeConnection: { + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + }, + activeCollectionMetadata: { + isTimeSeries: false, + sourceName: 'test.normal', + }, + activeCollectionSubTab: 'Documents', + }, + expected: + 'The connection is named "localhost:27017". The redacted connection string is "mongodb://localhost:27017/".\n\nThe user is on the "Documents" tab for the "test.view" namespace. "test.view" is a view on the "test.normal" collection.', + }, + ]; + + for (const testCase of testCases) { + const summary: Record = { + type: testCase.context.activeWorkspace?.type || 'No active tab', + }; + if (testCase.context.activeCollectionMetadata?.isTimeSeries) { + summary.isTimeSeries = true; + } + if (testCase.context.activeCollectionMetadata?.sourceName) { + summary.isView = true; + } + if (hasSubtab(testCase.context.activeWorkspace)) { + summary.subTab = testCase.context.activeWorkspace.subTab; + } + const summaryString = Object.entries(summary) + .map(([k, v]) => `${k}=${v}`) + .join(','); + it(`renders the expected prompt for ${summaryString}`, function () { + const result = buildContextPrompt(testCase.context); + expect(result.id).to.match(/^system-context-/); + expect(result.metadata?.isSystemContext).to.equal(true); + expect(result.role).to.equal('system'); + expect(result.parts).to.have.lengthOf(1); + const text = hasText(result.parts[0]) ? result.parts[0].text : ''; + expect(text).equal(testCase.expected); + }); + } + }); }); + +function hasSubtab(obj: unknown): obj is { subTab: string } { + return ( + typeof obj === 'object' && + obj !== null && + 'subTab' in obj && + typeof (obj as any).subTab === 'string' + ); +} + +function hasText(obj: unknown): obj is { text: string } { + return ( + typeof obj === 'object' && + obj !== null && + 'text' in obj && + typeof (obj as any).text === 'string' + ); +} diff --git a/packages/compass-assistant/src/prompts.ts b/packages/compass-assistant/src/prompts.ts index 5faea6cef94..a9995f296c6 100644 --- a/packages/compass-assistant/src/prompts.ts +++ b/packages/compass-assistant/src/prompts.ts @@ -1,4 +1,12 @@ -import type { ConnectionInfo } from '@mongodb-js/connection-info'; +import { + getConnectionTitle, + type ConnectionInfo, +} from '@mongodb-js/connection-info'; +import type { + CollectionSubtab, + WorkspaceTab, +} from '@mongodb-js/workspace-info'; +import type { CollectionMetadata } from 'mongodb-collection-model'; import { redactConnectionString } from 'mongodb-connection-string-url'; import type { AssistantMessage } from './compass-assistant-provider'; @@ -225,3 +233,83 @@ ${connectionError}`, }, }; }; + +export function buildContextPrompt({ + activeWorkspace, + activeConnection, + activeCollectionMetadata, + activeCollectionSubTab, +}: { + activeWorkspace: WorkspaceTab | null; + activeConnection: Pick | null; + activeCollectionMetadata: Pick< + CollectionMetadata, + 'isTimeSeries' | 'sourceName' + // TODO(COMPASS-10173): isClustered, isFLE, isSearchIndexesSupported, isDataLake, isAtlas, serverVersion + > | null; + activeCollectionSubTab: CollectionSubtab | null; +}): AssistantMessage { + const parts: string[] = []; + + if (activeConnection) { + const connectionName = getConnectionTitle(activeConnection); + const redactedConnectionString = redactConnectionString( + activeConnection.connectionOptions.connectionString + ); + parts.push( + `The connection is named "${connectionName}". The redacted connection string is "${redactedConnectionString}".` + ); + } + + if (activeWorkspace) { + const isNamespaceTab = hasNamespace(activeWorkspace); + const tabName = activeCollectionSubTab || activeWorkspace.type; + const namespacePart = isNamespaceTab + ? ` for the "${activeWorkspace.namespace}" namespace` + : ''; + const lines = [`The user is on the "${tabName}" tab${namespacePart}.`]; + if (isNamespaceTab && activeConnection && activeCollectionMetadata) { + if (activeCollectionMetadata.isTimeSeries) { + lines.push( + `"${activeWorkspace.namespace}" is a time-series collection.` + ); + } + + if (activeCollectionMetadata.sourceName) { + lines.push( + `"${activeWorkspace.namespace}" is a view on the "${activeCollectionMetadata.sourceName}" collection.` + ); + } + } + parts.push(lines.join(' ')); + } else { + parts.push(`The user does not have any tabs open.`); + } + + const text = parts.join('\n\n'); + + const prompt: AssistantMessage = { + id: `system-context-${Date.now()}`, + parts: [ + { + type: 'text', + text, + }, + ], + metadata: { + isSystemContext: true, + }, + role: 'system', + }; + + return prompt; +} + +function hasNamespace(obj: unknown): obj is { namespace: string } { + return ( + typeof obj === 'object' && + obj !== null && + 'namespace' in obj && + typeof (obj as any).namespace === 'string' + ); +} diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index dbcac3a2312..59e135055c4 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -50,6 +50,7 @@ "dependencies": { "@faker-js/faker": "^9.0.0", "@mongodb-js/atlas-service": "^0.73.0", + "@mongodb-js/compass-assistant": "^1.20.0", "@mongodb-js/compass-app-registry": "^9.4.29", "@mongodb-js/compass-app-stores": "^7.75.0", "@mongodb-js/compass-components": "^1.59.2", diff --git a/packages/compass-collection/src/components/collection-tab.tsx b/packages/compass-collection/src/components/collection-tab.tsx index 61436a66fdf..413dad979de 100644 --- a/packages/compass-collection/src/components/collection-tab.tsx +++ b/packages/compass-collection/src/components/collection-tab.tsx @@ -24,6 +24,7 @@ import { useGlobalAppRegistry, useLocalAppRegistry, } from '@mongodb-js/compass-app-registry'; +import { useSyncAssistantGlobalState } from '@mongodb-js/compass-assistant'; type CollectionSubtabTrackingId = Lowercase extends infer U ? U extends string @@ -207,6 +208,9 @@ const CollectionTabWithMetadata: React.FunctionComponent< const tabs = useCollectionTabs(pluginProps); const activeTabIndex = tabs.findIndex((tab) => tab.name === currentTab); + useSyncAssistantGlobalState('activeCollectionMetadata', collectionMetadata); + useSyncAssistantGlobalState('activeCollectionSubTab', currentTab); + return (
diff --git a/packages/compass-connections/src/index.tsx b/packages/compass-connections/src/index.tsx index 772d2e76e19..60e2f2b79f6 100644 --- a/packages/compass-connections/src/index.tsx +++ b/packages/compass-connections/src/index.tsx @@ -1,7 +1,7 @@ import { preferencesLocator } from 'compass-preferences-model/provider'; import { registerCompassPlugin } from '@mongodb-js/compass-app-registry'; import type { connect as devtoolsConnect } from 'mongodb-data-service'; -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import { createLoggerLocator } from '@mongodb-js/compass-logging/provider'; import { connectionStorageLocator } from '@mongodb-js/connection-storage/provider'; import type { @@ -21,10 +21,14 @@ import { import { ConnectionsStoreContext, ConnectionActionsProvider, + useConnectionsList, } from './stores/store-context'; export type { ConnectionFeature } from './utils/connection-supports'; export { connectionSupports, connectable } from './utils/connection-supports'; -import { compassAssistantServiceLocator } from '@mongodb-js/compass-assistant'; +import { + compassAssistantServiceLocator, + useSyncAssistantGlobalState, +} from '@mongodb-js/compass-assistant'; import { useInitialValue } from '@mongodb-js/compass-components'; const ConnectionsComponent: React.FunctionComponent<{ @@ -70,6 +74,16 @@ const ConnectionsComponent: React.FunctionComponent<{ */ onFailToLoadConnections: (error: Error) => void; }> = ({ children }) => { + const activeConnections = useConnectionsList((connection) => { + return connection.status === 'connected'; + }); + const activeConnectionsInfo = useMemo(() => { + return activeConnections.map((connection) => { + return connection.info; + }); + }, [activeConnections]); + useSyncAssistantGlobalState('activeConnections', activeConnectionsInfo); + return ( {children} diff --git a/packages/compass-workspaces/package.json b/packages/compass-workspaces/package.json index c8371a7ce69..754a7606e8b 100644 --- a/packages/compass-workspaces/package.json +++ b/packages/compass-workspaces/package.json @@ -53,6 +53,7 @@ "dependencies": { "@mongodb-js/compass-app-registry": "^9.4.29", "@mongodb-js/compass-app-stores": "^7.75.0", + "@mongodb-js/compass-assistant": "^1.20.0", "@mongodb-js/compass-components": "^1.59.2", "@mongodb-js/compass-connections": "^1.89.0", "@mongodb-js/compass-logging": "^1.7.25", diff --git a/packages/compass-workspaces/src/components/index.tsx b/packages/compass-workspaces/src/components/index.tsx index 76adf448eaf..baf0a02d909 100644 --- a/packages/compass-workspaces/src/components/index.tsx +++ b/packages/compass-workspaces/src/components/index.tsx @@ -19,6 +19,7 @@ import type { import Workspaces from './workspaces'; import { connect } from '../stores/context'; import { WorkspacesServiceProvider } from '../provider'; +import { useSyncAssistantGlobalState } from '@mongodb-js/compass-assistant'; type WorkspacesWithSidebarProps = { /** @@ -115,6 +116,7 @@ const WorkspacesWithSidebar: React.FunctionComponent< useEffect(() => { onChange.current(activeTab, activeTabCollectionInfo); }, [activeTab, activeTabCollectionInfo, onChange]); + useSyncAssistantGlobalState('activeWorkspace', activeTab); return (
{ activeTab, activeTabCollectionInfo: activeTab?.type === 'Collection' - ? state.collectionInfo[activeTab.namespace] + ? state.collectionInfo[ + `${activeTab.connectionId}.${activeTab.namespace}` + ] : null, }; })(WorkspacesWithSidebar); diff --git a/packages/databases-collections-list/src/collections.tsx b/packages/databases-collections-list/src/collections.tsx index dd57c1bc902..a1fdde721f5 100644 --- a/packages/databases-collections-list/src/collections.tsx +++ b/packages/databases-collections-list/src/collections.tsx @@ -41,7 +41,7 @@ const CollectionBadges: React.FunctionComponent = ({ children }) => { const collectionBadgeStyles = css({ gap: spacing[100], - 'white-space': 'nowrap', + whiteSpace: 'nowrap', }); const viewOnStyles = css({