Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions src/angie-mcp-sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof jest.spyOn>;

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('');
});
});
});
27 changes: 22 additions & 5 deletions src/angie-mcp-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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<void> {
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);
Expand Down
2 changes: 1 addition & 1 deletion src/iframe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof setTimeout>;
} ) as unknown as typeof setTimeout;

mockContentWindow = {
Expand Down
3 changes: 2 additions & 1 deletion src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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( {
Expand All @@ -90,6 +90,7 @@ export const listenToSDK = ( appState: AppState ) => {
requestId,
prompt,
context,
options,
},
}, appState.iframeUrlObject?.origin || '' );
} else {
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export interface AngieTriggerRequest {
}& Record<string, any>;
options?: {
timeout?: number;
newChat?: boolean;
autoSend?: boolean;
};
}

Expand Down
Loading