diff --git a/frontend/src/lib/components/RunForm.svelte b/frontend/src/lib/components/RunForm.svelte
index a83a9be252..929f39de4f 100644
--- a/frontend/src/lib/components/RunForm.svelte
+++ b/frontend/src/lib/components/RunForm.svelte
@@ -146,6 +146,10 @@
* 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
buttonText?: string
schedulable?: boolean
detailed?: boolean
@@ -174,6 +178,7 @@
runnable,
runAction,
claimRun = undefined,
+ noHelperScript = false,
buttonText = 'Run',
schedulable = true,
detailed = true,
@@ -364,11 +369,13 @@
{#key reloadArgs}
| undefined =>
this.#runForms.get(toolId)?.draft?.args
+ /** Take over what the form is waiting on, for a surface handing the call back with what
+ * the reader typed into it. A snapshot for the same reason the draft is one: these are
+ * `$state` proxies off another component, and the draft must not alias them. */
+ setRunFormArgs = (toolId: string, args: Record) => {
+ const entry = this.#runForms.get(toolId)
+ if (entry) entry.draft.args = ($state.snapshot(args) ?? {}) as Record
+ }
+
handleRunFormCancel = (toolId: string) => {
// The card's own copy is settled here rather than only in the tool's fn, which a form
// restored from history no longer has: Cancel is that card's one way out, and while it
diff --git a/frontend/src/lib/components/details/FlowDetail.svelte b/frontend/src/lib/components/details/FlowDetail.svelte
index a288a41c75..c56d7c1294 100644
--- a/frontend/src/lib/components/details/FlowDetail.svelte
+++ b/frontend/src/lib/components/details/FlowDetail.svelte
@@ -399,6 +399,16 @@
runForm.setArgs(pendingRun.args ?? {})
})
+ // The form here is a copy, so what the reader typed into it would be lost when the call
+ // 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.
+ $effect(() => {
+ const call = pendingRun
+ if (!call) return
+ return () => call.handBack(args ?? {})
+ })
+
// The dev workspace's editor is not one the session panel can host, so from a preview tab
// it opens in a new browser tab, as the session editors' own entry does.
function editInFork(e: Event | undefined, itemPath: string) {
@@ -936,6 +946,7 @@
runnable={flow}
{runAction}
claimRun={pendingRun}
+ noHelperScript={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 cf28c5de62..0c136067a1 100644
--- a/frontend/src/lib/components/details/ScriptDetail.svelte
+++ b/frontend/src/lib/components/details/ScriptDetail.svelte
@@ -448,6 +448,16 @@
runForm.setArgs(pendingRun.args ?? {})
})
+ // The form here is a copy, so what the reader typed into it would be lost when the call
+ // 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.
+ $effect(() => {
+ const call = pendingRun
+ if (!call) return
+ return () => call.handBack(args ?? {})
+ })
+
// Read once on purpose: these args seed the form, so tracking the fragment would
// overwrite what the user has typed whenever it changes.
let hash = untrack(() => locationHash)
@@ -1144,6 +1154,7 @@
runnable={script}
{runAction}
claimRun={pendingRun}
+ noHelperScript={pendingRun?.planModeActive}
bind:args
schedulable={!pendingRun}
bind:this={runForm}
diff --git a/frontend/src/lib/components/details/pendingRun.ts b/frontend/src/lib/components/details/pendingRun.ts
index 844030c499..c277732ca5 100644
--- a/frontend/src/lib/components/details/pendingRun.ts
+++ b/frontend/src/lib/components/details/pendingRun.ts
@@ -13,10 +13,16 @@ export type PendingRun = {
* mount: a tab already showing this item is reused for the next request, so a latch that
* only remembered "seeded" would leave the previous call's arguments on screen. */
toolCallId: string
- /** What the model proposed, already narrowed to the deployed schema by the tool. The
- * draft the card edits, not a copy of it, so the two surfaces fill one set of arguments.
- * Gone once the call stops waiting, which a page mid-press outlives. */
+ /** What the form is waiting on: the model's proposal, or whatever the card was last left
+ * holding. Gone once the call stops waiting, which a page mid-press outlives. */
args: Record | undefined
+ /** Whether the chat is in plan mode, which promised no runs. The page is standing in for
+ * a chat card, so it owes that promise too — and the dynamic-select helper runs its job on
+ * mount, where nothing about confirming the run can gate it. */
+ planModeActive: boolean
+ /** Give the call back holding what the reader typed here, so the card they return to
+ * offers those arguments rather than the ones this page opened with. */
+ handBack: (args: Record) => void
/** Take the call, before the form mints a `password` field into an ephemeral workspace
* variable. False means this press runs nothing: plan mode is on, or a press is already
* in flight. Both have to be answered here rather than in {@link submit}, which the form
@@ -31,8 +37,6 @@ export type PendingRun = {
submit: (args: Record) => boolean
/** Refuse the call, as the card's Cancel does. Not an undo of the prefill: there is
* nothing to restore it to, and leaving the call parked with an emptied form would strand
- * the turn. `reason` is what the model is told instead of "cancelled" — for the page that
- * settles the call by running the item some other way, where reporting a refusal would
- * have the model say nothing ran. */
- decline: (reason?: string) => void
+ * the turn. */
+ decline: () => void
}
diff --git a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts
index 2f66d98fd9..542be7b511 100644
--- a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts
+++ b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts
@@ -601,6 +601,10 @@ function createRuntime(session: Session): SessionRuntime {
return {
toolCallId,
args: manager.pendingRunFormArgs(toolCallId),
+ // Read here rather than on the page, so the page's own `$derived` rebuilds when the
+ // posture changes while its form sits open.
+ planModeActive: manager.planModeActive,
+ handBack: (args) => manager.setRunFormArgs(toolCallId, args),
// Both guards belong ahead of the form's own `processSecretArgs`, which writes
// ephemeral variables to the workspace: plan mode can be switched on while the form
// sits here, and a second press would mint a second set of them.