diff --git a/frontend/src/lib/components/details/FlowDetail.svelte b/frontend/src/lib/components/details/FlowDetail.svelte index bae6e9f314..c43522e1b4 100644 --- a/frontend/src/lib/components/details/FlowDetail.svelte +++ b/frontend/src/lib/components/details/FlowDetail.svelte @@ -27,6 +27,8 @@ import { Badge as HeaderBadge, Alert } from '$lib/components/common' import MoveDrawer from '$lib/components/MoveDrawer.svelte' import RunForm from '$lib/components/RunForm.svelte' + import { processSecretArgs } from '$lib/components/secretArgUtils' + import type { Schema } from '$lib/common' import ShareModal from '$lib/components/ShareModal.svelte' import { enterpriseLicense, userStore, userWorkspaces, workspaceStore } from '$lib/stores' import { useOperatorBuilderFlows } from '$lib/operatorWriteRights' @@ -323,11 +325,21 @@ conversationId: string, additionalInputs?: Record ): Promise { + // A chat flow's inputs reach the job straight from the composer, with no RunForm in + // between to mint a secret as it is typed — so this is the only place a value the + // schema marks `password` can become a reference. Without it the literal is stored in + // the job's arguments, where anyone who can read the run can read it. Idempotent, so a + // reference that was already minted costs a walk and no round trip. + const requestBody = await processSecretArgs( + { user_message: userMessage, ...(additionalInputs ?? {}) }, + flow?.schema as Schema | undefined, + workspace + ) const run = await JobService.runFlowByPath({ workspace: workspace!, path, memoryId: conversationId, - requestBody: { user_message: userMessage, ...(additionalInputs ?? {}) }, + requestBody, skipPreprocessor: true }) return run diff --git a/frontend/src/lib/components/flows/conversations/FlowChat.svelte b/frontend/src/lib/components/flows/conversations/FlowChat.svelte index 9d4b0e7b94..ed9e72cfa5 100644 --- a/frontend/src/lib/components/flows/conversations/FlowChat.svelte +++ b/frontend/src/lib/components/flows/conversations/FlowChat.svelte @@ -191,12 +191,24 @@ return panel?.host.offerMessage(text) ?? false } - /** Set the flow's non-message inputs for the run a chat tool proposed. Separate from the + /** Set the flow's non-message inputs to the run a chat tool proposed. Separate from the * message because the two can land apart: a composer the reader is typing in declines the - * message, and the inputs still belong to the proposal they can send afterwards. */ + * message, and the inputs still belong to the proposal they can send afterwards. + * + * Every input the schema declares is taken from the proposal, including the ones it leaves + * out — those go back to their default. Merging instead would let a value the reader saved + * on an earlier visit ride along in a run the agent announced without it, which is a + * different run from the one the chat described. Inputs the composer owns rather than the + * schema (its model wiring, its attachments) are not the proposal's to touch. */ export function applyInputs(inputs: Record): void { - if (Object.keys(inputs).length === 0) return - inputValues = { ...inputValues, ...inputs } + const declared = Object.keys(additionalInputsSchema?.properties ?? {}) + if (declared.length === 0) return + const next = { ...inputValues } + for (const key of declared) { + if (key in inputs) next[key] = inputs[key] + else delete next[key] + } + inputValues = next } /** Whether the conversation on screen has a composer to offer a message to. A caller that diff --git a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts index 73f0e90e18..ac25e031ea 100644 --- a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts +++ b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts @@ -624,7 +624,21 @@ function createRuntime(session: Session): SessionRuntime { const target = previewTargetForSessionTarget(kind, path) if (target?.type !== 'item') return false seededRunArgs.set(seededKey(kind, path), { args, seq: ++seededRunSeq }) - previewTabs.open({ ...target, item: { ...target.item, summary }, mode: 'view' }) + const viewTarget = { ...target, item: { ...target.item, summary }, mode: 'view' as const } + // A viewer already open on an older version of this item keeps that pin when it is + // merely focused, and Run there executes that version — not the deployment these + // arguments were prepared against. Re-point it instead, which drops the pin. + const pinned = previewTabs.tabs.find((t) => { + const slot = resolvePreviewTab(t.url) + return ( + slot.kind === 'viewer' && + slot.viewerKind === kind && + slot.path === path && + t.url.includes('version=') + ) + }) + if (pinned) previewTabs.navigate(viewTarget, pinned.id) + else previewTabs.open(viewTarget) return true } manager.closeRunForm = (toolCallId) => previewTabs.closeRunForm(toolCallId)