101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources'
|
|
import { randomUUID } from 'crypto'
|
|
import { setPromptId } from 'src/bootstrap/state.js'
|
|
import type {
|
|
AttachmentMessage,
|
|
SystemMessage,
|
|
UserMessage,
|
|
} from 'src/types/message.js'
|
|
import { logEvent } from '../../services/analytics/index.js'
|
|
import type { PermissionMode } from '../../types/permissions.js'
|
|
import { createUserMessage } from '../messages.js'
|
|
import { logOTelEvent, redactIfDisabled } from '../telemetry/events.js'
|
|
import { startInteractionSpan } from '../telemetry/sessionTracing.js'
|
|
import {
|
|
matchesKeepGoingKeyword,
|
|
matchesNegativeKeyword,
|
|
} from '../userPromptKeywords.js'
|
|
|
|
export function processTextPrompt(
|
|
input: string | Array<ContentBlockParam>,
|
|
imageContentBlocks: ContentBlockParam[],
|
|
imagePasteIds: number[],
|
|
attachmentMessages: AttachmentMessage[],
|
|
uuid?: string,
|
|
permissionMode?: PermissionMode,
|
|
isMeta?: boolean,
|
|
): {
|
|
messages: (UserMessage | AttachmentMessage | SystemMessage)[]
|
|
shouldQuery: boolean
|
|
} {
|
|
const promptId = randomUUID()
|
|
setPromptId(promptId)
|
|
|
|
const userPromptText =
|
|
typeof input === 'string'
|
|
? input
|
|
: input.find(block => block.type === 'text')?.text || ''
|
|
startInteractionSpan(userPromptText)
|
|
|
|
// Emit user_prompt OTEL event for both string (CLI) and array (SDK/VS Code)
|
|
// input shapes. Previously gated on `typeof input === 'string'`, so VS Code
|
|
// sessions never emitted user_prompt (anthropics/claude-code#33301).
|
|
// For array input, use the LAST text block: createUserContent pushes the
|
|
// user's message last (after any <ide_selection>/attachment context blocks),
|
|
// so .findLast gets the actual prompt. userPromptText (first block) is kept
|
|
// unchanged for startInteractionSpan to preserve existing span attributes.
|
|
const otelPromptText =
|
|
typeof input === 'string'
|
|
? input
|
|
: input.findLast(block => block.type === 'text')?.text || ''
|
|
if (otelPromptText) {
|
|
void logOTelEvent('user_prompt', {
|
|
prompt_length: String(otelPromptText.length),
|
|
prompt: redactIfDisabled(otelPromptText),
|
|
'prompt.id': promptId,
|
|
})
|
|
}
|
|
|
|
const isNegative = matchesNegativeKeyword(userPromptText)
|
|
const isKeepGoing = matchesKeepGoingKeyword(userPromptText)
|
|
logEvent('tengu_input_prompt', {
|
|
is_negative: isNegative,
|
|
is_keep_going: isKeepGoing,
|
|
})
|
|
|
|
// If we have pasted images, create a message with image content
|
|
if (imageContentBlocks.length > 0) {
|
|
// Build content: text first, then images below
|
|
const textContent =
|
|
typeof input === 'string'
|
|
? input.trim()
|
|
? [{ type: 'text' as const, text: input }]
|
|
: []
|
|
: input
|
|
const userMessage = createUserMessage({
|
|
content: [...textContent, ...imageContentBlocks],
|
|
uuid: uuid,
|
|
imagePasteIds: imagePasteIds.length > 0 ? imagePasteIds : undefined,
|
|
permissionMode,
|
|
isMeta: isMeta || undefined,
|
|
})
|
|
|
|
return {
|
|
messages: [userMessage, ...attachmentMessages],
|
|
shouldQuery: true,
|
|
}
|
|
}
|
|
|
|
const userMessage = createUserMessage({
|
|
content: input,
|
|
uuid,
|
|
permissionMode,
|
|
isMeta: isMeta || undefined,
|
|
})
|
|
|
|
return {
|
|
messages: [userMessage, ...attachmentMessages],
|
|
shouldQuery: true,
|
|
}
|
|
}
|