feat: show LLM request and response data in API request items

- Added request and response fields to ClineApiReqInfo interface
- Store request data when initiating API calls in Task.ts
- Accumulate and store response data during streaming in Task.ts
- Updated ChatRow component to display request/response in expandable UI
- Added click handler to API request items to toggle visibility
- Display request as formatted JSON and response as markdown

Fixes #9119
This commit is contained in:
Roo Code 2025-11-08 08:12:00 +00:00
parent e98f4b9057
commit f77f4b4ac7
3 changed files with 64 additions and 2 deletions

View file

@ -1859,8 +1859,19 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// message.
const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
// Store the request content as a formatted string
const requestContent = JSON.stringify(
{
role: "user",
content: finalUserContent,
},
null,
2,
)
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
apiProtocol,
request: requestContent,
} satisfies ClineApiReqInfo)
await this.saveClineMessages()
@ -1880,6 +1891,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// anyways, so it remains solely for legacy purposes to keep track
// of prices in tasks from history (it's worth removing a few months
// from now).
// Store accumulated response text for the API request
let accumulatedResponse = ""
const updateApiReqMsg = (cancelReason?: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
if (lastApiReqIndex < 0 || !this.clineMessages[lastApiReqIndex]) {
return
@ -1917,6 +1931,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
cost: totalCost ?? costResult.totalCost,
cancelReason,
streamingFailedMessage,
response: accumulatedResponse || undefined,
} satisfies ClineApiReqInfo)
}
@ -2014,6 +2029,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
break
case "text": {
assistantMessage += chunk.text
accumulatedResponse += chunk.text
// Parse raw assistant message chunk into content blocks.
const prevLength = this.assistantMessageContent.length

View file

@ -464,6 +464,7 @@ export interface ClineAskUseMcpServer {
export interface ClineApiReqInfo {
request?: string
response?: string
tokensIn?: number
tokensOut?: number
cacheWrites?: number

View file

@ -170,6 +170,7 @@ export const ChatRowContent = ({
const [editedContent, setEditedContent] = useState("")
const [editMode, setEditMode] = useState<Mode>(mode || "code")
const [editImages, setEditImages] = useState<string[]>([])
const [showRequestResponse, setShowRequestResponse] = useState(false)
// Handle message events for image selection during edit mode
useEffect(() => {
@ -1070,12 +1071,15 @@ export const ChatRowContent = ({
const isApiRequestInProgress =
apiReqCancelReason === undefined && apiRequestFailedMessage === undefined && cost === undefined
// Parse the API request info to get request and response data
const apiReqInfo = safeJsonParse<ClineApiReqInfo>(message.text)
return (
<>
<div
className={`group text-sm transition-opacity ${
isApiRequestInProgress ? "opacity-100" : "opacity-40 hover:opacity-100"
}`}
apiReqInfo?.request || apiReqInfo?.response ? "cursor-pointer" : ""
} ${isApiRequestInProgress ? "opacity-100" : "opacity-40 hover:opacity-100"}`}
style={{
...headerStyle,
marginBottom:
@ -1084,10 +1088,21 @@ export const ChatRowContent = ({
? 10
: 0,
justifyContent: "space-between",
}}
onClick={() => {
if (apiReqInfo?.request || apiReqInfo?.response) {
setShowRequestResponse(!showRequestResponse)
}
}}>
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
{icon}
{title}
{(apiReqInfo?.request || apiReqInfo?.response) && (
<span
className={`codicon codicon-chevron-${showRequestResponse ? "up" : "down"}`}
style={{ fontSize: "12px", opacity: 0.7 }}
/>
)}
</div>
<div
className="text-xs text-vscode-dropdown-foreground border-vscode-dropdown-border/50 border px-1.5 py-0.5 rounded-lg"
@ -1095,6 +1110,36 @@ export const ChatRowContent = ({
${Number(cost || 0)?.toFixed(4)}
</div>
</div>
{showRequestResponse && (apiReqInfo?.request || apiReqInfo?.response) && (
<div className="ml-6 mt-2 mb-4">
{apiReqInfo?.request && (
<div className="mb-3">
<div className="text-xs font-semibold mb-1 text-vscode-descriptionForeground">
Request to LLM:
</div>
<CodeAccordian
code={apiReqInfo.request}
language="json"
isExpanded={true}
onToggleExpand={() => {}}
/>
</div>
)}
{apiReqInfo?.response && (
<div>
<div className="text-xs font-semibold mb-1 text-vscode-descriptionForeground">
Response from LLM:
</div>
<CodeAccordian
code={apiReqInfo.response}
language="markdown"
isExpanded={true}
onToggleExpand={() => {}}
/>
</div>
)}
</div>
)}
{(((cost === null || cost === undefined) && apiRequestFailedMessage) ||
apiReqStreamingFailedMessage) && (
<ErrorRow