From 35824f5815e7fef8b4c6551790d278b6abb42245 Mon Sep 17 00:00:00 2001 From: Mark Miro Date: Mon, 29 Sep 2025 08:42:16 -0700 Subject: [PATCH 1/6] Add fix code button --- src/components/outputs/shared-with-iframe/AnsiOutput.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/outputs/shared-with-iframe/AnsiOutput.tsx b/src/components/outputs/shared-with-iframe/AnsiOutput.tsx index 3bbbc97b..8da6b18e 100644 --- a/src/components/outputs/shared-with-iframe/AnsiOutput.tsx +++ b/src/components/outputs/shared-with-iframe/AnsiOutput.tsx @@ -1,5 +1,7 @@ import React from "react"; import Ansi from "ansi-to-react"; +import { Button } from "@/components/ui/button"; +import { Bug } from "lucide-react"; interface AnsiOutputProps { children: string; @@ -77,6 +79,10 @@ export const AnsiErrorOutput: React.FC<{ )} + ); }; From d563bf671d3b736e9559bef5d060996b27b3ad04 Mon Sep 17 00:00:00 2001 From: Mark Miro Date: Mon, 29 Sep 2025 08:45:12 -0700 Subject: [PATCH 2/6] Add button --- src/components/outputs/shared-with-iframe/AnsiOutput.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/outputs/shared-with-iframe/AnsiOutput.tsx b/src/components/outputs/shared-with-iframe/AnsiOutput.tsx index 8da6b18e..0cf73698 100644 --- a/src/components/outputs/shared-with-iframe/AnsiOutput.tsx +++ b/src/components/outputs/shared-with-iframe/AnsiOutput.tsx @@ -1,7 +1,6 @@ -import React from "react"; import Ansi from "ansi-to-react"; -import { Button } from "@/components/ui/button"; import { Bug } from "lucide-react"; +import React from "react"; interface AnsiOutputProps { children: string; From 9325413be61208eb41d3da4d0fad8e6aa18ff93a Mon Sep 17 00:00:00 2001 From: Mark Miro Date: Mon, 29 Sep 2025 09:32:47 -0700 Subject: [PATCH 3/6] Hook up fix code button --- .../notebook/cell/ExecutableCell.tsx | 10 ++++++ src/components/outputs/MaybeCellOutputs.tsx | 11 +++++- .../outputs/shared-with-iframe/AnsiOutput.tsx | 8 ++++- .../outputs/shared-with-iframe/comms.ts | 22 ++++++++++-- src/hooks/useAddCell.ts | 36 ++++++++++++++++++- 5 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/components/notebook/cell/ExecutableCell.tsx b/src/components/notebook/cell/ExecutableCell.tsx index d28e99c4..4d4f31ce 100644 --- a/src/components/notebook/cell/ExecutableCell.tsx +++ b/src/components/notebook/cell/ExecutableCell.tsx @@ -39,6 +39,7 @@ import { MaybeCellOutputs } from "@/components/outputs/MaybeCellOutputs.js"; import { useToolApprovals } from "@/hooks/useToolApprovals.js"; import { AiToolApprovalOutput } from "../../outputs/shared-with-iframe/AiToolApprovalOutput.js"; import { cn } from "@/lib/utils.js"; +import { IframeFixCodeEvent } from "@/components/outputs/shared-with-iframe/comms.js"; // Cell-specific styling configuration const getCellStyling = (cellType: "code" | "sql" | "ai") => { @@ -338,6 +339,14 @@ export const ExecutableCell: React.FC = ({ cell.executionState === "running" || staleOutputs.length > 0); + const handleFixCode = useCallback( + (event: IframeFixCodeEvent) => { + console.log("handleFixCode", event); + addCell(cell.id, "ai", "after", JSON.stringify(event)); + }, + [addCell, cell.id] + ); + return ( = ({ isLoading={cell.executionState === "running" && !hasOutputs} outputs={hasOutputs ? outputs : staleOutputs} showOutput={showOutput} + onFixCode={handleFixCode} /> diff --git a/src/components/outputs/MaybeCellOutputs.tsx b/src/components/outputs/MaybeCellOutputs.tsx index 98e406c4..625103a3 100644 --- a/src/components/outputs/MaybeCellOutputs.tsx +++ b/src/components/outputs/MaybeCellOutputs.tsx @@ -4,7 +4,10 @@ import { OutputData, SAFE_MIME_TYPES } from "@runtimed/schema"; import { groupConsecutiveStreamOutputs } from "@/util/output-grouping"; import { useQuery } from "@livestore/react"; import { useMemo, useState } from "react"; -import { useIframeCommsParent } from "./shared-with-iframe/comms"; +import { + IframeFixCodeEvent, + useIframeCommsParent, +} from "./shared-with-iframe/comms"; import { SingleOutput } from "./shared-with-iframe/SingleOutput"; import { useDebounce } from "react-use"; import { OutputsContainer } from "./shared-with-iframe/OutputsContainer"; @@ -19,11 +22,13 @@ export const MaybeCellOutputs = ({ shouldAlwaysUseIframe = false, isLoading, showOutput, + onFixCode, }: { outputs: readonly OutputData[]; shouldAlwaysUseIframe?: boolean; isLoading: boolean; showOutput: boolean; + onFixCode?: (event: IframeFixCodeEvent) => void; }) => { const outputDeltas = useQuery( outputsDeltasQuery(outputs.map((output) => output.id)) @@ -60,6 +65,7 @@ export const MaybeCellOutputs = ({ outputs={processedOutputs} className="transition-[height] duration-150 ease-out" isReact + onFixCode={onFixCode} /> ) : ( @@ -83,6 +89,7 @@ interface IframeOutputProps { isReact?: boolean; defaultHeight?: string; onDoubleClick?: () => void; + onFixCode?: (event: IframeFixCodeEvent) => void; } export const IframeOutput: React.FC = ({ @@ -93,12 +100,14 @@ export const IframeOutput: React.FC = ({ onHeightChange, defaultHeight = "0px", onDoubleClick, + onFixCode, }) => { const { iframeRef, iframeHeight } = useIframeCommsParent({ defaultHeight, onHeightChange, outputs, onDoubleClick, + onFixCode, }); const [debouncedIframeHeight, setDebouncedIframeHeight] = diff --git a/src/components/outputs/shared-with-iframe/AnsiOutput.tsx b/src/components/outputs/shared-with-iframe/AnsiOutput.tsx index 0cf73698..e2cd852d 100644 --- a/src/components/outputs/shared-with-iframe/AnsiOutput.tsx +++ b/src/components/outputs/shared-with-iframe/AnsiOutput.tsx @@ -1,6 +1,7 @@ import Ansi from "ansi-to-react"; import { Bug } from "lucide-react"; import React from "react"; +import { sendFromIframe } from "./comms"; interface AnsiOutputProps { children: string; @@ -78,7 +79,12 @@ export const AnsiErrorOutput: React.FC<{ )} - diff --git a/src/components/outputs/shared-with-iframe/comms.ts b/src/components/outputs/shared-with-iframe/comms.ts index e38e7741..9cea04c9 100644 --- a/src/components/outputs/shared-with-iframe/comms.ts +++ b/src/components/outputs/shared-with-iframe/comms.ts @@ -22,11 +22,19 @@ type IframeDoubleClickEvent = { type: "iframe-double-click"; }; +export type IframeFixCodeEvent = { + type: "iframe-fix-code"; + ename?: string; + evalue?: string; + traceback?: string[] | string; +}; + export type ToIframeEvent = UpdateOutputsEvent; export type FromIframeEvent = | IframeHeightEvent | IframeLoadedEvent - | IframeDoubleClickEvent; + | IframeDoubleClickEvent + | IframeFixCodeEvent; export function sendFromIframe(event: FromIframeEvent) { window.parent.postMessage(event, "*"); @@ -60,11 +68,13 @@ export function useIframeCommsParent({ onHeightChange, outputs, onDoubleClick, + onFixCode, }: { defaultHeight: string; onHeightChange?: (height: number) => void; outputs?: OutputData[]; onDoubleClick?: () => void; + onFixCode?: (event: IframeFixCodeEvent) => void; }) { const iframeRef = useRef(null); @@ -98,6 +108,14 @@ export function useIframeCommsParent({ if (event.data && event.data.type === "iframe-double-click") { onDoubleClick?.(); } + if (event.data && event.data.type === "iframe-fix-code") { + onFixCode?.({ + type: "iframe-fix-code", + ename: event.data.ename, + evalue: event.data.evalue, + traceback: event.data.traceback, + }); + } }; // Add message listener @@ -106,7 +124,7 @@ export function useIframeCommsParent({ return () => { removeParentMessageListener(handleMessage); }; - }, [onHeightChange, onDoubleClick]); + }, [onHeightChange, onDoubleClick, onFixCode]); useEffect(() => { // We cannot send content to iframe before it is loaded diff --git a/src/hooks/useAddCell.ts b/src/hooks/useAddCell.ts index f4bf1f55..8db37e61 100644 --- a/src/hooks/useAddCell.ts +++ b/src/hooks/useAddCell.ts @@ -18,7 +18,8 @@ export const useAddCell = () => { ( cellId?: string, cellType: CellType = "code", - position: "before" | "after" = "after" + position: "before" | "after" = "after", + source?: string ) => { const cellReferences = store.query(queries.cellsWithIndices$); const newCellId = `cell-${Date.now()}-${Math.random() @@ -99,6 +100,39 @@ export const useAddCell = () => { ); } + if (source) { + // set cell source + store.commit( + events.cellSourceChanged({ + id: newCellId, + source, + modifiedBy: userId, + }) + ); + + // hide cell input + store.commit( + events.cellSourceVisibilityToggled({ + id: newCellId, + sourceVisible: false, + actorId: userId, + }) + ); + + // run cell + store.commit( + events.executionRequested({ + cellId: newCellId, + actorId: userId, + requestedBy: userId, + queueId: `exec-${Date.now()}-${Math.random().toString(36).slice(2)}`, + executionCount: + store.query(queries.cellQuery.byId(newCellId))?.executionCount || + 0 + 1, + }) + ); + } + // Focus the new cell after creation setTimeout(() => store.setSignal(focusedCellSignal$, newCellId), 0); From d9c9fc63eca60e4dc17e146d329c50aaec963178 Mon Sep 17 00:00:00 2001 From: Mark Miro Date: Wed, 15 Oct 2025 11:48:04 -0700 Subject: [PATCH 4/6] Revert all changes --- .../notebook/cell/ExecutableCell.tsx | 10 ------ src/components/outputs/MaybeCellOutputs.tsx | 11 +----- .../outputs/shared-with-iframe/AnsiOutput.tsx | 13 +------ .../outputs/shared-with-iframe/comms.ts | 22 ++---------- src/hooks/useAddCell.ts | 36 +------------------ 5 files changed, 5 insertions(+), 87 deletions(-) diff --git a/src/components/notebook/cell/ExecutableCell.tsx b/src/components/notebook/cell/ExecutableCell.tsx index 36553971..0843a827 100644 --- a/src/components/notebook/cell/ExecutableCell.tsx +++ b/src/components/notebook/cell/ExecutableCell.tsx @@ -42,7 +42,6 @@ import { MaybeCellOutputs } from "@/components/outputs/MaybeCellOutputs.js"; import { useToolApprovals } from "@/hooks/useToolApprovals.js"; import { AiToolApprovalOutput } from "../../outputs/shared-with-iframe/AiToolApprovalOutput.js"; import { cn } from "@/lib/utils.js"; -import { IframeFixCodeEvent } from "@/components/outputs/shared-with-iframe/comms.js"; import { cycleCellType } from "@/util/cycle-cell-type.js"; // Cell-specific styling configuration @@ -375,14 +374,6 @@ export const ExecutableCell: React.FC = ({ cell.executionState === "running" || staleOutputs.length > 0); - const handleFixCode = useCallback( - (event: IframeFixCodeEvent) => { - console.log("handleFixCode", event); - addCell(cell.id, "ai", "after", JSON.stringify(event)); - }, - [addCell, cell.id] - ); - return ( = ({ isLoading={cell.executionState === "running" && !hasOutputs} outputs={hasOutputs ? outputs : staleOutputs} showOutput={showOutput} - onFixCode={handleFixCode} /> diff --git a/src/components/outputs/MaybeCellOutputs.tsx b/src/components/outputs/MaybeCellOutputs.tsx index 625103a3..98e406c4 100644 --- a/src/components/outputs/MaybeCellOutputs.tsx +++ b/src/components/outputs/MaybeCellOutputs.tsx @@ -4,10 +4,7 @@ import { OutputData, SAFE_MIME_TYPES } from "@runtimed/schema"; import { groupConsecutiveStreamOutputs } from "@/util/output-grouping"; import { useQuery } from "@livestore/react"; import { useMemo, useState } from "react"; -import { - IframeFixCodeEvent, - useIframeCommsParent, -} from "./shared-with-iframe/comms"; +import { useIframeCommsParent } from "./shared-with-iframe/comms"; import { SingleOutput } from "./shared-with-iframe/SingleOutput"; import { useDebounce } from "react-use"; import { OutputsContainer } from "./shared-with-iframe/OutputsContainer"; @@ -22,13 +19,11 @@ export const MaybeCellOutputs = ({ shouldAlwaysUseIframe = false, isLoading, showOutput, - onFixCode, }: { outputs: readonly OutputData[]; shouldAlwaysUseIframe?: boolean; isLoading: boolean; showOutput: boolean; - onFixCode?: (event: IframeFixCodeEvent) => void; }) => { const outputDeltas = useQuery( outputsDeltasQuery(outputs.map((output) => output.id)) @@ -65,7 +60,6 @@ export const MaybeCellOutputs = ({ outputs={processedOutputs} className="transition-[height] duration-150 ease-out" isReact - onFixCode={onFixCode} /> ) : ( @@ -89,7 +83,6 @@ interface IframeOutputProps { isReact?: boolean; defaultHeight?: string; onDoubleClick?: () => void; - onFixCode?: (event: IframeFixCodeEvent) => void; } export const IframeOutput: React.FC = ({ @@ -100,14 +93,12 @@ export const IframeOutput: React.FC = ({ onHeightChange, defaultHeight = "0px", onDoubleClick, - onFixCode, }) => { const { iframeRef, iframeHeight } = useIframeCommsParent({ defaultHeight, onHeightChange, outputs, onDoubleClick, - onFixCode, }); const [debouncedIframeHeight, setDebouncedIframeHeight] = diff --git a/src/components/outputs/shared-with-iframe/AnsiOutput.tsx b/src/components/outputs/shared-with-iframe/AnsiOutput.tsx index e2cd852d..3bbbc97b 100644 --- a/src/components/outputs/shared-with-iframe/AnsiOutput.tsx +++ b/src/components/outputs/shared-with-iframe/AnsiOutput.tsx @@ -1,7 +1,5 @@ -import Ansi from "ansi-to-react"; -import { Bug } from "lucide-react"; import React from "react"; -import { sendFromIframe } from "./comms"; +import Ansi from "ansi-to-react"; interface AnsiOutputProps { children: string; @@ -79,15 +77,6 @@ export const AnsiErrorOutput: React.FC<{ )} - ); }; diff --git a/src/components/outputs/shared-with-iframe/comms.ts b/src/components/outputs/shared-with-iframe/comms.ts index 9cea04c9..e38e7741 100644 --- a/src/components/outputs/shared-with-iframe/comms.ts +++ b/src/components/outputs/shared-with-iframe/comms.ts @@ -22,19 +22,11 @@ type IframeDoubleClickEvent = { type: "iframe-double-click"; }; -export type IframeFixCodeEvent = { - type: "iframe-fix-code"; - ename?: string; - evalue?: string; - traceback?: string[] | string; -}; - export type ToIframeEvent = UpdateOutputsEvent; export type FromIframeEvent = | IframeHeightEvent | IframeLoadedEvent - | IframeDoubleClickEvent - | IframeFixCodeEvent; + | IframeDoubleClickEvent; export function sendFromIframe(event: FromIframeEvent) { window.parent.postMessage(event, "*"); @@ -68,13 +60,11 @@ export function useIframeCommsParent({ onHeightChange, outputs, onDoubleClick, - onFixCode, }: { defaultHeight: string; onHeightChange?: (height: number) => void; outputs?: OutputData[]; onDoubleClick?: () => void; - onFixCode?: (event: IframeFixCodeEvent) => void; }) { const iframeRef = useRef(null); @@ -108,14 +98,6 @@ export function useIframeCommsParent({ if (event.data && event.data.type === "iframe-double-click") { onDoubleClick?.(); } - if (event.data && event.data.type === "iframe-fix-code") { - onFixCode?.({ - type: "iframe-fix-code", - ename: event.data.ename, - evalue: event.data.evalue, - traceback: event.data.traceback, - }); - } }; // Add message listener @@ -124,7 +106,7 @@ export function useIframeCommsParent({ return () => { removeParentMessageListener(handleMessage); }; - }, [onHeightChange, onDoubleClick, onFixCode]); + }, [onHeightChange, onDoubleClick]); useEffect(() => { // We cannot send content to iframe before it is loaded diff --git a/src/hooks/useAddCell.ts b/src/hooks/useAddCell.ts index 8db37e61..f4bf1f55 100644 --- a/src/hooks/useAddCell.ts +++ b/src/hooks/useAddCell.ts @@ -18,8 +18,7 @@ export const useAddCell = () => { ( cellId?: string, cellType: CellType = "code", - position: "before" | "after" = "after", - source?: string + position: "before" | "after" = "after" ) => { const cellReferences = store.query(queries.cellsWithIndices$); const newCellId = `cell-${Date.now()}-${Math.random() @@ -100,39 +99,6 @@ export const useAddCell = () => { ); } - if (source) { - // set cell source - store.commit( - events.cellSourceChanged({ - id: newCellId, - source, - modifiedBy: userId, - }) - ); - - // hide cell input - store.commit( - events.cellSourceVisibilityToggled({ - id: newCellId, - sourceVisible: false, - actorId: userId, - }) - ); - - // run cell - store.commit( - events.executionRequested({ - cellId: newCellId, - actorId: userId, - requestedBy: userId, - queueId: `exec-${Date.now()}-${Math.random().toString(36).slice(2)}`, - executionCount: - store.query(queries.cellQuery.byId(newCellId))?.executionCount || - 0 + 1, - }) - ); - } - // Focus the new cell after creation setTimeout(() => store.setSignal(focusedCellSignal$, newCellId), 0); From 4c69c54632f0165aeebd5c70888ae0a9332af301 Mon Sep 17 00:00:00 2001 From: Mark Miro Date: Wed, 15 Oct 2025 11:49:57 -0700 Subject: [PATCH 5/6] Change `useAddCell` to support source with all changes in a single transaction --- src/hooks/useAddCell.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/hooks/useAddCell.ts b/src/hooks/useAddCell.ts index f4bf1f55..26f3101c 100644 --- a/src/hooks/useAddCell.ts +++ b/src/hooks/useAddCell.ts @@ -18,7 +18,8 @@ export const useAddCell = () => { ( cellId?: string, cellType: CellType = "code", - position: "before" | "after" = "after" + position: "before" | "after" = "after", + source?: string ) => { const cellReferences = store.query(queries.cellsWithIndices$); const newCellId = `cell-${Date.now()}-${Math.random() @@ -99,6 +100,34 @@ export const useAddCell = () => { ); } + if (source) { + const cellEvents = [ + // set cell source + events.cellSourceChanged({ + id: newCellId, + source, + modifiedBy: userId, + }), + // hide cell input + events.cellSourceVisibilityToggled({ + id: newCellId, + sourceVisible: false, + actorId: userId, + }), + // run cell + events.executionRequested({ + cellId: newCellId, + actorId: userId, + requestedBy: userId, + queueId: `exec-${Date.now()}-${Math.random().toString(36).slice(2)}`, + executionCount: + (store.query(queries.cellQuery.byId(newCellId))?.executionCount || + 0) + 1, + }), + ]; + store.commit(...cellEvents); + } + // Focus the new cell after creation setTimeout(() => store.setSignal(focusedCellSignal$, newCellId), 0); From 2db57754d68033c2cc533b851a3722fbbf77965c Mon Sep 17 00:00:00 2001 From: Mark Miro Date: Thu, 16 Oct 2025 09:56:33 -0700 Subject: [PATCH 6/6] Rewrite fix code feature --- .../notebook/cell/ExecutableCell.tsx | 2 + src/components/outputs/MaybeCellOutputs.tsx | 14 +++- src/components/outputs/MaybeFixCodeButton.tsx | 75 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/components/outputs/MaybeFixCodeButton.tsx diff --git a/src/components/notebook/cell/ExecutableCell.tsx b/src/components/notebook/cell/ExecutableCell.tsx index 0843a827..015a79ad 100644 --- a/src/components/notebook/cell/ExecutableCell.tsx +++ b/src/components/notebook/cell/ExecutableCell.tsx @@ -613,6 +613,8 @@ export const ExecutableCell: React.FC = ({ + {cellType === "code" && ( + + )} {/* TODO: consider rendering an empty iframewhen we have a safe output currently rendered but cell input has changed */} {shouldUseIframe ? ( output.outputType === "error"); + + if ( + !errorOutput || + !errorOutput.data || + typeof errorOutput.data !== "string" + ) { + return null; + } + + return ( + + + + ); +} + +const FixCodeButton = ({ + cellId, + errorOutputData, + isLoading, +}: { + cellId: string; + errorOutputData: string; + isLoading: boolean; +}) => { + const { addCell } = useAddCell(); + + const handleFixCode = useCallback(() => { + const formattedErrorOutputData = formatErrorOutputData(errorOutputData); + const message = formatAiMessage(formattedErrorOutputData); + addCell(cellId, "ai", "after", message); + }, [addCell, cellId, errorOutputData]); + + return ( + + ); +}; + +/** Add code block to the error message */ +function formatAiMessage(errorString: string) { + return `Fix the error:\n \`\`\`json\n${errorString}\n\`\`\`\n`; +} + +/** Formats the error for AI model to fix */ +function formatErrorOutputData(errorOutputData: string) { + return JSON.stringify(JSON.parse(errorOutputData), null, 2); +}