Files
windmill/frontend/src/lib/common.ts
T
Guilhem LemouelandClaude Opus 5 0e20a5d751 feat(ai-chat): render flow chat mode through the AI session chat components
The flow chat ran on its own components; this points it at the ones the AI
session chat already uses, so the two surfaces share a transcript, a composer
and a sidebar instead of keeping two of each.

The seam is `ChatViewHost` (copilot/chat/chatViewHost.ts): the view components
read the host rather than `AIChatManager` directly, and `getChatViewHost()`
falls back to the session manager, so the copilot's call sites are unchanged.
`FlowChatViewHost` is the second adapter, over `FlowChatManager`.

What the flow chat gains from the move:

- turns running in several conversations at once, with a status, a queue and a
  Stop per chat, and an unread count on the rail
- attachments, uploaded to the workspace's object storage for the worker to read
- a composer that speaks for the agent steps a message is fed to: the model, the
  thinking effort, and the flow inputs the agent reads straight out of
  `flow_input`; everything else is asked for in a Configure-inputs modal
- tool cards with the call and the result, the model's reasoning, and a step name
  per answer once a conversation holds more than one agent
- Retry, which replays the failed turn's own run arguments read back from its job
- named and renamable conversations, and test chats kept out of the deployed
  flow's list

Backend: conversation rows carry an MCP tool's call and result and the model's
reasoning, which live nowhere else; every row gets a job so retention can empty
it; and the providers parse a non-streaming answer's reasoning.

Stacked on #11134, which keeps the windmill-chat SDK for external frontends and
raw apps while the in-app flow chat runs on these components.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 11:46:45 +02:00

189 lines
4.6 KiB
TypeScript

import type { Script, ScriptLang } from './gen'
export type OwnerKind = 'group' | 'user' | 'folder'
export type ActionKind = 'Create' | 'Update' | 'Delete' | 'Execute'
export type SupportedLanguage = Script['language']
export interface PropertyDisplayInfo {
property: SchemaProperty
name: string
isRequired: boolean
path: string[]
index: number
propertiesNumber: number
}
export type EnumType = string[] | { value: string; label: string }[] | undefined
export interface SchemaProperty {
type: string | undefined
description?: string
pattern?: string
default?: any
enum?: EnumType
contentEncoding?: 'base64' | 'binary'
format?: string
items?: {
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
contentEncoding?: 'base64'
enum?: string[]
resourceType?: string
properties?: { [name: string]: SchemaProperty }
required?: string[]
}
min?: number
max?: number
/** Height a string field's text area opens at, in rows. */
minRows?: number
currency?: string
currencyLocale?: string
multiselect?: boolean
customErrorMessage?: string
properties?: { [name: string]: SchemaProperty }
required?: string[]
showExpr?: string
hideWhenChatEnabled?: boolean
/** Why the oneOf variant is chat mode's to pick. Set = selector disabled, reason shown. */
lockOneOfWhenChatEnabled?: string
password?: boolean
order?: string[]
nullable?: boolean
dateFormat?: string
title?: string
placeholder?: string
oneOf?: SchemaProperty[]
originalType?: string
disabled?: boolean
'x-no-s3-storage-workspace-warning'?: string
'x-auto-generate'?: boolean
}
export interface ModalSchemaProperty {
selectedType?: string
description: string
name: string
required: boolean
min?: number
max?: number
currency?: string
currencyLocale?: string
multiselect?: boolean
format?: string
pattern?: string
enum_?: EnumType
default?: any
items?: { type?: 'string' | 'number'; enum?: string[] }
contentEncoding?: 'base64' | 'binary'
schema?: Schema
customErrorMessage?: string
showExpr?: string
password?: boolean
nullable?: boolean
dateFormat?: string
title?: string
placeholder?: string
oneOf?: SchemaProperty[]
}
export function modalToSchema(schema: ModalSchemaProperty): SchemaProperty {
return {
type: schema.selectedType,
description: schema.description,
pattern: schema.pattern,
default: schema.default,
enum: schema.enum_,
items: schema.items,
contentEncoding: schema.contentEncoding,
format: schema.format,
customErrorMessage: schema.customErrorMessage,
properties: schema.schema?.properties,
required: schema.schema?.required,
min: schema.min,
max: schema.max,
currency: schema.currency,
currencyLocale: schema.currencyLocale,
multiselect: schema.multiselect,
showExpr: schema.showExpr,
password: schema.password,
nullable: schema.nullable,
dateFormat: schema.dateFormat,
title: schema.title,
placeholder: schema.placeholder,
oneOf: schema.oneOf
}
}
export type Schema = {
$schema: string | undefined
type: string
'x-windmill-dyn-select-code'?: string
'x-windmill-dyn-select-lang'?: ScriptLang
properties: { [name: string]: SchemaProperty }
order?: string[]
required: string[]
}
export function mergeSchema(
schema: Schema | Record<string, any>,
enum_payload: Record<string, any> = {}
) {
if (!schema.properties || !enum_payload) {
return schema
}
let new_schema: Schema = JSON.parse(JSON.stringify(schema))
for (let [key, value] of Object.entries(new_schema.properties ?? {})) {
if (enum_payload[key]) {
value.enum = enum_payload[key]
value['disableCreate'] = true
}
}
return new_schema
}
export type Meta = { ownerKind: OwnerKind; owner: string; name: string }
type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
? Acc[number]
: Enumerate<N, [...Acc, Acc['length']]>
/** An inclusive range of integer numbers */
export type IntRange<F extends number, T extends number> =
| F
| Exclude<Enumerate<T>, Enumerate<F>>
| T
export function pathToMeta(path: string, hideUser: boolean): Meta {
const splitted = path.split('/')
let ownerKind: OwnerKind
if (splitted[0] == 'g') {
ownerKind = 'group'
} else if (splitted[0] == 'f') {
ownerKind = 'folder'
} else if (splitted[0] == 'u') {
ownerKind = 'user'
} else {
console.error('Not recognized owner:' + splitted[0])
return {
ownerKind: hideUser ? 'folder' : 'user',
owner: '',
name: ''
}
}
return {
ownerKind,
owner: splitted[1],
name: splitted.slice(2).join('/')
}
}
export function prettyLanguage(lang: string) {
switch (lang) {
case 'nativets':
return 'Native TypeScript'
default:
return lang.charAt(0).toUpperCase() + lang.slice(1)
}
}