feat: replace flow summary card with breadcrumb + frame the flow as a chat-owned card

- FlowBuilder: replace the SummaryPathDisplay box (path / summary stack)
  with an inline breadcrumb (Workflow icon + "Flows" / folder /
  summary-or-leaf, separated by ChevronRight). The breadcrumb derives
  $pathStore segments via $derived (breadcrumbParts/Folder/Leaf) so it
  can be referenced from the template without {@const}, which is not
  allowed as a direct child of <div>. Drops the now-unused
  SummaryPathDisplay import.
- design-sandbox: position the right pane as a contained "chat-owned"
  workspace — p-2 pl-0 padding plus an inner rounded-xl border / shadow-sm
  card wrapping FlowWrapper. Drop the left pane's right border so the
  chat reads as full-bleed primary content. Add a chat-side header
  (h-12 to match FlowBuilder's max-h-12 row) with a MessageSquare icon
  and a conversationTitle derived from the first user message in the
  current chat (truncated to 60 chars). Hide the splitpanes divider
  visually via a scoped :global(.design-sandbox-splitpanes ...) rule
  while keeping the hit area so the split is still resizable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-04-29 12:27:38 +02:00
co-authored by Claude Opus 4.7
parent 63197f682d
commit b28b814e95
2 changed files with 80 additions and 22 deletions
+26 -9
View File
@@ -59,7 +59,9 @@
CheckCircle,
RefreshCw,
CheckCheck,
Disc
Disc,
ChevronRight,
Workflow
} from 'lucide-svelte'
import Awareness from './Awareness.svelte'
import { getAllModules } from './flows/flowExplorer'
@@ -71,7 +73,6 @@
import { resetAllTodos, skipAllTodos } from '$lib/tutorialUtils'
import { tutorialsToDo } from '$lib/stores'
import { getTutorialIndex } from '$lib/tutorials/config'
import SummaryPathDisplay from './SummaryPathDisplay.svelte'
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
import FlowYamlEditor from './flows/header/FlowYamlEditor.svelte'
import { type TriggerContext, type ScheduleTrigger } from './triggers'
@@ -622,6 +623,15 @@
const flowEditorDrawer = writable<FlowEditorDrawer | undefined>(undefined)
const history = initHistory(untrack(() => flowStore).val)
const pathStore = writable<string>(untrack(() => pathStoreInit) ?? initialPath)
// Breadcrumb derivations for the flow header (replaces SummaryPathDisplay).
const breadcrumbParts = $derived(($pathStore ?? '').split('/').filter(Boolean))
const breadcrumbFolder = $derived(
breadcrumbParts.length > 1 ? breadcrumbParts.slice(0, -1).join('/') : ''
)
const breadcrumbLeaf = $derived(
breadcrumbParts.length > 0 ? breadcrumbParts[breadcrumbParts.length - 1] : 'untitled'
)
const captureOn = writable<boolean>(false)
const showCaptureHint = writable<boolean | undefined>(undefined)
const flowInputEditorStateStore = writable<FlowInputEditorState>({
@@ -1150,13 +1160,20 @@
<div
class="justify-between flex flex-row items-center pl-2 pr-4 space-x-4 scrollbar-hidden overflow-x-auto max-h-12 h-full relative"
>
<div class="flex w-full gap-8 items-center min-w-0">
<SummaryPathDisplay
bind:summary={flowStore.val.summary}
bind:path={$pathStore}
kind="flow"
editable
/>
<div class="flex w-full gap-1.5 items-center min-w-0 text-xs">
<Workflow class="w-3.5 h-3.5 text-secondary shrink-0" />
<span class="text-secondary shrink-0">Flows</span>
{#if breadcrumbFolder}
<ChevronRight class="w-3 h-3 text-secondary opacity-60 shrink-0" />
<span class="text-secondary truncate font-mono">{breadcrumbFolder}</span>
{/if}
<ChevronRight class="w-3 h-3 text-secondary opacity-60 shrink-0" />
<span
class="font-medium text-primary truncate"
title={flowStore.val.summary || breadcrumbLeaf}
>
{flowStore.val.summary || breadcrumbLeaf}
</span>
</div>
<div class="gap-4 flex-row hidden md:flex whitespace-nowrap">
@@ -14,6 +14,7 @@
previewModalState,
closePreviewModal
} from '$lib/components/copilot/chat/previewModalState.svelte'
import { MessageSquare } from 'lucide-svelte'
// Fake conversation: user asks the assistant to build a 3-step user-engagement flow.
const fakeMessages: DisplayMessage[] = [
@@ -203,23 +204,49 @@
let customUi: FlowBuilderWhitelabelCustomUi = {
aiAgent: true
}
// Conversation title shown in the chat topbar. Derived from the first user
// message in the current chat, truncated. Falls back to a placeholder while
// the chat is empty.
const conversationTitle = $derived.by(() => {
const firstUser = aiChatManager.displayMessages.find((m) => m.role === 'user')
if (!firstUser) return 'New conversation'
const text = firstUser.content.trim()
const truncated = text.length > 60 ? text.slice(0, 60).trimEnd() + '…' : text
return truncated
})
</script>
<div class="h-full w-full flex flex-col min-h-0">
<Splitpanes horizontal={false} class="flex-1 min-h-0">
<Pane size={50} minSize={20} class="flex flex-col min-h-0 border-r border-light">
<AIChat />
<Splitpanes horizontal={false} class="design-sandbox-splitpanes flex-1 min-h-0">
<Pane size={50} minSize={20} class="flex flex-col min-h-0">
<div class="flex flex-row items-center gap-2 px-3 h-12 bg-surface shrink-0">
<MessageSquare class="w-3.5 h-3.5 text-secondary shrink-0" />
<span class="text-xs font-medium text-primary truncate" title={conversationTitle}>
{conversationTitle}
</span>
</div>
<div class="flex-1 min-h-0">
<AIChat />
</div>
</Pane>
<Pane size={50} minSize={30} class="flex flex-col min-h-0">
<FlowWrapper
disableAi
pathStoreInit="u/admin/sandbox_flow"
{customUi}
selectedId={undefined}
newFlow
{flowStore}
{flowStateStore}
/>
<!-- The right pane is a contained, padded card to make it visually
clear that the flow view "belongs to" the chat on the left rather
than being its own root view. -->
<Pane size={50} minSize={30} class="flex flex-col min-h-0 p-2 pl-0">
<div
class="flex-1 min-h-0 rounded-xl border border-light bg-surface overflow-hidden shadow-sm"
>
<FlowWrapper
disableAi
pathStoreInit="u/admin/sandbox_flow"
{customUi}
selectedId={undefined}
newFlow
{flowStore}
{flowStateStore}
/>
</div>
</Pane>
</Splitpanes>
</div>
@@ -245,3 +272,17 @@
<ScheduleEditorInner bind:this={scheduleEditor} useDrawer={false} />
</div>
</Modal2>
<style>
/* Hide the visible splitpanes divider while keeping the hit area so the
panel is still resizable. Scoped to this page via the wrapper class. */
:global(.design-sandbox-splitpanes > .splitpanes__splitter) {
background-color: transparent !important;
background-image: none !important;
border: none !important;
}
:global(.design-sandbox-splitpanes > .splitpanes__splitter::before),
:global(.design-sandbox-splitpanes > .splitpanes__splitter::after) {
display: none !important;
}
</style>