Skip to content
Open
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
15 changes: 15 additions & 0 deletions canvas/src/__tests__/helpers/mainPanelMcpExpectations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ import {
EXA_MCP_LOCAL_API_KEY_ENV,
EXA_MCP_REMOTE_URL,
} from 'grph-shared/search/exaMcpSsot'
import {
EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON,
EXA_SEARCH_API_DOCS_URL,
EXA_SEARCH_API_ENDPOINT,
EXA_SEARCH_API_INVOCATION_TEXT,
} from 'grph-shared/search/exaSearchApiSsot'
import {
CLOUDFLARE_AI_GATEWAY_ACCOUNT_HEADER,
CLOUDFLARE_AI_GATEWAY_MCP_DOCS_URL,
Expand Down Expand Up @@ -515,6 +521,11 @@ export function assertMcpHubSurfacesExaMcpConfig(container: Element): void {
'exaMcp.max_results',
'exaMcp.fetch_content_limit',
'exaMcp.require_fetch_review',
'exaMcp.search_api.endpoint',
'exaMcp.search_api.default_request',
'exaMcp.search_api.response_contract',
'exaMcp.search_api.invocation',
'exaMcp.search_api.docs_url',
'exaMcp.remote_config.codex',
'exaMcp.remote_config.generic',
'web_search_exa',
Expand All @@ -535,6 +546,10 @@ export function assertMcpHubSurfacesExaMcpConfig(container: Element): void {
EXA_MCP_DOCS_MARKDOWN_URL,
EXA_MCP_GITHUB_URL,
EXA_MCP_DASHBOARD_URL,
EXA_SEARCH_API_ENDPOINT,
EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON,
EXA_SEARCH_API_INVOCATION_TEXT,
EXA_SEARCH_API_DOCS_URL,
'restart_mcp_client_after_config_change',
'Open Exa MCP Docs',
'Open FloatingPanel Chat UI',
Expand Down
122 changes: 122 additions & 0 deletions canvas/src/__tests__/mainPanelExaIntegrations.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import IntegrationsHubView from '@/features/panels/views/IntegrationsHubView'
import { ExaSearchSkillsCommandsProjection } from '@/features/integrations/ExaSearchSkillsCommandsProjection'
import { initJsdomHarness } from '@/tests/lib/jsdomHarness'
import { initWindowHarness } from '@/tests/lib/windowHarness'
import { MemoryStorage } from '@/tests/lib/memoryStorage'
import { installDeterministicRaf, mountReactRoot, unmountReactRoot } from '@/tests/lib/reactRootHarness'
import { useGraphStore } from '@/hooks/useGraphStore'
import {
EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON,
EXA_SEARCH_API_DEPRECATED_FIELDS,
EXA_SEARCH_API_DOC_AREA,
EXA_SEARCH_API_DOCS_URL,
EXA_SEARCH_API_ENDPOINT,
EXA_SEARCH_API_INVOCATION_TEXT,
buildExaCodingAgentSearchRequest,
} from 'grph-shared/search/exaSearchApiSsot'

async function withRendered(element: React.ReactElement, assertions: (container: Element) => void): Promise<void> {
const storage = new MemoryStorage()
const { restore: restoreWindow } = initWindowHarness({ storage })
const { dom, restore: restoreDom } = initJsdomHarness()
let root: ReturnType<typeof createRoot> | null = null
try {
const anyWindow = dom.window as unknown as { requestAnimationFrame?: (callback: (time: number) => void) => number }
anyWindow.requestAnimationFrame = installDeterministicRaf(dom.window)
useGraphStore.getState().resetAll()
const container = dom.window.document.createElement('section')
dom.window.document.body.appendChild(container)
root = createRoot(container as unknown as HTMLElement)
await mountReactRoot(root, element, { window: dom.window, frames: 4 })
assertions(container)
} finally {
if (root) await unmountReactRoot(root, { window: dom.window })
restoreDom()
restoreWindow()
}
}

const renderedValues = (container: Element): string => Array.from(
container.querySelectorAll<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>('input, textarea, select'),
element => element.value,
).join('\n')

export async function testIntegrationsHubSurfacesExaCodingAgentSearchContract() {
await withRendered(React.createElement(IntegrationsHubView), container => {
const searchableText = `${container.textContent || ''}\n${renderedValues(container)}`
;[
EXA_SEARCH_API_DOC_AREA,
'exaSearchApi.endpoint',
'exaSearchApi.request.query',
'exaSearchApi.request.type',
'exaSearchApi.request.numResults',
'exaSearchApi.request.contents.highlights',
'exaSearchApi.request.structured_output',
'exaSearchApi.response.contract',
'exaSearchApi.errors.statuses',
'exaSearchApi.deprecated.fields',
'exaSearchApi.invocation',
EXA_SEARCH_API_ENDPOINT,
EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON,
EXA_SEARCH_API_INVOCATION_TEXT,
EXA_SEARCH_API_DOCS_URL,
'Open Exa Search API Coding-Agent Guide',
'Open FloatingPanel Skills & Commands',
].forEach(token => {
if (!searchableText.includes(token)) {
throw new Error(`expected MainPanel Integrations to include Exa coding-agent token ${JSON.stringify(token)}`)
}
})
if (!container.querySelector('[data-kg-anchor^="integrations-row-exa-search-"]')) {
throw new Error('expected Exa Search API integration rows to use stable Exa anchors')
}
;['YOUR_EXA_API_KEY', 'your_api_key', 'exaApiKey=', 'sk_test_', 'sk_live_'].forEach(secret => {
if (searchableText.includes(secret)) throw new Error(`expected Exa integration to omit secret material ${secret}`)
})
})
}

export function testExaCodingAgentSearchRequestUsesCurrentBoundedContract() {
const request = buildExaCodingAgentSearchRequest({
query: ' inspect current repository issue ',
type: 'unsupported',
numResults: 999,
})
if (
request.query !== 'inspect current repository issue'
|| request.type !== 'auto'
|| request.numResults !== 100
|| request.contents.highlights !== true
) {
throw new Error(`expected sanitized Exa coding-agent request, got ${JSON.stringify(request)}`)
}
;['useAutoprompt', 'numSentences', 'tokensNum', 'livecrawl (string)'].forEach(field => {
if (!EXA_SEARCH_API_DEPRECATED_FIELDS.some(candidate => candidate === field)) {
throw new Error(`expected shared Exa contract to reject deprecated field ${field}`)
}
})
}

export async function testFloatingSkillsCommandsSurfacesCanonicalExaInvocation() {
await withRendered(React.createElement(ExaSearchSkillsCommandsProjection), container => {
const projection = container.querySelector('[data-kg-exa-search-skills-projection="configuration-only"]')
const invocation = projection?.querySelector('[data-kg-exa-search-invocation="canonical"]')
const text = projection?.textContent || ''
if (
!projection
|| invocation?.getAttribute('data-kg-exa-search-invocation-chip-renderer') !== 'shared-markdown-sigil'
|| !text.includes('/tool.catalog')
|| !text.includes('#tool-routing')
|| !text.includes('@tool-provider')
|| !text.includes('no browser-side API key or direct search call')
|| projection.querySelector(`a[href="${EXA_SEARCH_API_DOCS_URL}"]`) === null
) {
throw new Error(`expected FloatingPanel Skills & Commands Exa canonical projection, got ${JSON.stringify(text)}`)
}
if (text.includes('/exa') || text.includes('#exa') || text.includes('@exa')) {
throw new Error(`expected Exa projection to avoid provider-specific invocation aliases, got ${JSON.stringify(text)}`)
}
})
}
8 changes: 7 additions & 1 deletion canvas/src/__tests__/mainPanelSkillsCommands.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export async function testFloatingPanelSkillsCommandsViewReusesMediaPanelLayout(
const subjectGroupToggle = container.querySelector('[data-kg-skills-commands-grammar-toggle="subject"]') as HTMLButtonElement | null
const objectGroupToggle = container.querySelector('[data-kg-skills-commands-grammar-toggle="object"]') as HTMLButtonElement | null
const motionCaptureProjection = container.querySelector('[data-kg-motion-capture-projection="skills"]')
const exaSearchProjection = container.querySelector('[data-kg-exa-search-skills-projection="configuration-only"]')
const exaSearchInvocation = exaSearchProjection?.querySelector('[data-kg-exa-search-invocation="canonical"]')
const motionCaptureInvocation = motionCaptureProjection?.querySelector('[data-kg-motion-capture-invocation="canonical"]')
const motionCaptureWebMcp = motionCaptureProjection?.querySelector('[data-kg-motion-capture-web-mcp="1"]')
if (
Expand All @@ -271,13 +273,17 @@ export async function testFloatingPanelSkillsCommandsViewReusesMediaPanelLayout(
!(sharedSearchToggle instanceof dom.window.HTMLButtonElement) ||
skillsSearchToggle !== sharedSearchToggle ||
!motionCaptureProjection ||
!exaSearchProjection ||
!exaSearchInvocation?.textContent?.includes('/tool.catalog') ||
!exaSearchInvocation.textContent.includes('#tool-routing') ||
!exaSearchInvocation.textContent.includes('@tool-provider') ||
motionCaptureProjection.getAttribute('data-kg-motion-capture-runtime-ready') === null ||
!motionCaptureInvocation?.textContent?.includes('/motion.control') ||
!motionCaptureInvocation.textContent.includes('@canvas') ||
!motionCaptureInvocation.textContent.includes('#pose') ||
!motionCaptureWebMcp?.textContent?.includes('knowgrph.control_local_motion_control')
) {
throw new Error('Expected Skills & Commands to reuse the shared Media layout and canonical Motion Capture invocation projection')
throw new Error('Expected Skills & Commands to reuse the shared Media layout and canonical Exa/Motion Capture invocation projections')
}
const groupDisclosureActions = container.querySelector('[data-kg-skills-commands-disclosure-actions="header"]') as HTMLElement | null
const groupDisclosureButton = groupDisclosureActions?.querySelector('button') as HTMLButtonElement | null
Expand Down
1 change: 1 addition & 0 deletions canvas/src/features/canvas/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type PropsPanelOpenEventDetail = {
export type FloatingPanelOpenEventDetail = {
tab?:
| 'inspector'
| 'skillsCommands'
| 'node'
| 'view'
| 'camera'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react'
import { Search } from 'lucide-react'
import { renderAgenticOsInvocationKeywordChip } from '@/features/agentic-os/agenticOsInvocationChips'
import { renderMarkdownSigilInlineText } from '@/lib/ui/MarkdownSigilText'
import { UI_INLINE_CHIP_GROUP_CLASSNAME } from '@/lib/ui/textLayout'
import { UI_THEME_TOKENS } from '@/lib/ui/theme-tokens'
import { cn } from '@/lib/utils'
import {
EXA_SEARCH_API_DEFAULT_NUM_RESULTS,
EXA_SEARCH_API_DEFAULT_SEARCH_TYPE,
EXA_SEARCH_API_DOCS_URL,
EXA_SEARCH_API_INVOCATION_TEXT,
} from 'grph-shared/search/exaSearchApiSsot'

export function ExaSearchSkillsCommandsProjection() {
return (
<section
className={cn('mx-1 mb-2 grid gap-1 rounded border p-2 text-[10px]', UI_THEME_TOKENS.panel.border, UI_THEME_TOKENS.panel.bg)}
aria-label="Exa Search API coding-agent integration"
data-kg-exa-search-skills-projection="configuration-only"
>
<header className="flex min-w-0 items-center justify-between gap-2">
<span className="flex items-center gap-1 font-semibold">
<Search className="h-3.5 w-3.5" aria-hidden="true" /> Exa Search
</span>
<a
className={cn('underline underline-offset-2', UI_THEME_TOKENS.text.secondary)}
href={EXA_SEARCH_API_DOCS_URL}
target="_blank"
rel="noreferrer"
>
Coding-agent guide
</a>
</header>
<p className={UI_THEME_TOKENS.text.secondary}>
{EXA_SEARCH_API_DEFAULT_SEARCH_TYPE} · {EXA_SEARCH_API_DEFAULT_NUM_RESULTS} results · highlights · host-owned auth
</p>
<code
className={cn(UI_INLINE_CHIP_GROUP_CLASSNAME, 'min-w-0 overflow-hidden font-mono text-[9px]', UI_THEME_TOKENS.text.secondary)}
data-kg-exa-search-invocation="canonical"
data-kg-exa-search-invocation-chip-renderer="shared-markdown-sigil"
>
{renderMarkdownSigilInlineText(EXA_SEARCH_API_INVOCATION_TEXT, {
renderKeywordChip: ({ value, className }) => renderAgenticOsInvocationKeywordChip({
value,
className,
sourceLink: false,
}),
})}
</code>
<p className={UI_THEME_TOKENS.text.tertiary}>Reference contract only · no browser-side API key or direct search call</p>
</section>
)
}
44 changes: 44 additions & 0 deletions canvas/src/features/panels/views/exaMcpApiDocs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ import {
EXA_MCP_TOOL_PROFILES,
normalizeExaMcpToolNames,
} from 'grph-shared/search/exaMcpSsot'
import {
EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON,
EXA_SEARCH_API_DEFAULT_SEARCH_TYPE,
EXA_SEARCH_API_DOCS_URL,
EXA_SEARCH_API_ENDPOINT,
EXA_SEARCH_API_INVOCATION_TEXT,
EXA_SEARCH_API_RESPONSE_FIELDS,
} from 'grph-shared/search/exaSearchApiSsot'

export { EXA_MCP_DOC_AREA, EXA_MCP_DOCS_URL }

Expand Down Expand Up @@ -169,6 +177,42 @@ const EXA_MCP_DOC_ROWS: ReadonlyArray<ExaMcpDocRow> = [
tooltipDefaultValue: EXA_MCP_DEFAULT_MAX_RESULTS,
searchHints: ['numResults', 'result limit', 'evidence pack'],
},
{
key: 'search_api.endpoint',
typeLabel: 'endpoint',
value: `POST ${EXA_SEARCH_API_ENDPOINT}`,
responsibility: 'Source-owned Search API endpoint behind Exa MCP coding-agent search guidance.',
notes: 'This row is a contract projection only; the browser does not call Exa or store bearer credentials.',
searchHints: ['coding agent search api', EXA_SEARCH_API_ENDPOINT],
},
{
key: 'search_api.default_request',
typeLabel: 'object',
value: EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON,
responsibility: 'Shared coding-agent request preview with auto search, bounded results, and highlight content.',
searchHints: ['contents highlights', EXA_SEARCH_API_DEFAULT_SEARCH_TYPE, 'numResults'],
},
{
key: 'search_api.response_contract',
typeLabel: 'string[]',
value: JSON.stringify(EXA_SEARCH_API_RESPONSE_FIELDS),
responsibility: 'Preserve request identity, evidence, structured output, and upstream cost reporting across MCP handoff.',
searchHints: ['requestId', 'searchType', 'costDollars.total', 'grounded output'],
},
{
key: 'search_api.invocation',
typeLabel: '/ # @',
value: EXA_SEARCH_API_INVOCATION_TEXT,
responsibility: 'Project the canonical Agentic Canvas OS tool-routing invocation without provider-specific aliases.',
searchHints: ['/tool.catalog', '#tool-routing', '@tool-provider'],
},
{
key: 'search_api.docs_url',
typeLabel: 'url',
value: EXA_SEARCH_API_DOCS_URL,
responsibility: 'Canonical Exa Search API guide for coding agents used by the shared request contract.',
searchHints: ['Exa Search API coding agent guide', EXA_SEARCH_API_DOCS_URL],
},
{
key: 'fetch_content_limit',
typeLabel: 'integer',
Expand Down
Loading
Loading