Files
windmill/frontend/src/lib/components/AIAgentLogViewer.svelte
T
Guilhem 3b5c1657c7 fix(frontend): improve runs detail page (#7694)
* separate flow and status with splitpane

* fix autoscroll behavior

* Flow log viewer nit

* not graph viewer

* Improve flow job result

* Create job detail header to replace metadata

* Remove FlowPreviewResult

* Create compact job header

* Use job header in runs page

* Improve runs page run preview

* Use flow header in detail section

* Show logs for script steps

* Clean old schedule status

* Limit result height

* Script run detail improvement

* Script run preview improvement

* Fix csv table overflow

* surface tertiary as background for Inputs

* nit

* Improve runs detail skeleton

* fix check

* nit

* Improve node definition

* fix flow module component overflow

* Use component DataTable for flow schema viewer

* Add language icon to step detail

* improve run header

* Improve Job detail header

* nit

* restore isOwner logic

* Handle resume flows

* restore execution status in run preview

* restore flow execution status in the preview

* flow preview, add status bar

* nit module status

* nit

* Remove flor preview result

* nit

* nit

* fix flow result card

* nit

* nit

* nit

* improve field selection on runs detail based on job type

* Improve column layout

* create JobStatusIcon component

* remove job status badge icons

* improve compact version

* use shared job field display

* improve job detail field display

* fix badge alignment

* increase padding

* nit

* nit

* improve compact display

* make background darker for metadata

* use auto layout

* fix auto layout

* improve display

* fix truncate logic

* fix compact

* improve flex adaptibility

* improve responsive layout

* improve extra compact header

* nit

* remove unused icons

* nit

* Improve flow result display

* nit

* merge progressbar and execution status

* handle canceled flow better
2026-02-03 17:20:35 +00:00

220 lines
5.3 KiB
Svelte

<script lang="ts">
import type { GraphModuleState } from './graph'
import {
JobService,
type CompletedJob,
type FlowModule,
type FlowStatusModule,
type Job
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import FlowLogViewerWrapper from './FlowLogViewerWrapper.svelte'
import { z } from 'zod'
import { onMount } from 'svelte'
import type { AgentTool } from './flows/agentToolUtils'
type AgentActionWithContent = NonNullable<FlowStatusModule['agent_actions']>[number] & {
content?: unknown
}
const resultSchema = z.object({
messages: z.array(
z.object({
role: z.string(),
content: z.unknown(),
agent_action: z
.union([
z.object({
type: z.literal('tool_call'),
job_id: z.string(),
module_id: z.string(),
function_name: z.string()
}),
z.object({
type: z.literal('mcp_tool_call'),
call_id: z.string(),
function_name: z.string(),
resource_path: z.string(),
arguments: z.record(z.any(), z.any()).optional()
}),
z.object({
type: z.literal('message')
}),
z.object({
type: z.literal('web_search')
})
])
.optional()
})
)
})
interface Props {
tools: AgentTool[]
agentJob: Partial<CompletedJob> & Pick<CompletedJob, 'id'> & { type: 'CompletedJob' }
workspaceId?: string | undefined
storedToolCallJobs?: Record<number, Job>
onToolJobLoaded?: (job: Job, idx: number) => void
noPadding?: boolean
}
let {
tools,
agentJob,
workspaceId,
onToolJobLoaded,
storedToolCallJobs,
noPadding = false
}: Props = $props()
const fakeModuleStates: Record<string, GraphModuleState> = $state({})
async function loadMissingJobs(agentActions: AgentActionWithContent[]) {
const promises = agentActions.map(async (toolCall, idx) => {
if (toolCall.type === 'tool_call') {
let job: Job | undefined = storedToolCallJobs?.[idx]
if (!job || job.type !== 'CompletedJob') {
job = await JobService.getJob({
id: toolCall.job_id,
workspace: workspaceId ?? $workspaceStore!
})
}
fakeModuleStates[idx.toString()] = {
args: job.args,
type: job['success'] ? 'Success' : 'Failure',
logs: job.logs,
result: job['result'],
job_id: toolCall.job_id
}
onToolJobLoaded?.(job, idx)
} else if (toolCall.type === 'mcp_tool_call') {
fakeModuleStates[idx.toString()] = {
type: 'Success',
args: toolCall.arguments ?? {},
logs: '',
result: toolCall.content
}
} else if (toolCall.type === 'web_search') {
fakeModuleStates[idx.toString()] = {
type: 'Success',
args: {},
logs: '',
result: toolCall.content
}
} else {
fakeModuleStates[idx.toString()] = {
type: 'Success',
args: {},
logs: '',
result: toolCall.content
}
}
})
await Promise.all(promises)
}
let job: Partial<Job> | undefined = $state(undefined)
async function loadToolCalls() {
let parsedResult = resultSchema.safeParse(agentJob.result)
if (!parsedResult.success) {
console.error('Invalid result', parsedResult.error)
return
}
let agentActions = parsedResult.data.messages
.map(
(m) =>
(m.agent_action?.type === 'message'
? {
type: 'message',
content: m.content
}
: m.agent_action?.type === 'tool_call'
? {
type: 'tool_call',
job_id: m.agent_action.job_id,
module_id: m.agent_action.module_id,
function_name: m.agent_action.function_name
}
: m.agent_action?.type === 'mcp_tool_call'
? {
type: 'mcp_tool_call',
content: m.content,
call_id: m.agent_action.call_id,
function_name: m.agent_action.function_name,
arguments: m.agent_action.arguments
}
: m.agent_action?.type === 'web_search'
? {
type: 'web_search',
content: m.content
}
: undefined) as AgentActionWithContent | undefined
)
.filter((m) => m !== undefined)
await loadMissingJobs(agentActions)
job = {
...agentJob,
raw_flow: {
modules: agentActions
.map((toolCall, idx) => {
if (toolCall.type === 'message') {
return {
id: idx.toString(),
value: {
type: 'identity' as const
}
}
} else if (toolCall.type === 'mcp_tool_call') {
return {
id: idx.toString(),
value: {
type: 'identity' as const
},
summary: toolCall.function_name,
arguments: toolCall.arguments
}
} else if (toolCall.type === 'web_search') {
return {
id: idx.toString(),
value: {
type: 'identity' as const
},
summary: 'Web Search'
}
} else {
const module = tools.find((m) => m.summary === toolCall.function_name)
return module
? ({
...module,
id: idx.toString()
} as FlowModule)
: undefined
}
})
.filter((m) => m !== undefined)
}
}
}
onMount(() => {
loadToolCalls()
})
</script>
{#if job}
<div class={noPadding ? '' : 'p-2'}>
<FlowLogViewerWrapper
{job}
localModuleStates={fakeModuleStates}
{workspaceId}
render={true}
onSelectedIteration={async () => {}}
mode="aiagent"
/>
</div>
{/if}