diff --git a/frontend/src/lib/components/RunForm.svelte b/frontend/src/lib/components/RunForm.svelte index 929f39de4f..7f7f6b3220 100644 --- a/frontend/src/lib/components/RunForm.svelte +++ b/frontend/src/lib/components/RunForm.svelte @@ -146,10 +146,12 @@ * if the press then ends before `runAction`, so a guard that counts presses can stop * counting this one. */ claimRun?: { claim: () => boolean; release: () => void } - /** Withhold the dynamic-select helper, which runs the `dynselect-` entrypoint on mount - * rather than on Run. For a form standing in for a chat call while plan mode is on: - * nothing about pressing Run gates it, and plan mode promised no runs. */ - noHelperScript?: boolean + /** Take no writes from the reader: no edits, and no dynamic-select helper. Both write + * before Run is ever pressed — a `password` field mints an ephemeral variable as it is + * typed, and the helper runs the `dynselect-` entrypoint when the field mounts — so + * nothing about gating Run can stop them. For a form standing in for a chat card while + * plan mode is on, which promised neither. */ + argsReadonly?: boolean buttonText?: string schedulable?: boolean detailed?: boolean @@ -178,7 +180,7 @@ runnable, runAction, claimRun = undefined, - noHelperScript = false, + argsReadonly = false, buttonText = 'Run', schedulable = true, detailed = true, @@ -369,13 +371,14 @@ {#key reloadArgs}
{ - const call = pendingRun - if (!call) return - return () => call.handBack(args ?? {}) + const id = pendingRun?.toolCallId + if (!id) return + const call = untrack(() => pendingRun) + return () => + untrack(() => { + // Only what this call's own form held. The next call for this item re-points the + // same tab without remounting, and its seeding may land first — handing this one + // the arguments of a proposal that was never on screen for it. + if (seededCallId === id) call?.handBack(args ?? {}) + }) }) // The dev workspace's editor is not one the session panel can host, so from a preview tab @@ -946,7 +957,7 @@ runnable={flow} {runAction} claimRun={pendingRun} - noHelperScript={pendingRun?.planModeActive} + argsReadonly={pendingRun?.planModeActive} bind:args schedulable={!pendingRun} bind:this={runForm} diff --git a/frontend/src/lib/components/details/ScriptDetail.svelte b/frontend/src/lib/components/details/ScriptDetail.svelte index 0c136067a1..09d0829aa3 100644 --- a/frontend/src/lib/components/details/ScriptDetail.svelte +++ b/frontend/src/lib/components/details/ScriptDetail.svelte @@ -452,10 +452,21 @@ // goes back to the card — the card would then offer, and run, the arguments this page // opened with. Written on the way out rather than per keystroke: one hand-back, whether // the tab closed, was re-pointed, or the reader picked a version. + // + // Keyed on the call rather than the object naming it, which is rebuilt whenever the draft + // or the posture changes: this must not re-run on either, since its own write to that + // draft would be what re-ran it. $effect(() => { - const call = pendingRun - if (!call) return - return () => call.handBack(args ?? {}) + const id = pendingRun?.toolCallId + if (!id) return + const call = untrack(() => pendingRun) + return () => + untrack(() => { + // Only what this call's own form held. The next call for this item re-points the + // same tab without remounting, and its seeding may land first — handing this one + // the arguments of a proposal that was never on screen for it. + if (seededCallId === id) call?.handBack(args ?? {}) + }) }) // Read once on purpose: these args seed the form, so tracking the fragment would @@ -1154,7 +1165,7 @@ runnable={script} {runAction} claimRun={pendingRun} - noHelperScript={pendingRun?.planModeActive} + argsReadonly={pendingRun?.planModeActive} bind:args schedulable={!pendingRun} bind:this={runForm}