From 4f7c44813acf1113234993d69f5d72a3bdc2ac08 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 01:22:07 +0000 Subject: [PATCH 1/5] fix(compiler): Wait for charm initialization before navigating The issue was that navigateTo was being called immediately with the result from compileAndRun, but the result Cell is initially undefined and only gets populated asynchronously after compilation completes. Solution follows the same pattern used in chatbot-list-view.tsx: - Use a lift to monitor the result Cell reactively - Only navigate once the result is populated (not undefined) - Use an isNavigated flag to ensure navigation only happens once Changes: - Add navigateWhenReady lift that waits for result to be ready - Update visit handler to use the lift instead of calling navigateTo directly - Show actual error messages in the UI instead of generic "fix the errors" This fixes the "blackhole" issue where navigateTo appeared to do nothing. --- recipes/compiler.tsx | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/recipes/compiler.tsx b/recipes/compiler.tsx index c3cd0f1f2..ca848f80e 100644 --- a/recipes/compiler.tsx +++ b/recipes/compiler.tsx @@ -1,13 +1,16 @@ /// import { Cell, + cell, compileAndRun, Default, handler, ifElse, + lift, NAME, navigateTo, recipe, + toSchema, UI, } from "commontools"; @@ -27,6 +30,25 @@ const updateCode = handler< }, ); +// Lift that waits for the compiled result to be ready before navigating +// Similar to the storeCharm pattern in chatbot-list-view.tsx +const navigateWhenReady = lift( + toSchema<{ + result: any; + isNavigated: Cell; + }>(), + undefined, + ({ result, isNavigated }) => { + // Only navigate once the result is populated and we haven't navigated yet + if (result && !isNavigated.get()) { + console.log("navigateWhenReady: result is ready, navigating", result); + isNavigated.set(true); + return navigateTo(result); + } + return undefined; + }, +); + const visit = handler< { detail: { value: string } }, { code: string } @@ -37,9 +59,16 @@ const visit = handler< main: "/main.tsx", }); - console.log("result", result); + console.log("visit: compileAndRun returned result cell", result); - return navigateTo(result); + // Use a cell to track if we've already navigated + const isNavigated = cell(false); + + // Use the lift to wait for result to be ready before navigating + return navigateWhenReady({ + result, + isNavigated, + }); }, ); @@ -63,7 +92,7 @@ export default recipe( /> {ifElse( error, - fix the errors, + fix the error: {error}, From faf54b7b27a3626ac447353b71dcf00689331fef Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:01:13 +1000 Subject: [PATCH 2/5] fix(compiler): Reuse compileAndRun result instead of creating new action The previous approach called compileAndRun inside the handler, which: - Created a NEW compileAndRun action on every handler invocation - Caused double invocation due to re-renders from pending state changes - Hit the run index check at compile-and-run.ts:191 and bailed early Solution: - Call compileAndRun once in the recipe body (already done for error display) - Reuse the stable result cell from that single compileAndRun call - Handler just calls navigateTo(result) on the existing cell - navigateTo is an Action, so it re-runs when result populates This is much simpler and avoids the double invocation issue entirely. No need for the navigateWhenReady lift workaround! --- recipes/compiler.tsx | 56 +++++++++++++------------------------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/recipes/compiler.tsx b/recipes/compiler.tsx index ca848f80e..d14d9588d 100644 --- a/recipes/compiler.tsx +++ b/recipes/compiler.tsx @@ -1,16 +1,13 @@ /// import { Cell, - cell, compileAndRun, Default, handler, ifElse, - lift, NAME, navigateTo, recipe, - toSchema, UI, } from "commontools"; @@ -30,52 +27,29 @@ const updateCode = handler< }, ); -// Lift that waits for the compiled result to be ready before navigating -// Similar to the storeCharm pattern in chatbot-list-view.tsx -const navigateWhenReady = lift( - toSchema<{ - result: any; - isNavigated: Cell; - }>(), - undefined, - ({ result, isNavigated }) => { - // Only navigate once the result is populated and we haven't navigated yet - if (result && !isNavigated.get()) { - console.log("navigateWhenReady: result is ready, navigating", result); - isNavigated.set(true); - return navigateTo(result); - } - return undefined; +const visit = handler< + unknown, + { result: Cell } +>( + (_, { result }) => { + console.log("visit: navigating to compiled result", result); + return navigateTo(result); }, ); -const visit = handler< - { detail: { value: string } }, - { code: string } +const handleEditContent = handler< + { code: string }, + { code: Cell } >( - (_, state) => { - const { result } = compileAndRun({ - files: [{ name: "/main.tsx", contents: state.code }], - main: "/main.tsx", - }); - - console.log("visit: compileAndRun returned result cell", result); - - // Use a cell to track if we've already navigated - const isNavigated = cell(false); - - // Use the lift to wait for result to be ready before navigating - return navigateWhenReady({ - result, - isNavigated, - }); + (event, { code }) => { + code.set(event.code); }, ); export default recipe( "Compiler", ({ code }) => { - const { error, errors } = compileAndRun({ + const { result, error, errors } = compileAndRun({ files: [{ name: "/main.tsx", contents: code }], main: "/main.tsx", }); @@ -94,7 +68,7 @@ export default recipe( error, fix the error: {error}, Navigate To Charm , @@ -102,6 +76,8 @@ export default recipe( ), code, + updateCode: handleEditContent({ code }), + visit: visit({ result }), }; }, ); From e7765e59c9fbd940008f34bcdeaa1bb57a9b5118 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:05:16 +1000 Subject: [PATCH 3/5] Copy across cancellation pattern from `llm.ts` --- .../runner/src/builtins/compile-and-run.ts | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/runner/src/builtins/compile-and-run.ts b/packages/runner/src/builtins/compile-and-run.ts index 231eebc88..5f3356fc8 100644 --- a/packages/runner/src/builtins/compile-and-run.ts +++ b/packages/runner/src/builtins/compile-and-run.ts @@ -28,12 +28,13 @@ import { CompilerError } from "@commontools/js-runtime/typescript"; export function compileAndRun( inputsCell: Cell>, sendResult: (tx: IExtendedStorageTransaction, result: any) => void, - _addCancel: (cancel: () => void) => void, + addCancel: (cancel: () => void) => void, cause: any, parentCell: Cell, runtime: IRuntime, ): Action { - let currentRun = 0; + let requestId: string | undefined = undefined; + let abortController: AbortController | undefined = undefined; let previousCallHash: string | undefined = undefined; let cellsInitialized = false; let pending: Cell; @@ -52,6 +53,12 @@ export function compileAndRun( | undefined >; + // This is called when the recipe containing this node is being stopped. + addCancel(() => { + // Abort any in-flight compilation if it's still pending. + abortController?.abort("Recipe stopped"); + }); + return (tx: IExtendedStorageTransaction) => { if (!cellsInitialized) { pending = runtime.getCell( @@ -97,7 +104,7 @@ export function compileAndRun( sendResult(tx, { pending, result, error, errors }); cellsInitialized = true; } - const thisRun = ++currentRun; + const pendingWithLog = pending.withTx(tx); const resultWithLog = result.withTx(tx); const errorWithLog = error.withTx(tx); @@ -135,6 +142,11 @@ export function compileAndRun( if (hash === previousCallHash) return; previousCallHash = hash; + // Abort any in-flight compilation before starting a new one + abortController?.abort("New compilation started"); + abortController = new AbortController(); + requestId = crypto.randomUUID(); + runtime.runner.stop(result); resultWithLog.set(undefined); errorWithLog.set(undefined); @@ -156,10 +168,15 @@ export function compileAndRun( // Now we're sure that we have a new file to compile pendingWithLog.set(true); + // Capture requestId for this compilation run + const thisRequestId = requestId; + const compilePromise = runtime.harness.run(program) .catch( (err) => { - if (thisRun !== currentRun) return; + // Only process this error if the request hasn't been superseded + if (requestId !== thisRequestId) return; + if (abortController?.signal.aborted) return; runtime.editWithRetry((asyncTx) => { // Extract structured errors if this is a CompilerError @@ -180,7 +197,9 @@ export function compileAndRun( }); }, ).finally(() => { - if (thisRun !== currentRun) return; + // Only update pending if this is still the current request + if (requestId !== thisRequestId) return; + if (abortController?.signal.aborted) return; runtime.editWithRetry((asyncTx) => { pending.withTx(asyncTx).set(false); @@ -188,7 +207,10 @@ export function compileAndRun( }); compilePromise.then((recipe) => { - if (thisRun !== currentRun) return; + // Only run the result if this is still the current request + if (requestId !== thisRequestId) return; + if (abortController?.signal.aborted) return; + if (recipe) { // TODO(ja): to support editting of existing charms / running with // inputs from other charms, we will need to think more about From 679e7413b9b665ea5845c4f93165e7de1fe97003 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:11:19 +1000 Subject: [PATCH 4/5] Move `compiler.tsx` to patterns package --- {recipes => packages/patterns}/compiler.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {recipes => packages/patterns}/compiler.tsx (100%) diff --git a/recipes/compiler.tsx b/packages/patterns/compiler.tsx similarity index 100% rename from recipes/compiler.tsx rename to packages/patterns/compiler.tsx From b45395e05e253f1f3c6e1f3d83e0e4774524ffea Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Sun, 9 Nov 2025 12:19:22 -0800 Subject: [PATCH 5/5] fix(compiler): Clear pending state after cancellation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove abort signal check from finally block to ensure pending state is always reset to false when compilation completes, whether successfully, with error, or cancelled. This prevents the compile action from getting stuck in pending=true state after cancellation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/runner/src/builtins/compile-and-run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runner/src/builtins/compile-and-run.ts b/packages/runner/src/builtins/compile-and-run.ts index 5f3356fc8..ef663b926 100644 --- a/packages/runner/src/builtins/compile-and-run.ts +++ b/packages/runner/src/builtins/compile-and-run.ts @@ -199,7 +199,7 @@ export function compileAndRun( ).finally(() => { // Only update pending if this is still the current request if (requestId !== thisRequestId) return; - if (abortController?.signal.aborted) return; + // Always clear pending state, even if cancelled, to avoid stuck state runtime.editWithRetry((asyncTx) => { pending.withTx(asyncTx).set(false);