Files
windmill/frontend/src/lib/components/copilot/chat/ToolDiffCard.svelte
T
GuilhemandClaude Opus 5.5 1de54eeea4 feat(ai-chat): show native script edit diffs (#11278)
* feat(ai-chat): show native script edit diffs

* fix: render streamed edit diffs

* fix: render in-editor script edit diffs

* style: soften diff context separators

* fix: render full script update diffs

* fix: support empty script diffs

* fix: bound streamed tool diffs

* fix: preserve trailing diff context

* fix: defer collapsed tool diffs

* fix: retain full-code streaming previews

* fix: preserve created script diffs

* fix: disambiguate collapsed diff keys

* fix: render final newline diffs

* fix: preserve multiline diff highlighting

* fix: highlight diff sides as complete sources

* fix: bound completed tool diffs

* fix: diff large completed tool edits

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: bound tool diff rendering

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: bound tool diff highlighting and expand omitted rows

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: reveal omitted diff rows in chunks and bound long-line highlighting

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: collapse written scripts, skip empty diff bodies and fit line numbers

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep whole-file tool diffs collapsed while running

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: classify whole-file tool calls by argument shape

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: reopen failed diff cards and keep empty streamed replacements

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:44:40 +02:00

104 lines
3.4 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import type { ToolDisplayMessage } from './shared'
import ChatCollapsibleCard from './ChatCollapsibleCard.svelte'
import ToolCodeDiffView from './ToolCodeDiffView.svelte'
import ToolPreviewCard from './ToolPreviewCard.svelte'
import {
argumentsCarryWholeFile,
diffLineCounts,
toolCodeDiff,
toolDiffLineCounts,
toolDiffLines
} from './toolCodeDiff'
interface Props {
message: ToolDisplayMessage
}
let { message }: Props = $props()
const diff = $derived(toolCodeDiff(message))
const isStreaming = $derived(Boolean(message.isStreamingArguments))
const hasDiff = $derived(Boolean(diff && (diff.before !== '' || diff.after !== '')))
// The card body is a bordered box: opened with nothing in it, it draws an empty line.
const hasBody = $derived(hasDiff || Boolean(message.error))
// Keyed by call id: a bare flag would carry the expansion onto the next message that
// reuses this instance. An active or failed call opens by itself, and remains open
// when it settles so the new diff does not disappear under the user. A call whose arguments
// are a whole file (`write_script`, full-code `edit_code`) opens only on failure. The key
// also changes on failure, so a collapse made while running cannot hide the error.
const toggleKey = $derived(`${message.tool_call_id}:${message.error ? 'error' : ''}`)
let toggled = $state<{ id: string; open: boolean } | undefined>(undefined)
const opensByDefault = $derived(
Boolean(
message.error ||
(hasDiff &&
!argumentsCarryWholeFile(message) &&
(message.isLoading || message.isQueued || message.isStreamingArguments))
)
)
$effect(() => {
if (opensByDefault && toggled?.id !== toggleKey) {
toggled = { id: toggleKey, open: true }
}
})
const expanded = $derived(
hasBody && (toggled?.id === toggleKey ? toggled.open : opensByDefault)
)
const lines = $derived(diff && expanded ? toolDiffLines(diff, isStreaming) : undefined)
const counts = $derived(
diff && expanded
? isStreaming
? diffLineCounts(diff, true)
: toolDiffLineCounts(lines ?? [])
: undefined
)
const isRunning = $derived(Boolean(message.isLoading && !message.needsConfirmation))
const showPreviewChip = $derived(
Boolean(message.previewCard && !message.isLoading && !message.error)
)
</script>
{#snippet headerRight()}
<div class="flex items-center gap-2 shrink-0">
{#if counts && (counts.added > 0 || counts.removed > 0)}
<span class="font-mono text-2xs">
<span class="text-green-600 dark:text-green-400">+{counts.added}</span>
<span class="text-red-600 dark:text-red-400">−{counts.removed}</span>
</span>
{/if}
{#if showPreviewChip && message.previewCard}
<ToolPreviewCard card={message.previewCard} />
{/if}
</div>
{/snippet}
<ChatCollapsibleCard
label={message.content}
{expanded}
onToggle={() => (toggled = { id: toggleKey, open: !expanded })}
toggleable={hasBody}
shimmer={isRunning}
class={message.isQueued && !message.error
? 'opacity-60 hover:opacity-100 transition-opacity'
: ''}
labelClass="truncate"
contentClass="p-0 overflow-hidden space-y-0"
{headerRight}
>
{#if diff && hasDiff && expanded}
<ToolCodeDiffView {diff} diffLines={lines} streaming={isStreaming} />
{/if}
{#if message.error}
<div
class="px-3 py-2 text-2xs text-red-600 dark:text-red-400 whitespace-pre-wrap break-words {hasDiff
? 'border-t border-border-light'
: ''}"
>
{message.error}
</div>
{/if}
</ChatCollapsibleCard>