From e2a91ca2b141a0bee64ddfafe3cbf3ff86b6fb35 Mon Sep 17 00:00:00 2001 From: Guilhem Date: Fri, 18 Sep 2026 11:31:00 +0200 Subject: [PATCH] feat: flow chat job-backed detail, smooth streaming and answer chrome (#11186) * feat: flow chat job-backed detail, smooth streaming and answer chrome * chore: document setFlowPath, drop unused chatIdentity, tighten comments * fix: read a nested agent tool's call from its own job * fix: show only the model's arguments on a tool card read from its job * feat: add attachments to the chat sdk message type * refactor: read a flow chat message's files from the message, not its run * fix: keep every job-backed answer when reads finish out of order * refactor: build flow chat tool cards from the conversation row alone * feat: keep the model picker's current choice when a flow chat retry replays its run * fix: export ChatAttachment from the chat sdk entry point * fix: drop a conversation list for the flow path before setFlowPath * fix: return the current listing, not old-path rows, after setFlowPath * fix: treat a listing stale on kind or path as one stale listing * revert: drop setFlowPath and rebuild the chat when the flow path changes * fix: refuse a flow chat retry once any turn ran while it read the run * docs: say where a display message's createdAt comes from * style: drop the rule between New chat and the conversation list --- .../lib/components/FlowPreviewContent.svelte | 10 +- frontend/src/lib/components/ScrollFade.svelte | 66 ++++ .../copilot/chat/AIChatDisplay.svelte | 27 +- .../copilot/chat/AIChatManager.svelte.ts | 10 +- .../copilot/chat/AIChatMessage.svelte | 4 +- .../copilot/chat/AssistantMessage.svelte | 55 ++- .../copilot/chat/ToolExecutionDisplay.svelte | 19 +- .../src/lib/components/copilot/chat/shared.ts | 6 +- .../copilot/chat/typewriterReveal.ts | 7 + .../components/flows/content/FlowInput.svelte | 1 + .../flows/conversations/FlowChat.svelte | 50 ++- .../conversations/FlowChatInterface.svelte | 11 +- .../FlowConversationsSidebar.svelte | 2 +- .../flows/conversations/flowChatProps.ts | 19 + .../conversations/flowChatViewHost.svelte.ts | 292 +++++++++++---- .../conversations/flowChatViewHost.test.ts | 343 +++++++++++++++--- .../conversations/messageAttachments.test.ts | 25 ++ .../flows/conversations/messageAttachments.ts | 48 +++ .../(logged)/flows/get/[...path]/+page.svelte | 112 +++--- 19 files changed, 893 insertions(+), 214 deletions(-) create mode 100644 frontend/src/lib/components/ScrollFade.svelte create mode 100644 frontend/src/lib/components/flows/conversations/flowChatProps.ts create mode 100644 frontend/src/lib/components/flows/conversations/messageAttachments.test.ts create mode 100644 frontend/src/lib/components/flows/conversations/messageAttachments.ts diff --git a/frontend/src/lib/components/FlowPreviewContent.svelte b/frontend/src/lib/components/FlowPreviewContent.svelte index 32b46cb2be..5d543ae24d 100644 --- a/frontend/src/lib/components/FlowPreviewContent.svelte +++ b/frontend/src/lib/components/FlowPreviewContent.svelte @@ -471,7 +471,9 @@ return jobId ?? '' }} conversationKind="test" + frame="boxed" path={$pathStore} + identity={$initialPathStore || fakeInitialPath} inputSchema={flowStore.val.schema} flowModules={flowStore.val.value?.modules} /> @@ -558,7 +560,13 @@ {/if} {/if} -
+ +
{#if flowHasChanged()}
+ /** + * A soft edge on a scroller, so content scrolling out of view fades instead of being + * cut against whatever borders it. + * + * Rendered as an overlay in the scroller's positioned ancestor rather than inside the + * scroller: `sticky` would resolve against the scroller's padding box and leave the + * first few pixels unfaded. It shows only when there is something hidden in that + * direction, so a transcript that fits shows no edge at all. + */ + import { twMerge } from 'tailwind-merge' + + interface Props { + /** The scrolling element this masks. */ + scroller: HTMLElement | undefined + edge?: 'top' | 'bottom' + /** Tailwind colour stop to fade from — the surface the scroller sits on. */ + from?: string + /** Tailwind height of the fade band. */ + height?: string + class?: string + } + + let { + scroller, + edge = 'top', + from = 'from-surface', + height = 'h-4', + class: className = '' + }: Props = $props() + + let hidden = $state(true) + + $effect(() => { + const el = scroller + if (!el) return + const update = () => { + // A pixel of slack: fractional scroll offsets otherwise leave the bottom edge + // showing on a scroller that is already at its end. + hidden = + edge === 'top' ? el.scrollTop <= 1 : el.scrollTop + el.clientHeight >= el.scrollHeight - 1 + } + update() + el.addEventListener('scroll', update, { passive: true }) + // Content arriving or the pane resizing changes what is hidden without a scroll. + const observer = new ResizeObserver(update) + observer.observe(el) + if (el.firstElementChild) observer.observe(el.firstElementChild) + return () => { + el.removeEventListener('scroll', update) + observer.disconnect() + } + }) + + + diff --git a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte index 0085ca701f..b7f662b148 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte @@ -35,6 +35,7 @@ import ChatQuickActions from './ChatQuickActions.svelte' import ContextUsageIndicator from './ContextUsageIndicator.svelte' import AIChatModelSettings from './AIChatModelSettings.svelte' + import ScrollFade from '$lib/components/ScrollFade.svelte' import AssistantSettingsModal from './AssistantSettingsModal.svelte' import { SkillsMenu } from './skills/skillsMenu.svelte' import { McpMenu } from '$lib/components/mcp/mcpMenu.svelte' @@ -591,6 +592,15 @@ // The typing-dots indicator implies the AI is busy, which is misleading while // the loop is parked on the user; surface a text pill instead so users know to // act on the tool above. + // A step name hangs its icon in the column's left padding (see AssistantMessage), so a + // transcript carrying one widens the padding, on both sides to keep the column centred. + const agentGutter = $derived(messages.some((m) => m.role === 'assistant' && m.stepName)) + const columnClass = $derived( + wideLayout + ? `w-full max-w-3xl mx-auto ${agentGutter ? 'px-8' : 'px-7'}` + : `w-full max-w-2xl mx-auto ${agentGutter ? 'px-8' : 'px-3'}` + ) + const waitingForUserAction = $derived(chatHost.loading && !!pendingUserAction(messages)) // Gated on `loading` because a card restored from history still looks parked: @@ -823,12 +833,7 @@ the panel, or the Escape-to-stop focus check would wrongly reject them. --> bind:this={scrollElement} onscroll={onScroll} > -
+
{#each messages as message, messageIndex (messageIndex)} {/if}
+ + {#if showScrollToLatest}
{/if} -
+ +
{#if showFlowPendingActionControls}