Files
windmill/frontend/src/lib/components/ModulePreviewResultViewer.svelte
T
hugocasaandClaude Opus 5 a08992834d feat: render an AI agent result as its answer, not as raw JSON (#11051)
* 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>
2026-09-18 14:38:46 +02:00

128 lines
3.7 KiB
Svelte

<script lang="ts">
import LogViewer from './LogViewer.svelte'
import JobProgressBar from '$lib/components/jobs/JobProgressBar.svelte'
import ScriptFix from './copilot/ScriptFix.svelte'
import type DiffEditor from './DiffEditor.svelte'
import type Editor from './Editor.svelte'
import { type Script, type Job, type FlowModule } from '$lib/gen'
import OutputPickerInner from '$lib/components/flows/propPicker/OutputPickerInner.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import type { FlowEditorContext, OutputViewerJob } from './flows/types'
import { getContext } from 'svelte'
import { getStringError } from './copilot/chat/utils'
interface Props {
lang: Script['language']
editor: Editor | undefined
diffEditor: DiffEditor | undefined
loopStatus?: { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' } | undefined
testJob?: Job & { result_stream?: string }
scriptProgress?: number | undefined
mod: FlowModule
testIsLoading?: boolean
disableMock?: boolean
disableHistory?: boolean
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
loadingJob?: boolean
tagLabel?: string
}
let {
lang,
editor,
diffEditor,
loopStatus = undefined,
scriptProgress = $bindable(undefined),
testJob = undefined,
mod,
testIsLoading = false,
disableMock = false,
disableHistory = false,
onUpdateMock,
loadingJob = false,
tagLabel = undefined
}: Props = $props()
const { stepsInputArgs, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
let outputPickerInner: OutputPickerInner | undefined = $state(undefined)
export function getOutputPickerInner() {
return outputPickerInner
}
const selectedJob: OutputViewerJob = $derived.by(
() => outputPickerInner?.getSelectedJob?.() ?? undefined
)
const logJob = $derived(testJob ?? selectedJob)
const preview = $derived.by(() => outputPickerInner?.getPreview?.())
</script>
<Splitpanes horizontal>
<Pane size={65} minSize={10} class="text-sm text-primary">
{#if scriptProgress}
<JobProgressBar job={testJob} {scriptProgress} compact={true} />
{/if}
<OutputPickerInner
{testJob}
fullResult
moduleId={mod.id}
closeOnOutsideClick={true}
getLogs
{onUpdateMock}
mock={mod.mock}
isLoading={testIsLoading || loadingJob}
path={`path` in mod.value ? mod.value.path : ''}
{loopStatus}
{disableMock}
{disableHistory}
bind:this={outputPickerInner}
>
{#snippet copilot_fix()}
{@const stepError =
selectedJob?.type === 'CompletedJob' && !selectedJob.success
? getStringError(selectedJob.result)
: undefined}
{#if lang && editor && diffEditor && stepsInputArgs.getStepArgs(mod.id) && stepError}
<ScriptFix {lang} error={stepError} jobId={selectedJob?.id} moduleId={mod.id} />
{/if}
{/snippet}
</OutputPickerInner>
</Pane>
<Pane size={35} minSize={10}>
{#if (mod.mock?.enabled && preview !== 'job' && testJob?.type !== 'QueuedJob') || preview === 'mock'}
<LogViewer
small
content={undefined}
isLoading={false}
tag={undefined}
customEmptyMessage="Using pinned data"
{tagLabel}
/>
{:else}
<LogViewer
small
jobId={logJob?.id}
duration={logJob?.['duration_ms']}
mem={logJob?.['mem_peak']}
content={logJob?.['logs']}
isLoading={(testIsLoading && logJob?.['running'] == false) || loadingJob}
tag={logJob?.['tag']}
{tagLabel}
onLogsResolved={(logs) => {
if (
mod.id &&
flowStateStore?.val[mod.id] &&
flowStateStore.val[mod.id].previewJobId === logJob?.id
) {
flowStateStore.val[mod.id] = {
...flowStateStore.val[mod.id],
previewLogs: logs
}
}
}}
/>
{/if}
</Pane>
</Splitpanes>