Skip to content
This repository was archived by the owner on Aug 1, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
481 changes: 242 additions & 239 deletions lib/prompt-editor/src/PromptEditor.tsx

Large diffs are not rendered by default.

429 changes: 225 additions & 204 deletions lib/prompt-editor/src/v2/PromptEditor.tsx

Large diffs are not rendered by default.

53 changes: 27 additions & 26 deletions lib/shared/src/lexicalEditor/editorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { type ContextItem, ContextItemSource } from '../codebase-context/message
import type { RangeData } from '../common/range'
import { displayPath } from '../editor/displayPath'
import type { PromptString } from '../prompt/prompt-string'
import { memoize } from '../utils'
import { AT_MENTION_SERIALIZED_PREFIX, deserializeParagraph } from './atMentionsSerializer'
import {
CONTEXT_ITEM_MENTION_NODE_TYPE,
Expand Down Expand Up @@ -147,36 +148,36 @@ export function serializedPromptEditorStateFromText(text: string): SerializedPro
}
}

export function serializedPromptEditorStateFromChatMessage(
chatMessage: ChatMessage
): SerializedPromptEditorState {
function isCompatibleVersionEditorState(value: unknown): value is SerializedPromptEditorState {
if (!value) {
return false
}
export const serializedPromptEditorStateFromChatMessage = memoize(
(chatMessage: ChatMessage): SerializedPromptEditorState => {
function isCompatibleVersionEditorState(value: unknown): value is SerializedPromptEditorState {
if (!value) {
return false
}

const editorState = value as SerializedPromptEditorState
const editorState = value as SerializedPromptEditorState

// We can read this if the version of the serialized text is compatible
// or its minimum version is compatible.
return (
SUPPORTED_READER_VERSIONS.includes(editorState.v) ||
SUPPORTED_READER_VERSIONS.includes(editorState.minReaderV ?? DEFAULT_MIN_READER_V)
)
}
// We can read this if the version of the serialized text is compatible
// or its minimum version is compatible.
return (
SUPPORTED_READER_VERSIONS.includes(editorState.v) ||
SUPPORTED_READER_VERSIONS.includes(editorState.minReaderV ?? DEFAULT_MIN_READER_V)
)
}

if (isCompatibleVersionEditorState(chatMessage.editorState)) {
return chatMessage.editorState
}
if (isCompatibleVersionEditorState(chatMessage.editorState)) {
return chatMessage.editorState
}

// Fall back to using plain text for chat messages that don't have a serialized Lexical editor
// state that we recognize.
//
// It would be smoother to automatically import or convert textual @-mentions to the Lexical
// mention nodes, but that would add a lot of extra complexity for the relatively rare use case
// of editing old messages in your chat history.
return serializedPromptEditorStateFromText(chatMessage.text ? chatMessage.text.toString() : '')
}
// Fall back to using plain text for chat messages that don't have a serialized Lexical editor
// state that we recognize.
//
// It would be smoother to automatically import or convert textual @-mentions to the Lexical
// mention nodes, but that would add a lot of extra complexity for the relatively rare use case
// of editing old messages in your chat history.
return serializedPromptEditorStateFromText(chatMessage.text ? chatMessage.text.toString() : '')
}
)

