|
25 | 25 | import { SvelteSet } from 'svelte/reactivity'; |
26 | 26 |
|
27 | 27 | type ToolSegment = |
| 28 | + | { kind: 'content'; content: string; parentId: string } |
28 | 29 | | { kind: 'thinking'; content: string } |
29 | | - | { kind: 'tool'; toolCalls: ApiChatCompletionToolCall[]; parentId: string }; |
| 30 | + | { |
| 31 | + kind: 'tool'; |
| 32 | + toolCalls: ApiChatCompletionToolCall[]; |
| 33 | + parentId: string; |
| 34 | + inThinking: boolean; |
| 35 | + }; |
30 | 36 | type ToolParsed = { expression?: string; result?: string; duration_ms?: number }; |
31 | 37 | type CollectedToolMessage = { |
32 | 38 | toolCallId?: string | null; |
|
115 | 121 | toolMessagesCollectedProp ?? (message as MessageWithToolExtras)._toolMessagesCollected ?? null |
116 | 122 | ); |
117 | 123 |
|
| 124 | + let hasRegularContent = $derived.by(() => { |
| 125 | + if (messageContent?.trim()) return true; |
| 126 | + return (segments ?? []).some((s) => s.kind === 'content' && Boolean(s.content?.trim())); |
| 127 | + }); |
| 128 | +
|
118 | 129 | const toolCalls = $derived( |
119 | 130 | Array.isArray(toolCallContent) ? (toolCallContent as ApiChatCompletionToolCall[]) : null |
120 | 131 | ); |
|
265 | 276 | if (name === 'code_interpreter_javascript') return 'Code Interpreter (JavaScript)'; |
266 | 277 | return name || `Call #${index + 1}`; |
267 | 278 | } |
| 279 | +
|
| 280 | + function segmentToolInThinking(segment: ToolSegment): boolean { |
| 281 | + if (segment.kind !== 'tool') return false; |
| 282 | + const maybe = segment as unknown as { inThinking?: unknown }; |
| 283 | + if (typeof maybe.inThinking === 'boolean') return maybe.inThinking; |
| 284 | + // Back-compat fallback: if we don't know, treat as in-reasoning when there is a thinking block. |
| 285 | + return Boolean(thinkingContent); |
| 286 | + } |
268 | 287 | </script> |
269 | 288 |
|
270 | 289 | <div |
|
276 | 295 | <ChatMessageThinkingBlock |
277 | 296 | reasoningContent={segments && segments.length ? null : thinkingContent} |
278 | 297 | isStreaming={!message.timestamp || isLoading()} |
279 | | - hasRegularContent={!!messageContent?.trim()} |
| 298 | + {hasRegularContent} |
280 | 299 | > |
281 | 300 | {#if segments && segments.length} |
282 | 301 | {#each segments as segment, segIndex (segIndex)} |
283 | 302 | {#if segment.kind === 'thinking'} |
284 | 303 | <div class="text-xs leading-relaxed break-words whitespace-pre-wrap"> |
285 | 304 | {segment.content} |
286 | 305 | </div> |
287 | | - {:else if segment.kind === 'tool'} |
| 306 | + {:else if segment.kind === 'tool' && segmentToolInThinking(segment)} |
288 | 307 | {#each segment.toolCalls as toolCall, index (toolCall.id ?? `${segIndex}-${index}`)} |
289 | 308 | {@const argsParsed = parseArguments(toolCall)} |
290 | 309 | {@const parsed = advanceToolResult(toolCall)} |
|
354 | 373 | </ChatMessageThinkingBlock> |
355 | 374 | {/if} |
356 | 375 |
|
357 | | - {#if !thinkingContent && segments && segments.length} |
358 | | - {#each segments as segment, segIndex (segIndex)} |
359 | | - {#if segment.kind === 'tool'} |
360 | | - {#each segment.toolCalls as toolCall, index (toolCall.id ?? `${segIndex}-${index}`)} |
361 | | - {@const argsParsed = parseArguments(toolCall)} |
362 | | - {@const parsed = advanceToolResult(toolCall)} |
363 | | - {@const collectedResult = toolMessagesCollected |
364 | | - ? toolMessagesCollected.find((c) => c.toolCallId === toolCall.id)?.parsed?.result |
365 | | - : undefined} |
366 | | - {@const collectedDurationMs = toolMessagesCollected |
367 | | - ? toolMessagesCollected.find((c) => c.toolCallId === toolCall.id)?.parsed?.duration_ms |
368 | | - : undefined} |
369 | | - {@const durationMs = parsed?.duration_ms ?? collectedDurationMs} |
370 | | - {@const durationText = formatDurationSeconds(durationMs)} |
371 | | - <div |
372 | | - class="mt-2 space-y-1 rounded-md border border-dashed border-muted-foreground/40 bg-muted/40 px-2.5 py-2" |
373 | | - data-testid="tool-call-block" |
374 | | - > |
375 | | - <div class="flex items-center justify-between gap-2"> |
376 | | - <div class="flex items-center gap-1 text-xs font-semibold"> |
377 | | - <Wrench class="h-3.5 w-3.5" /> |
378 | | - <span>{getToolLabel(toolCall, index)}</span> |
379 | | - </div> |
380 | | - {#if durationText} |
381 | | - <BadgeChatStatistic icon={Clock} value={durationText} /> |
382 | | - {/if} |
383 | | - </div> |
384 | | - {#if argsParsed} |
385 | | - <div class="text-[12px] text-muted-foreground">Arguments</div> |
386 | | - {#if 'pairs' in argsParsed} |
387 | | - {#each argsParsed.pairs as pair (pair.key)} |
388 | | - <div class="mt-1 rounded-sm bg-background/70 px-2 py-1.5"> |
389 | | - <div class="text-[12px] font-semibold text-foreground">{pair.key}</div> |
390 | | - {#if pair.key === 'code' && toolCall.function?.name === 'code_interpreter_javascript'} |
391 | | - <MarkdownContent |
392 | | - class="mt-0.5 text-[12px] leading-snug" |
393 | | - content={toFencedCodeBlock(pair.value, 'javascript')} |
394 | | - /> |
395 | | - {:else} |
396 | | - <pre |
397 | | - class="mt-0.5 font-mono text-[12px] leading-snug break-words whitespace-pre-wrap"> |
398 | | -{pair.value} |
399 | | - </pre> |
400 | | - {/if} |
401 | | - </div> |
402 | | - {/each} |
403 | | - {:else} |
404 | | - <pre class="font-mono text-[12px] leading-snug break-words whitespace-pre-wrap"> |
405 | | -{argsParsed.raw} |
406 | | - </pre> |
407 | | - {/if} |
408 | | - {/if} |
409 | | - {#if parsed && parsed.result !== undefined} |
410 | | - <div class="text-[12px] text-muted-foreground">Result</div> |
411 | | - <div class="rounded-sm bg-background/80 px-2 py-1 font-mono text-[12px]"> |
412 | | - {parsed.result} |
413 | | - </div> |
414 | | - {:else if collectedResult !== undefined} |
415 | | - <div class="text-[12px] text-muted-foreground">Result</div> |
416 | | - <div class="rounded-sm bg-background/80 px-2 py-1 font-mono text-[12px]"> |
417 | | - {collectedResult} |
418 | | - </div> |
419 | | - {/if} |
420 | | - </div> |
421 | | - {/each} |
422 | | - {/if} |
423 | | - {/each} |
424 | | - {/if} |
425 | | - |
426 | 376 | {#if message?.role === 'assistant' && isLoading() && !message?.content?.trim()} |
427 | 377 | <div class="mt-6 w-full max-w-[48rem]" in:fade> |
428 | 378 | <div class="processing-container"> |
|
474 | 424 | {:else if message.role === 'assistant'} |
475 | 425 | {#if config().disableReasoningFormat} |
476 | 426 | <pre class="raw-output">{messageContent}</pre> |
| 427 | + {:else if segments && segments.length} |
| 428 | + {#each segments as segment, segIndex (segIndex)} |
| 429 | + {#if segment.kind === 'content'} |
| 430 | + <MarkdownContent content={segment.content ?? ''} /> |
| 431 | + {:else if segment.kind === 'tool' && (!thinkingContent || !segmentToolInThinking(segment))} |
| 432 | + {#each segment.toolCalls as toolCall, index (toolCall.id ?? `${segIndex}-${index}`)} |
| 433 | + {@const argsParsed = parseArguments(toolCall)} |
| 434 | + {@const parsed = advanceToolResult(toolCall)} |
| 435 | + {@const collectedResult = toolMessagesCollected |
| 436 | + ? toolMessagesCollected.find((c) => c.toolCallId === toolCall.id)?.parsed?.result |
| 437 | + : undefined} |
| 438 | + {@const collectedDurationMs = toolMessagesCollected |
| 439 | + ? toolMessagesCollected.find((c) => c.toolCallId === toolCall.id)?.parsed?.duration_ms |
| 440 | + : undefined} |
| 441 | + {@const durationMs = parsed?.duration_ms ?? collectedDurationMs} |
| 442 | + {@const durationText = formatDurationSeconds(durationMs)} |
| 443 | + <div |
| 444 | + class="mt-2 space-y-1 rounded-md border border-dashed border-muted-foreground/40 bg-muted/40 px-2.5 py-2" |
| 445 | + data-testid="tool-call-block" |
| 446 | + > |
| 447 | + <div class="flex items-center justify-between gap-2"> |
| 448 | + <div class="flex items-center gap-1 text-xs font-semibold"> |
| 449 | + <Wrench class="h-3.5 w-3.5" /> |
| 450 | + <span>{getToolLabel(toolCall, index)}</span> |
| 451 | + </div> |
| 452 | + {#if durationText} |
| 453 | + <BadgeChatStatistic icon={Clock} value={durationText} /> |
| 454 | + {/if} |
| 455 | + </div> |
| 456 | + {#if argsParsed} |
| 457 | + <div class="text-[12px] text-muted-foreground">Arguments</div> |
| 458 | + {#if 'pairs' in argsParsed} |
| 459 | + {#each argsParsed.pairs as pair (pair.key)} |
| 460 | + <div class="mt-1 rounded-sm bg-background/70 px-2 py-1.5"> |
| 461 | + <div class="text-[12px] font-semibold text-foreground">{pair.key}</div> |
| 462 | + {#if pair.key === 'code' && toolCall.function?.name === 'code_interpreter_javascript'} |
| 463 | + <MarkdownContent |
| 464 | + class="mt-0.5 text-[12px] leading-snug" |
| 465 | + content={toFencedCodeBlock(pair.value, 'javascript')} |
| 466 | + /> |
| 467 | + {:else} |
| 468 | + <pre |
| 469 | + class="mt-0.5 font-mono text-[12px] leading-snug break-words whitespace-pre-wrap"> |
| 470 | +{pair.value} |
| 471 | + </pre> |
| 472 | + {/if} |
| 473 | + </div> |
| 474 | + {/each} |
| 475 | + {:else} |
| 476 | + <pre class="font-mono text-[12px] leading-snug break-words whitespace-pre-wrap"> |
| 477 | +{argsParsed.raw} |
| 478 | + </pre> |
| 479 | + {/if} |
| 480 | + {/if} |
| 481 | + {#if parsed && parsed.result !== undefined} |
| 482 | + <div class="text-[12px] text-muted-foreground">Result</div> |
| 483 | + <div class="rounded-sm bg-background/80 px-2 py-1 font-mono text-[12px]"> |
| 484 | + {parsed.result} |
| 485 | + </div> |
| 486 | + {:else if collectedResult !== undefined} |
| 487 | + <div class="text-[12px] text-muted-foreground">Result</div> |
| 488 | + <div class="rounded-sm bg-background/80 px-2 py-1 font-mono text-[12px]"> |
| 489 | + {collectedResult} |
| 490 | + </div> |
| 491 | + {/if} |
| 492 | + </div> |
| 493 | + {/each} |
| 494 | + {/if} |
| 495 | + {/each} |
477 | 496 | {:else} |
478 | 497 | <MarkdownContent content={messageContent ?? ''} /> |
479 | 498 | {/if} |
|
0 commit comments