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}