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 all 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
171 changes: 72 additions & 99 deletions vscode/webviews/chat/Transcript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
type Guardrails,
type Model,
type NLSSearchDynamicFilter,
PromptString,
type SerializedPromptEditorValue,
deserializeContextItem,
isAbortErrorOrSocketHangUp,
Expand All @@ -14,7 +15,6 @@
type FC,
memo,
useCallback,
useContext,
useEffect,
useImperativeHandle,
useMemo,
Expand All @@ -25,7 +25,6 @@
import type { ApiPostMessage } from '../Chat'
import { getVSCodeAPI } from '../utils/VSCodeApi'
import { SpanManager } from '../utils/spanManager'
import { getTraceparentFromSpanContext } from '../utils/telemetry'
import { useOmniBox } from '../utils/useOmniBox'
import type { CodeBlockActionsProps } from './ChatMessageContent/ChatMessageContent'
import {
Expand All @@ -37,7 +36,7 @@
import { type Context, type Span, context, trace } from '@opentelemetry/api'
import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client'
import * as uuid from 'uuid'
import { isCodeSearchContextItem } from '../../src/context/openctx/codeSearch'

import { useClientActionListener } from '../client/clientState'
import { useLocalStorage } from '../components/hooks'

Expand All @@ -50,6 +49,7 @@
import { LoadingDots } from './components/LoadingDots'
import { ScrollbarMarkers } from './components/ScrollbarMarkers'
import { LastEditorContext } from './context'
import { MOCK_LONG_RESPONSE } from './mockData'

interface TranscriptProps {
activeChatContext?: Context
Expand Down Expand Up @@ -97,11 +97,11 @@
activeChatContext,
setActiveChatContext,
chatEnabled,
transcript,
transcript: originalTranscript,
tokenUsage,
models,
userInfo,
messageInProgress,
messageInProgress: originalMessageInProgress,
guardrails,
postMessage,
copyButtonOnSubmit,
Expand All @@ -110,6 +110,66 @@
welcomeContent,
} = props

// Simulation state
const [isSimulating, setIsSimulating] = useState(false)
const [simulationTranscript, setSimulationTranscript] = useState<ChatMessage[]>([])
const [simulationMessageInProgress, setSimulationMessageInProgress] = useState<ChatMessage | null>(
null
)

// Use simulation state when active, otherwise use original props
const transcript = isSimulating ? simulationTranscript : originalTranscript
const messageInProgress = isSimulating ? simulationMessageInProgress : originalMessageInProgress

// Simulation function
const startSimulation = useCallback(() => {
setIsSimulating(true)

// Create a human message
const humanMessage: ChatMessage = {
speaker: 'human',
text: PromptString.unsafe_fromUserQuery(

Check failure on line 131 in vscode/webviews/chat/Transcript.tsx

View workflow job for this annotation

GitHub Actions / safe-prompts-lint

New `unsafe_fromUserQuery` invocation found. This is not safe. Please use one of the PromptString helpers instead.
'Please help me implement a complex feature with multiple code examples.'
),
intent: 'chat',
}

// Set initial transcript with human message
setSimulationTranscript([humanMessage])

// Start streaming assistant response
const assistantMessage: ChatMessage = {
speaker: 'assistant',
text: PromptString.unsafe_fromLLMResponse(''),

Check failure on line 143 in vscode/webviews/chat/Transcript.tsx

View workflow job for this annotation

GitHub Actions / safe-prompts-lint

New `unsafe_fromLLMResponse` invocation found. This is not safe. Please use one of the PromptString helpers instead.
intent: 'chat',
}

setSimulationMessageInProgress(assistantMessage)

// Long response with multiple code snippets
const fullResponse = MOCK_LONG_RESPONSE

// Simulate streaming by updating text character by character
let currentIndex = 0
const streamingInterval = setInterval(() => {
if (currentIndex < fullResponse.length) {
const currentText = fullResponse.substring(0, currentIndex + 1)
setSimulationMessageInProgress(prev =>
prev ? { ...prev, text: PromptString.unsafe_fromLLMResponse(currentText) } : null

Check failure on line 158 in vscode/webviews/chat/Transcript.tsx

View workflow job for this annotation

GitHub Actions / safe-prompts-lint

New `unsafe_fromLLMResponse` invocation found. This is not safe. Please use one of the PromptString helpers instead.
)
currentIndex++
} else {
// Streaming complete
clearInterval(streamingInterval)
setSimulationTranscript(prev => [
...prev,
{ ...assistantMessage, text: PromptString.unsafe_fromLLMResponse(fullResponse) },

Check failure on line 166 in vscode/webviews/chat/Transcript.tsx

View workflow job for this annotation

GitHub Actions / safe-prompts-lint

New `unsafe_fromLLMResponse` invocation found. This is not safe. Please use one of the PromptString helpers instead.
])
setSimulationMessageInProgress(null)
}
}, 0)
}, [])

const interactions = useMemo(
() => transcriptToInteractionPairs(transcript, messageInProgress),
[transcript, messageInProgress]
Expand Down Expand Up @@ -243,6 +303,7 @@
? lastHumanEditorRef
: undefined
}
startSimulation={startSimulation}
/>
)
},
Expand All @@ -260,6 +321,7 @@
smartApply,
interactions,
messageInProgress,
startSimulation,
]
)

