@redocly/theme 0.70.0-next.5 → 0.70.0-next.6

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 (87) hide show
  1. package/lib/components/Buttons/ConnectMCPButton.js +8 -13
  2. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.js +1 -1
  3. package/lib/components/Catalog/CatalogFilter/CatalogFilterDateRange.js +2 -1
  4. package/lib/components/Dropdown/Dropdown.d.ts +4 -2
  5. package/lib/components/Dropdown/Dropdown.js +28 -8
  6. package/lib/components/Filter/FilterDateRange.js +36 -2
  7. package/lib/components/Search/SearchAiActivity.d.ts +10 -0
  8. package/lib/components/Search/SearchAiActivity.js +278 -0
  9. package/lib/components/Search/SearchAiDialog.js +5 -1
  10. package/lib/components/Search/SearchAiMessage.d.ts +2 -1
  11. package/lib/components/Search/SearchAiMessage.js +20 -108
  12. package/lib/components/Search/SearchItem.js +1 -8
  13. package/lib/components/Search/variables.js +15 -2
  14. package/lib/components/Select/SelectInput.js +2 -8
  15. package/lib/components/SplitButton/SplitButton.js +37 -3
  16. package/lib/core/constants/mcp.d.ts +2 -2
  17. package/lib/core/constants/mcp.js +3 -8
  18. package/lib/core/constants/search.js +5 -1
  19. package/lib/core/hooks/index.d.ts +1 -0
  20. package/lib/core/hooks/index.js +1 -0
  21. package/lib/core/hooks/menu/use-collapse.js +1 -1
  22. package/lib/core/hooks/search/use-search-ai-activity.d.ts +11 -0
  23. package/lib/core/hooks/search/use-search-ai-activity.js +15 -0
  24. package/lib/core/hooks/use-connect-mcp-button.js +2 -1
  25. package/lib/core/hooks/use-full-height.d.ts +1 -1
  26. package/lib/core/hooks/use-full-height.js +8 -8
  27. package/lib/core/styles/global.d.ts +2 -0
  28. package/lib/core/styles/global.js +80 -76
  29. package/lib/core/styles/index.d.ts +1 -0
  30. package/lib/core/styles/index.js +3 -1
  31. package/lib/core/styles/search-ai-appear.d.ts +1 -0
  32. package/lib/core/styles/search-ai-appear.js +18 -0
  33. package/lib/core/types/dropdown.d.ts +1 -0
  34. package/lib/core/types/dropdown.js +3 -0
  35. package/lib/core/types/index.d.ts +1 -0
  36. package/lib/core/types/l10n.d.ts +1 -1
  37. package/lib/core/types/mcp.d.ts +2 -1
  38. package/lib/core/types/search.d.ts +23 -0
  39. package/lib/core/utils/assign-ref.d.ts +2 -0
  40. package/lib/core/utils/assign-ref.js +12 -0
  41. package/lib/core/utils/content-segments.d.ts +12 -1
  42. package/lib/core/utils/content-segments.js +90 -5
  43. package/lib/core/utils/dropdown.d.ts +2 -0
  44. package/lib/core/utils/dropdown.js +12 -0
  45. package/lib/core/utils/index.d.ts +2 -0
  46. package/lib/core/utils/index.js +2 -0
  47. package/lib/core/utils/mcp.d.ts +2 -1
  48. package/lib/core/utils/mcp.js +5 -0
  49. package/lib/core/utils/search-ai-activity.d.ts +4 -0
  50. package/lib/core/utils/search-ai-activity.js +83 -0
  51. package/lib/markdoc/tags/connect-mcp.js +15 -0
  52. package/package.json +2 -2
  53. package/src/components/Buttons/ConnectMCPButton.tsx +8 -14
  54. package/src/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.tsx +3 -1
  55. package/src/components/Catalog/CatalogFilter/CatalogFilterDateRange.tsx +3 -2
  56. package/src/components/Dropdown/Dropdown.tsx +28 -6
  57. package/src/components/Filter/FilterDateRange.tsx +3 -2
  58. package/src/components/Search/SearchAiActivity.tsx +318 -0
  59. package/src/components/Search/SearchAiDialog.tsx +4 -3
  60. package/src/components/Search/SearchAiMessage.tsx +27 -135
  61. package/src/components/Search/SearchItem.tsx +2 -9
  62. package/src/components/Search/variables.ts +15 -2
  63. package/src/components/Select/SelectInput.tsx +2 -8
  64. package/src/components/SplitButton/SplitButton.tsx +8 -2
  65. package/src/core/constants/mcp.ts +3 -9
  66. package/src/core/constants/search.ts +5 -1
  67. package/src/core/hooks/__mocks__/index.ts +1 -0
  68. package/src/core/hooks/index.ts +1 -0
  69. package/src/core/hooks/menu/use-collapse.ts +1 -1
  70. package/src/core/hooks/search/use-search-ai-activity.ts +31 -0
  71. package/src/core/hooks/use-connect-mcp-button.ts +7 -2
  72. package/src/core/hooks/use-full-height.ts +8 -8
  73. package/src/core/styles/global.ts +80 -75
  74. package/src/core/styles/index.ts +2 -1
  75. package/src/core/styles/search-ai-appear.ts +16 -0
  76. package/src/core/types/dropdown.ts +1 -0
  77. package/src/core/types/index.ts +1 -0
  78. package/src/core/types/l10n.ts +1 -1
  79. package/src/core/types/mcp.ts +3 -1
  80. package/src/core/types/search.ts +21 -0
  81. package/src/core/utils/assign-ref.ts +9 -0
  82. package/src/core/utils/content-segments.ts +98 -6
  83. package/src/core/utils/dropdown.ts +14 -0
  84. package/src/core/utils/index.ts +2 -0
  85. package/src/core/utils/mcp.ts +7 -1
  86. package/src/core/utils/search-ai-activity.ts +110 -0
  87. package/src/markdoc/tags/connect-mcp.ts +16 -1
