mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 08:04:25 +00:00
perf: drop the deployed schema from a card that stopped showing a form
This commit is contained in:
@@ -190,6 +190,11 @@ const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3
|
||||
// (panel teardown, save-and-clear) pass their own reason, so the queued-message
|
||||
// flush can tell "the user wants to move on" from "the turn was torn down".
|
||||
const USER_CANCEL_REASON = 'user_cancelled'
|
||||
// Applied wherever a run form stops rendering. Only the form reads the deployed schema,
|
||||
// so past that point it is a copy of the script's declarations — password and file
|
||||
// defaults with them — persisted for the life of the chat.
|
||||
const settledRunForm = (runForm: RunFormDisplay): RunFormDisplay =>
|
||||
runForm.submitted || runForm.canceled ? { ...runForm, schema: undefined } : runForm
|
||||
// Built-in `/compact` session command — summarizes the conversation locally
|
||||
// instead of sending a turn to the model. Matched on the whole input so a
|
||||
// regular message that merely mentions "/compact" mid-sentence is unaffected.
|
||||
@@ -1840,7 +1845,7 @@ export class AIChatManager {
|
||||
isLoading: false,
|
||||
error: 'Cancelled by user',
|
||||
content: `Run of "${message.runForm.path}" cancelled by user`,
|
||||
runForm: { ...message.runForm, canceled: true }
|
||||
runForm: settledRunForm({ ...message.runForm, canceled: true })
|
||||
}
|
||||
: message
|
||||
)
|
||||
@@ -1854,7 +1859,7 @@ export class AIChatManager {
|
||||
#patchRunForm = (toolId: string, patch: Partial<RunFormDisplay>) => {
|
||||
this.displayMessages = this.displayMessages.map((message) =>
|
||||
message.role === 'tool' && message.tool_call_id === toolId && message.runForm
|
||||
? { ...message, runForm: { ...message.runForm, ...patch } }
|
||||
? { ...message, runForm: settledRunForm({ ...message.runForm, ...patch }) }
|
||||
: message
|
||||
)
|
||||
}
|
||||
@@ -4643,7 +4648,7 @@ export class AIChatManager {
|
||||
? { ...message.userQuestion, canceled: true }
|
||||
: undefined,
|
||||
runForm: message.runForm
|
||||
? { ...message.runForm, canceled: runState === 'idle' }
|
||||
? settledRunForm({ ...message.runForm, canceled: runState === 'idle' })
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +261,25 @@ describe('AIChatManager run form', () => {
|
||||
expect(manager.displayMessages[0].isLoading).toBe(true)
|
||||
})
|
||||
|
||||
// Only the rendered form reads the schema, and a settled card renders none. Kept, it
|
||||
// would sit in history for the life of the chat with the script's own password and
|
||||
// file defaults inside it.
|
||||
it('drops the schema from a card that has stopped showing a form', () => {
|
||||
const manager = new AIChatManager()
|
||||
const runForm = { path: 'f/a/b', schema: { properties: { tok: { password: true } } }, args: {} }
|
||||
manager.displayMessages = [
|
||||
{ role: 'tool', tool_call_id: 'call_r', content: '', isLoading: true, runForm }
|
||||
]
|
||||
|
||||
manager.handleRunFormCancel('call_r')
|
||||
|
||||
expect(manager.displayMessages[0].runForm?.schema).toBeUndefined()
|
||||
expect(manager.displayMessages[0].runForm?.canceled).toBe(true)
|
||||
})
|
||||
|
||||
// The tool reads the deployed schema before it asks for arguments. A stop during that
|
||||
// read drains the callbacks and settles the card, so a waiter installed afterwards was
|
||||
// one no rendered form could resolve: the turn stayed loading until a second stop.
|
||||
it('installs no run-form waiter once the turn is stopped', async () => {
|
||||
const manager = new AIChatManager()
|
||||
// The turn the tool is running under; cancel aborts it.
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
const workspace = $derived(aiChatManager.operatingWorkspace)
|
||||
|
||||
// Deep copies, not spreads: runForm comes off displayMessages ($state), so its nested
|
||||
// values are proxies that $state() hands back untouched. SchemaForm edits objects and
|
||||
// arrays in place, so a shallow copy would write every keystroke — a password typed
|
||||
// into a nested field included — straight into the persisted transcript. The schema
|
||||
// goes the same way: SchemaForm binds it and reorders its properties on mount.
|
||||
// values are proxies that $state() hands back untouched. SchemaForm edits both in
|
||||
// place — and binds the schema, reordering its properties on mount — so a shallow
|
||||
// copy would write every keystroke, a password typed into a nested field included,
|
||||
// straight into the persisted transcript.
|
||||
let args = $state($state.snapshot(runForm.args ?? {}) as Record<string, any>)
|
||||
let schema = $state($state.snapshot(runForm.schema) as Record<string, any>)
|
||||
let schema = $state($state.snapshot(runForm.schema ?? {}) as Record<string, any>)
|
||||
|
||||
const properties = $derived(schema?.properties ?? {})
|
||||
const hasArgs = $derived(Object.keys(properties).length > 0)
|
||||
|
||||
@@ -558,8 +558,11 @@ export function answeredChoices(q: UserQuestionDisplay): string[] | undefined {
|
||||
export type RunFormDisplay = {
|
||||
path: string
|
||||
summary?: string
|
||||
/** Of the DEPLOYED script, not a draft. */
|
||||
schema: Record<string, any>
|
||||
/** Of the DEPLOYED script, not a draft. Only the rendered form reads it, so it is
|
||||
* dropped once one of the flags below unmounts that form: kept, every settled card
|
||||
* would carry a copy of the schema — password and file defaults included — in
|
||||
* history forever. */
|
||||
schema?: Record<string, any>
|
||||
/** Prefill only: the card's `parameters` records what the job started with. */
|
||||
args: Record<string, any>
|
||||
/** Proposed arguments the schema does not declare, so they have no field. Named on
|
||||
|
||||
Reference in New Issue
Block a user