From c8350480df4337277fc966ebe97b08aa0c67c164 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Mon, 31 Aug 2026 15:55:53 +0200 Subject: [PATCH] feat: render flow chat mode through the AI session chat components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flow chat mode drew its own bubbles from chat/ChatMessage and chat/ChatInput while the AI session chat rendered the same kinds of message far better a directory away. FlowChatViewHost maps a flow run's conversation onto ChatViewHost, so both now render through AIChatDisplay. Flow chat gains thinking blocks, workspace-path links, collapsible tool cards, the typing indicator, scroll-to-latest, and queueing a message typed while the run is in flight. FlowChatManager keeps its conversations, SSE and polling untouched; the sidebar, the inputs modal and its localStorage are unchanged. An assistant message can carry the flow step that produced it, rendered only when a conversation holds more than one distinct step — with a single agent the label repeats on every message and says nothing. It is counted over the transcript rather than the flow's current steps, since a conversation outlives edits to the flow. AssistantMessage also renders a windmill_s3_object result through DisplayResult, as chat/ChatMessage did. FlowChatInterface claims a min-height once there are messages: the editor's Test-flow panel stacks the chat above the job result in an auto-height column, where the transcript's absolute scroller would otherwise resolve to zero. chat/ChatMessage and chat/ChatInput stay for AppChat and its customCss hooks. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Hw1yzWHyqqZmQrviUEhr5b --- .../copilot/chat/AssistantMessage.svelte | 23 ++- .../src/lib/components/copilot/chat/shared.ts | 3 + .../flows/conversations/FlowChat.svelte | 13 +- .../conversations/FlowChatInterface.svelte | 167 +++++++++--------- .../conversations/flowChatViewHost.svelte.ts | 156 ++++++++++++++++ .../(logged)/flows/get/[...path]/+page.svelte | 1 + 6 files changed, 274 insertions(+), 89 deletions(-) create mode 100644 frontend/src/lib/components/flows/conversations/flowChatViewHost.svelte.ts diff --git a/frontend/src/lib/components/copilot/chat/AssistantMessage.svelte b/frontend/src/lib/components/copilot/chat/AssistantMessage.svelte index 787d9ef4fc..b01e696df2 100644 --- a/frontend/src/lib/components/copilot/chat/AssistantMessage.svelte +++ b/frontend/src/lib/components/copilot/chat/AssistantMessage.svelte @@ -13,6 +13,7 @@ workspaceItemRegistry } from './workspaceItems.svelte' import { markdownProse } from '$lib/components/markdownProse' + import DisplayResult from '$lib/components/DisplayResult.svelte' interface Props { message: DisplayMessage @@ -58,6 +59,20 @@ return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s` } + const stepName = $derived(message.role === 'assistant' ? message.stepName : undefined) + + // A flow step can return a file rather than text; the raw JSON would be + // unreadable, so hand it to the result viewer instead of the markdown renderer. + const s3Object = $derived.by(() => { + if (!message.content.startsWith('{')) return undefined + try { + const parsed = JSON.parse(message.content) + return parsed?.type === 'windmill_s3_object' && parsed?.s3 ? parsed : undefined + } catch { + return undefined + } + }) + const candidatePaths = $derived(extractCandidatePaths(message.content)) const rendererPlugin = { renderer: { @@ -111,7 +126,13 @@ {/if} -{#if message.content} +{#if stepName} +
{stepName}
+{/if} + +{#if s3Object} + +{:else if message.content}
diff --git a/frontend/src/lib/components/copilot/chat/shared.ts b/frontend/src/lib/components/copilot/chat/shared.ts index b485be51b3..967d358166 100644 --- a/frontend/src/lib/components/copilot/chat/shared.ts +++ b/frontend/src/lib/components/copilot/chat/shared.ts @@ -629,6 +629,9 @@ export type AssistantDisplayMessage = BaseDisplayMessage & { * would look like it is still streaming forever. */ streaming?: boolean + /** Flow step that produced this message, when the conversation is a flow run + * rather than a copilot turn. Rendered as a label above the content. */ + stepName?: string } /** diff --git a/frontend/src/lib/components/flows/conversations/FlowChat.svelte b/frontend/src/lib/components/flows/conversations/FlowChat.svelte index 7966caa8c5..127e33dbc4 100644 --- a/frontend/src/lib/components/flows/conversations/FlowChat.svelte +++ b/frontend/src/lib/components/flows/conversations/FlowChat.svelte @@ -17,6 +17,8 @@ path: string hideSidebar?: boolean inputSchema?: Record + /** Wider centered column, for the full-page chat. */ + wideLayout?: boolean } let { @@ -25,7 +27,8 @@ useStreaming = false, path, hideSidebar = false, - inputSchema = undefined + inputSchema = undefined, + wideLayout = false }: Props = $props() const flowEditorContext = getContext('FlowEditorContext') @@ -72,5 +75,11 @@ {#if !hideSidebar} {/if} - + diff --git a/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte b/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte index ab52d06102..8756353777 100644 --- a/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte +++ b/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte @@ -1,9 +1,10 @@