mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix(chat): list a flow's chats under the path its runs record
The editor's chat asked for conversations under the path the flow was opened at, while a preview run recorded them under the path being edited. On a flow that has never been deployed those differ from the first keystroke, so every test chat was written somewhere the sidebar never looked and vanished as soon as the list refreshed. `path` is now the path a run records, and what makes the panel a different panel moves to `identity`. They are not the same question: a chat holds conversations, a running turn and the rows it is writing, so it is replaced rather than re-pointed — but replacing it on every keystroke in the path field would throw away the turn being typed alongside. `chatFlowKey` is the flow half of that identity and `chatIdentity` the whole of it, so the panel key and anything filed under the flow cannot drift apart. A conversation keeps the path it was created under, so renaming a flow that already has chats leaves those behind under the old path. That is the trade: the sidebar follows what runs record, and what ran before a rename recorded something else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
404419588c
commit
769e7eebbe
@@ -481,7 +481,8 @@
|
||||
}}
|
||||
conversationKind="test"
|
||||
frame="boxed"
|
||||
path={$initialPathStore || fakeInitialPath}
|
||||
path={$pathStore}
|
||||
identity={$initialPathStore || fakeInitialPath}
|
||||
inputSchema={flowStore.val.schema}
|
||||
flowModules={flowStore.val.value?.modules}
|
||||
/>
|
||||
|
||||
@@ -814,7 +814,8 @@
|
||||
<FlowChat
|
||||
onRunFlow={runFlowWithMessage}
|
||||
conversationKind="test"
|
||||
path={$initialPathStore || fakeInitialPath}
|
||||
path={$pathStore}
|
||||
identity={$initialPathStore || fakeInitialPath}
|
||||
useStreaming={shouldUseStreaming}
|
||||
inputSchema={flowStore.val.schema}
|
||||
flowModules={flowStore.val.value?.modules}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import type { FlowChatProps } from './flowChatProps'
|
||||
import { chatIdentity, type FlowChatProps } from './flowChatProps'
|
||||
import FlowChatPanel from './FlowChatPanel.svelte'
|
||||
|
||||
let props: FlowChatProps = $props()
|
||||
@@ -19,7 +19,7 @@
|
||||
* editor acts on a session's fork while `workspaceStore` stays where the reader left it.
|
||||
*/
|
||||
const chatKey = $derived(
|
||||
`${flowEditorContext?.opWorkspace?.() ?? $workspaceStore ?? ''}:${props.path}`
|
||||
chatIdentity(flowEditorContext?.opWorkspace?.() ?? $workspaceStore, props)
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { useWorkspaceStorageConfigured } from '$lib/components/inputTransformEnv.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import FlowChatModelSettings from './FlowChatModelSettings.svelte'
|
||||
import { chatFlowKey } from './flowChatProps'
|
||||
import {
|
||||
agentModelGap,
|
||||
agentModelWiringInputs,
|
||||
@@ -33,6 +34,8 @@
|
||||
/** The flow's modules, used to find which inputs an AI agent step reads directly. */
|
||||
flowModules?: FlowModule[]
|
||||
path: string
|
||||
/** What makes this a different chat, when that is not the path — see `FlowChatProps`. */
|
||||
identity?: string
|
||||
/** The flow's description, shown under the empty transcript's prompt. */
|
||||
description?: string
|
||||
wideLayout?: boolean
|
||||
@@ -44,6 +47,7 @@
|
||||
additionalInputsSchema,
|
||||
flowModules,
|
||||
path,
|
||||
identity = undefined,
|
||||
description = undefined,
|
||||
wideLayout = false
|
||||
}: Props = $props()
|
||||
@@ -113,8 +117,14 @@
|
||||
// value, an author's default — is made safe before it reaches the provider.
|
||||
const runInputs = $derived(withoutRejectedEffort(modelWiring, effectiveInputs))
|
||||
|
||||
// Filed under the flow alone, which is what these settings have always been keyed on —
|
||||
// so the same path in two workspaces shares them, and a model or resource stored by one
|
||||
// reaches the other. Scoping the key to the workspace is a change of its own: it orphans
|
||||
// every entry readers already have, and the fix belongs with whatever migrates them.
|
||||
// `wmill dev` is the one surface whose key moves: it names no flow, so every flow it
|
||||
// opened shared a single bucket, and the path it does have is the better key.
|
||||
function getStorageKey(): string {
|
||||
return `${STORAGE_KEY_PREFIX}${path}`
|
||||
return `${STORAGE_KEY_PREFIX}${chatFlowKey({ path, identity })}`
|
||||
}
|
||||
|
||||
function loadInputsFromStorage(): Record<string, any> | null {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<!--
|
||||
One chat, for one flow in one workspace. Mounted only by `FlowChat`, which keys it on
|
||||
that pair: the manager created here holds the conversations, the turn running in one of
|
||||
them and the rows that turn is writing, so pointing an existing panel at another flow
|
||||
would leave an upload, a launch or a poll landing in the chat that replaced it.
|
||||
`chatIdentity` — the workspace and, where the path is still being typed, something that
|
||||
holds still for the flow instead. The manager created here holds the conversations, the
|
||||
turn running in one of them and the rows that turn is writing, so pointing an existing
|
||||
panel at another flow would leave an upload, a launch or a poll landing in the chat that
|
||||
replaced it.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
@@ -24,6 +26,7 @@
|
||||
deploymentInProgress = false,
|
||||
useStreaming = false,
|
||||
path,
|
||||
identity = undefined,
|
||||
description = undefined,
|
||||
inputSchema = undefined,
|
||||
flowModules = undefined,
|
||||
@@ -35,8 +38,8 @@
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
// The flow and the workspace this panel was built for. `FlowChat` keys on the same pair,
|
||||
// so neither can change under it: what they change is which panel exists.
|
||||
// The workspace this panel was built for. `FlowChat` keys on it, so it cannot change
|
||||
// under the panel: what it changes is which panel exists.
|
||||
const workspace = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
const manager = createFlowChatManager()
|
||||
@@ -51,7 +54,13 @@
|
||||
manager.canFilterConversationKind = conversationKind !== 'deployed'
|
||||
|
||||
// The manager's inputs, kept current rather than set once: `useStreaming` follows the
|
||||
// flow's last step, which an edit can change while a turn is running.
|
||||
// flow's last step, which an edit can change while a turn is running, and `path` follows
|
||||
// the path field as its author types. A path that moves takes the next list request and
|
||||
// the next run with it, so what a turn records is what the sidebar asks for — which is
|
||||
// the point, since listing under a path runs do not record is how a chat loses the
|
||||
// conversations it is creating. A conversation keeps the path it was created under
|
||||
// (`get_or_create_conversation_with_id` leaves an existing row alone), so renaming a
|
||||
// flow that already has chats leaves those behind under the old path.
|
||||
$effect(() => {
|
||||
manager.initialize(onRunFlow, path, useStreaming)
|
||||
})
|
||||
@@ -109,6 +118,7 @@
|
||||
{additionalInputsSchema}
|
||||
{flowModules}
|
||||
{path}
|
||||
{identity}
|
||||
{description}
|
||||
{wideLayout}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,15 @@ export interface FlowChatProps {
|
||||
) => Promise<string | undefined>
|
||||
useStreaming?: boolean
|
||||
deploymentInProgress?: boolean
|
||||
/** The flow the chat runs and lists conversations for. Must be the path a run records,
|
||||
* or a conversation is stored under one path and looked for under another. */
|
||||
path: string
|
||||
/**
|
||||
* What makes this a different chat, when that is not the path. An unsaved flow's path
|
||||
* changes as its author types, and the chat is replaced whenever this changes — so the
|
||||
* editor passes something that holds still for the flow it is editing.
|
||||
*/
|
||||
identity?: string
|
||||
/** The flow's own description, shown where the chat has room for it: the empty
|
||||
* transcript, and the sidebar once a conversation has replaced it. */
|
||||
description?: string
|
||||
@@ -34,3 +42,28 @@ export interface FlowChatProps {
|
||||
* owns the surface — the editor's panel shows it on the graph. */
|
||||
parallelTurns?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Which flow a chat is for, as something that holds still.
|
||||
*
|
||||
* `identity` where a surface has one, because `path` follows the path field as its author
|
||||
* types and anything filed under the flow has to survive that — the panel is deliberately
|
||||
* not remounted for a path change. A surface with no identity to give has no such field
|
||||
* either, so its path is already the stable answer.
|
||||
*/
|
||||
export function chatFlowKey(props: Pick<FlowChatProps, 'path' | 'identity'>): string {
|
||||
// `||`, not `??`: the editor builds `identity` from values that are both empty on a
|
||||
// surface with no flow of its own to name, and an empty identity is no identity.
|
||||
return props.identity || props.path
|
||||
}
|
||||
|
||||
/**
|
||||
* What makes one chat a different chat: the flow it is for, in the workspace it runs in.
|
||||
* `FlowChat` keys the panel on this, so nothing inside a mounted panel can see it change.
|
||||
*/
|
||||
export function chatIdentity(
|
||||
workspace: string | undefined,
|
||||
props: Pick<FlowChatProps, 'path' | 'identity'>
|
||||
): string {
|
||||
return `${workspace ?? ''}:${chatFlowKey(props)}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user