export function contextItemsFromPromptEditorValue(
state: SerializedPromptEditorState
Expand Down
19 changes: 19 additions & 0 deletions lib/shared/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import isEqual from 'lodash/isEqual'
import { logError } from './logger'

type PromiseResolverFn<T, E> = (value?: T, err?: E) => void
Expand Down Expand Up @@ -187,3 +188,21 @@ export type PartialDeep<T> = {
? PartialDeep<T[P]>
: T[P]
}

export function memoize<T extends (...args: any[]) => any>(
func: T
): (...args: Parameters<T>) => ReturnType<T> {
let lastArguments: any[] | null = null
let lastCalculatedValue: ReturnType<T> | null = null

return (...args: Parameters<T>): ReturnType<T> => {
if (isEqual(lastArguments, args)) {
return lastCalculatedValue as ReturnType<T>
}

lastArguments = args
lastCalculatedValue = func(args)

return lastCalculatedValue as ReturnType<T>
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think lodash has a memoize function as well... does that not work for our purposes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it does have memoize function, but this function gathers all arguments in key-value cache store, which will produce another place which GC can free up, so I remember only the last result

1 change: 1 addition & 0 deletions vscode/webviews/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc
const dispatchClientAction = useClientActionDispatcher()

const clientConfigAttribution = clientConfig?.attribution ?? 'none'

const guardrails = useMemo(() => {
return createGuardrailsImpl(clientConfigAttribution, (snippet: string) => {
vscodeAPI.postMessage({
Expand Down
1 change: 0 additions & 1 deletion vscode/webviews/Chat.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const meta: Meta<typeof Chat> = {
postMessage: () => {},
onMessage: () => () => {},
},
setView: () => {},
models: FIXTURE_MODELS,
guardrails: new MockNoGuardrails(),
} satisfies React.ComponentProps<typeof Chat>,
Expand Down
7 changes: 0 additions & 7 deletions vscode/webviews/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type {
import styles from './Chat.module.css'
import { Transcript } from './chat/Transcript'

import type { View } from './tabs'
import type { VSCodeWrapper } from './utils/VSCodeApi'
import { SpanManager } from './utils/spanManager'
import { getTraceparentFromSpanContext } from './utils/telemetry'
Expand All @@ -36,10 +35,7 @@ interface ChatboxProps {
models: Model[]
vscodeAPI: Pick<VSCodeWrapper, 'postMessage' | 'onMessage'>
guardrails: Guardrails
showWelcomeMessage?: boolean
showIDESnippetActions?: boolean
setView: (view: View) => void
isWorkspacesUpgradeCtaEnabled?: boolean
}

export const Chat: React.FunctionComponent<React.PropsWithChildren<ChatboxProps>> = ({
Expand All @@ -50,10 +46,7 @@ export const Chat: React.FunctionComponent<React.PropsWithChildren<ChatboxProps>
vscodeAPI,
chatEnabled = true,
guardrails,
showWelcomeMessage = true,
showIDESnippetActions = true,
setView,
isWorkspacesUpgradeCtaEnabled,
}) => {
const transcriptRef = useRef(transcript)
transcriptRef.current = transcript
Expand Down
12 changes: 7 additions & 5 deletions vscode/webviews/CodyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type CodyNotice,
FeatureFlag,
type Guardrails,
type Model,
type UserProductSubscription,
type WebviewToExtensionAPI,
firstValueFrom,
Expand All @@ -28,6 +29,8 @@ import { useUserAccountInfo } from './utils/useConfig'
import { useFeatureFlag } from './utils/useFeatureFlags'
import { TabViewContext } from './utils/useTabView'

const DEFAULT_CHAT_MODELS: Model[] = []

interface CodyPanelProps {
view: View
setView: (view: View) => void
Expand Down Expand Up @@ -93,11 +96,13 @@ export const CodyPanel: FunctionComponent<CodyPanelProps> = ({
[api.mcpSettings]
)
)
// workspace upgrade eligibility should be that the flag is set, is on dotcom and only has one account. This prevents enterprise customers that are logged into multiple endpoints from seeing the CTA
// Workspace upgrade eligibility should be that the flag is set, is on dotcom and only has one account.
// This prevents enterprise customers that are logged into multiple endpoints from seeing the CTA
const isWorkspacesUpgradeCtaEnabled =
useFeatureFlag(FeatureFlag.SourcegraphTeamsUpgradeCTA) &&
isDotComUser &&
config.endpointHistory?.length === 1

useEffect(() => {
onExternalApiReady?.(externalAPI)
}, [onExternalApiReady, externalAPI])
Expand Down Expand Up @@ -162,13 +167,10 @@ export const CodyPanel: FunctionComponent<CodyPanelProps> = ({
messageInProgress={messageInProgress}
transcript={transcript}
tokenUsage={tokenUsage}
models={chatModels || []}
models={chatModels || DEFAULT_CHAT_MODELS}
vscodeAPI={vscodeAPI}
guardrails={guardrails}
showIDESnippetActions={showIDESnippetActions}
showWelcomeMessage={showWelcomeMessage}
setView={setView}
isWorkspacesUpgradeCtaEnabled={isWorkspacesUpgradeCtaEnabled}
/>
)}
{view === View.History && (
Expand Down
40 changes: 17 additions & 23 deletions vscode/webviews/chat/Transcript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export const Transcript: FC<TranscriptProps> = props => {
[]
)

const scrollTotheBottom = useCallback(() => {
const scrollToBottom = useCallback(() => {
scrollableContainer?.scroll({
top: scrollableContainer?.scrollHeight,
behavior: 'smooth',
Expand Down Expand Up @@ -285,7 +285,7 @@ export const Transcript: FC<TranscriptProps> = props => {
{scrollableContainer && <ScrollbarMarkers scrollContainer={scrollableContainer} />}

{!isAtBottomDebounced && interactions.length > 1 && (
<ScrollDown onClick={scrollTotheBottom} />
<ScrollDown onClick={scrollToBottom} />
)}

<div className="tw-bg-[var(--vscode-input-background)]">
Expand Down Expand Up @@ -409,11 +409,20 @@ const TranscriptInteraction: FC<TranscriptInteractionProps> = memo(props => {
} = props

const { activeChatContext, setActiveChatContext } = props

const humanEditorRef = useRef<PromptEditorRefAPI | null>(null)
const lastEditorRef = useContext(LastEditorContext)
useImperativeHandle(parentEditorRef, () => humanEditorRef.current)

const [selectedIntent, setSelectedIntent] = useState<ChatMessage['intent']>(humanMessage?.intent)
// We track, ephemerally, the code blocks that are being regenerated so
// we can show an accurate loading indicator or error message on those
// blocks.
const [regeneratingCodeBlocks, setRegeneratingCodeBlocks] = useState<RegeneratingCodeBlockState[]>(
[]
)

const vscodeAPI = useMemo(() => getVSCodeAPI(), [])

useImperativeHandle(parentEditorRef, () => humanEditorRef.current)

// Reset intent to 'chat' when there are no interactions (new chat)
useEffect(() => {
Expand Down Expand Up @@ -482,7 +491,6 @@ const TranscriptInteraction: FC<TranscriptInteractionProps> = memo(props => {
[humanMessage, setActiveChatContext, isLastSentInteraction, lastEditorRef]
)

const vscodeAPI = getVSCodeAPI()
const onStop = useCallback(() => {
vscodeAPI.postMessage({
command: 'abort',
Expand All @@ -500,17 +508,11 @@ const TranscriptInteraction: FC<TranscriptInteractionProps> = memo(props => {
const timeToFirstTokenSpan = useRef<Span>()
const hasRecordedFirstToken = useRef(false)

const [isLoading, setIsLoading] = useState(assistantMessage?.isLoading)

const [isThoughtProcessOpened, setThoughtProcessOpened] = useLocalStorage(
'cody.thinking-space.open',
true
)

useEffect(() => {
setIsLoading(assistantMessage?.isLoading)
}, [assistantMessage])

const humanMessageText = humanMessage.text
const smartApplyWithInstruction = useMemo(() => {
if (!smartApply) return undefined
Expand Down Expand Up @@ -617,10 +619,10 @@ const TranscriptInteraction: FC<TranscriptInteractionProps> = memo(props => {
context.with(activeChatContext, startRenderSpan)
}
// Case 2: End rendering if loading is complete and a render span exists
else if (!isLoading && renderSpan.current) {
else if (!assistantMessage?.isLoading && renderSpan.current) {
endRenderSpan()
}
}, [assistantMessage, activeChatContext, setActiveChatContext, spanManager, isLoading])
}, [assistantMessage, activeChatContext, setActiveChatContext, spanManager])

const humanMessageInfo = useMemo(() => {
// See SRCH-942: it's critical to memoize this value to avoid repeated
Expand Down Expand Up @@ -651,12 +653,6 @@ const TranscriptInteraction: FC<TranscriptInteractionProps> = memo(props => {
[humanMessage, onUserAction, selectedIntent]
)

// We track, ephemerally, the code blocks that are being regenerated so
// we can show an accurate loading indicator or error message on those
// blocks.
const [regeneratingCodeBlocks, setRegeneratingCodeBlocks] = useState<RegeneratingCodeBlockState[]>(
[]
)
useClientActionListener(
{ isActive: true, selector: event => Boolean(event.regenerateStatus) },
useCallback(event => {
Expand Down Expand Up @@ -687,7 +683,7 @@ const TranscriptInteraction: FC<TranscriptInteractionProps> = memo(props => {

const onRegenerate = useCallback(
(code: string, language?: string) => {
if (assistantMessage) {
if (assistantMessage?.index !== undefined) {
const id = uuid.v4()
regenerateCodeBlock({ id, code, language, index: assistantMessage.index })
setRegeneratingCodeBlocks(blocks => [
Expand All @@ -698,7 +694,7 @@ const TranscriptInteraction: FC<TranscriptInteractionProps> = memo(props => {
console.warn('tried to regenerate a code block, but there is no assistant message')
}
},
[assistantMessage]
[assistantMessage?.index]
)

const isAgenticMode = useMemo(
Expand Down Expand Up @@ -770,7 +766,6 @@ const TranscriptInteraction: FC<TranscriptInteractionProps> = memo(props => {
key={assistantMessage.index}
userInfo={userInfo}
models={models}
chatEnabled={chatEnabled}
message={assistantMessage}
copyButtonOnSubmit={copyButtonOnSubmit}
insertButtonOnSubmit={insertButtonOnSubmit}
Expand All @@ -781,7 +776,6 @@ const TranscriptInteraction: FC<TranscriptInteractionProps> = memo(props => {
humanMessage={humanMessageInfo}
isLoading={isLastSentInteraction && assistantMessage.isLoading}
smartApply={isAgenticMode ? undefined : smartApplyWithInstruction}
isLastSentInteraction={isLastSentInteraction}
setThoughtProcessOpened={setThoughtProcessOpened}
isThoughtProcessOpened={isThoughtProcessOpened}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export const AssistantMessageCell: FunctionComponent<{
humanMessage: PriorHumanMessageInfo | null

userInfo: UserAccountInfo
chatEnabled: boolean
isLoading: boolean

copyButtonOnSubmit?: CodeBlockActionsProps['copyButtonOnSubmit']
Expand All @@ -54,7 +53,6 @@ export const AssistantMessageCell: FunctionComponent<{

postMessage?: ApiPostMessage
guardrails: Guardrails
isLastSentInteraction: boolean
}> = memo(
({
message,
Expand All @@ -69,7 +67,6 @@ export const AssistantMessageCell: FunctionComponent<{
postMessage,
guardrails,
smartApply,
isLastSentInteraction: isLastInteraction,
isThoughtProcessOpened,
setThoughtProcessOpened,
}) => {
Expand All @@ -79,9 +76,7 @@ export const AssistantMessageCell: FunctionComponent<{
)
const chatModel = useChatModelByID(message.model, models)
const isAborted = isAbortErrorOrSocketHangUp(message.error)

const hasLongerResponseTime = chatModel?.tags?.includes(ModelTag.StreamDisabled)

const messageIntent = humanMessage?.intent ?? message.intent ?? 'chat'
const isSearchIntent = messageIntent === 'search'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ export const HumanMessageCell: FC<HumanMessageCellProps> = ({ message, ...otherP
}

const messageJSON = JSON.stringify(message)

const initialEditorState = useMemo(
() => serializedPromptEditorStateFromChatMessage(JSON.parse(messageJSON)),
[messageJSON]
Expand Down
2 changes: 2 additions & 0 deletions vscode/webviews/components/MarkdownFromCody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const MarkdownFromCody: FunctionComponent<{
}

let _markdownPluginProps: ReturnType<typeof markdownPluginProps> | undefined

function markdownPluginProps(
prefixRemarkPlugins: Pluggable[] = []
): Pick<ComponentProps<typeof Markdown>, 'rehypePlugins' | 'remarkPlugins'> {
Expand Down Expand Up @@ -181,5 +182,6 @@ function markdownPluginProps(
],
remarkPlugins: [...prefixRemarkPlugins, remarkGFM, remarkAttachFilePathToCodeBlocks],
}

return _markdownPluginProps
}
Loading
Loading