Skip to content

Commit bc6445c

Browse files
whoistheycursoragentclaude
authored
feat(web): text file attachments for Ask (#1374)
* feat(web): add language model inputModalities capability plumbing Add an optional `inputModalities` declaration to language model config and expose a resolved capability set to the client. - Schema: add optional `inputModalities` (`text` | `image` | `pdf`) to every provider definition in `schemas/v3/languageModel.json` and regenerate the schema types/snippets. - Add a fail-closed `resolveModelInputModalities` resolver that defaults to text-only when a model does not declare its input modalities. - Expose the resolved `inputModalities` on the client-safe `LanguageModelInfo` (populated via `getConfiguredLanguageModelsInfo` and the MCP ask path). This is groundwork for chat file attachments. It adds no attachment UI and no live provider capability probing yet. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: add CHANGELOG entry for language model inputModalities Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(schemas): split document types out of inputModalities inputModalities now only enumerates true perceptual channels (text | image | audio | video). Document/container formats like PDF move to a separate fail-closed `supportedDocumentTypes` field, since PDF is not a model modality but a format providers decompose into text/image internally. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(schemas): clarify what counts as a document type Tighten the inputModalities / supportedDocumentTypes descriptions to remove the implication that omitting supportedDocumentTypes blocks all non-text attachments. Clarify the taxonomy: single-medium files (images, audio, video) and plain-text files (.txt, .md) are governed by inputModalities; supportedDocumentTypes only gates rich compound container formats like PDF. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): widen getLanguageModelKey param to keyable subset LanguageModelInfo now has required inputModalities/supportedDocumentTypes, so a raw LanguageModel config (where those are optional) is no longer assignable to it. getLanguageModelKey only reads provider/model/displayName, so type its parameter as that Pick subset, letting both LanguageModel and LanguageModelInfo be keyed. Fixes the docker build type check. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(schemas,web): keep schema dist fresh and resolve types from source Two dev-experience fixes for the stale-build-output footgun: - schemas watch now runs `yarn build` (generate + tsc) instead of generate-only, so editing a schema JSON during `yarn dev` refreshes dist (both the .d.ts types and the runtime index.schema.js used by ajv), not just the generated source. - web tsconfig maps @sourcebot/schemas/v3|v2/* to the package source, so type-checking and the IDE read committed source directly instead of stale built .d.ts. Web only imports .type files (erased at compile), so there is no bundling/runtime impact. Co-authored-by: Cursor <cursoragent@cursor.com> * First pass file attachments, picker and drag and drop, with preview cards, wired into user message via xml-like tags similar to system context * escape key handle for modal and add missing description component * refactor(web): resolve model capabilities from models.dev, not config.json Re-source language model input-modality / document capabilities from the models.dev catalog instead of hand-declared config.json fields, aligning with the move to de-emphasize on-disk config in favor of automatic resolution (the same catalog already backs context-window resolution). - Revert the inputModalities/supportedDocumentTypes additions to schemas/v3/languageModel.json and all regenerated artifacts; capabilities are no longer declared in config.json. - Extract the shared models.dev catalog plumbing (fetch/TTL/negative-cache/ stale-while-revalidate/provider-id overrides) into modelsDevCatalog.server.ts, now consumed by both context-window and capability resolution. - Add models.dev-backed resolveModelCapabilities (modelCapabilities.server.ts), partitioning the catalog's modalities.input list into Sourcebot's inputModalities (channels) and supportedDocumentTypes (containers); falls back to text-only for uncatalogued / self-hosted models. The client-safe LanguageModelInfo contract is unchanged; only the resolution backend moved. Co-authored-by: Cursor <cursoragent@cursor.com> * paste-to-attachment handling, raw paste keychord enabled, toast with fallback * stronger typing for contract * remove blocking models.dev catalog request and add cache warm on startup * cleanup warming * remove button from toast * add separate state to track pending attachments to avoid visual flicker of clear-on-submit * explicitly import already hoisted uuid and change bad crypto call for uuid use * add changelog entry * remove granular file attachment controls in favor of single per-message cap of around 60-80k tokens * pass attachments through the login/upgrade redirect * persist a stable id on text attachments Carry the pending attachment's client id through to the persisted message instead of stripping it. This gives every text attachment a durable handle from the moment the feature ships, so later attachment-referencing work has no backwards-compatibility gap for attachments created before the field existed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5d5a063 commit bc6445c

23 files changed

Lines changed: 936 additions & 58 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- [EE] Added a context-window usage gauge to the Ask Sourcebot chat details, showing how much of the selected model's context window each turn occupies. Window sizes are resolved from the models.dev catalog. [#1370](https://github.com/sourcebot-dev/sourcebot/pull/1370)
1818
- Added language model input-modality and document capability resolution, automatically resolved from the models.dev catalog (falls back to text-only for uncatalogued/self-hosted models). [#1372](https://github.com/sourcebot-dev/sourcebot/pull/1372)
1919
- [EE] Added DPoP sender-constrained OAuth tokens for MCP clients. [#1395](https://github.com/sourcebot-dev/sourcebot/pull/1395)
20+
- [EE] Added text file attachments to Ask Sourcebot, letting users attach text/code/config files to a chat message via the paperclip button, drag-and-drop, or paste, with large pastes auto-converted to attachments. [#1374](https://github.com/sourcebot-dev/sourcebot/pull/1374)
2021

2122
### Fixed
2223
- Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367)

packages/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@
177177
"react-day-picker": "^9.14.0",
178178
"react-device-detect": "^2.2.3",
179179
"react-dom": "19.2.4",
180+
"react-dropzone": "^15.0.0",
180181
"react-hook-form": "^7.53.0",
181182
"react-hotkeys-hook": "^4.5.1",
182183
"react-icons": "^5.6.0",
@@ -200,6 +201,7 @@
200201
"tailwindcss-animate": "^1.0.7",
201202
"use-stick-to-bottom": "^1.1.3",
202203
"usehooks-ts": "^3.1.0",
204+
"uuid": "^14.0.0",
203205
"vscode-icons-js": "^11.6.1",
204206
"zod": "^3.25.76",
205207
"zod-to-json-schema": "^3.24.5"

packages/web/src/app/(app)/askgh/[owner]/[repo]/components/landingPage.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import Image from 'next/image';
44
import { SearchModeSelector } from "@/app/(app)/components/searchModeSelector";
55
import { Separator } from "@/components/ui/separator";
6-
import { ChatBox } from "@/features/chat/components/chatBox";
6+
import { ChatBox, ChatBoxHandle } from "@/features/chat/components/chatBox";
77
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
8+
import { ChatPaneDropzone } from "@/features/chat/components/chatBox/chatPaneDropzone";
89
import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner";
910
import { LanguageModelInfo, RepoSearchScope } from "@/features/chat/types";
1011
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
1112
import { DISABLED_MCP_SERVER_IDS_LOCAL_STORAGE_KEY } from "@/features/chat/constants";
1213
import { getRepoImageSrc } from '@/lib/utils';
13-
import { useMemo, useState } from "react";
14+
import { useMemo, useRef, useState } from "react";
1415
import { useLocalStorage } from "usehooks-ts";
1516

1617
interface LandingPageProps {
@@ -33,6 +34,7 @@ export const LandingPage = ({
3334
const { createNewChatThread, isLoading } = useCreateNewChatThread();
3435
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
3536
const [disabledMcpServerIds, setDisabledMcpServerIds] = useLocalStorage<string[]>(DISABLED_MCP_SERVER_IDS_LOCAL_STORAGE_KEY, [], { initializeWithValue: false });
37+
const chatBoxRef = useRef<ChatBoxHandle>(null);
3638
const isChatBoxDisabled = languageModels.length === 0;
3739

3840
const selectedSearchScopes = useMemo(() => [
@@ -67,11 +69,16 @@ export const LandingPage = ({
6769
</div>
6870

6971
{/* ChatBox */}
70-
<div className="w-full">
72+
<ChatPaneDropzone
73+
className="w-full"
74+
onFilesDropped={(files) => chatBoxRef.current?.addFiles(files)}
75+
disabled={isChatBoxDisabled}
76+
>
7177
<div className="border rounded-md w-full shadow-sm">
7278
<ChatBox
73-
onSubmit={(children) => {
74-
createNewChatThread(children, selectedSearchScopes, disabledMcpServerIds);
79+
ref={chatBoxRef}
80+
onSubmit={(children, _editor, attachments) => {
81+
createNewChatThread(children, selectedSearchScopes, disabledMcpServerIds, attachments);
7582
}}
7683
className="min-h-[50px]"
7784
isRedirecting={isLoading}
@@ -107,7 +114,7 @@ export const LandingPage = ({
107114
{isChatBoxDisabled && (
108115
<NotConfiguredErrorBanner className="mt-4" />
109116
)}
110-
</div>
117+
</ChatPaneDropzone>
111118
</div>
112119
</div>
113120
)

packages/web/src/app/(app)/chat/chatLandingPage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CustomSlateEditor } from "@/features/chat/customSlateEditor";
55
import { ServiceErrorException } from "@/lib/serviceError";
66
import { isServiceError, measure } from "@/lib/utils";
77
import { LandingPageChatBox } from "./components/landingPageChatBox";
8+
import { ChatLandingDropzone } from "./components/chatLandingDropzone";
89
import { RepositoryCarousel } from "../components/repositoryCarousel";
910
import { Separator } from "@/components/ui/separator";
1011
import { DemoCards } from "./components/demoCards";
@@ -56,7 +57,7 @@ export async function ChatLandingPage() {
5657
})() : undefined;
5758

5859
return (
59-
<div className="flex flex-col items-center h-full overflow-hidden">
60+
<ChatLandingDropzone disabled={languageModels.length === 0}>
6061
<div className="flex flex-col items-center h-full overflow-y-auto pt-8 pb-8 md:pt-16 w-full px-5">
6162
<div className="max-h-44 w-auto">
6263
<SourcebotLogo
@@ -92,6 +93,6 @@ export async function ChatLandingPage() {
9293
</>
9394
)}
9495
</div>
95-
</div>
96+
</ChatLandingDropzone>
9697
)
9798
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use client';
2+
3+
import { ChatBoxHandle } from "@/features/chat/components/chatBox";
4+
import { ChatPaneDropzone } from "@/features/chat/components/chatBox/chatPaneDropzone";
5+
import { createContext, ReactNode, useCallback, useContext, useRef } from "react";
6+
7+
type RegisterChatBoxHandle = (handle: ChatBoxHandle | null) => void;
8+
9+
const LandingChatBoxContext = createContext<RegisterChatBoxHandle | null>(null);
10+
11+
// Lets the (nested) landing chat box register its imperative handle so the
12+
// pane-level drop zone can forward dropped files into it. Returns a no-op when
13+
// rendered outside the provider.
14+
export const useRegisterLandingChatBox = (): RegisterChatBoxHandle => {
15+
return useContext(LandingChatBoxContext) ?? (() => { });
16+
}
17+
18+
interface ChatLandingDropzoneProps {
19+
disabled?: boolean;
20+
children: ReactNode;
21+
}
22+
23+
// Wraps the entire unstarted-chat landing pane in a drag-and-drop target.
24+
// The chat box lives deeper in the tree (and behind a server/client boundary),
25+
// so it registers its handle via context rather than a direct ref.
26+
export const ChatLandingDropzone = ({ disabled, children }: ChatLandingDropzoneProps) => {
27+
const handleRef = useRef<ChatBoxHandle | null>(null);
28+
29+
const register = useCallback<RegisterChatBoxHandle>((handle) => {
30+
handleRef.current = handle;
31+
}, []);
32+
33+
return (
34+
<LandingChatBoxContext.Provider value={register}>
35+
<ChatPaneDropzone
36+
className="flex flex-col items-center h-full overflow-hidden"
37+
onFilesDropped={(files) => handleRef.current?.addFiles(files)}
38+
disabled={disabled}
39+
>
40+
{children}
41+
</ChatPaneDropzone>
42+
</LandingChatBoxContext.Provider>
43+
)
44+
}

