@brainfish-ai/devdoc 0.1.48 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cli/commands/deploy.js +16 -11
  2. package/package.json +1 -1
  3. package/renderer/app/[...slug]/client.js +17 -0
  4. package/renderer/app/[...slug]/page.js +125 -0
  5. package/renderer/app/api/assets/[...path]/route.js +23 -4
  6. package/renderer/app/api/chat/route.js +188 -25
  7. package/renderer/app/api/collections/route.js +95 -2
  8. package/renderer/app/api/deploy/route.js +4 -0
  9. package/renderer/app/api/suggestions/route.js +98 -10
  10. package/renderer/app/globals.css +33 -0
  11. package/renderer/app/layout.js +83 -8
  12. package/renderer/components/docs/mdx/cards.js +16 -45
  13. package/renderer/components/docs/mdx/file-tree.js +102 -0
  14. package/renderer/components/docs/mdx/index.js +7 -0
  15. package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
  16. package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
  17. package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
  18. package/renderer/components/docs-viewer/content/content-router.js +1 -1
  19. package/renderer/components/docs-viewer/content/doc-page.js +36 -28
  20. package/renderer/components/docs-viewer/index.js +223 -58
  21. package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
  22. package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
  23. package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
  24. package/renderer/components/docs-viewer/sidebar/index.js +2 -1
  25. package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
  26. package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
  27. package/renderer/hooks/use-route-state.js +44 -56
  28. package/renderer/lib/api-docs/agent/indexer.js +73 -12
  29. package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
  30. package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
  31. package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
  32. package/renderer/lib/cache/purge.js +98 -0
  33. package/renderer/lib/docs-link-utils.js +146 -0
  34. package/renderer/lib/docs-navigation-context.js +3 -2
  35. package/renderer/lib/docs-navigation.js +50 -41
  36. package/renderer/lib/rate-limit.js +203 -0
