@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
@@ -1266,4 +1266,37 @@ code[data-line-numbers] > [data-line]::before {
1266
1266
  .agent-suggestion-item {
1267
1267
  animation: suggestion-fade-in 0.2s ease-out forwards;
1268
1268
  animation-fill-mode: both;
1269
+ }
1270
+
1271
+ /* Sandbox tabs styling */
1272
+ .sandbox-tabs {
1273
+ background-color: hsl(240 10% 20%);
1274
+ border-bottom: 1px solid hsl(240 10% 25%);
1275
+ }
1276
+
1277
+ .sandbox-tab-active {
1278
+ background-color: hsl(240 10% 14%);
1279
+ color: hsl(0 0% 95%);
1280
+ }
1281
+
1282
+ .sandbox-tab-inactive {
1283
+ color: hsl(240 5% 65%);
1284
+ }
1285
+
1286
+ .sandbox-tab-inactive:hover {
1287
+ color: hsl(0 0% 90%);
1288
+ background-color: hsl(240 10% 18%);
1289
+ }
1290
+
1291
+ .sandbox-tab-indicator {
1292
+ background-color: hsl(var(--primary));
1293
+ }
1294
+
1295
+ .sandbox-tab-close {
1296
+ color: hsl(240 5% 55%);
1297
+ }
1298
+
1299
+ .sandbox-tab-close:hover {
1300
+ color: hsl(0 0% 95%);
1301
+ background-color: hsl(240 10% 25%);
1269
1302
  }
@@ -1,6 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { headers } from "next/headers";
2
3
  import { Geist, Geist_Mono } from "next/font/google";
3
4
  import { ThemeProvider } from "@/components/theme-provider";
5
+ import { extractProjectContext } from "@/lib/multi-tenant/context";
6
+ import { getProjectContent } from "@/lib/storage/blob";
4
7
  import "./globals.css";
5
8
  const geistSans = Geist({
6
9
  variable: "--font-geist-sans",
@@ -14,15 +17,87 @@ const geistMono = Geist_Mono({
14
17
  "latin"
15
18
  ]
16
19
  });
17
- export const metadata = {
18
- title: "Documentation",
19
- description: "API Documentation and Reference",
20
- icons: {
21
- icon: '/icon.png',
22
- shortcut: '/icon.png',
23
- apple: '/icon.png'
20
+ // Dynamic metadata for multi-tenant projects
21
+ export async function generateMetadata() {
22
+ const headersList = await headers();
23
+ const hostname = headersList.get("host") || "localhost";
24
+ const context = extractProjectContext(hostname);
25
+ // Default metadata for non-multi-tenant (local development)
26
+ if (!context.isMultiTenant || !context.slug) {
27
+ return {
28
+ title: "Documentation",
29
+ description: "API Documentation and Reference",
30
+ icons: {
31
+ icon: '/icon.png',
32
+ shortcut: '/icon.png',
33
+ apple: '/icon.png'
34
+ }
35
+ };
24
36
  }
25
- };
37
+ // Fetch project data for multi-tenant sites
38
+ try {
39
+ const projectContent = await getProjectContent(context.slug);
40
+ if (projectContent) {
41
+ const docsConfig = JSON.parse(projectContent.docsJson);
42
+ const themeConfig = projectContent.themeJson ? JSON.parse(projectContent.themeJson) : null;
43
+ const title = docsConfig.name || "Documentation";
44
+ const description = docsConfig.description || `${title} - API Documentation and Reference`;
45
+ const logoUrl = themeConfig?.logo?.url ? `https://${context.slug}.devdoc.sh/api/assets/${context.slug}${themeConfig.logo.url.replace('/assets', '')}` : null;
46
+ const faviconUrl = docsConfig.favicon ? `https://${context.slug}.devdoc.sh/api/assets/${context.slug}${docsConfig.favicon.replace('/assets', '')}` : '/icon.png';
47
+ return {
48
+ title: {
49
+ default: title,
50
+ template: `%s | ${title}`
51
+ },
52
+ description,
53
+ icons: {
54
+ icon: faviconUrl,
55
+ shortcut: faviconUrl,
56
+ apple: faviconUrl
57
+ },
58
+ openGraph: {
59
+ title,
60
+ description,
61
+ siteName: title,
62
+ type: "website",
63
+ url: `https://${context.slug}.devdoc.sh`,
64
+ ...logoUrl && {
65
+ images: [
66
+ {
67
+ url: logoUrl,
68
+ width: 1200,
69
+ height: 630,
70
+ alt: title
71
+ }
72
+ ]
73
+ }
74
+ },
75
+ twitter: {
76
+ card: logoUrl ? "summary_large_image" : "summary",
77
+ title,
78
+ description,
79
+ ...logoUrl && {
80
+ images: [
81
+ logoUrl
82
+ ]
83
+ }
84
+ }
85
+ };
86
+ }
87
+ } catch (error) {
88
+ console.error("[Metadata] Error fetching project:", error);
89
+ }
90
+ // Fallback metadata
91
+ return {
92
+ title: "Documentation",
93
+ description: "API Documentation and Reference",
94
+ icons: {
95
+ icon: '/icon.png',
96
+ shortcut: '/icon.png',
97
+ apple: '/icon.png'
98
+ }
99
+ };
100
+ }
26
101
  export const viewport = {
27
102
  width: "device-width",
28
103
  initialScale: 1,
@@ -6,6 +6,7 @@ import { cn } from '@/lib/utils';
6
6
  import { ArrowRight, ArrowSquareOut } from '@phosphor-icons/react';
7
7
  import * as PhosphorIcons from '@phosphor-icons/react';
8
8
  import { useDocsNavigation } from '@/lib/docs-navigation-context';
9
+ import { resolveLink, isExternalLink } from '@/lib/docs-link-utils';
9
10
  // Dynamic icon resolver for Phosphor icons
10
11
  function getIcon(iconName) {
11
12
  if (!iconName) return null;
@@ -44,41 +45,20 @@ export function Card({ title, children, icon, iconSize = 'md', href, horizontal
44
45
  const Icon = isIconString ? getIcon(icon) : null;
45
46
  const hasCustomIcon = !isIconString && icon != null;
46
47
  const sizeConfig = ICON_SIZES[iconSize];
47
- const isExternal = href?.startsWith('http') || href?.startsWith('//');
48
+ const isExternal = href ? isExternalLink(href) : false;
48
49
  const docsNav = useDocsNavigation();
49
- // Check if this is a tab switch link (/api-reference, /changelog)
50
- const isTabSwitchLink = (href === '/api-reference' || href === '/changelog') && docsNav?.isApiDocsView;
51
- // Check if this is an internal docs link (starts with / but not external or tab switch)
52
- const isInternalDocsLink = href?.startsWith('/') && !isExternal && !isTabSwitchLink && docsNav?.isApiDocsView;
53
- // Extract slug from href - removes /docs/ prefix if present, or just the leading /
54
- const getSlugFromHref = (path)=>{
55
- let slug = path;
56
- if (slug.startsWith('/docs/')) {
57
- slug = slug.slice(6); // Remove '/docs/'
58
- } else if (slug.startsWith('/')) {
59
- slug = slug.slice(1); // Remove leading '/'
60
- }
61
- return slug;
62
- };
63
- // Get tab ID from href
64
- const getTabIdFromHref = (path)=>{
65
- if (path === '/api-reference') return 'api-reference';
66
- if (path === '/changelog') return 'changelog';
67
- return 'docs';
68
- };
69
- // Handle click for internal navigation
70
- const handleClick = (e)=>{
71
- if (!href || !docsNav) return;
72
- if (isTabSwitchLink) {
73
- e.preventDefault();
74
- const tabId = getTabIdFromHref(href);
75
- docsNav.switchToTab(tabId);
76
- } else if (isInternalDocsLink) {
77
- e.preventDefault();
78
- const slug = getSlugFromHref(href);
79
- docsNav.navigateToPage(slug);
80
- }
81
- };
50
+ // Resolve link using shared utility
51
+ const resolvedHref = React.useMemo(()=>{
52
+ if (!href) return href;
53
+ return resolveLink(href, {
54
+ activeTab: docsNav?.activeTab,
55
+ tabIds: docsNav?.tabIds
56
+ });
57
+ }, [
58
+ href,
59
+ docsNav?.activeTab,
60
+ docsNav?.tabIds
61
+ ]);
82
62
  const showArrow = arrow ?? isExternal;
83
63
  const cardContent = /*#__PURE__*/ _jsxs("div", {
84
64
  className: cn('docs-card group relative rounded-lg border border-border bg-transparent p-4 transition-all h-full', href && 'hover:border-primary/50 hover:bg-muted/30 cursor-pointer', horizontal ? 'docs-card-horizontal flex items-start gap-4' : 'flex flex-col justify-start', className),
@@ -141,18 +121,9 @@ export function Card({ title, children, icon, iconSize = 'md', href, horizontal
141
121
  children: cardContent
142
122
  });
143
123
  }
144
- // For internal docs links or tab switch links in ApiDocs view, use click handler
145
- if (isInternalDocsLink || isTabSwitchLink) {
146
- return /*#__PURE__*/ _jsx("a", {
147
- href: href,
148
- onClick: handleClick,
149
- className: "docs-card-link block h-full cursor-pointer",
150
- children: cardContent
151
- });
152
- }
153
- // Standard Next.js Link for all other internal links
124
+ // Standard Next.js Link for all internal links (using resolved href)
154
125
  return /*#__PURE__*/ _jsx(Link, {
155
- href: href,
126
+ href: resolvedHref || href,
156
127
  className: "docs-card-link block h-full",
157
128
  children: cardContent
158
129
  });
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { cn } from '@/lib/utils';
5
+ import { File as FileIcon, Folder as FolderIcon, FolderOpen as FolderOpenIcon } from '@phosphor-icons/react';
6
+ // Context to track nesting depth
7
+ const DepthContext = /*#__PURE__*/ React.createContext(0);
8
+ export function FileTree({ children, className }) {
9
+ return /*#__PURE__*/ _jsx("div", {
10
+ className: cn('file-tree my-4 rounded-lg border border-border bg-muted/30 p-4 font-mono text-sm', className),
11
+ children: /*#__PURE__*/ _jsx(DepthContext.Provider, {
12
+ value: 0,
13
+ children: children
14
+ })
15
+ });
16
+ }
17
+ export function Folder({ name, children, defaultOpen = true, comment }) {
18
+ const [isOpen, setIsOpen] = React.useState(defaultOpen);
19
+ const depth = React.useContext(DepthContext);
20
+ const childArray = React.Children.toArray(children);
21
+ const hasChildren = childArray.length > 0;
22
+ return /*#__PURE__*/ _jsxs("div", {
23
+ className: "file-tree-folder",
24
+ children: [
25
+ /*#__PURE__*/ _jsxs("div", {
26
+ className: cn('flex items-center gap-2 py-0.5 hover:bg-muted/50 rounded cursor-pointer', depth > 0 && 'ml-4'),
27
+ style: {
28
+ paddingLeft: depth > 0 ? `${(depth - 1) * 16}px` : 0
29
+ },
30
+ onClick: ()=>hasChildren && setIsOpen(!isOpen),
31
+ children: [
32
+ depth > 0 && /*#__PURE__*/ _jsx("span", {
33
+ className: "text-muted-foreground/50 select-none",
34
+ children: hasChildren ? isOpen ? '▼' : '▶' : '─'
35
+ }),
36
+ isOpen && hasChildren ? /*#__PURE__*/ _jsx(FolderOpenIcon, {
37
+ className: "h-4 w-4 text-primary shrink-0",
38
+ weight: "fill"
39
+ }) : /*#__PURE__*/ _jsx(FolderIcon, {
40
+ className: "h-4 w-4 text-primary shrink-0",
41
+ weight: "fill"
42
+ }),
43
+ /*#__PURE__*/ _jsxs("span", {
44
+ className: "text-foreground",
45
+ children: [
46
+ name,
47
+ "/"
48
+ ]
49
+ }),
50
+ comment && /*#__PURE__*/ _jsxs("span", {
51
+ className: "text-muted-foreground ml-2",
52
+ children: [
53
+ "# ",
54
+ comment
55
+ ]
56
+ })
57
+ ]
58
+ }),
59
+ isOpen && hasChildren && /*#__PURE__*/ _jsx(DepthContext.Provider, {
60
+ value: depth + 1,
61
+ children: /*#__PURE__*/ _jsx("div", {
62
+ className: "file-tree-children border-l border-border/50 ml-2",
63
+ children: children
64
+ })
65
+ })
66
+ ]
67
+ });
68
+ }
69
+ // Alias for MDX usage
70
+ export { Folder as TreeFolder };
71
+ export function File({ name, comment }) {
72
+ const depth = React.useContext(DepthContext);
73
+ return /*#__PURE__*/ _jsxs("div", {
74
+ className: cn('flex items-center gap-2 py-0.5 hover:bg-muted/50 rounded', depth > 0 && 'ml-4'),
75
+ style: {
76
+ paddingLeft: depth > 0 ? `${(depth - 1) * 16}px` : 0
77
+ },
78
+ children: [
79
+ depth > 0 && /*#__PURE__*/ _jsx("span", {
80
+ className: "text-muted-foreground/50 select-none",
81
+ children: "─"
82
+ }),
83
+ /*#__PURE__*/ _jsx(FileIcon, {
84
+ className: "h-4 w-4 text-muted-foreground shrink-0",
85
+ weight: "regular"
86
+ }),
87
+ /*#__PURE__*/ _jsx("span", {
88
+ className: "text-foreground",
89
+ children: name
90
+ }),
91
+ comment && /*#__PURE__*/ _jsxs("span", {
92
+ className: "text-muted-foreground ml-2",
93
+ children: [
94
+ "# ",
95
+ comment
96
+ ]
97
+ })
98
+ ]
99
+ });
100
+ }
101
+ // Alias for MDX usage
102
+ export { File as TreeFile };
@@ -31,6 +31,8 @@ export { Badge } from './badge';
31
31
  export { Mermaid } from './mermaid';
32
32
  // File Embeds
33
33
  export { PDF, Audio, Download } from './file-embeds';
34
+ // File Tree
35
+ export { FileTree, TreeFolder, TreeFile } from './file-tree';
34
36
  // Changelog
35
37
  export { ChangelogEntry, ChangelogTitle, ChangelogDescription, ChangelogImage, BreakingChange, MoreUpdates, UpdateItem } from './changelog';
36
38
  // Landing Page Components
@@ -53,6 +55,7 @@ import { Highlight, Marker, Underline } from './highlight';
53
55
  import { Badge } from './badge';
54
56
  import { Mermaid } from './mermaid';
55
57
  import { PDF, Audio, Download } from './file-embeds';
58
+ import { FileTree, TreeFolder, TreeFile } from './file-tree';
56
59
  import { ChangelogEntry, ChangelogTitle, ChangelogDescription, ChangelogImage, BreakingChange, MoreUpdates, UpdateItem } from './changelog';
57
60
  import { Hero, Pre, Tagline, Headline, Description, CommandBox, Section, Center, FeatureGrid, FeatureItem, ButtonLink, Spacer, Divider } from './landing';
58
61
  export const mdxComponents = {
@@ -108,6 +111,10 @@ export const mdxComponents = {
108
111
  PDF,
109
112
  Audio,
110
113
  Download,
114
+ // File Tree
115
+ FileTree,
116
+ Folder: TreeFolder,
117
+ File: TreeFile,
111
118
  // Changelog
112
119
  ChangelogEntry,
113
120
  ChangelogTitle,
@@ -6,7 +6,7 @@ import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } fro
6
6
  import { ArrowUp, Square, Image as ImageIcon, X } from '@phosphor-icons/react';
7
7
  import { Button } from '@/components/ui/button';
8
8
  import { encodeCardMessage, detectErrorType } from './cards';
9
- import { buildEndpointIndex, searchEndpoints, findRequestById, formatRequestForAI } from '@/lib/api-docs/agent/indexer';
9
+ import { searchEndpoints, findRequestById, formatRequestForAI } from '@/lib/api-docs/agent/indexer';
10
10
  import { ChatMessage, TypingIndicator } from './chat-message';
11
11
  import { cn } from '@/lib/utils';
12
12
  import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
@@ -130,7 +130,7 @@ import { useMobile } from '@/lib/api-docs/mobile-context';
130
130
  }
131
131
  // Storage key for chat history
132
132
  const CHAT_STORAGE_KEY = 'brainfish-agent-chat';
133
- export function AgentChat({ collection, currentEndpoint, onNavigate, onPrefill, apiSummary, debugContext, onDebugContextConsumed, explainContext, onExplainContextConsumed, onOpenGlobalAuth, onNavigateToAuthTab, onNavigateToParamsTab, onNavigateToBodyTab, onNavigateToHeadersTab, onNavigateToDocSection, onNavigateToDocPage, onHasMessagesChange }) {
133
+ export function AgentChat({ collection, currentEndpoint, onNavigate, onPrefill, onPrefillGraphQLVariables, graphqlPlaygroundState, apiSummary, debugContext, onDebugContextConsumed, explainContext, onExplainContextConsumed, onOpenGlobalAuth, onNavigateToAuthTab, onNavigateToParamsTab, onNavigateToBodyTab, onNavigateToHeadersTab, onNavigateToDocSection, onNavigateToDocPage, onHasMessagesChange }) {
134
134
  const messagesEndRef = useRef(null);
135
135
  const textareaRef = useRef(null);
136
136
  const processedDebugRef = useRef(null);
@@ -145,16 +145,21 @@ export function AgentChat({ collection, currentEndpoint, onNavigate, onPrefill,
145
145
  const apiSpecUrl = process.env.NEXT_PUBLIC_OPENAPI_URL || collection.name || 'default';
146
146
  const { workspace } = useWorkspace(apiSpecUrl, collection.name);
147
147
  const { notes, createNote, updateNote, deleteNote, deleteAllNotes, refresh: refreshNotes } = useNotes(workspace?.id || null);
148
- // Build endpoint index for search
149
- const endpointIndex = useMemo(()=>buildEndpointIndex(collection), [
150
- collection
148
+ // Use endpoint index from collection (pre-built by Collections API)
149
+ const endpointIndex = useMemo(()=>collection.endpointIndex || [], [
150
+ collection.endpointIndex
151
151
  ]);
152
- // Suggestions - use shared hook
153
- const { suggestions, isLoading: loadingSuggestions } = useSuggestions({
154
- currentEndpoint: currentEndpoint ? {
152
+ // Memoize currentEndpoint for suggestions to prevent unnecessary re-fetches
153
+ const currentEndpointForSuggestions = useMemo(()=>currentEndpoint ? {
155
154
  id: currentEndpoint.id,
156
155
  name: currentEndpoint.name
157
- } : null,
156
+ } : null, [
157
+ currentEndpoint?.id,
158
+ currentEndpoint?.name
159
+ ]);
160
+ // Suggestions - use shared hook
161
+ const { suggestions, isLoading: loadingSuggestions } = useSuggestions({
162
+ currentEndpoint: currentEndpointForSuggestions,
158
163
  endpointIndex
159
164
  });
160
165
  const [showSuggestions, setShowSuggestions] = useState(true);
@@ -356,6 +361,9 @@ export function AgentChat({ collection, currentEndpoint, onNavigate, onPrefill,
356
361
  const nameLower = ep.name.toLowerCase();
357
362
  const pathLower = ep.path.toLowerCase();
358
363
  const descLower = (ep.description || '').toLowerCase();
364
+ const paramsLower = ep.parameters.map((p)=>p.toLowerCase());
365
+ const tagsLower = ep.tags.map((t)=>t.toLowerCase());
366
+ const isGraphQL = ep.type === 'graphql';
359
367
  if (nameLower.includes(queryLower)) score += 100;
360
368
  if (pathLower.includes(queryLower)) score += 80;
361
369
  if (descLower.includes(queryLower)) score += 60;
@@ -364,15 +372,29 @@ export function AgentChat({ collection, currentEndpoint, onNavigate, onPrefill,
364
372
  if (pathLower.includes(token)) score += 25;
365
373
  if (descLower.includes(token)) score += 15;
366
374
  if (ep.method.toLowerCase() === token) score += 30;
375
+ // GraphQL-specific scoring: match operation type keywords
376
+ if (isGraphQL) {
377
+ const opType = ep.operationType?.toLowerCase();
378
+ if (opType === token || token === 'queries' && opType === 'query' || token === 'mutations' && opType === 'mutation' || token === 'subscriptions' && opType === 'subscription') {
379
+ score += 40;
380
+ }
381
+ }
382
+ // Match parameters/variables
383
+ if (paramsLower.some((p)=>p.includes(token))) score += 25;
384
+ // Match tags
385
+ if (tagsLower.some((t)=>t.includes(token))) score += 20;
367
386
  }
368
387
  if (score > 0) {
388
+ // For GraphQL, show operation type instead of HTTP method
389
+ const displayMethod = isGraphQL ? ep.operationType?.toUpperCase() || 'GRAPHQL' : ep.method;
390
+ const displayPath = isGraphQL ? `${ep.operationType}:${ep.name}` : ep.path;
369
391
  results.push({
370
392
  type: 'endpoint',
371
393
  id: ep.id,
372
394
  title: ep.name,
373
395
  description: ep.description || undefined,
374
- method: ep.method,
375
- path: ep.path,
396
+ method: displayMethod,
397
+ path: displayPath,
376
398
  score
377
399
  });
378
400
  }
@@ -651,6 +673,20 @@ export function AgentChat({ collection, currentEndpoint, onNavigate, onPrefill,
651
673
  }
652
674
  });
653
675
  }
676
+ if (toolName === 'prefill_graphql_variables') {
677
+ const { variables } = toolInput;
678
+ if (onPrefillGraphQLVariables) {
679
+ onPrefillGraphQLVariables(variables);
680
+ }
681
+ addToolOutput({
682
+ tool: 'prefill_graphql_variables',
683
+ toolCallId,
684
+ output: {
685
+ success: true,
686
+ variables
687
+ }
688
+ });
689
+ }
654
690
  if (toolName === 'get_endpoint_details') {
655
691
  const { endpointId } = toolInput;
656
692
  const fullRequest = findRequestById(collection, endpointId);
@@ -700,6 +736,34 @@ export function AgentChat({ collection, currentEndpoint, onNavigate, onPrefill,
700
736
  });
701
737
  }
702
738
  }
739
+ // Handle get_current_graphql_state tool - returns current GraphQL playground state
740
+ if (toolName === 'get_current_graphql_state') {
741
+ if (graphqlPlaygroundState) {
742
+ addToolOutput({
743
+ tool: 'get_current_graphql_state',
744
+ toolCallId,
745
+ output: {
746
+ success: true,
747
+ query: graphqlPlaygroundState.query,
748
+ variables: graphqlPlaygroundState.variables,
749
+ response: graphqlPlaygroundState.response,
750
+ responseTime: graphqlPlaygroundState.responseTime,
751
+ error: graphqlPlaygroundState.error,
752
+ isLoading: graphqlPlaygroundState.isLoading,
753
+ hasResponse: graphqlPlaygroundState.response !== null
754
+ }
755
+ });
756
+ } else {
757
+ addToolOutput({
758
+ tool: 'get_current_graphql_state',
759
+ toolCallId,
760
+ output: {
761
+ success: false,
762
+ error: 'No GraphQL playground state available. Navigate to a GraphQL operation first.'
763
+ }
764
+ });
765
+ }
766
+ }
703
767
  // Handle switch_to_notes tool
704
768
  if (toolName === 'switch_to_notes') {
705
769
  switchToNotes();
@@ -1,16 +1,80 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useCallback } from 'react';
3
+ import { useState, useCallback, useMemo } from 'react';
4
4
  import { Copy, Check, Trash } from '@phosphor-icons/react';
5
5
  import { cn } from '@/lib/utils';
6
6
  import { MarkdownRenderer } from '../../shared/markdown-renderer';
7
7
  import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
8
8
  import { ToolCallDisplay } from './tool-call-display';
9
+ // Primary tools: search, navigate, endpoint operations - shown first
10
+ const PRIMARY_TOOLS = new Set([
11
+ 'search',
12
+ 'search_endpoints',
13
+ 'search_docs',
14
+ 'navigate',
15
+ 'navigate_to_endpoint',
16
+ 'navigate_to_doc_page',
17
+ 'navigate_to_doc_section',
18
+ 'scroll_to_section',
19
+ 'get_endpoint_details',
20
+ 'prefill_parameters',
21
+ 'prefill_graphql_variables',
22
+ 'check_request_validity',
23
+ 'send_request',
24
+ 'switch_to_docs',
25
+ 'switch_to_api_client'
26
+ ]);
27
+ // Secondary tools: notes/file operations - shown after primary tools
28
+ const SECONDARY_TOOLS = new Set([
29
+ 'switch_to_notes',
30
+ 'list_notes',
31
+ 'create_folder',
32
+ 'create_file',
33
+ 'open_file',
34
+ 'write_file',
35
+ 'delete_file'
36
+ ]);
9
37
  export function AssistantMessage({ content, messageId, toolInvocations, isStreaming, onDeleteMessage, onNavigate, onNavigateToDocPage, onOpenFile }) {
38
+ // Separate tool invocations into primary (query/navigate) and secondary (notes) groups
39
+ const { primaryTools, secondaryTools } = useMemo(()=>{
40
+ if (!toolInvocations || toolInvocations.length === 0) {
41
+ return {
42
+ primaryTools: [],
43
+ secondaryTools: []
44
+ };
45
+ }
46
+ const primary = [];
47
+ const secondary = [];
48
+ for (const tool of toolInvocations){
49
+ if (PRIMARY_TOOLS.has(tool.toolName)) {
50
+ primary.push(tool);
51
+ } else if (SECONDARY_TOOLS.has(tool.toolName)) {
52
+ secondary.push(tool);
53
+ } else {
54
+ // Unknown tools go to primary by default
55
+ primary.push(tool);
56
+ }
57
+ }
58
+ return {
59
+ primaryTools: primary,
60
+ secondaryTools: secondary
61
+ };
62
+ }, [
63
+ toolInvocations
64
+ ]);
10
65
  return /*#__PURE__*/ _jsxs("div", {
11
66
  className: cn("aui-assistant-message-root", "fade-in slide-in-from-bottom-1 animate-in duration-150", "relative mx-auto w-full max-w-[var(--thread-max-width)] py-3"),
12
67
  "data-role": "assistant",
13
68
  children: [
69
+ primaryTools.length > 0 && /*#__PURE__*/ _jsx("div", {
70
+ className: "mb-2 px-2 space-y-1",
71
+ children: primaryTools.map((tool)=>/*#__PURE__*/ _jsx(ToolCallDisplay, {
72
+ tool: tool,
73
+ onNavigate: onNavigate,
74
+ onNavigateToDocPage: onNavigateToDocPage,
75
+ onOpenFile: onOpenFile
76
+ }, tool.toolCallId))
77
+ }),
14
78
  content && /*#__PURE__*/ _jsx("div", {
15
79
  className: "aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed",
16
80
  children: /*#__PURE__*/ _jsx("div", {
@@ -20,9 +84,9 @@ export function AssistantMessage({ content, messageId, toolInvocations, isStream
20
84
  })
21
85
  })
22
86
  }),
23
- toolInvocations && toolInvocations.length > 0 && /*#__PURE__*/ _jsx("div", {
87
+ secondaryTools.length > 0 && /*#__PURE__*/ _jsx("div", {
24
88
  className: "mt-2 px-2 space-y-1",
25
- children: toolInvocations.map((tool)=>/*#__PURE__*/ _jsx(ToolCallDisplay, {
89
+ children: secondaryTools.map((tool)=>/*#__PURE__*/ _jsx(ToolCallDisplay, {
26
90
  tool: tool,
27
91
  onNavigate: onNavigate,
28
92
  onNavigateToDocPage: onNavigateToDocPage,