packages/web/src/app/(app)/chat/components/landingPageChatBox.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { LanguageModelInfo, SearchScope } from "@/features/chat/types";
77
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
88
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
99
import { useState } from "react";
10+
import { useRegisterLandingChatBox } from "./chatLandingDropzone";
1011
import { useLocalStorage } from "usehooks-ts";
1112
import { DISABLED_MCP_SERVER_IDS_LOCAL_STORAGE_KEY, SELECTED_SEARCH_SCOPES_LOCAL_STORAGE_KEY } from "@/features/chat/constants";
1213
import { SearchModeSelector } from "../../components/searchModeSelector";
@@ -31,14 +32,16 @@ export const LandingPageChatBox = ({
3132
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>(SELECTED_SEARCH_SCOPES_LOCAL_STORAGE_KEY, [], { initializeWithValue: false });
3233
const [disabledMcpServerIds, setDisabledMcpServerIds] = useLocalStorage<string[]>(DISABLED_MCP_SERVER_IDS_LOCAL_STORAGE_KEY, [], { initializeWithValue: false });
3334
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
35+
const registerChatBox = useRegisterLandingChatBox();
3436
const isChatBoxDisabled = languageModels.length === 0;
3537

3638
return (
3739
<div className="w-full max-w-[800px] mt-4">
3840
<div className="border rounded-md w-full shadow-sm">
3941
<ChatBox
40-
onSubmit={(children) => {
41-
createNewChatThread(children, selectedSearchScopes, disabledMcpServerIds);
42+
ref={registerChatBox}
43+
onSubmit={(children, _editor, attachments) => {
44+
createNewChatThread(children, selectedSearchScopes, disabledMcpServerIds, attachments);
4245
}}
4346
className="min-h-[50px]"
4447
isRedirecting={isLoading}
@@ -74,6 +77,6 @@ export const LandingPageChatBox = ({
7477
{isChatBoxDisabled && (
7578
<NotConfiguredErrorBanner className="mt-4" />
7679
)}
77-
</div >
80+
</div>
7881
)
7982
}

packages/web/src/ee/features/chat/agent.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { randomUUID } from "crypto";
2222
import _dedent from "dedent";
2323
import { ANSWER_TAG, FILE_REFERENCE_PREFIX } from "@/features/chat/constants";
2424
import { Source } from "@/features/chat/types";
25-
import { addLineNumbers, fileReferenceToString, getAnswerPartFromAssistantMessage, getTurnProgressState, getUserMessageText } from "@/features/chat/utils";
25+
import { addLineNumbers, fileReferenceToString, formatAttachmentsForPrompt, getAnswerPartFromAssistantMessage, getTurnProgressState, getUserMessageAttachments, getUserMessageText } from "@/features/chat/utils";
2626
import { createTools } from "./tools";
2727
import { getConnectedMcpClients } from "@/ee/features/chat/mcp/mcpClientFactory";
2828
import { getMcpTools, McpToolsResult } from "@/ee/features/chat/mcp/mcpToolSets";
@@ -105,9 +105,16 @@ export const createMessageStream = async ({
105105
let messageHistory: ModelMessage[] =
106106
messages.map((message, index): ModelMessage | undefined => {
107107
if (message.role === 'user') {
108+
// Fold inline-text attachments into this turn's content (not the
109+
// system prompt) so they stay bound to their turn, re-emitted from
110+
// the persisted parts.
111+
const text = getUserMessageText(message);
112+
const attachmentsBlock = formatAttachmentsForPrompt(
113+
getUserMessageAttachments(message),
114+
);
108115
return {
109116
role: 'user',
110-
content: getUserMessageText(message),
117+
content: attachmentsBlock ? `${text}\n\n${attachmentsBlock}` : text,
111118
};
112119
}
113120

packages/web/src/ee/features/chat/components/chatThread/chatThread.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useToast } from '@/components/hooks/use-toast';
44
import { Button } from '@/components/ui/button';
55
import { Separator } from '@/components/ui/separator';
66
import { CustomSlateEditor } from '@/features/chat/customSlateEditor';
7-
import { AdditionalChatRequestParams, CustomEditor, LanguageModelInfo, SBChatMessage, SearchScope, Source } from '@/features/chat/types';
7+
import { AdditionalChatRequestParams, AttachmentData, CustomEditor, LanguageModelInfo, SBChatMessage, SearchScope, Source } from '@/features/chat/types';
88
import { createUIMessage, getAllMentionElements, getTurnProgressState, getUserMessageText, resetEditor, slateContentToString } from '@/features/chat/utils';
99
import { useChat } from '@ai-sdk/react';
1010
import { CreateUIMessage, DefaultChatTransport, lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai';
@@ -15,8 +15,9 @@ import { useStickToBottom } from 'use-stick-to-bottom';
1515
import { Descendant } from 'slate';
1616
import { useMessagePairs } from '../../useMessagePairs';
1717
import { useSelectedLanguageModel } from '@/features/chat/useSelectedLanguageModel';
18-
import { ChatBox } from '@/features/chat/components/chatBox';
18+
import { ChatBox, ChatBoxHandle } from '@/features/chat/components/chatBox';
1919
import { ChatBoxToolbar } from '@/features/chat/components/chatBox/chatBoxToolbar';
20+
import { ChatPaneDropzone } from '@/features/chat/components/chatBox/chatPaneDropzone';
2021
import { ChatThreadListItem } from './chatThreadListItem';
2122
import { ErrorBanner } from './errorBanner';
2223
import { McpFailedServersBanner } from './mcpFailedServersBanner';
@@ -72,6 +73,7 @@ export const ChatThread = ({
7273
}: ChatThreadProps) => {
7374
const [isErrorBannerVisible, setIsErrorBannerVisible] = useState(false);
7475
const hasSubmittedInputMessage = useRef(false);
76+
const chatBoxRef = useRef<ChatBoxHandle>(null);
7577
const { scrollRef, contentRef, scrollToBottom, isAtBottom } = useStickToBottom({ initial: false });
7678
const { toast } = useToast();
7779
const router = useRouter();
@@ -347,11 +349,11 @@ export const ChatThread = ({
347349
}
348350
}, [error]);
349351

350-
const onSubmit = useCallback(async (children: Descendant[], editor: CustomEditor) => {
352+
const onSubmit = useCallback(async (children: Descendant[], editor: CustomEditor, attachments: AttachmentData[]) => {
351353
const text = slateContentToString(children);
352354
const mentions = getAllMentionElements(children);
353355

354-
const message = createUIMessage(text, mentions.map(({ data }) => data), selectedSearchScopes, disabledMcpServerIds);
356+
const message = createUIMessage(text, mentions.map(({ data }) => data), selectedSearchScopes, disabledMcpServerIds, attachments);
355357
sendMessage(message);
356358

357359
scrollToBottom();
@@ -381,6 +383,11 @@ export const ChatThread = ({
381383
return (
382384
<ToolApprovalProvider value={addToolApprovalResponse}>
383385
<McpServerIconContext.Provider value={mcpServerIconMap}>
386+
<ChatPaneDropzone
387+
className="flex flex-col flex-1 min-h-0 w-full"
388+
onFilesDropped={(files) => chatBoxRef.current?.addFiles(files)}
389+
disabled={!isOwner || languageModels.length === 0}
390+
>
384391
{error && (
385392
<ErrorBanner
386393
error={error}
@@ -470,6 +477,7 @@ export const ChatThread = ({
470477
<div className="border rounded-md w-full shadow-sm">
471478
<CustomSlateEditor>
472479
<ChatBox
480+
ref={chatBoxRef}
473481
onSubmit={onSubmit}
474482
className="min-h-[80px]"
475483
preferredSuggestionsBoxPlacement="top-start"
@@ -520,6 +528,7 @@ export const ChatThread = ({
520528
</div>
521529
)}
522530
</div>
531+
</ChatPaneDropzone>
523532
</McpServerIconContext.Provider>
524533
</ToolApprovalProvider>
525534
);

packages/web/src/ee/features/chat/components/chatThread/chatThreadListItem.tsx

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import { CSSProperties, forwardRef, memo, useCallback, useEffect, useMemo, useRe
88
import scrollIntoView from 'scroll-into-view-if-needed';
99
import { Reference, referenceSchema, SBChatMessage, Source } from "@/features/chat/types";
1010
import { useExtractReferences } from '../../useExtractReferences';
11-
import { getAnswerPartFromAssistantMessage, getLastStepParts, getUserMessageText, groupMessageIntoSteps, isSBChatToolPart, repairReferences } from '@/features/chat/utils';
11+
import { getAnswerPartFromAssistantMessage, getLastStepParts, getUserMessageAttachments, getUserMessageText, groupMessageIntoSteps, isSBChatToolPart, repairReferences } from '@/features/chat/utils';
1212
import { AnswerCard } from './answerCard';
13+
import { MessageAttachments } from './messageAttachments';
1314
import { DetailsCard } from './detailsCard';
1415
import { ApprovalRequestedToolPart, ToolApprovalBanner } from './toolApprovalBanner';
1516
import { MarkdownRenderer, REFERENCE_PAYLOAD_ATTRIBUTE } from './markdownRenderer';
@@ -68,6 +69,10 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
6869
return getUserMessageText(userMessage);
6970
}, [userMessage]);
7071

72+
const userAttachments = useMemo(() => {
73+
return getUserMessageAttachments(userMessage);
74+
}, [userMessage]);
75+
7176
// Take the assistant message and repair any references that are not properly formatted.
7277
// This applies to parts that are text (i.e., text & reasoning).
7378
const assistantMessage = useMemo(() => {
@@ -418,27 +423,30 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
418423
ref={leftPanelRef}
419424
className="py-4 h-full"
420425
>
421-
<div className="flex flex-row gap-2 mb-4">
422-
{isTurnInProgress ? (
423-
<Loader2 className="w-4 h-4 animate-spin flex-shrink-0 mt-1.5" />
424-
) : (
425-
<CheckCircle className="w-4 h-4 text-green-700 flex-shrink-0 mt-1.5" />
426+
<div className="mb-4">
427+
{userAttachments.length > 0 && (
428+
<MessageAttachments attachments={userAttachments} className="mb-1.5 ml-6" />
426429
)}
427-
<MarkdownRenderer
428-
content={userQuestion.trim()}
429-
className="prose-p:m-0"
430-
escapeHtml={true}
431-
/>
430+
431+
<div className="flex flex-row gap-2">
432+
{isTurnInProgress ? (
433+
<Loader2 className="w-4 h-4 animate-spin flex-shrink-0 mt-1.5" />
434+
) : (
435+
<CheckCircle className="w-4 h-4 text-green-700 flex-shrink-0 mt-1.5" />
436+
)}
437+
<MarkdownRenderer
438+
content={userQuestion.trim()}
439+
className="prose-p:m-0"
440+
escapeHtml={true}
441+
/>
442+
</div>
432443
</div>
433444

434445
{isThinking && (
435-
<div className="space-y-4 mb-4">
436-
<Skeleton className="h-4 max-w-32" />
437-
<div className="space-y-2">
438-
<Skeleton className="h-3 max-w-72" />
439-
<Skeleton className="h-3 max-w-64" />
440-
<Skeleton className="h-3 max-w-56" />
441-
</div>
446+
<div className="space-y-2 mb-4">
447+
<Skeleton className="h-3 w-full max-w-80" />
448+
<Skeleton className="h-3 w-full max-w-72" />
449+
<Skeleton className="h-3 w-full max-w-56" />
442450
</div>
443451
)}
444452

packages/web/src/ee/features/chat/components/chatThread/detailsCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ export const StepPartRenderer = ({ part, toolTokenUsageMap }: { part: SBChatMess
537537
case 'data-source':
538538
case 'data-mcp-server':
539539
case 'data-mcp-failed-server':
540+
case 'data-attachment':
540541
case 'file':
541542
case 'source-document':
542543
case 'source-url':

0 commit comments

Comments
 (0)