Expand Down Expand Up @@ -381,6 +443,7 @@
isLastSentInteraction: boolean
priorAssistantMessageIsLoading: boolean
editorRef?: React.RefObject<PromptEditorRefAPI | null>
startSimulation: () => void
}

export type RegeneratingCodeBlockState = {
Expand All @@ -406,11 +469,11 @@
copyButtonOnSubmit,
smartApply,
editorRef: parentEditorRef,
startSimulation,
} = 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)
Expand All @@ -423,65 +486,6 @@
}
}, [humanMessage, isFirstInteraction, isLastInteraction])

const onUserAction = useCallback(
(action: 'edit' | 'submit', manuallySelectedIntent: ChatMessage['intent']) => {
// Start the span as soon as the user initiates the action
const startMark = performance.mark('startSubmit')
const spanManager = new SpanManager('cody-webview')
const span = spanManager.startSpan('chat-interaction', {
attributes: {
sampled: true,
'render.state': 'started',
'startSubmit.mark': startMark.startTime,
},
})

if (!span) {
throw new Error('Failed to start span for chat interaction')
}

const spanContext = trace.setSpan(context.active(), span)
setActiveChatContext(spanContext)
const currentSpanContext = span.spanContext()

const traceparent = getTraceparentFromSpanContext(currentSpanContext)

// Serialize the editor value after starting the span
const editorValue = humanEditorRef.current?.getSerializedValue()
if (!editorValue) {
console.error('Failed to serialize editor value')
return
}

const commonProps = {
editorValue,
traceparent,
manuallySelectedIntent,
}

if (action === 'edit') {
// Remove search context chips from the next input so that the user cannot
// reference search results that don't exist anymore.
// This is a no-op if the input does not contain any search context chips.
// NOTE: Doing this for the penultimate input only seems to suffice because
// editing a message earlier in the transcript will clear the conversation
// and reset the last input anyway.
if (isLastSentInteraction) {
lastEditorRef.current?.filterMentions(item => !isCodeSearchContextItem(item))
}
editHumanMessage({
messageIndexInTranscript: humanMessage.index,
...commonProps,
})
} else {
submitHumanMessage({
...commonProps,
})
}
},
[humanMessage, setActiveChatContext, isLastSentInteraction, lastEditorRef]
)

// Omnibox is enabled if the user is not a dotcom user and the omnibox is enabled
const omniboxEnabled = useOmniBox() && !userInfo?.isDotComUser

Expand Down Expand Up @@ -639,22 +643,13 @@

const onHumanMessageSubmit = useCallback(
(intentOnSubmit: ChatMessage['intent']) => {
// Current intent is the last selected intent if any or the current intent of the human message
const currentIntent = selectedIntent || humanMessage?.intent
// If no intent on submit provided, use the current intent instead
const newIntent = intentOnSubmit === undefined ? currentIntent : intentOnSubmit
setSelectedIntent(newIntent)
if (humanMessage.isUnsentFollowup) {
onUserAction('submit', newIntent)
} else {
// Use onUserAction directly with the new intent
onUserAction('edit', newIntent)
}
// Start simulation instead of actual submission
startSimulation()
// Set the unsent followup flag to false after submitting
// to makes sure the last editor for Agent mode gets reset.
humanMessage.isUnsentFollowup = false
},
[humanMessage, onUserAction, selectedIntent]
[humanMessage, startSimulation]
)

const onSelectedFiltersUpdate = useCallback(
Expand Down Expand Up @@ -870,28 +865,6 @@
}, 50)
}

function submitHumanMessage({
editorValue,
manuallySelectedIntent,
traceparent,
}: {
editorValue: SerializedPromptEditorValue
manuallySelectedIntent?: ChatMessage['intent']
traceparent: string
}): void {
getVSCodeAPI().postMessage({
command: 'submit',
text: editorValue.text,
editorState: editorValue.editorState,
contextItems: editorValue.contextItems.map(deserializeContextItem),
manuallySelectedIntent,
traceparent,
})
setTimeout(() => {
focusLastHumanMessageEditor()
}, 50)
}

function reevaluateSearchWithSelectedFilters({
messageIndexInTranscript,
selectedFilters,
Expand Down
Loading
Loading