From 53698025854b97b17869604f615fe9c3f1aa9290 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Wed, 7 Oct 2026 07:24:28 -0700 Subject: [PATCH] refactor(sessions): hand the form back on destroy rather than from an effect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every way the page stops carrying a call and still holds edits destroys the component: the tab closes, or its url changes and `loadKey` remounts it. So the hand-back is a destroy hook, not a reaction to the call changing — which needed a key and two `untrack`s to keep its own write from re-running it. The call is the one this form was seeded for, captured where it is seeded. By the time the hook runs the tab has already stopped naming it, so reading it then would find nothing to hand back to. Co-Authored-By: Claude Opus 5 (1M context) --- .../lib/components/details/FlowDetail.svelte | 28 ++++++++----------- .../components/details/ScriptDetail.svelte | 28 ++++++++----------- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/frontend/src/lib/components/details/FlowDetail.svelte b/frontend/src/lib/components/details/FlowDetail.svelte index 5d03ddb926..92f9f273dc 100644 --- a/frontend/src/lib/components/details/FlowDetail.svelte +++ b/frontend/src/lib/components/details/FlowDetail.svelte @@ -391,34 +391,28 @@ // Seeded once per call rather than once per mount: a tab already showing this item is // reused for the next request, so a latch on "seeded" would leave the previous call's // arguments on screen. + // + // An effect because neither end of this is an event: the form does not exist when the + // reader presses Preview — the page fetches the deployed item first — so there is nothing + // to seed until `runForm` lands, and a call arriving on an already-open tab has to seed a + // form that is already mounted. `onMount` answers neither. let seededCallId: string | undefined = undefined + let seededCall: PendingRun | undefined = undefined $effect(() => { if (!pendingRun || !runForm) return if (seededCallId === pendingRun.toolCallId) return seededCallId = pendingRun.toolCallId + seededCall = pendingRun 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. + // opened with. Written on the way out rather than per keystroke. // - // 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 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 call this page was holding, not whichever one it holds now: by the time this runs + // the tab has usually already stopped naming it, which is what ended the ownership. + onDestroy(() => seededCall?.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. diff --git a/frontend/src/lib/components/details/ScriptDetail.svelte b/frontend/src/lib/components/details/ScriptDetail.svelte index e8f814caa0..02b789eaa7 100644 --- a/frontend/src/lib/components/details/ScriptDetail.svelte +++ b/frontend/src/lib/components/details/ScriptDetail.svelte @@ -440,34 +440,28 @@ // Seeded once per call rather than once per mount: a tab already showing this item is // reused for the next request, so a latch on "seeded" would leave the previous call's // arguments on screen. + // + // An effect because neither end of this is an event: the form does not exist when the + // reader presses Preview — the page fetches the deployed item first — so there is nothing + // to seed until `runForm` lands, and a call arriving on an already-open tab has to seed a + // form that is already mounted. `onMount` answers neither. let seededCallId: string | undefined = undefined + let seededCall: PendingRun | undefined = undefined $effect(() => { if (!pendingRun || !runForm) return if (seededCallId === pendingRun.toolCallId) return seededCallId = pendingRun.toolCallId + seededCall = pendingRun 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. + // opened with. Written on the way out rather than per keystroke. // - // 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 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 call this page was holding, not whichever one it holds now: by the time this runs + // the tab has usually already stopped naming it, which is what ended the ownership. + onDestroy(() => seededCall?.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.