Files
windmill/chat-sdk/src/react.ts
T
GuilhemandClaude Opus 5 f367eaf6d0 feat: run turns in several flow chat conversations at once (#11202)
* feat: run turns in several flow chat conversations at once

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep finished turns finished and cached chats current in the flow chat pool

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: attribute a turn's rows by job id as well as sequence

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: count only real stream updates and retry the job-id read

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep a chat that holds an unsent draft, and take one back when its first turn is withdrawn

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: ignore a stale running-turn snapshot, keep a withdrawn chat's draft, poll after clean stream ends

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: follow the turn running now when the listing named one already over

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep replacement turns and SSE fallback moving

* fix: keep replacement turn handoffs active

* fix: preserve unread badge line height

* fix: settle local fallback handoffs

* fix: settle refused turn handoffs

* fix: scope turn handoffs to conversation

* fix: drop stale turn handoffs

* refactor: move the queued message and 409 handling into per-conversation turns

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address cubic's review of the parallel flow chat turns

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: clear a stale failure on refresh, and tighten the docs and test waits

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: recover running rows past the first page, and drop the failure a re-read disproves

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: check the running-turn query at compile time, and narrow what a refresh clears

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: settle a failed turn only from an answer that turn wrote

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: settle a failed turn from its own answer, and only while it is still the failure shown

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: drop a failure whose answer arrived even when a newer turn owns the error

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: free an answered failure whatever the turn that started meanwhile is doing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: drop a rows read that a turn outran, rather than merging it under newer messages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: drop a rows read whose conversation was left and opened again

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: hand over a file still being read when its composer goes

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: count a drop's routing as work in flight, so its file is handed over too

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: hold the send until every file a conversation is owed has landed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: keep a panel mounted per conversation instead of handing its draft over

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the withdrawn chat whose composer was written in, not the empty one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the chat in front of the reader when both withdrawn composers were written in

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep a retry's own run arguments when a turn elsewhere refuses it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: name panels apart across pools, and read a flow's inputs when its chat is built

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 11:36:30 +02:00

79 lines
3.2 KiB
TypeScript

import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
import { createChat } from './chat'
import type { Chat, ChatOptions, ChatState } from './types'
export type UseWindmillChat = ChatState &
Pick<
Chat,
| 'sendMessage'
| 'resumeTurn'
| 'stop'
| 'newConversation'
| 'selectConversation'
| 'loadConversations'
| 'deleteConversation'
| 'renameConversation'
| 'loadOlderMessages'
| 'refreshMessages'
> & { chat: Chat }
/**
* A chat on a chat-mode flow. The chat is created once per `flowPath`, `baseUrl`,
* `workspace`, `history`, `storageKey`, credential and presence of `run`, and
* destroyed on unmount. A credential change is a new user, whose chat must not
* carry the previous one's state: a different token string, or a switch between no
* token, a token string and a token function, all recreate it. A token function is
* read through a ref on every call, so a new closure per render changes what the
* next call runs and nothing else; pass a `storageKey` per user when local history
* must not be shared. `run`, the callbacks and `inputs` are read the same way: the
* latest render's values go with the next message.
*/
export function useWindmillChat(options: ChatOptions): UseWindmillChat {
const latest = useRef(options)
latest.current = options
const credential =
typeof options.token === 'function' ? 'fn' : typeof options.token === 'string' ? `str:${options.token}` : 'none'
// A custom runner replaces the deployed flow call, so its presence is part of what the chat is.
const customRun = options.run !== undefined
const chat = useMemo(
() =>
createChat({
...options,
// Sent per message from the latest render instead, so a removed key stays removed.
inputs: undefined,
token:
typeof options.token === 'function'
? () => {
const token = latest.current.token
return typeof token === 'function' ? token() : (token ?? '')
}
: options.token,
run: customRun ? (args, turn) => (latest.current.run ?? options.run!)(args, turn) : undefined,
onFinish: (turn) => latest.current.onFinish?.(turn),
onError: (error, turn) => latest.current.onError?.(error, turn)
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[options.flowPath, options.baseUrl, options.workspace, options.history, options.storageKey, credential, customRun]
)
useEffect(() => () => chat.destroy(), [chat])
const state = useSyncExternalStore(chat.subscribe, chat.getState, chat.getState)
return useMemo(
() => ({
...state,
chat,
sendMessage: (text, options) =>
chat.sendMessage(text, { ...options, inputs: { ...latest.current.inputs, ...options?.inputs } }),
resumeTurn: chat.resumeTurn,
stop: chat.stop,
newConversation: chat.newConversation,
selectConversation: chat.selectConversation,
loadConversations: chat.loadConversations,
deleteConversation: chat.deleteConversation,
renameConversation: chat.renameConversation,
loadOlderMessages: chat.loadOlderMessages,
refreshMessages: chat.refreshMessages
}),
[state, chat]
)
}