File size: 3,272 Bytes
8b11f3a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | 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,
}
}
|