Skip to content

Extension dialogs show text typed in an earlier dialog instead of their own prefill #1357

Open

Description

Description

In the web UI, an extension editor dialog that opens after an input dialog shows the text typed into the input, not its own prefill. If the user submits without looking, the extension gets the old text back.

This is not only about the next dialog. Every later input or editor dialog in the same chat view can start with the last typed text. A page reload clears it.

The cause is how the React Compiler compiles the per-request draft (line numbers are from main at 52ae5478).

ChatTranscript builds the PendingExtUi object in a useMemo keyed by the request. The memo callback has a local let draft and the draft() / rememberDraft() closures (apps/web/src/chat/ChatTranscript.tsx:558-581). The chat spec says the draft lives in that per-request object, and "a reply or dismissal ends the request and its draft with it" (apps/web/src/chat/SPEC.md:527-530).

The web app is built with the React Compiler in every build mode (apps/web/vite.config.ts:28, babel-plugin-react-compiler 1.0.0). In the compiled ChatTranscript, request and reply still change with the request. But the draft, rememberDraft and claimReveal closures are cached in slots that are filled only once, when the first request comes. Every later request gets the same closures, so they all read and write the draft variable of the first request. From the dev build, shortened:

if ($[127] === Symbol.for("react.memo_cache_sentinel")) {
	t44 = () => draft;
	t45 = (text) => { draft = text; };
	t46 = () => { /* claimReveal */ };
	$[127] = t44; $[128] = t45; $[129] = t46;
} else {
	t44 = $[127]; t45 = $[128]; t46 = $[129];
}

ExtUiPrompt then starts from draft ?? prefill (apps/web/src/chat/ExtUiPrompt.tsx:35-37), so the shared draft wins over the new prefill.

To check this I added "use no memo" to ChatTranscript locally. With the same steps, the editor showed its prefill.

claimReveal is cached in the same block, so its revealed flag is shared too. From reading the compiled code, I think only the first tool-anchored dialog in a chat view scrolls itself into view. I didn't test this in the app.

An idea for the fix: keep the draft somewhere that doesn't depend on how the compiler caches closures, for example a ref keyed by the request id.

Steps to reproduce

  1. Add a global extension, for example ~/.pi/agent/extensions/dialogs.ts:

    export default function (pi) {
    	pi.registerCommand("dialogs", {
    		description: "input dialog, then editor dialog with a prefill",
    		handler: async (_args, ctx) => {
    			const typed = await ctx.ui.input("Step 1: type a name", "a name");
    			const edited = await ctx.ui.editor("Step 2: edit the template", "PREFILL FROM EXTENSION");
    			ctx.ui.notify(`input returned ${JSON.stringify(typed)}; editor returned ${JSON.stringify(edited)}`, "info");
    		},
    	});
    }
  2. Start ThinkRail with bun run dev, open a project and start a chat. No provider is needed: an extension command doesn't call the model.

  3. Send /dialogs. Type typed-into-input into the input dialog and press Submit.

  4. Look at the editor dialog, then press Submit without changes.

Expected behavior

The editor dialog shows PREFILL FROM EXTENSION. The notice says editor returned "PREFILL FROM EXTENSION".

Actual behavior

The editor dialog shows typed-into-input. The notice says input returned "typed-into-input"; editor returned "typed-into-input".

In a second run I typed first-run into the input and pressed Submit, then pressed Cancel in the editor and sent /dialogs again. The new input dialog already had first-run in it. After a page reload the first dialog is empty again.

Environment

  • OS: macOS 27.0.1
  • Bun version (bun --version): 1.4.4
  • Node version (node --version): v24.10.0
  • ThinkRail version / commit: main 52ae5478, pi 1.5.2, web UI from bun run dev in a Chromium-based browser

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions