The previous commit kept the effort when switching to a provider the registry has
no rules for, reasoning that discarding a real setting on the strength of never
having heard of the provider was wrong. With the thinking row now hidden for those
providers, keeping it is worse: `resolveEffectiveReasoning` sends an explicitly set
effort whatever the model, so the level rides out on every turn while nothing on
screen shows it and nothing can clear it — and a provider that rejects the field
fails every turn with no visible cause.
Dropping matches what the session chat did before this PR. The flow chat is
unaffected: its own guard returns before `carriedReasoning` is reached, and its
effort input stays askable in the Configure-inputs modal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
The session chat renders the same model button, so it carried the same flaw the
flow chat just lost: on `customai` / `groq` / `togetherai` it drew "Not supported
by this model" and `selectModel` dropped the pinned effort, with no slider left to
put one back. Both now read `known`.
`carriedReasoning` is where that belongs — asked what survives a model change, the
answer for a model we have no rules for is "what you had". The flow chat keeps its
own guard on top, since writing the value back would still touch storage.
`turnFailed` moves to turnTranscript.ts, whose subject is exactly this — how a
turn reads from its rows — and which can be imported by a test without dragging in
Monaco. Five cases pin it: recovered tool, terminal failure, still streaming, the
window's end, and a turn with nothing in it yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
- `turnFailed` marked a turn failed on any unsuccessful tool row, but a failing
tool is handed back to the agent, which routinely recovers and answers. That
put a Retry on turns that succeeded — and retrying re-runs the whole flow,
side effects included. It now reads the turn's terminal row, and reports
nothing while the turn is still streaming. `error` on a user row means what it
means in the copilot: the request never produced an answer.
- The reasoning registry answered `supported: false` for every model on a
provider family it has no rules for, so `customai` — which fronts any
OpenAI-compatible endpoint — got "Not supported by this model" stated as a
fact, its effort input hidden from the modal, and its stored value cleared to
`''` on every model pick, shadowing the author's schema default for good.
`ReasoningCapability` now carries `known`, and the composer neither claims
anything nor touches the value where it cannot read the model.
- `flow-base.md`: a static provider field is not "shown read-only"; the composer
draws no control for it at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
- Picking a model that cannot think left the old `reasoning_effort` in the flow
input and the run sent it anyway. The composer now writes the effort alongside
the model — carried where the new model has that level, cleared where it does
not — through `carriedReasoning`, the rule the session chat already applied
inline and now shares.
- `reasoning_effort` no longer falls back to the Configure-inputs modal on a
model that cannot reason. The popover always owns it once a model is chosen:
the slider, or the row saying the model cannot think. On such a model the
value is not a choice but a fact, so offering it in the modal only invited
setting a level the provider rejects.
- `memory_id` names the conversation a chat turn belongs to and is a query
parameter, so no caller could supply it through `args` — and the error said
only that it was required. It now says where it goes, `test_run_flow` supplies
one for a chat-enabled flow, and its new `conversation_id` lets a caller
continue a conversation instead of always starting a fresh one, which is the
only way to test that an agent's memory works.
- `flow-base.md` gains a Chat-Mode Flows section: wiring `provider` fields to
flow inputs is what puts the model and thinking controls in the composer, and
a fully static provider gives a chat that cannot change its model.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
Addresses the CI reviewers on f41174e, plus what the local rounds found on top.
- An image is re-encoded to PNG/JPEG on the way in but kept the picked filename,
and the worker reads an attachment's media type from the object key alone
(`mime_guess::from_path`), never from the stored content type — so a `.webp`
reached the provider as PNG bytes labelled webp, which Anthropic rejects.
`storedAttachmentName` now makes the extension match the encoding; the same
rule covers a PDF picked without one, whose key would guess as `image/png`.
- The `is_test` backfill matched no existing conversation: no message row held
the flow job before this release, so it now walks to the run's root, ordered
as `get_root_job_id` is — `flow_innermost_root_job` stops at the closest flow
scope, which would miss an agent inside a subflow.
- A stored value for the attachments input no longer rides an attachment-free
message: the paperclip is that input's only writer wherever it is offered.
- The lowest thinking stop is now clickable from the `default` state, where the
range input's value already read 0 and so fired no event.
- Dropped `footerControls` and the `settleStreamingRows` export, neither of
which ever had a caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
Nine review rounds over the same rule: the composer may only offer what the
flow accepts, and an input it declines to edit must stay askable in the modal.
- The reasoning slider gains an off stop where a model disables by omission
(Claude 4.x), which had no off at all; `reasoningDisplay` gates it on
`canDisable` alone and reads an unset effort as already off there.
- `reasoning_effort` and `user_attachments` are promoted out of Configure
inputs only when the composer actually renders their control — a model that
cannot reason, a workspace with no object storage, or a target whose schema
cannot hold an s3 object all leave the field in the modal.
- Message chips run through `redactSecretArgs`/`redactFileArgs`, so a password
input no longer renders in cleartext under every user message.
- `FlowChat`'s init effect no longer tracks the open conversation: its teardown
is `cleanup()`, which was aborting the first turn of a fresh chat mid-send.
- `JobBackedStore` fetches six at a time instead of one request per row.
- `is_test` is required in the `FlowConversation` schema, matching the Rust
struct, so the deployed page's composer cannot lock on a missing field.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
Four more from the CI review, and one regression of my own.
An agent step carries a placeholder transform for every key of its schema, so
"all agents must agree on the attachments input" read those placeholders as
competing readers and any flow with two agents lost the composer's paperclip. An
agent is a reader only when its transform actually references a flow input;
anything else has no say. What still vetoes is the real ambiguity: two agents
reading different inputs, an expression naming two, or a name the schema does
not declare.
The rest of that rule, applied where it was still missing: an agent whose
provider expression cannot be read now voids the whole resolution rather than
letting the others speak for it; the gap warning is asked of each agent, so one
agent with an empty model still warns however well its neighbour is configured;
the attachment cap counts what is queued, since a queue merges into one turn on
flush; and the conversation a turn belongs to is read before its attachments
upload, with the sidebar holding still meanwhile.
`isWaitingForResponse` had no reset when a launch failed. That cost a stuck
composer before; with the sidebar now holding on the same flag it would have
cost a page reload, so a failed launch clears all three.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
Six findings from the CI review shared a shape: the composer read part of the
flow's configuration, and where the reading came up short it assumed the
permissive answer — offering a control, a file, or a send the run would not
honour.
The resolvers now say so instead of guessing. `resolveAgentModelWiring` agrees a
provider field only when every agent supplies it the same way, and anything else
becomes `undecided`: neither editable nor a value to show, and told apart from a
field nobody supplies — a distinction `agentModelGap` needs, since agents that
disagree about a model are not agents with no model. The resource picker asks
only for the kind a flow fixes, because `setFields` cannot write a kind the flow
does not expose. Attachments are offered wherever the workspace has storage the
upload endpoint can resolve, not only S3 — the shared helper that already made
this decision was generalised rather than copied. A flow input holding one file
caps the whole turn, enforced as files are attached and again before uploading,
so a queue built over several turns cannot smuggle a second one past it. And
switching which chats are listed no longer moves the conversation under a turn
that is being dispatched or is already running.
The other end of the same seam: `sendRequest` answers whether the draft became a
turn, and a refused one now puts the draft back — in the host, which is where the
copilot already does it, so a refusal restores exactly once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
`streaming_tool_calls_to_openai` takes the reasoning block by value, so reading
the text off it afterwards borrowed a moved value. Only the `bedrock` feature
compiles this file, which is why it passed every check but CI's full one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
The kind filter is gone from a deployed flow's chat: it has no test chats of its
own, and offering to list someone's editor scratch put that scratch in front of
the flow's users. The editor keeps it.
The composer no longer contradicts what a flow run accepts. An AI agent step
refuses a run with no `user_message`, so a host declares `requiresMessageText`
and the send button says why it is disabled instead of taking the draft and
dropping it; the queue flush reads the same rule before draining. What a message
was sent with is now keyed by the row it belongs to, so an earlier message keeps
its thumbnail once the next one goes out, and a queued blob has a chip like every
other attachment.
Flow inputs the composer already has a control for — the model button's provider
fields — stop repeating themselves as context chips under every message, and the
inputs the flow declares defaults for start on them rather than on nothing, which
is what the flow's own form would do.
`runHeldElsewhere`, `operatingWorkspace`, `hasPendingRunForm` and
`isRunFormPending` join the seam, so the shared display stops asking the app-wide
manager questions only the current host can answer. A name typed on a chat that
has not run yet is written once its first turn creates the row, over the title
the server derives from the message. The rename field is a TextInput beside the
row rather than a raw input inside its button.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
A chat's messages are deleted with the jobs behind them, and the user row now
carries its run, so retention can empty a conversation completely. When it does,
the `flow_conversation` row and the agent memory keyed on it are all that is
left, and nothing else collects them — `ai_agent_memory` has no job id for
retention to match on. Both delete paths (the retention sweep in
`windmill_common::jobs` and the `/jobs/delete` endpoint) now drop a conversation
that has no messages left, and its memory with it. The trigger is the emptiness,
so a partly-purged chat keeps what it still has.
Two statements rather than one CTE: a data-modifying CTE reads the snapshot from
before the message delete, so every conversation would still look non-empty.
Both halves are pinned by a test on each path.
Also from review: the rename endpoint bounds the title three short of the
column, since the helper appends an ellipsis; and `OpenAIResponsesSSEParser`
loses the reasoning field it never wrote — that stream has no reasoning-summary
event, so the value could only ever be empty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
Main rewrote parts of the chat view this branch had just moved behind
ChatViewHost, so the conflicts were the same lines under two names.
Resolved by keeping main's behaviour and reading it through the host:
- `runHeldElsewhere` and `operatingWorkspace` join the interface, so the
typing indicator, the past-chats buttons and a message's workspace mean
the same thing for a flow chat as for a session.
- The skills and MCP menus main added take an AIChatManager and render
only under GLOBAL, so AIChatDisplay resolves the manager for those two
alone.
- `AIChatModelSettings` keeps this branch's config-object form and gains
main's `promptSettings` prop, which now gates the Parameters entries and
the prompts modal.
- `composerBoxClass(disabled)` replaces the `COMPOSER_BOX` constant, so
main's disabled treatment applies to both composer branches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
Chats get a title from their first message and can be renamed in place;
Rename and Delete moved into an ellipsis menu on the row, matching the
sessions sidebar. The transcript sits in a rounded panel on both the
deployed flow page and the test drawer.
Two pieces come out of FlowChatManager. turnTranscript.ts holds the rules
for turning stream events into transcript rows, with unit tests — it owned
two ordering bugs (thinking arriving in the same chunk as the tool call it
led to, and a row settling before its last chunk). jobBackedStore.svelte.ts
holds the three rules the tool-call and message-input stores both need:
ask once, answer empty until it lands, cache the empty answer for a purged
job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QN7VboDEm9HAB1t4sMxMdE
* fix: disable a dynamic input when its schema field is disabled
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* refactor: extract the run form's argument hygiene into job_args
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: give Tabs an opt-in sliding selection indicator
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* refactor: share the chat's scroll-fade measurement
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: give the chat a run-form contract and incremental job output
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: run and test a script from the chat through an argument form
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: carry a chat run's card and job across saves and reloads
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: render a chat run as a tool call row with its form, logs and result
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: open a pending run form in the sessions preview pane
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* test: benchmark running a deployed script from the chat
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: offer a test run's dynamic options from the draft it previews
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: say that a test run's dynselect helper executes on form display
* fix: send a schema default the model omitted when yolo skips the form
* fix: infer a test run's schema when the stored one declares no properties
* docs: tighten the note on the form's mount-time helper job
* fix: apply a nested schema default the bypass posture counts as answered
* fix: apply a declared default to a null value and an optional nested field
* fix: check required fields inside a supplied optional object before bypassing
* fix: read required args as own properties before bypassing the form
* fix: stop the turn from the run form's action row in the preview panel
* refactor: drop the run-form prediction and share its secret minting
* refactor: prefill a proposed secret instead of emptying the field
* docs: correct the comments the run-form prediction left behind
* fix: keep a proposed secret out of the chat's stored messages
* docs: say what a literal secret argument now does
* test: restore the copilotInfo export the aiStore mock omits
* docs: cut the run form's helper-script note to its constraints
* refactor: settle a run form from one entry and fetch a job's logs once
* fix: separate colliding secret paths, gate plan mode, keep polled logs
* fix: mint before the form opens, skip empty fields, show what ran
* revert: mint a run form's secrets at submit, not before it opens
* fix: settle a cancelled run card on the form's arguments, not the proposal
* fix: settle a stopped run form like a cancelled one, and keep an empty secret empty
* fix: snapshot a run's arguments before minting its secrets
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The public read-only run view had no way out. A team member who lands on a
shared link had to rebuild the /run URL by hand to retry, cancel or edit the
job. Add an "Open full view" link in the header bar, pointing at
/run/{id}?workspace={workspace} in the same tab; signed-out visitors get the
login page with a redirect back to the run.
Claude-Session: https://claude.ai/code/session_019xBcJ5EB5uhwqWysHbG4w6
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>