mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix(ai-chat): show a flow chat message's attachments while its run is in flight
A user row reads its attachments back from the job it started, and the row the composer adds on send has no job id until the conversation is refetched, so the thumbnail only appeared after a reload. Hold what the composer sent and render that on the newest user row until its job id lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
This commit is contained in:
co-authored by
Claude Opus 5
parent
87bccb10ff
commit
0a63bda357
@@ -15,7 +15,11 @@ import type { AttachedTextFile } from '$lib/components/copilot/chat/textFileUtil
|
||||
import { HelpersService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { randomUUID } from '$lib/utils/uuid'
|
||||
import { MessageInputsStore } from './messageInputContext.svelte'
|
||||
import {
|
||||
attachmentsToMessageInputs,
|
||||
MessageInputsStore,
|
||||
type MessageInputs
|
||||
} from './messageInputContext.svelte'
|
||||
|
||||
/** What an AI agent step reads out of `user_attachments`. */
|
||||
type S3Attachment = { s3: string; filename?: string }
|
||||
@@ -53,13 +57,18 @@ function toDisplayMessage(
|
||||
showStepNames: boolean,
|
||||
inputs: MessageInputsStore,
|
||||
toolCalls: ToolCallStore,
|
||||
failed: boolean
|
||||
failed: boolean,
|
||||
pendingInputs: MessageInputs | undefined
|
||||
): DisplayMessage {
|
||||
switch (message.message_type) {
|
||||
case 'user': {
|
||||
// What the turn ran with, read back from its job — the message row itself
|
||||
// keeps only the text. Renders through the same lanes the copilot uses.
|
||||
const { images, contextElements } = inputs.get(message.job_id)
|
||||
// The row the composer just added has no job id yet, so until the run is
|
||||
// persisted its own attachments stand in.
|
||||
const { images, contextElements } = message.job_id
|
||||
? inputs.get(message.job_id)
|
||||
: (pendingInputs ?? { images: [], contextElements: [] })
|
||||
return {
|
||||
role: 'user',
|
||||
index: userIndex,
|
||||
@@ -150,6 +159,9 @@ export class FlowChatViewHost implements ChatViewHost {
|
||||
() => new Set(this.#manager.messages.map((m) => m.step_name).filter(Boolean)).size > 1
|
||||
)
|
||||
|
||||
// What the turn in flight was sent with, until its own row carries a job id.
|
||||
#pendingInputs = $state<MessageInputs | undefined>(undefined)
|
||||
|
||||
#messageInputs = new MessageInputsStore(() => this.#options.workspace?.())
|
||||
#toolCalls = new ToolCallStore(() => this.#options.workspace?.())
|
||||
|
||||
@@ -157,6 +169,15 @@ export class FlowChatViewHost implements ChatViewHost {
|
||||
let userIndex = 0
|
||||
const showStepNames = this.#showStepNames
|
||||
const messages = this.#manager.messages
|
||||
// Only the newest user row can be the one the composer just added: every earlier
|
||||
// row without a job id predates the column and has no inputs to show.
|
||||
let lastUserIndex = -1
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].message_type === 'user') {
|
||||
lastUserIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return messages.map((message, i) =>
|
||||
toDisplayMessage(
|
||||
message,
|
||||
@@ -164,7 +185,8 @@ export class FlowChatViewHost implements ChatViewHost {
|
||||
showStepNames,
|
||||
this.#messageInputs,
|
||||
this.#toolCalls,
|
||||
message.message_type === 'user' && turnFailed(messages, i)
|
||||
message.message_type === 'user' && turnFailed(messages, i),
|
||||
i === lastUserIndex ? this.#pendingInputs : undefined
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -216,11 +238,13 @@ export class FlowChatViewHost implements ChatViewHost {
|
||||
const args = { ...(this.#options.additionalInputs?.() ?? {}) }
|
||||
const target = this.#options.attachmentsTarget?.()
|
||||
const attachments = [...(options.images ?? []), ...(options.blobs ?? [])]
|
||||
this.#pendingInputs = undefined
|
||||
if (target && attachments.length > 0) {
|
||||
this.#uploading = true
|
||||
try {
|
||||
const uploaded = await this.#uploadAttachments(attachments)
|
||||
args[target.name] = target.multiple ? uploaded : uploaded[0]
|
||||
this.#pendingInputs = attachmentsToMessageInputs(options.images ?? [], options.blobs ?? [])
|
||||
} catch (e) {
|
||||
sendUserToast(
|
||||
`Could not upload the attachments: ${e instanceof Error ? e.message : String(e)}`,
|
||||
|
||||
@@ -97,6 +97,21 @@ export function argsToMessageInputs(
|
||||
return images.length > 0 || contextElements.length > 0 ? { images, contextElements } : EMPTY
|
||||
}
|
||||
|
||||
/**
|
||||
* The same lanes, built from what the composer just sent. The turn in flight has no
|
||||
* job yet, so its row cannot read its inputs back from one; the data URLs are still
|
||||
* in hand, so the thumbnails need no fetch.
|
||||
*/
|
||||
export function attachmentsToMessageInputs(
|
||||
images: AttachedImage[],
|
||||
blobs: { name: string }[]
|
||||
): MessageInputs {
|
||||
const contextElements = blobs.map((blob) =>
|
||||
createAttachedFileContextElement(blob.name, `Attached file · ${blob.name}`)
|
||||
)
|
||||
return images.length > 0 || contextElements.length > 0 ? { images, contextElements } : EMPTY
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-conversation cache of run arguments by job id. One fetch per turn, kept only
|
||||
* for as long as the chat is mounted.
|
||||
|
||||
Reference in New Issue
Block a user