diff --git a/src/angie-mcp-sdk.test.ts b/src/angie-mcp-sdk.test.ts index 7b8ba68..303e272 100644 --- a/src/angie-mcp-sdk.test.ts +++ b/src/angie-mcp-sdk.test.ts @@ -546,4 +546,143 @@ describe('AngieMcpSdk', () => { expect(mockPostMessageToAngieIframe).not.toHaveBeenCalled(); }); }); + + describe('parseHashParams', () => { + it('should parse prompt from hash', () => { + const params = (sdk as any).parseHashParams('#angie-prompt=Hello%20world'); + expect(params.get('angie-prompt')).toBe('Hello world'); + }); + + it('should parse prompt with newChat and autoSend', () => { + const params = (sdk as any).parseHashParams('#angie-prompt=Fix%20error&angie-newChat=true&angie-autoSend=true'); + expect(params.get('angie-prompt')).toBe('Fix error'); + expect(params.get('angie-newChat')).toBe('true'); + expect(params.get('angie-autoSend')).toBe('true'); + }); + + it('should return null for missing params', () => { + const params = (sdk as any).parseHashParams('#angie-prompt=Hello'); + expect(params.get('angie-newChat')).toBeNull(); + expect(params.get('angie-autoSend')).toBeNull(); + }); + + it('should handle hash without # prefix', () => { + const params = (sdk as any).parseHashParams('angie-prompt=Test'); + expect(params.get('angie-prompt')).toBe('Test'); + }); + }); + + describe('handlePromptHash', () => { + let postMessageSpy: ReturnType; + + beforeEach(() => { + postMessageSpy = jest.spyOn(window, 'postMessage').mockImplementation((message: any) => { + if (message?.type === 'sdk-trigger-angie') { + const responseEvent = new MessageEvent('message', { + data: { + type: 'sdk-trigger-angie-response', + payload: { + success: true, + requestId: message.payload.requestId, + response: 'Triggered', + }, + }, + }); + window.dispatchEvent(responseEvent); + } + }); + mockAngieDetector.isReady.mockReturnValue(true); + mockAngieDetector.waitForReady.mockResolvedValue({ isReady: true }); + (sdk as any).isInitialized = true; + }); + + afterEach(() => { + window.location.hash = ''; + postMessageSpy.mockRestore(); + }); + + it('should do nothing when hash has no angie-prompt', async () => { + window.location.hash = '#other-param=value'; + + await (sdk as any).handlePromptHash(); + + expect(postMessageSpy).not.toHaveBeenCalled(); + }); + + it('should do nothing for empty prompt', async () => { + window.location.hash = '#angie-prompt='; + + await (sdk as any).handlePromptHash(); + + expect(postMessageSpy).not.toHaveBeenCalled(); + }); + + it('should trigger with newChat=true and autoSend=true when params are set', async () => { + window.location.hash = '#angie-prompt=Fix%20error&angie-newChat=true&angie-autoSend=true'; + + await (sdk as any).handlePromptHash(); + + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'sdk-trigger-angie', + payload: expect.objectContaining({ + prompt: 'Fix error', + options: expect.objectContaining({ + newChat: true, + autoSend: true, + }), + }), + }), + expect.anything() + ); + }); + + it('should default newChat and autoSend to false when not in hash', async () => { + window.location.hash = '#angie-prompt=Just%20a%20prompt'; + + await (sdk as any).handlePromptHash(); + + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'sdk-trigger-angie', + payload: expect.objectContaining({ + prompt: 'Just a prompt', + options: expect.objectContaining({ + newChat: false, + autoSend: false, + }), + }), + }), + expect.anything() + ); + }); + + it('should support newChat=true without autoSend', async () => { + window.location.hash = '#angie-prompt=Hello&angie-newChat=true'; + + await (sdk as any).handlePromptHash(); + + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'sdk-trigger-angie', + payload: expect.objectContaining({ + prompt: 'Hello', + options: expect.objectContaining({ + newChat: true, + autoSend: false, + }), + }), + }), + expect.anything() + ); + }); + + it('should clear hash after successful trigger', async () => { + window.location.hash = '#angie-prompt=Test&angie-newChat=true&angie-autoSend=true'; + + await (sdk as any).handlePromptHash(); + + expect(window.location.hash).toBe(''); + }); + }); }); \ No newline at end of file diff --git a/src/angie-mcp-sdk.ts b/src/angie-mcp-sdk.ts index 09015f0..a6fcfe7 100644 --- a/src/angie-mcp-sdk.ts +++ b/src/angie-mcp-sdk.ts @@ -12,6 +12,11 @@ import { AngieLocalServerConfig, AngieLocalServerTransport, AngieRemoteServerCon export { DEFAULT_CONTAINER_ID } from './config'; +const HASH_PARAM_PROMPT = 'angie-prompt'; +const HASH_PARAM_NEW_CHAT = 'angie-newChat'; +const HASH_PARAM_AUTO_SEND = 'angie-autoSend'; +const HASH_SOURCE = 'hash-parameter'; + type FeatureToggle = { enabled: boolean }; type PromptSuggestion = { label: string; value: string }; @@ -382,33 +387,45 @@ export class AngieMcpSdk { return `${this.instanceId}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; } + private parseHashParams(hash: string): URLSearchParams { + const paramString = hash.startsWith('#') ? hash.substring(1) : hash; + return new URLSearchParams(paramString); + } + private async handlePromptHash(): Promise { const hash = window.location.hash; - if (!hash.startsWith('#angie-prompt=')) { + if (!hash.includes(`${HASH_PARAM_PROMPT}=`)) { return; } try { - const promptEncoded = hash.replace('#angie-prompt=', ''); - const prompt = decodeURIComponent(promptEncoded); + const params = this.parseHashParams(hash); + const prompt = params.get(HASH_PARAM_PROMPT) || ''; if (!prompt) { this.logger.warn('Empty prompt detected in hash'); return; } - this.logger.log('Detected prompt in hash:', prompt); + const newChat = params.get(HASH_PARAM_NEW_CHAT) === 'true'; + const autoSend = params.get(HASH_PARAM_AUTO_SEND) === 'true'; + + this.logger.log('Detected prompt in hash:', { prompt, newChat, autoSend }); await this.waitForReady(); const response = await this.triggerAngie({ prompt, context: { - source: 'hash-parameter', + source: HASH_SOURCE, pageUrl: window.location.href, timestamp: new Date().toISOString(), }, + options: { + newChat, + autoSend, + }, }); this.logger.log('Triggered successfully from hash:', response); diff --git a/src/iframe.test.ts b/src/iframe.test.ts index 6e75318..0f5ee4a 100644 --- a/src/iframe.test.ts +++ b/src/iframe.test.ts @@ -24,7 +24,7 @@ describe( 'disableNavigationPrevention', () => { global.setTimeout = jest.fn( ( callback: () => void ) => { callback(); - return 0 as unknown as NodeJS.Timeout; + return 0 as unknown as ReturnType; } ) as unknown as typeof setTimeout; mockContentWindow = { diff --git a/src/sdk.ts b/src/sdk.ts index 2945e31..b8ded70 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -81,7 +81,7 @@ export const listenToSDK = ( appState: AppState ) => { sdkLogger.log( 'SDK Trigger Angie received', event.data ); try { - const { requestId, prompt, context } = event.data.payload; + const { requestId, prompt, context, options } = event.data.payload; if ( appState.iframe ) { appState.iframe.contentWindow?.postMessage( { @@ -90,6 +90,7 @@ export const listenToSDK = ( appState: AppState ) => { requestId, prompt, context, + options, }, }, appState.iframeUrlObject?.origin || '' ); } else { diff --git a/src/types.ts b/src/types.ts index 7316bed..d97f852 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,6 +94,8 @@ export interface AngieTriggerRequest { }& Record; options?: { timeout?: number; + newChat?: boolean; + autoSend?: boolean; }; }