mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 08:02:38 +00:00
* feat: render the flow chat through the shared session chat components Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: settle the flow chat queue on the turn's outcome and guard IME enter Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: keep copilot free-tier banners and deploy-locked sends out of the flow chat Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
153 lines
6.5 KiB
TypeScript
153 lines
6.5 KiB
TypeScript
import { getContext, setContext } from 'svelte'
|
|
import type { AIMode, AIAutonomyMode } from './AIChatManager.svelte'
|
|
import { getAiChatManager } from './aiChatManagerContext'
|
|
import type { DisplayMessage, Tool } from './shared'
|
|
import type { ContextElement } from './context'
|
|
import type { AttachedImage } from './imageUtils'
|
|
import type { AttachedTextFile } from './textFileUtils'
|
|
import type { PasteAttachment } from './pasteTokens'
|
|
import type { AttachedFilesStore } from './files/attachedFiles.svelte'
|
|
import type { SessionArtifactsStore } from './artifacts/artifactsState.svelte'
|
|
import type { ArtifactVersionTarget } from '$lib/components/sessions/previewRouter'
|
|
import type { FlowAIChatHelpers } from './flow/core'
|
|
import type { AppAIChatHelpers } from './app/core'
|
|
import type AIChatInput from './AIChatInput.svelte'
|
|
|
|
export type ChatSendRequestOptions = {
|
|
instructions?: string
|
|
pastes?: PasteAttachment[]
|
|
images?: AttachedImage[]
|
|
files?: AttachedTextFile[]
|
|
/** Selected-context snapshot for this turn, in place of the live selection. Set
|
|
* whenever a send settles its context ahead of the turn. A host with no context
|
|
* of its own ignores it. */
|
|
contextOverride?: ContextElement[]
|
|
/** Where `contextOverride` came from. 'pinned': chips picked for THIS message, so
|
|
* they are consumed from the live selection on send. 'replay': an edit or retry
|
|
* resending an older message's context, already consumed long ago. */
|
|
contextOverrideOrigin?: 'pinned' | 'replay'
|
|
}
|
|
|
|
/**
|
|
* What the chat view components (AIChatDisplay and everything it renders) need
|
|
* from whatever is driving the conversation. AIChatManager implements it for the
|
|
* copilot's own LLM loop; FlowChatViewHost implements it over a flow run's
|
|
* conversation so both chats render through the same components.
|
|
*
|
|
* Each affordance is gated by the field that answers for it — attachments by
|
|
* `supportsMessageAttachments`, the model button by `supportsModelSettings`, and so on —
|
|
* so a host turns on exactly what it can serve, and a new one is a matter of answering
|
|
* these fields rather than of being a copilot. `mode` is the exception, still read
|
|
* directly for chrome that only the copilot has.
|
|
*/
|
|
export interface ChatViewHost {
|
|
// Transcript
|
|
displayMessages: DisplayMessage[]
|
|
/** API-level messages. Only the count is read (context usage visibility). */
|
|
messages: readonly unknown[]
|
|
contextTokens: number
|
|
/** The workspace a message's paths and jobs resolve against, which a fork session
|
|
* pins away from the navigated one. */
|
|
readonly operatingWorkspace: string | undefined
|
|
loading: boolean
|
|
/** A turn this tab can neither follow nor stop, held by another tab on the same chat. */
|
|
readonly runHeldElsewhere: boolean
|
|
loadingLabel: string | undefined
|
|
compacting: boolean
|
|
currentReply: string
|
|
currentReasoning: string
|
|
currentReasoningActive: boolean
|
|
readonly reasoningHiddenIndicatorLabel: string | undefined
|
|
readonly automaticScroll: boolean
|
|
enableAutomaticScroll: () => void
|
|
disableAutomaticScroll: () => void
|
|
|
|
// Composer
|
|
instructions: string
|
|
readonly sendInFlight: boolean
|
|
/** Resolves to whether the draft was consumed as a turn. */
|
|
sendRequest: (options?: ChatSendRequestOptions) => Promise<boolean | undefined>
|
|
cancel: (reason?: string) => void
|
|
setAiChatInput: (aiChatInput: AIChatInput | null) => void
|
|
readonly queuedMessage: string
|
|
queuedContext: ContextElement[] | undefined
|
|
readonly queuedImages: AttachedImage[]
|
|
readonly queuedFiles: AttachedTextFile[]
|
|
queueMessage: (
|
|
text: string,
|
|
images?: AttachedImage[],
|
|
context?: ContextElement[],
|
|
files?: AttachedTextFile[]
|
|
) => void
|
|
dequeueMessage: () => void
|
|
setComposerStaged: (key: string, editingIndex: number | null, bytes: number) => void
|
|
clearComposerStaged: (key: string) => void
|
|
attachmentBytesExcluding: (selfKey: string) => number
|
|
|
|
// Per-message actions
|
|
storedImages: (displayMessageIndex: number) => AttachedImage[] | undefined
|
|
retryRequest: (messageIndex: number) => void
|
|
restartGeneration: (
|
|
displayMessageIndex: number,
|
|
newContent?: string,
|
|
pastes?: PasteAttachment[],
|
|
images?: AttachedImage[],
|
|
editedContext?: ContextElement[],
|
|
files?: AttachedTextFile[]
|
|
) => void | Promise<void>
|
|
handleUserQuestionAnswer: (toolId: string, choices: string[]) => boolean
|
|
handleToolConfirmation: (toolId: string, confirmed: boolean) => void
|
|
/** A tool is waiting on a run form the user is filling in. Escape belongs to that form
|
|
* then, not to the turn — see AIChatDisplay's window handler. */
|
|
readonly hasPendingRunForm: boolean
|
|
isRunFormPending: (toolCallId: string) => boolean
|
|
|
|
// Copilot-only surfaces. Left undefined/false by hosts that have no LLM loop
|
|
// of their own; the chrome they drive hides itself.
|
|
mode?: AIMode
|
|
isSessionChat: boolean
|
|
/** Model + reasoning picker. Off where the model is configured elsewhere. */
|
|
supportsModelSettings: boolean
|
|
/** Click a user message to edit and resend it. Needs a host that can rewind
|
|
* its own transcript, which a host replaying a server-side run cannot. */
|
|
supportsMessageEditing: boolean
|
|
/** The `+` menu's file entry and drag-and-drop onto the panel. */
|
|
supportsMessageAttachments: boolean
|
|
/** The turn needs text: attachments alone cannot be sent. True where the consumer
|
|
* requires a message of its own — an AI agent step refuses a run with neither a
|
|
* `user_message` nor manual memory. */
|
|
requiresMessageText: boolean
|
|
/** The `+` menu's folder entries, backed by `attachedFiles`. A linked folder is a
|
|
* live handle on the user's disk, so only a host reading files in the browser has one. */
|
|
supportsLinkedFolders: boolean
|
|
/** `accept` for the file picker. */
|
|
attachmentAccept: string
|
|
tools: Tool<any>[]
|
|
autonomyMode: AIAutonomyMode
|
|
setAutonomyMode: (mode: AIAutonomyMode) => void
|
|
readonly autoAcceptEditsActive: boolean
|
|
readonly autoAcceptEditsAvailable: boolean
|
|
readonly autoAcceptToolConfirmationsAvailable: boolean
|
|
readonly planModeAvailable: boolean
|
|
attachedFiles: AttachedFilesStore
|
|
artifacts: SessionArtifactsStore
|
|
openArtifact?: (artifactId: string, name: string, version?: ArtifactVersionTarget) => void
|
|
flowAiChatHelpers?: FlowAIChatHelpers
|
|
appAiChatHelpers?: AppAIChatHelpers
|
|
}
|
|
|
|
const CHAT_VIEW_HOST_CONTEXT_KEY = 'chatViewHost'
|
|
|
|
export function setChatViewHost(host: ChatViewHost) {
|
|
setContext(CHAT_VIEW_HOST_CONTEXT_KEY, host)
|
|
}
|
|
|
|
/**
|
|
* Resolve the host driving the chat in this subtree. Falls back to the
|
|
* AIChatManager (scoped instance or app-wide singleton) so every existing
|
|
* copilot chat keeps working without setting anything.
|
|
*/
|
|
export function getChatViewHost(): ChatViewHost {
|
|
return getContext<ChatViewHost>(CHAT_VIEW_HOST_CONTEXT_KEY) ?? getAiChatManager()
|
|
}
|