@@ -119,6 +119,10 @@ export function SearchAiDialog({
119
119
 
120
120
  // oxlint-disable-next-line react-you-might-not-need-an-effect/no-pass-data-to-parent -- merges the streamed assistant response into the lifted conversation state as chunks arrive
121
121
  setConversation((prev) => {
122
+ // A reset can land between this effect's render and its update.
123
+ if (prev.length === 0) {
124
+ return prev;
125
+ }
122
126
  const lastMessage = prev[prev.length - 1];
123
127
  const content: string = response || '';
124
128
 
@@ -214,9 +218,6 @@ export function SearchAiDialog({
214
218
  messageId={item.messageId}
215
219
  feedback={item.feedback}
216
220
  onFeedbackChange={handleFeedbackChange}
217
- toolCalls={
218
- item.toolCalls || (index === conversation.length - 1 ? toolCalls : undefined)
219
- }
220
221
  contentSegments={item.contentSegments}
221
222
  />
222
223
  ))}
@@ -6,9 +6,9 @@ import {
6
6
  type SearchAiMessageResource,
7
7
  type ToolCall,
8
8
  type ContentSegment,
9
- ToolCallName,
10
9
  } from '@redocly/theme/core/types';
11
- import { TOOL_CALL_DISPLAY_TEXT, AiSearchConversationRole } from '@redocly/theme/core/constants';
10
+ import { AiSearchConversationRole } from '@redocly/theme/core/constants';
11
+ import { groupToolCallSegments, hasToolCallsAndText } from '@redocly/theme/core/utils';
12
12
  import { Link } from '@redocly/theme/components/Link/Link';
13
13
  import { Tag } from '@redocly/theme/components/Tag/Tag';
14
14
  import { useThemeHooks } from '@redocly/theme/core/hooks';
@@ -18,43 +18,14 @@ import { AiStarsIcon } from '@redocly/theme/icons/AiStarsIcon/AiStarsIcon';
18
18
  import { CheckmarkOutlineIcon } from '@redocly/theme/icons/CheckmarkOutlineIcon/CheckmarkOutlineIcon';
19
19
  import { SearchAiActionButtons } from '@redocly/theme/components/Search/SearchAiActionButtons';
20
20
  import { SearchAiNegativeFeedbackForm } from '@redocly/theme/components/Search/SearchAiNegativeFeedbackForm';
21
+ import { SearchAiActivity } from '@redocly/theme/components/Search/SearchAiActivity';
22
+ import { searchAiAppearAnimation } from '@redocly/theme/core/styles';
21
23
 
22
24
  function MarkdownSegment({ text }: { text: string }): JSX.Element {
23
25
  const { useMarkdownText } = useThemeHooks();
24
26
  const markdown = useMarkdownText(text);
25
27
  return <ResponseText as="div" children={markdown} data-testid="response-text" />;
26
28
  }
27
- // The codemode `execute` tool passes a human-readable `description` of what its code
28
- // does; show that instead of a generic "Executing execute..." label.
29
- function getExecuteDescription(args: unknown): string | undefined {
30
- if (args && typeof args === 'object' && 'description' in args) {
31
- const { description } = args as { description?: unknown };
32
- if (typeof description === 'string' && description.trim().length > 0) {
33
- return description.trim();
34
- }
35
- }
36
- return undefined;
37
- }
38
-
39
- function getToolCallDisplayText(toolCall: ToolCall): {
40
- inProgressText: string;
41
- completedText: string;
42
- } {
43
- if (toolCall.name === ToolCallName.Execute) {
44
- const description = getExecuteDescription(toolCall.args);
45
- if (description) {
46
- return { inProgressText: description, completedText: description };
47
- }
48
- }
49
-
50
- return (
51
- TOOL_CALL_DISPLAY_TEXT[toolCall.name] ?? {
52
- inProgressText: `Executing ${toolCall.name}...`,
53
- completedText: `${toolCall.name} executed`,
54
- }
55
- );
56
- }
57
-
58
29
  export type SearchAiMessageProps = {
59
30
  role: AiSearchConversationRole;
60
31
  content: string;
@@ -64,6 +35,7 @@ export type SearchAiMessageProps = {
64
35
  messageId?: string;
65
36
  feedback?: FeedbackType;
66
37
  onFeedbackChange: (messageId: string, feedback: FeedbackType | undefined) => void;
38
+ /** @deprecated Not used; the message reads its tool calls from `contentSegments`. */
67
39
  toolCalls?: ToolCall[];
68
40
  contentSegments?: ContentSegment[];
69
41
  };
@@ -77,7 +49,6 @@ function SearchAiMessageComponent({
77
49
  messageId,
78
50
  feedback,
79
51
  onFeedbackChange,
80
- toolCalls = [],
81
52
  contentSegments = [{ type: 'text' as const, text: content }],
82
53
  }: SearchAiMessageProps): JSX.Element {
83
54
  const { useTranslate, useTelemetry } = useThemeHooks();
@@ -90,7 +61,8 @@ function SearchAiMessageComponent({
90
61
 
91
62
  const showSuccessMessage = feedbackSent && feedback;
92
63
 
93
- const isLoading = isThinking && content.length === 0 && toolCalls.length === 0;
64
+ const groupedSegments = groupToolCallSegments(contentSegments, isThinking);
65
+ const isFullWidth = hasToolCallsAndText(contentSegments);
94
66
 
95
67
  const sendFeedbackTelemetry = (feedbackValue: FeedbackType, dislikeReason?: string) => {
96
68
  if (!messageId) return;
@@ -143,29 +115,18 @@ function SearchAiMessageComponent({
143
115
  margin="0 var(--spacing-xs) 0 0"
144
116
  />
145
117
  )}
146
- <MessageContentWrapper>
118
+ <MessageContentWrapper $isFullWidth={isFullWidth}>
147
119
  {role === AiSearchConversationRole.ASSISTANT ? (
148
120
  <>
149
- <MessageWrapper $role={role}>
150
- {contentSegments.map((segment, index) => {
151
- if (segment.type === 'tool') {
152
- const toolCallCompleted = Boolean(segment.toolCall.result);
153
-
154
- const { inProgressText, completedText } = getToolCallDisplayText(
155
- segment.toolCall,
156
- );
157
-
158
- const toolCallDisplayText = toolCallCompleted ? completedText : inProgressText;
159
-
121
+ <MessageWrapper $role={role} $isFullWidth={isFullWidth}>
122
+ {groupedSegments.map((segment, index) => {
123
+ if (segment.type === 'tools') {
160
124
  return (
161
- <ToolCallsInfoWrapper key={`tool-${index}`} data-testid="tool-calls-info">
162
- <ToolCallInfoItem>
163
- <DocumentIcon size="14px" color="--search-ai-text-color" />
164
- <ToolCallText $isSearching={!toolCallCompleted}>
165
- {toolCallDisplayText}
166
- </ToolCallText>
167
- </ToolCallInfoItem>
168
- </ToolCallsInfoWrapper>
125
+ <ToolCallsActivity
126
+ key={`tools-${index}`}
127
+ toolCalls={segment.toolCalls}
128
+ isActive={segment.isActive}
129
+ />
169
130
  );
170
131
  }
171
132
 
@@ -193,14 +154,7 @@ function SearchAiMessageComponent({
193
154
  </ResourcesWrapper>
194
155
  </>
195
156
  )}
196
- {isLoading && (
197
- <ThinkingDotsWrapper data-testid="thinking-dots-wrapper">
198
- <ThinkingDot />
199
- <ThinkingDot />
200
- <ThinkingDot />
201
- </ThinkingDotsWrapper>
202
- )}
203
- {content.length > 0 && (
157
+ {!isThinking && content.length > 0 && (
204
158
  <FeedbackWrapper>
205
159
  <SearchAiActionButtons
206
160
  content={content}
@@ -261,7 +215,6 @@ export const SearchAiMessage = memo(SearchAiMessageComponent, (prevProps, nextPr
261
215
  prevProps.feedback === nextProps.feedback &&
262
216
  prevProps.onFeedbackChange === nextProps.onFeedbackChange &&
263
217
  areResourcesEqual(prevProps.resources, nextProps.resources) &&
264
- prevProps.toolCalls?.length === nextProps.toolCalls?.length &&
265
218
  prevProps.contentSegments === nextProps.contentSegments
266
219
  );
267
220
  });
@@ -275,9 +228,10 @@ const SearchAiMessageWrapper = styled.div<{ $role: string }>`
275
228
  $role === AiSearchConversationRole.USER ? 'flex-end' : 'flex-start'};
276
229
  `;
277
230
 
278
- const MessageContentWrapper = styled.div`
231
+ const MessageContentWrapper = styled.div<{ $isFullWidth: boolean }>`
279
232
  display: flex;
280
233
  flex-direction: column;
234
+ flex-grow: ${({ $isFullWidth }) => ($isFullWidth ? 1 : 0)};
281
235
  gap: var(--spacing-sm);
282
236
  max-width: 80%;
283
237
  min-width: 0;
@@ -300,13 +254,13 @@ const ResponseText = styled(Markdown)`
300
254
  }
301
255
  `;
302
256
 
303
- const MessageWrapper = styled.div<{ $role: string }>`
257
+ const MessageWrapper = styled.div<{ $role: string; $isFullWidth?: boolean }>`
304
258
  padding: ${({ $role }) =>
305
259
  $role === AiSearchConversationRole.USER
306
260
  ? 'var(--spacing-sm)'
307
261
  : 'var(--spacing-sm) var(--spacing-sm) var(--spacing-xs) var(--spacing-sm)'};
308
262
  border-radius: var(--border-radius-xl);
309
- width: fit-content;
263
+ width: ${({ $isFullWidth }) => ($isFullWidth ? 'auto' : 'fit-content')};
310
264
  max-width: 100%;
311
265
  word-wrap: break-word;
312
266
  white-space: pre-wrap;
@@ -327,12 +281,14 @@ const ResourcesWrapper = styled.div`
327
281
  display: flex;
328
282
  flex-direction: column;
329
283
  margin: 0;
284
+ ${searchAiAppearAnimation}
330
285
  `;
331
286
  const FeedbackWrapper = styled.div`
332
287
  display: flex;
333
288
  flex-direction: row;
334
289
  gap: var(--search-ai-feedback-gap);
335
290
  margin-top: var(--spacing-sm);
291
+ ${searchAiAppearAnimation}
336
292
  `;
337
293
 
338
294
  const ResourcesTitle = styled.div`
@@ -367,43 +323,6 @@ const ResourceTag = styled(Tag).attrs({ textTransform: 'none' })`
367
323
  }
368
324
  `;
369
325
 
370
- const ThinkingDotsWrapper = styled.div`
371
- display: flex;
372
- gap: var(--search-ai-thinking-dots-gap);
373
- padding: var(--search-ai-thinking-dots-padding);
374
- `;
375
-
376
- const ThinkingDot = styled.div`
377
- width: var(--search-ai-thinking-dot-size);
378
- height: var(--search-ai-thinking-dot-size);
379
- border-radius: 50%;
380
- background: var(--search-ai-thinking-dot-color);
381
- animation: bounce 1.4s infinite ease-in-out;
382
-
383
- &:nth-child(1) {
384
- animation-delay: -0.32s;
385
- }
386
- &:nth-child(2) {
387
- animation-delay: -0.16s;
388
- }
389
- &:nth-child(3) {
390
- animation-delay: 0s;
391
- }
392
-
393
- @keyframes bounce {
394
- 0%,
395
- 80%,
396
- 100% {
397
- opacity: 0.2;
398
- transform: scale(0.8);
399
- }
400
- 40% {
401
- opacity: 1;
402
- transform: scale(1);
403
- }
404
- }
405
- `;
406
-
407
326
  const SuccessMessageWrapper = styled.div`
408
327
  max-width: fit-content;
409
328
  display: flex;
@@ -420,37 +339,10 @@ const SuccessMessageText = styled.div`
420
339
  color: var(--color-success-darker);
421
340
  `;
422
341
 
423
- const ToolCallsInfoWrapper = styled.div`
424
- display: flex;
425
- flex-direction: column;
426
- gap: var(--spacing-xxs);
342
+ const ToolCallsActivity = styled(SearchAiActivity)`
427
343
  margin: 0 0 var(--spacing-sm) 0;
428
- font-size: var(--font-size-xs);
429
- color: var(--search-ai-text-color);
430
- opacity: 0.6;
431
- `;
432
-
433
- const ToolCallInfoItem = styled.div`
434
- display: flex;
435
- align-items: center;
436
- gap: var(--spacing-xxs);
437
- `;
438
-
439
- const ToolCallText = styled.span<{ $isSearching?: boolean }>`
440
- font-weight: var(--font-weight-regular);
441
344
 
442
- ${({ $isSearching }) =>
443
- $isSearching &&
444
- `
445
- animation: pulse 1.5s ease-in-out infinite;
446
-
447
- @keyframes pulse {
448
- 0%, 100% {
449
- opacity: 1;
450
- }
451
- 50% {
452
- opacity: 0.4;
453
- }
454
- }
455
- `}
345
+ &:last-child {
346
+ margin-bottom: var(--spacing-xxs);
347
+ }
456
348
  `;
@@ -9,7 +9,7 @@ import { Link } from '@redocly/theme/components/Link/Link';
9
9
  import { Image } from '@redocly/theme/components/Image/Image';
10
10
  import { searchHighlight } from '@redocly/theme/components/Search/SearchHighlight';
11
11
  import { Badge } from '@redocly/theme/components/Badge/Badge';
12
- import { trimText, getOperationColor } from '@redocly/theme/core/utils';
12
+ import { assignRef, trimText, getOperationColor } from '@redocly/theme/core/utils';
13
13
  import { ReturnKeyIcon } from '@redocly/theme/icons/ReturnKeyIcon/ReturnKeyIcon';
14
14
  import { useThemeHooks } from '@redocly/theme/core/hooks';
15
15
 
@@ -61,14 +61,7 @@ export function SearchItem({
61
61
  tabIndex={0}
62
62
  innerRef={(el: HTMLAnchorElement) => {
63
63
  ref.current = el;
64
-
65
- if (!innerRef) return;
66
-
67
- if (typeof innerRef === 'function') {
68
- innerRef(el);
69
- } else {
70
- innerRef.current = el;
71
- }
64
+ assignRef(innerRef, el);
72
65
  }}
73
66
  data-component-name="Search/SearchItem"
74
67
  >
@@ -199,8 +199,21 @@ export const search = css`
199
199
  --search-ai-suggestions-title-font-weight: var(--font-weight-light);
200
200
  --search-ai-suggestions-text-color: var(--text-color-description);
201
201
 
202
- --search-ai-thinking-dots-gap: 4px;
203
- --search-ai-thinking-dots-padding: 4px 0;
202
+ --search-ai-activity-font-size: var(--font-size-base);
203
+ --search-ai-activity-line-height: var(--line-height-sm);
204
+ --search-ai-activity-count-font-size: var(--font-size-sm);
205
+ --search-ai-activity-count-bg-color: var(--border-color-secondary);
206
+ --search-ai-activity-text-color: var(--text-color-description);
207
+ --search-ai-activity-text-color-hover: var(--text-color-primary);
208
+ --search-ai-activity-detail-color: var(--text-color-secondary);
209
+ --search-ai-activity-shimmer-color: var(--text-color-primary);
210
+ --search-ai-activity-rail-color: var(--border-color-primary);
211
+ --search-ai-activity-icon-size: 14px;
212
+ --search-ai-activity-fade-duration: 200ms;
213
+ --search-ai-activity-expand-duration: 250ms;
214
+ --search-ai-activity-shimmer-duration: 2s;
215
+ --search-ai-activity-easing: cubic-bezier(0.32, 0.72, 0, 1);
216
+
204
217
  --search-ai-thinking-dot-size: 6px;
205
218
  --search-ai-thinking-dot-color: var(--search-ai-gradient);
206
219
 
@@ -6,6 +6,7 @@ import type { SelectOption } from '@redocly/theme/core/types';
6
6
  import { Tag } from '@redocly/theme/components/Tag/Tag';
7
7
  import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
8
8
  import { Button } from '@redocly/theme/components/Button/Button';
9
+ import { assignRef } from '@redocly/theme/core/utils';
9
10
 
10
11
  type SelectInputProps<T> = {
11
12
  id?: string;
@@ -125,14 +126,7 @@ export function SelectInput<T>(props: SelectInputProps<T>): React.ReactNode {
125
126
  if (!input) return;
126
127
 
127
128
  inputRef.current = input;
128
-
129
- if (!forwardedInputRef) return;
130
-
131
- if (typeof forwardedInputRef === 'function') {
132
- forwardedInputRef(input);
133
- } else {
134
- forwardedInputRef.current = input;
135
- }
129
+ assignRef(forwardedInputRef, input);
136
130
  }}
137
131
  $width={multiple ? (!searchValue && selectedOptions.length ? '10px' : 'auto') : '100%'}
138
132
  tabIndex={0}
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useRef } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { ComponentProps, JSX } from 'react';
@@ -45,9 +45,14 @@ export function SplitButton(props: SplitButtonProps): JSX.Element {
45
45
  } = props;
46
46
 
47
47
  const hasMenu = typeof children === 'function' || React.Children.toArray(children).length > 0;
48
+ const wrapperRef = useRef<HTMLDivElement>(null);
48
49
 
49
50
  return (
50
- <SplitButtonWrapper data-component-name="SplitButton/SplitButton" className={className}>
51
+ <SplitButtonWrapper
52
+ data-component-name="SplitButton/SplitButton"
53
+ className={className}
54
+ ref={wrapperRef}
55
+ >
51
56
  <ButtonGroup variant={variant} size={size}>
52
57
  {button}
53
58
  {hasMenu ? (
@@ -56,6 +61,7 @@ export function SplitButton(props: SplitButtonProps): JSX.Element {
56
61
  placement={placement}
57
62
  alignment={alignment}
58
63
  portalled={portalled}
64
+ alignmentAnchorRef={wrapperRef}
59
65
  withArrow
60
66
  onOpen={onOpen}
61
67
  onClose={onClose}
@@ -1,15 +1,9 @@
1
- import type { MCPOption } from '../types/mcp';
2
-
3
1
  export const DEFAULT_MCP_SERVER_NAME = 'mcp-server';
4
2
 
5
3
  export const MCP_ROUTE = '/mcp';
6
4
 
7
5
  export const PUBLIC_MCP_ROUTE = '/mcp-public';
8
6
 
9
- export const DEFAULT_MCP_OPTIONS: MCPOption[] = [
10
- 'cursor',
11
- 'vscode',
12
- 'claude-code',
13
- 'codex',
14
- 'copy',
15
- ];
7
+ export const MCP_OPTIONS = ['cursor', 'vscode', 'claude-code', 'codex', 'copy'] as const;
8
+
9
+ export const DEFAULT_MCP_OPTIONS = [...MCP_OPTIONS];
@@ -53,7 +53,7 @@ export const TOOL_CALL_DISPLAY_TEXT: Partial<
53
53
  > = {
54
54
  [ToolCallName.SearchDocumentation]: {
55
55
  inProgressText: 'Searching documentation...',
56
- completedText: 'Search completed',
56
+ completedText: 'Searched the documentation',
57
57
  },
58
58
  [ToolCallName.ListApis]: {
59
59
  inProgressText: 'Browsing APIs...',
@@ -75,6 +75,10 @@ export const TOOL_CALL_DISPLAY_TEXT: Partial<
75
75
  inProgressText: 'Reading API description...',
76
76
  completedText: 'Read API description',
77
77
  },
78
+ [ToolCallName.Execute]: {
79
+ inProgressText: 'Running code...',
80
+ completedText: 'Ran code',
81
+ },
78
82
  [ToolCallName.DescribeTools]: {
79
83
  inProgressText: 'Discovering available tools...',
80
84
  completedText: 'Discovered available tools',
@@ -23,6 +23,7 @@ export * from '../menu/use-mobile-menu-levels';
23
23
  export * from '../feedback/use-report-dialog';
24
24
  export * from '../use-product-picker';
25
25
  export * from '../search/use-search-dialog';
26
+ export * from '../search/use-search-ai-activity';
26
27
  export * from '../use-language-picker';
27
28
  export * from './use-element-size';
28
29
  export * from './use-time-ago';
@@ -30,6 +30,7 @@ export * from './use-product-picker';
30
30
  export * from './search/use-search-dialog';
31
31
  export * from './search/use-search-filter';
32
32
  export * from './search/use-feedback-tooltip';
33
+ export * from './search/use-search-ai-activity';
33
34
  export * from './use-controlled-state';
34
35
  export * from './use-codeblock-tabs-controls';
35
36
  export * from './code-walkthrough/use-code-walkthrough';
@@ -207,5 +207,5 @@ export function useEvent<T extends (...args: any[]) => any>(callback?: T): T {
207
207
  ref.current = callback;
208
208
  });
209
209
 
210
- return useCallback(((...args: Parameters<T>) => ref.current?.(...args)) as T, []);
210
+ return useCallback((...args: Parameters<T>) => ref.current?.(...args), []) as T;
211
211
  }
@@ -0,0 +1,31 @@
1
+ import type { SearchAiActivityStatus, SearchAiActivityStep, ToolCall } from '../../types/search';
2
+
3
+ import { useThemeHooks } from '../use-theme-hooks';
4
+ import { getSearchAiActivityStatus, getSearchAiActivityStep } from '../../utils/search-ai-activity';
5
+
6
+ type UseSearchAiActivityParams = {
7
+ toolCalls: ToolCall[];
8
+ isActive: boolean;
9
+ };
10
+
11
+ type UseSearchAiActivityReturn = {
12
+ status: SearchAiActivityStatus;
13
+ steps: SearchAiActivityStep[];
14
+ };
15
+
16
+ export function useSearchAiActivity({
17
+ toolCalls,
18
+ isActive,
19
+ }: UseSearchAiActivityParams): UseSearchAiActivityReturn {
20
+ const { useTranslate } = useThemeHooks();
21
+ const { translate } = useTranslate();
22
+
23
+ const runningToolCall = isActive ? toolCalls.find((toolCall) => !toolCall.result) : undefined;
24
+
25
+ return {
26
+ status: getSearchAiActivityStatus(toolCalls, isActive, runningToolCall, translate),
27
+ steps: toolCalls.map((toolCall) =>
28
+ getSearchAiActivityStep(toolCall, isActive && !toolCall.result, translate),
29
+ ),
30
+ };
31
+ }
@@ -7,7 +7,7 @@ import { useThemeHooks } from './use-theme-hooks';
7
7
  import { useMCPConfig } from './use-mcp-config';
8
8
  import { ClipboardService } from '../utils/clipboard-service';
9
9
  import { DEFAULT_MCP_OPTIONS } from '../constants/mcp';
10
- import { generateClaudeCodeCommand, generateCodexCommand } from '../utils/mcp';
10
+ import { generateClaudeCodeCommand, generateCodexCommand, isMCPOption } from '../utils/mcp';
11
11
  import { getBaseDataAttributes } from '../utils/telemetry/get-base-data-attributes';
12
12
 
13
13
  export type McpButtonOptions = {
@@ -80,7 +80,12 @@ export function useConnectMCPButton({
80
80
  [translate],
81
81
  );
82
82
 
83
- const visibleOptions = useMemo(() => options.filter(Boolean), [options]);
83
+ // Markdoc reports attribute errors but still renders the tag, so `options` can be any value.
84
+ const visibleOptions = useMemo(
85
+ () =>
86
+ Array.isArray(options) ? [...new Set(options.filter(isMCPOption))] : [...DEFAULT_MCP_OPTIONS],
87
+ [options],
88
+ );
84
89
 
85
90
  return {
86
91
  copiedOption,
@@ -3,9 +3,9 @@ import throttle from 'lodash.throttle';
3
3
 
4
4
  import type { RefObject } from 'react';
5
5
 
6
- export function useFullHeight(element: RefObject<HTMLElement | null>): void {
6
+ export function useFullHeight(elementRef: RefObject<HTMLElement | null>): void {
7
7
  const handler = useCallback(() => {
8
- if (!element.current) {
8
+ if (!elementRef.current) {
9
9
  return;
10
10
  }
11
11
 
@@ -14,19 +14,19 @@ export function useFullHeight(element: RefObject<HTMLElement | null>): void {
14
14
  return;
15
15
  }
16
16
 
17
- const elementTop = element.current.getBoundingClientRect().top;
17
+ const elementTop = elementRef.current.getBoundingClientRect().top;
18
18
  const footerOffset = footer.offsetTop - window.scrollY - elementTop;
19
19
  if (footerOffset > window.innerHeight) {
20
- element.current.style.maxHeight = `${window.innerHeight - elementTop}px`;
20
+ elementRef.current.style.maxHeight = `${window.innerHeight - elementTop}px`;
21
21
  } else {
22
- element.current.style.maxHeight = `${footerOffset}px`;
22
+ elementRef.current.style.maxHeight = `${footerOffset}px`;
23
23
  }
24
- }, [element]);
24
+ }, [elementRef]);
25
25
 
26
26
  const throttledHandler = useMemo(() => throttle(handler, 150), [handler]);
27
27
 
28
28
  useEffect(() => {
29
- if (typeof window === 'undefined' || !element.current) {
29
+ if (typeof window === 'undefined' || !elementRef.current) {
30
30
  return () => {
31
31
  // nope
32
32
  };
@@ -45,5 +45,5 @@ export function useFullHeight(element: RefObject<HTMLElement | null>): void {
45
45
  window.removeEventListener('scroll', throttledHandler);
46
46
  window.removeEventListener('resize', throttledHandler);
47
47
  };
48
- }, [throttledHandler, element, handler]);
48
+ }, [throttledHandler, elementRef, handler]);
49
49
  }