@@ -44,7 +44,9 @@ export function ToolCallDisplay({ tool, onNavigate, onNavigateToDocPage, onOpenF
44
44
  toolName === 'search_endpoints' && 'Searching...',
45
45
  toolName === 'navigate_to_endpoint' && 'Navigating...',
46
46
  toolName === 'prefill_parameters' && 'Setting params...',
47
+ toolName === 'prefill_graphql_variables' && 'Setting variables...',
47
48
  toolName === 'get_endpoint_details' && 'Loading details...',
49
+ toolName === 'get_current_graphql_state' && 'Checking playground...',
48
50
  toolName === 'check_request_validity' && 'Validating request...',
49
51
  toolName === 'send_request' && /*#__PURE__*/ _jsxs("span", {
50
52
  className: "flex items-center gap-1.5",
@@ -300,6 +302,48 @@ export function ToolCallDisplay({ tool, onNavigate, onNavigateToDocPage, onOpenF
300
302
  ]
301
303
  });
302
304
  }
305
+ // Prefill GraphQL variables
306
+ if (toolName === 'prefill_graphql_variables') {
307
+ const data = result || args;
308
+ const varCount = data.variables ? Object.keys(data.variables).length : 0;
309
+ return /*#__PURE__*/ _jsxs("div", {
310
+ className: "flex items-center gap-2 text-xs py-1.5 px-2.5 rounded-md bg-muted/50 border border-border/50",
311
+ children: [
312
+ /*#__PURE__*/ _jsx(CheckCircle, {
313
+ className: "size-3.5 text-green-600 dark:text-green-500",
314
+ weight: "fill"
315
+ }),
316
+ /*#__PURE__*/ _jsxs("span", {
317
+ className: "text-muted-foreground",
318
+ children: [
319
+ "Set ",
320
+ varCount,
321
+ " GraphQL variable",
322
+ varCount !== 1 ? 's' : ''
323
+ ]
324
+ })
325
+ ]
326
+ });
327
+ }
328
+ // Get current GraphQL state - show minimal indicator (the AI will describe the state)
329
+ if (toolName === 'get_current_graphql_state') {
330
+ const data = result;
331
+ if (!data?.success) return null // Don't show if failed
332
+ ;
333
+ return /*#__PURE__*/ _jsxs("div", {
334
+ className: "flex items-center gap-2 text-xs py-1.5 px-2.5 rounded-md bg-muted/50 border border-border/50",
335
+ children: [
336
+ /*#__PURE__*/ _jsx(CheckCircle, {
337
+ className: "size-3.5 text-green-600 dark:text-green-500",
338
+ weight: "fill"
339
+ }),
340
+ /*#__PURE__*/ _jsx("span", {
341
+ className: "text-muted-foreground",
342
+ children: data?.hasResponse ? 'Read playground response' : 'Checked playground state'
343
+ })
344
+ ]
345
+ });
346
+ }
303
347
  // Search endpoints
304
348
  if (toolName === 'search_endpoints') {
305
349
  const results = result;
@@ -380,15 +424,16 @@ export function ToolCallDisplay({ tool, onNavigate, onNavigateToDocPage, onOpenF
380
424
  // Get endpoint details
381
425
  if (toolName === 'get_endpoint_details') {
382
426
  const endpoint = result;
383
- if (!endpoint) return null;
427
+ // Check for error or missing required fields
428
+ if (!endpoint || endpoint.error || !endpoint.name) return null;
384
429
  return /*#__PURE__*/ _jsxs("div", {
385
430
  className: "py-1.5 px-2.5 rounded-md bg-muted/50 border border-border/50",
386
431
  children: [
387
432
  /*#__PURE__*/ _jsxs("div", {
388
433
  className: "flex items-center gap-2 text-xs",
389
434
  children: [
390
- /*#__PURE__*/ _jsx(MethodBadge, {
391
- method: endpoint.method || 'GET',
435
+ endpoint.method && /*#__PURE__*/ _jsx(MethodBadge, {
436
+ method: endpoint.method,
392
437
  size: "sm"
393
438
  }),
394
439
  /*#__PURE__*/ _jsx("span", {
@@ -397,7 +442,7 @@ export function ToolCallDisplay({ tool, onNavigate, onNavigateToDocPage, onOpenF
397
442
  })
398
443
  ]
399
444
  }),
400
- /*#__PURE__*/ _jsx("code", {
445
+ endpoint.path && /*#__PURE__*/ _jsx("code", {
401
446
  className: "text-[11px] text-muted-foreground block truncate mt-0.5",
402
447
  children: endpoint.path
403
448
  })
@@ -163,7 +163,7 @@ import { GraphQLPlayground } from '../playground/graphql-playground';
163
163
  ]
164
164
  })
165
165
  }) : /*#__PURE__*/ _jsx("div", {
166
- className: "flex-1 flex items-center justify-center bg-background",
166
+ className: "h-full min-h-[60vh] flex items-center justify-center bg-background",
167
167
  children: /*#__PURE__*/ _jsxs("div", {
168
168
  className: "text-center text-muted-foreground",
169
169
  children: [
@@ -7,38 +7,41 @@ import { useDocsNavigation } from '@/lib/docs-navigation-context';
7
7
  import { useCodeCopy } from '@/hooks/use-code-copy';
8
8
  import { NotFoundPage } from './not-found-page';
9
9
  import { MDXErrorBoundary } from './mdx-error-boundary';
10
+ import { getLinkAction, isExternalLink } from '@/lib/docs-link-utils';
10
11
  // Custom Link component for MDX - uses docs navigation context
11
12
  function MdxLink({ href, children, ...props }) {
12
13
  const docsNav = useDocsNavigation();
13
14
  const handleClick = useCallback((e)=>{
14
- // Handle anchor links (same page)
15
- if (href?.startsWith('#')) {
16
- return; // Let default behavior handle anchor scrolling
17
- }
18
- // Handle internal doc links
19
- if (href?.startsWith('/') && docsNav?.isApiDocsView) {
20
- e.preventDefault();
21
- // Extract the slug from the path
22
- let slug = href;
23
- // Remove leading slash
24
- if (slug.startsWith('/')) {
25
- slug = slug.slice(1);
26
- }
27
- // Handle special paths
28
- if (slug.startsWith('api-reference')) {
29
- docsNav.switchToTab('api-reference');
15
+ if (!href) return;
16
+ const action = getLinkAction(href, {
17
+ activeTab: docsNav?.activeTab,
18
+ tabIds: docsNav?.tabIds
19
+ });
20
+ switch(action.type){
21
+ case 'external':
22
+ case 'anchor':
23
+ // Let default behavior handle these
30
24
  return;
31
- }
32
- if (slug.startsWith('changelog')) {
33
- docsNav.switchToTab('changelog');
25
+ case 'switchTab':
26
+ if (docsNav?.isApiDocsView) {
27
+ e.preventDefault();
28
+ docsNav.switchToTab(action.tabId);
29
+ }
30
+ return;
31
+ case 'navigatePage':
32
+ if (docsNav?.isApiDocsView) {
33
+ e.preventDefault();
34
+ // Remove docs/ prefix if present
35
+ let slug = action.slug;
36
+ if (slug.startsWith('docs/')) {
37
+ slug = slug.slice(5);
38
+ }
39
+ docsNav.navigateToPage(slug);
40
+ }
41
+ return;
42
+ case 'navigate':
43
+ // Use default link behavior
34
44
  return;
35
- }
36
- // Remove docs/ prefix if present
37
- if (slug.startsWith('docs/')) {
38
- slug = slug.slice(5);
39
- }
40
- // Navigate to the page
41
- docsNav.navigateToPage(slug);
42
45
  }
43
46
  }, [
44
47
  href,
@@ -48,7 +51,7 @@ function MdxLink({ href, children, ...props }) {
48
51
  // Using visited: to override browser's purple visited link color
49
52
  const linkClassName = "text-primary visited:text-primary font-medium underline decoration-primary/30 underline-offset-2 hover:decoration-primary transition-colors";
50
53
  // External links
51
- if (href && !href.startsWith('/') && !href.startsWith('#')) {
54
+ if (href && isExternalLink(href)) {
52
55
  return /*#__PURE__*/ _jsx("a", {
53
56
  href: href,
54
57
  target: "_blank",
@@ -67,7 +70,8 @@ function MdxLink({ href, children, ...props }) {
67
70
  });
68
71
  }
69
72
  // Import MDX components
70
- import { Note, Warning, Info, Tip, Check, Error as ErrorCallout, Callout, Card, CardGroup, Accordion, AccordionGroup, Steps, Step, Tabs, Tab, CodeGroup, Frame, Columns, Snippet, Latex, ParamField, ResponseField, Expandable, Iframe, Video, Loom, Image, Screenshot, Logo, Icon, Highlight, Marker, Underline, Badge, Mermaid, PDF, Audio, Download, // Landing Page Components
73
+ import { Note, Warning, Info, Tip, Check, Error as ErrorCallout, Callout, Card, CardGroup, Accordion, AccordionGroup, Steps, Step, Tabs, Tab, CodeGroup, Frame, Columns, Snippet, Latex, ParamField, ResponseField, Expandable, Iframe, Video, Loom, Image, Screenshot, Logo, Icon, Highlight, Marker, Underline, Badge, Mermaid, PDF, Audio, Download, // File Tree
74
+ FileTree, TreeFolder, TreeFile, // Landing Page Components
71
75
  Hero, Pre, Tagline, Headline, Description, CommandBox, Section, Center, FeatureGrid, FeatureItem, ButtonLink, Spacer, Divider } from '../../docs/mdx/index';
72
76
  // MDX components mapping
73
77
  const mdxComponents = {
@@ -123,6 +127,10 @@ const mdxComponents = {
123
127
  PDF,
124
128
  Audio,
125
129
  Download,
130
+ // File Tree
131
+ FileTree,
132
+ Folder: TreeFolder,
133
+ File: TreeFile,
126
134
  // Landing Page
127
135
  Hero,
128
136
  Pre,
@@ -11,7 +11,7 @@ import { DocPage } from './content/doc-page';
11
11
  import { ChangelogPage } from './content/changelog-page';
12
12
  import { GraphQLPlayground } from './playground/graphql-playground';
13
13
  import { makeBrainfishCollection } from '@/lib/api-docs/factories';
14
- import { buildSchema, isNonNullType, isListType } from 'graphql';
14
+ import { buildSchema, isNonNullType, isListType, isInputObjectType, isEnumType } from 'graphql';
15
15
  import { GlobalAuthModal } from './global-auth-modal';
16
16
  import { AuthProvider } from '@/lib/api-docs/auth';
17
17
  import { PlaygroundProvider, usePlaygroundPrefill } from '@/lib/api-docs/playground/context';
@@ -31,7 +31,6 @@ import { useRouteState } from '@/hooks/use-route-state';
31
31
  import { navigateToPage, navigateToEndpoint, navigateToSection, navigateToTab } from '@/lib/docs-navigation';
32
32
  import { useMobile } from '@/lib/api-docs/mobile-context';
33
33
  import { AgentPopupButton } from './agent/agent-popup-button';
34
- import { buildEndpointIndex } from '@/lib/api-docs/agent/indexer';
35
34
  // Helper to convert GraphQL type to string representation
36
35
  function graphqlTypeToString(type) {
37
36
  if (isNonNullType(type)) {
@@ -217,44 +216,70 @@ function generateExampleValue(type, schema, depth = 0) {
217
216
  }
218
217
  // Check if it's an enum type
219
218
  const schemaType = schema.getType(typeName);
220
- if (schemaType && 'getValues' in schemaType) {
221
- const enumType = schemaType;
222
- const values = enumType.getValues();
219
+ if (schemaType && isEnumType(schemaType)) {
220
+ const values = schemaType.getValues();
223
221
  if (values.length > 0) {
224
222
  return values[0].name // Return first enum value
225
223
  ;
226
224
  }
227
225
  }
228
- // Check if it's an input object type
229
- if (schemaType && 'getFields' in schemaType) {
230
- const inputType = schemaType;
231
- const fields = inputType.getFields();
226
+ // Check if it's an input object type (for filter inputs, etc.)
227
+ if (schemaType && isInputObjectType(schemaType)) {
228
+ const fields = schemaType.getFields();
232
229
  const result = {};
233
- // Only include required fields to keep examples concise
230
+ // First, include all required fields
234
231
  for (const [fieldName, field] of Object.entries(fields)){
235
232
  if (isNonNullType(field.type)) {
236
- // Use default value if available, otherwise generate
237
233
  if (field.defaultValue !== undefined) {
238
234
  result[fieldName] = field.defaultValue;
239
235
  } else {
240
- result[fieldName] = generateExampleValue(field.type, schema, depth + 1);
236
+ const value = generateExampleValue(field.type, schema, depth + 1);
237
+ if (value !== null) {
238
+ result[fieldName] = value;
239
+ }
241
240
  }
242
241
  }
243
242
  }
244
- // If no required fields, include first optional field
243
+ // If no required fields, include a useful optional field
244
+ // Prioritize common filter/query fields
245
245
  if (Object.keys(result).length === 0) {
246
- const firstField = Object.entries(fields)[0];
247
- if (firstField) {
248
- const [fname, fdef] = firstField;
249
- // Use default value if available
246
+ const fieldEntries = Object.entries(fields);
247
+ // Priority order for filter fields
248
+ const priorityFields = [
249
+ 'eq',
250
+ 'equals',
251
+ 'id',
252
+ 'code',
253
+ 'name',
254
+ 'in',
255
+ 'contains'
256
+ ];
257
+ let selectedField;
258
+ // Find a priority field first
259
+ for (const pf of priorityFields){
260
+ const found = fieldEntries.find(([name])=>name.toLowerCase() === pf);
261
+ if (found) {
262
+ selectedField = found;
263
+ break;
264
+ }
265
+ }
266
+ // Fall back to first field
267
+ if (!selectedField && fieldEntries.length > 0) {
268
+ selectedField = fieldEntries[0];
269
+ }
270
+ if (selectedField) {
271
+ const [fname, fdef] = selectedField;
250
272
  if (fdef.defaultValue !== undefined) {
251
273
  result[fname] = fdef.defaultValue;
252
274
  } else {
253
- result[fname] = generateExampleValue(fdef.type, schema, depth + 1);
275
+ const value = generateExampleValue(fdef.type, schema, depth + 1);
276
+ if (value !== null) {
277
+ result[fname] = value;
278
+ }
254
279
  }
255
280
  }
256
281
  }
257
- return result;
282
+ return Object.keys(result).length > 0 ? result : null;
258
283
  }
259
284
  // Unknown type - return null
260
285
  return null;
@@ -657,14 +682,27 @@ function DocsContent() {
657
682
  if (!collection) return;
658
683
  const request = findRequestById(collection, endpointId);
659
684
  if (request) {
660
- // Navigate to the endpoint in api-reference tab
661
- navigateToEndpoint('api-reference', endpointId);
685
+ // Find the correct tab for the endpoint
686
+ // If current tab is an OpenAPI tab, use it; otherwise find the first OpenAPI tab
687
+ const currentTabConfig = collection.navigationTabs?.find((t)=>t.id === activeTab);
688
+ const isCurrentTabOpenApi = currentTabConfig?.type === 'openapi' || currentTabConfig?.type === 'graphql';
689
+ let targetTab = activeTab;
690
+ if (!isCurrentTabOpenApi) {
691
+ // Find the first OpenAPI tab
692
+ const firstOpenApiTab = collection.navigationTabs?.find((t)=>t.type === 'openapi' || t.type === 'graphql');
693
+ if (firstOpenApiTab) {
694
+ targetTab = firstOpenApiTab.id;
695
+ }
696
+ }
697
+ // Navigate to the endpoint in the correct tab
698
+ navigateToEndpoint(targetTab, endpointId);
662
699
  // Reset tab navigation so the new endpoint can determine its default tab
663
700
  resetNavigation();
664
701
  }
665
702
  }, [
666
703
  collection,
667
- resetNavigation
704
+ resetNavigation,
705
+ activeTab
668
706
  ]);
669
707
  // Handler for agent prefilling parameters
670
708
  const handleAgentPrefill = useCallback((data)=>{
@@ -790,15 +828,18 @@ function DocsContent() {
790
828
  }
791
829
  // Handle initial navigation after collection loads
792
830
  setTimeout(()=>{
793
- const hash = window.location.hash.slice(1);
794
- const isTabOnly = hash && !hash.includes('/');
795
- if (!hash) {
796
- // No hash - navigate to first content of default tab
831
+ const pathname = window.location.pathname;
832
+ const pathParts = pathname.split('/').filter(Boolean);
833
+ const isTabOnly = pathParts.length === 1 // Just /tab-name
834
+ ;
835
+ if (pathParts.length === 0) {
836
+ // No path - navigate to first content of default tab
797
837
  navigateToFirstContent(defaultTabId);
798
838
  } else if (isTabOnly) {
799
- // Just a tab name (e.g., #guides) - navigate to first content
800
- navigateToFirstContent(hash);
839
+ // Just a tab name (e.g., /facebook-pages-api) - navigate to first content
840
+ navigateToFirstContent(pathParts[0]);
801
841
  }
842
+ // If path has content (e.g., /tab/page/slug), no auto-navigation needed
802
843
  // View mode (docs/playground/notes) is derived from URL by ModeProvider
803
844
  }, 0);
804
845
  }
@@ -1069,14 +1110,71 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
1069
1110
  const { setTheme } = useTheme();
1070
1111
  // Agent panel state - controls popup button and push animation
1071
1112
  const { isRightSidebarOpen, openRightSidebar } = useMobile();
1072
- // Build endpoint index for suggestions (memoized)
1073
- const endpointIndex = useMemo(()=>buildEndpointIndex(collection), [
1074
- collection
1113
+ // Memoize currentEndpoint for AgentPopupButton to prevent unnecessary re-renders
1114
+ // Using primitive deps intentionally to avoid re-creating object on every render
1115
+ const currentEndpointForPopup = useMemo(()=>selectedRequest ? {
1116
+ id: selectedRequest.id,
1117
+ name: selectedRequest.name
1118
+ } : null, // eslint-disable-next-line react-hooks/exhaustive-deps
1119
+ [
1120
+ selectedRequest?.id,
1121
+ selectedRequest?.name
1075
1122
  ]);
1076
1123
  // GraphQL state
1077
1124
  const [graphqlOperations, setGraphqlOperations] = useState([]);
1078
1125
  const [graphqlCollection, setGraphqlCollection] = useState(null);
1079
1126
  const [graphqlSchemaSDL, setGraphqlSchemaSDL] = useState(undefined);
1127
+ // External GraphQL variables (set by AI agent prefill tool)
1128
+ const [externalGraphQLVariables, setExternalGraphQLVariables] = useState(undefined);
1129
+ // Current GraphQL playground state (for AI context)
1130
+ const [graphqlPlaygroundState, setGraphqlPlaygroundState] = useState(null);
1131
+ // Handler for AI agent prefilling GraphQL variables
1132
+ const handlePrefillGraphQLVariables = useCallback((variables)=>{
1133
+ console.log('[Agent] GraphQL variables prefill requested:', variables);
1134
+ setExternalGraphQLVariables(variables);
1135
+ }, []);
1136
+ // Handler for GraphQL playground state changes (for AI context)
1137
+ const handleGraphQLStateChange = useCallback((state)=>{
1138
+ setGraphqlPlaygroundState(state);
1139
+ }, []);
1140
+ // Handler for GraphQL debug requests - convert to DebugContext format
1141
+ const handleGraphQLDebugRequest = useCallback((context)=>{
1142
+ const hasErrors = context.response?.errors && context.response.errors.length > 0;
1143
+ const debugCtx = {
1144
+ status: hasErrors ? 400 : 200,
1145
+ statusText: hasErrors ? 'GraphQL Error' : 'OK',
1146
+ responseBody: context.response ? JSON.stringify(context.response, null, 2) : '',
1147
+ errorMessage: context.error || context.response?.errors?.map((e)=>e.message).join(', '),
1148
+ endpointName: context.operationName || 'GraphQL Query',
1149
+ endpointMethod: 'QUERY',
1150
+ endpointPath: 'graphql',
1151
+ requestBody: JSON.stringify({
1152
+ query: context.query,
1153
+ variables: context.variables
1154
+ }, null, 2)
1155
+ };
1156
+ handleDebugRequest(debugCtx);
1157
+ }, [
1158
+ handleDebugRequest
1159
+ ]);
1160
+ // Handler for GraphQL explain requests - convert to DebugContext format
1161
+ const handleGraphQLExplainRequest = useCallback((context)=>{
1162
+ const explainCtx = {
1163
+ status: 200,
1164
+ statusText: 'OK',
1165
+ responseBody: context.response ? JSON.stringify(context.response, null, 2) : '',
1166
+ endpointName: context.operationName || 'GraphQL Query',
1167
+ endpointMethod: 'QUERY',
1168
+ endpointPath: 'graphql',
1169
+ requestBody: JSON.stringify({
1170
+ query: context.query,
1171
+ variables: context.variables
1172
+ }, null, 2)
1173
+ };
1174
+ handleExplainRequest(explainCtx);
1175
+ }, [
1176
+ handleExplainRequest
1177
+ ]);
1080
1178
  // Use route state for URL-based selection
1081
1179
  const routeState = useRouteState();
1082
1180
  // Derive selected GraphQL operation from URL
@@ -1112,13 +1210,6 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
1112
1210
  window.location.hash = hashPath;
1113
1211
  }
1114
1212
  }, []);
1115
- // Wrap agent navigate to switch to Docs mode and navigate to endpoint
1116
- const handleAgentNavigateWithModeSwitch = useCallback((endpointId)=>{
1117
- // handleAgentNavigate navigates via URL
1118
- handleAgentNavigate(endpointId);
1119
- }, [
1120
- handleAgentNavigate
1121
- ]);
1122
1213
  // Get the current content title for header
1123
1214
  // Find the active tab's config
1124
1215
  const activeTabConfig = collection.navigationTabs?.find((t)=>t.id === activeTab);
@@ -1135,12 +1226,21 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
1135
1226
  // Show GraphQL playground when graphql tab is active
1136
1227
  const showGraphQL = activeTabType === 'graphql';
1137
1228
  const activeGraphQLSchemas = activeTabConfig?.graphqlSchemas || [];
1138
- // Get the first schema path for stable dependency
1139
- const schemaPath = activeGraphQLSchemas[0]?.schema;
1140
- const schemaEndpoint = activeGraphQLSchemas[0]?.endpoint;
1141
- // Load GraphQL operations when graphql tab is active
1229
+ // Get the first GraphQL schema from any tab (for playground/sidebar)
1230
+ const firstGraphQLSchema = useMemo(()=>{
1231
+ for (const tab of collection.navigationTabs || []){
1232
+ if (tab.type === 'graphql' && tab.graphqlSchemas?.[0]) {
1233
+ return tab.graphqlSchemas[0];
1234
+ }
1235
+ }
1236
+ return null;
1237
+ }, [
1238
+ collection.navigationTabs
1239
+ ]);
1240
+ // Always load GraphQL schema when available (for AI agent search/navigate)
1241
+ // This ensures GraphQL endpoints are available regardless of active tab
1142
1242
  useEffect(()=>{
1143
- if (!showGraphQL || !schemaPath) {
1243
+ if (!firstGraphQLSchema?.schema) {
1144
1244
  setGraphqlOperations([]);
1145
1245
  setGraphqlCollection(null);
1146
1246
  setGraphqlSchemaSDL(undefined);
@@ -1149,30 +1249,89 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
1149
1249
  // Load and parse GraphQL schema
1150
1250
  const loadGraphQLSchema = async ()=>{
1151
1251
  try {
1152
- console.log('[DocsViewer] Loading GraphQL schema from:', schemaPath);
1153
- const response = await fetch(`/api/schema?path=${encodeURIComponent(schemaPath)}`);
1252
+ const response = await fetch(`/api/schema?path=${encodeURIComponent(firstGraphQLSchema.schema)}`);
1154
1253
  if (!response.ok) {
1155
- console.error('[DocsViewer] Failed to fetch schema:', response.status);
1156
1254
  return;
1157
1255
  }
1158
1256
  const schemaContent = await response.text();
1159
- console.log('[DocsViewer] Schema loaded, length:', schemaContent.length);
1160
1257
  setGraphqlSchemaSDL(schemaContent); // Store schema SDL for autocomplete
1161
1258
  const operations = parseGraphQLSchema(schemaContent);
1162
- console.log('[DocsViewer] Parsed operations:', operations.length);
1163
1259
  setGraphqlOperations(operations);
1164
1260
  // Convert to BrainfishCollection for sidebar
1165
- const gqlCollection = convertGraphQLToCollection(operations, schemaEndpoint || '');
1261
+ const gqlCollection = convertGraphQLToCollection(operations, firstGraphQLSchema.endpoint || '');
1166
1262
  setGraphqlCollection(gqlCollection);
1167
1263
  } catch (err) {
1168
1264
  console.error('[DocsViewer] Failed to load GraphQL schema:', err);
1169
1265
  }
1170
1266
  };
1171
1267
  loadGraphQLSchema();
1268
+ }, [
1269
+ firstGraphQLSchema?.schema,
1270
+ firstGraphQLSchema?.endpoint
1271
+ ]);
1272
+ // Active collection - GraphQL collection when GraphQL tab is active, otherwise REST collection
1273
+ // This is used by sidebar, agent chat, and other components that need the current API context
1274
+ const activeCollection = useMemo(()=>{
1275
+ return showGraphQL && graphqlCollection ? graphqlCollection : collection;
1172
1276
  }, [
1173
1277
  showGraphQL,
1174
- schemaPath,
1175
- schemaEndpoint
1278
+ graphqlCollection,
1279
+ collection
1280
+ ]);
1281
+ // Active request - the currently selected endpoint (REST or GraphQL)
1282
+ const activeRequest = useMemo(()=>{
1283
+ if (showGraphQL && selectedGraphQLOperation && graphqlCollection) {
1284
+ return findRequestById(graphqlCollection, selectedGraphQLOperation.id);
1285
+ }
1286
+ return selectedRequest;
1287
+ }, [
1288
+ showGraphQL,
1289
+ selectedGraphQLOperation,
1290
+ graphqlCollection,
1291
+ selectedRequest
1292
+ ]);
1293
+ // Use endpoint index from Collections API (already includes REST + GraphQL)
1294
+ // This is pre-built server-side for better performance and consistency
1295
+ const endpointIndex = collection.endpointIndex || [];
1296
+ // Wrap agent navigate to switch to Docs mode and navigate to endpoint
1297
+ // Uses activeCollection to support both REST and GraphQL endpoints
1298
+ const handleAgentNavigateWithModeSwitch = useCallback((endpointId)=>{
1299
+ // First check if the endpoint exists in the active collection (current tab)
1300
+ const requestInActive = findRequestById(activeCollection, endpointId);
1301
+ if (requestInActive) {
1302
+ navigateToEndpoint(activeTab, endpointId);
1303
+ return;
1304
+ }
1305
+ // Check if it's in the GraphQL collection (might be on different tab)
1306
+ if (graphqlCollection) {
1307
+ const requestInGraphQL = findRequestById(graphqlCollection, endpointId);
1308
+ if (requestInGraphQL) {
1309
+ // Find the GraphQL tab and navigate there
1310
+ const graphqlTab = collection.navigationTabs?.find((t)=>t.type === 'graphql');
1311
+ if (graphqlTab) {
1312
+ navigateToEndpoint(graphqlTab.id, endpointId);
1313
+ return;
1314
+ }
1315
+ }
1316
+ }
1317
+ // Check if it's in the REST collection (might be on different tab)
1318
+ const requestInREST = findRequestById(collection, endpointId);
1319
+ if (requestInREST) {
1320
+ // Find the first OpenAPI tab and navigate there
1321
+ const openApiTab = collection.navigationTabs?.find((t)=>t.type === 'openapi');
1322
+ if (openApiTab) {
1323
+ navigateToEndpoint(openApiTab.id, endpointId);
1324
+ return;
1325
+ }
1326
+ }
1327
+ // Fall back to parent's navigate
1328
+ handleAgentNavigate(endpointId);
1329
+ }, [
1330
+ activeCollection,
1331
+ activeTab,
1332
+ graphqlCollection,
1333
+ collection,
1334
+ handleAgentNavigate
1176
1335
  ]);
1177
1336
  // Auto-navigate to first GraphQL operation when none selected and no doc pages
1178
1337
  useEffect(()=>{
@@ -1329,16 +1488,21 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
1329
1488
  onNavigateToPage: handleSelectDocPage,
1330
1489
  onSwitchTab: onTabChange,
1331
1490
  activeTab: activeTab,
1491
+ tabIds: collection.navigationTabs?.map((t)=>t.id),
1332
1492
  children: /*#__PURE__*/ _jsx("div", {
1333
1493
  ref: contentRef,
1334
1494
  className: cn("docs-content-area flex-1 bg-background min-w-0", showChangelog || showGraphQL && selectedGraphQLOperation && !selectedDocPage ? "overflow-hidden flex flex-col" : "overflow-y-auto scroll-smooth"),
1335
1495
  children: showGraphQL && selectedGraphQLOperation && !selectedDocPage ? /*#__PURE__*/ _jsx("div", {
1336
1496
  className: "flex-1 flex flex-col h-full",
1337
1497
  children: /*#__PURE__*/ _jsx(GraphQLPlayground, {
1338
- endpoint: activeGraphQLSchemas[0]?.endpoint || '',
1498
+ endpoint: activeGraphQLSchemas[0]?.endpoint || firstGraphQLSchema?.endpoint || '',
1339
1499
  defaultQuery: selectedGraphQLOperation?.query,
1340
1500
  operations: graphqlOperations,
1341
1501
  selectedOperationId: selectedGraphQLOperation?.id,
1502
+ externalVariables: externalGraphQLVariables,
1503
+ onStateChange: handleGraphQLStateChange,
1504
+ onDebugRequest: handleGraphQLDebugRequest,
1505
+ onExplainRequest: handleGraphQLExplainRequest,
1342
1506
  hideExplorer: true,
1343
1507
  headers: {},
1344
1508
  theme: "dark",
@@ -1402,11 +1566,13 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
1402
1566
  ]
1403
1567
  }),
1404
1568
  /*#__PURE__*/ _jsx(RightSidebar, {
1405
- request: selectedRequest,
1569
+ request: activeRequest,
1406
1570
  collection: collection,
1407
1571
  apiSummary: collection.apiSummary,
1408
1572
  onNavigateToEndpoint: handleAgentNavigateWithModeSwitch,
1409
1573
  onPrefillParameters: handleAgentPrefill,
1574
+ onPrefillGraphQLVariables: handlePrefillGraphQLVariables,
1575
+ graphqlPlaygroundState: graphqlPlaygroundState,
1410
1576
  debugContext: debugContext,
1411
1577
  onClearDebugContext: clearDebugContext,
1412
1578
  explainContext: explainContext,
@@ -1444,17 +1610,16 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
1444
1610
  !isRightSidebarOpen && /*#__PURE__*/ _jsx(AgentPopupButton, {
1445
1611
  onClick: ()=>openRightSidebar(),
1446
1612
  onQuestionClick: (prompt)=>openRightSidebar(prompt),
1447
- currentEndpoint: selectedRequest ? {
1448
- id: selectedRequest.id,
1449
- name: selectedRequest.name
1450
- } : null,
1613
+ currentEndpoint: currentEndpointForPopup,
1451
1614
  endpointIndex: endpointIndex
1452
1615
  })
1453
1616
  ]
1454
1617
  });
1455
1618
  }
1456
1619
  // Wrapper component with providers
1457
- export function Docs() {
1620
+ export function Docs({ initialPath } = {}) {
1621
+ // initialPath is passed for SSR but useRouteState reads from usePathname() automatically
1622
+ void initialPath; // Acknowledge prop (used for SSR metadata matching)
1458
1623
  return /*#__PURE__*/ _jsx(AuthProvider, {
1459
1624
  children: /*#__PURE__*/ _jsx(ModeProvider, {
1460
1625
  children: /*#__PURE__*/ _jsx(PlaygroundProvider, {