mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 00:02:55 +00:00
* feat: render an AI agent result as its answer, not as raw JSON Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: sanitize agent markdown through the shared plugin chain Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: fold agent stream events incrementally per poll Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: separate the agent meta line from the result toggle group Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: add a transcript view of an agent run's conversation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: retire AIAgentLogViewer in favour of the transcript Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: show the partial transcript a max-iterations failure carries Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: move the agent meta line and system prompt below the conversation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: show an agent run as what it did, not as a conversation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: name the agent run breakdown a trace Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ai-agent): label the save-as-agent form fields per the guidelines Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ai-agent): reuse the resource form's path and description fields Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ai-agent): keep the action tags on a max-iterations failure Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ai-agent): keep offline replay inert and the streamed answer to one turn Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ai-agent): reset the streamed answer on providers that skip tool_call Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: carry the event type narrowing through the stream parser Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: survive a malformed message rather than take the viewer down Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: coerce agent messages once at the parse boundary Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: show an agent run as one scroll ending in its output Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep every streamed turn instead of dropping the narration Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: share the chat divider and drop the unsafe run auto-scroll Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: give the labelled divider a border colour and the standard pretty icon Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: pad the agent run below its badges as well as above Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: follow a streaming run's pane without moving the page Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: share the chat's stick-to-bottom mechanics with the agent run Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the answer's citations and end a turn's reasoning with the turn Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: read the agent stream through the windmill-chat sdk parser Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: show an agent run's thinking instead of falling back to raw json Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop a stream the fold cannot use from claiming the run pane Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: identify an agent run by more than the job id the replay withholds Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: drop the tests and comment lines that were not earning their place Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop a streamed turn's text shifting when a tool call closes it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
112 lines
3.7 KiB
Svelte
112 lines
3.7 KiB
Svelte
<script lang="ts">
|
|
import { untrack } from 'svelte'
|
|
import ChatCollapsibleCard from './copilot/chat/ChatCollapsibleCard.svelte'
|
|
import GfmMarkdown from './GfmMarkdown.svelte'
|
|
import { runPane } from './agentScroll'
|
|
import { createBottomSticker } from './stickToBottom'
|
|
import {
|
|
advanceAgentStream,
|
|
emptyAgentStreamProgress,
|
|
type AgentStreamProgress
|
|
} from './aiAgentResult'
|
|
|
|
interface Props {
|
|
/** The raw `result_stream` buffer, which only ever grows for a given run. */
|
|
raw: string
|
|
/** Identifies the run, so a viewer reused for another one starts over. */
|
|
streamKey?: string
|
|
}
|
|
|
|
let { raw, streamKey }: Props = $props()
|
|
|
|
let progress: AgentStreamProgress & { key?: string } = $state(emptyAgentStreamProgress())
|
|
|
|
$effect(() => {
|
|
raw
|
|
streamKey
|
|
untrack(() => {
|
|
// A shorter buffer than what was already folded in cannot be a longer
|
|
// version of the same stream, so treat it as a different one.
|
|
const continues = progress.key === streamKey && raw.length >= progress.consumed
|
|
const base = continues ? progress : emptyAgentStreamProgress()
|
|
progress = { ...advanceAgentStream(raw, base), key: streamKey }
|
|
if (!continues) {
|
|
// Another run, so the reader's decision to stop following the previous
|
|
// one does not carry over: this one starts at its latest output.
|
|
following = true
|
|
}
|
|
})
|
|
})
|
|
|
|
let stream = $derived(progress.stream)
|
|
|
|
let anchor: HTMLElement | undefined = $state()
|
|
// Carry the reader along as the text is written, unless they have scrolled up
|
|
// to read something — then the pane is theirs until they come back to the end.
|
|
let following = true
|
|
const sticker = createBottomSticker()
|
|
|
|
// The listener goes on the pane, not on this element: a scroll event fires on
|
|
// whatever actually scrolled and does not bubble, so a handler here would never
|
|
// run and the reader would be dragged back on every poll.
|
|
$effect(() => {
|
|
const pane = runPane(anchor)
|
|
if (!pane) {
|
|
return
|
|
}
|
|
const onScroll = () => {
|
|
if (!sticker.isOwnScroll()) {
|
|
following = sticker.isAtEnd(pane)
|
|
}
|
|
}
|
|
pane.addEventListener('scroll', onScroll, { passive: true })
|
|
return () => pane.removeEventListener('scroll', onScroll)
|
|
})
|
|
|
|
$effect(() => {
|
|
stream.current
|
|
stream.reasoning
|
|
stream.entries.length
|
|
if (following) {
|
|
sticker.scrollToEnd(runPane(anchor))
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<!-- One gap on the container rather than margins per child, and the same gap the
|
|
finished trace uses: a turn's text must not shift when a tool call turns it from
|
|
the live text into a row. The text at the bottom is deliberately unlabelled — a
|
|
turn that goes on to call a tool was narration, and the run's end settles which. -->
|
|
<div bind:this={anchor} class="flex flex-col w-full py-3 gap-1">
|
|
{#each stream.entries as entry, index (entry.kind === 'tool' ? entry.callId : index)}
|
|
{#if entry.kind === 'tool'}
|
|
<ChatCollapsibleCard
|
|
label={entry.name}
|
|
expanded={false}
|
|
toggleable={false}
|
|
shimmer={entry.running}
|
|
onToggle={() => {}}
|
|
labelClass={entry.success === false ? 'text-red-500' : ''}
|
|
/>
|
|
{:else}
|
|
<div>
|
|
<GfmMarkdown md={entry.content} noPadding />
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
|
|
{#if stream.current !== ''}
|
|
<div>
|
|
<!-- Same sanitizing chain as a finished output: a partial answer is written
|
|
by the same model and is no more trusted for arriving in pieces. -->
|
|
<GfmMarkdown md={stream.current} noPadding />
|
|
</div>
|
|
{:else if stream.reasoning !== ''}
|
|
<!-- Reasoning arrives before the text, so on its own it means the model is
|
|
still thinking rather than that this run has nothing to say. -->
|
|
<div class="text-secondary">
|
|
<GfmMarkdown md={stream.reasoning} prose="xs" noPadding />
|
|
</div>
|
|
{/if}
|
|
</div>
|