From cf6d4e3ac45c586fbe5147a1657e8b1e9e778ed0 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:30:26 +0200 Subject: [PATCH 1/5] fix(pi): preserve native input dialogs --- .../features/threads/PendingUserInputCard.tsx | 1 + .../features/threads/QuestionAttachments.tsx | 2 +- apps/server/src/mcp/toolkits/thread/tools.ts | 1 + apps/web/src/components/chat/ChatComposer.tsx | 4 +- .../chat/ComposerPendingUserInputPanel.tsx | 1 + packages/contracts/src/orchestrationV2.ts | 1 + packages/contracts/src/providerRuntime.ts | 1 + .../provider-pi/src/server/adapter.test.ts | 48 +++++++++++++++++++ packages/provider-pi/src/server/adapter.ts | 27 ++++++----- 9 files changed, 71 insertions(+), 15 deletions(-) diff --git a/apps/mobile/src/features/threads/PendingUserInputCard.tsx b/apps/mobile/src/features/threads/PendingUserInputCard.tsx index 6be28f295d638..bea236b6cdbc3 100644 --- a/apps/mobile/src/features/threads/PendingUserInputCard.tsx +++ b/apps/mobile/src/features/threads/PendingUserInputCard.tsx @@ -72,6 +72,7 @@ function QuestionText(props: { readonly markdown: string }) { return ( void openLink(href)} /> diff --git a/apps/mobile/src/features/threads/QuestionAttachments.tsx b/apps/mobile/src/features/threads/QuestionAttachments.tsx index 3b4449d3aff13..d90ca46908da6 100644 --- a/apps/mobile/src/features/threads/QuestionAttachments.tsx +++ b/apps/mobile/src/features/threads/QuestionAttachments.tsx @@ -216,7 +216,7 @@ export function QuestionAttachments(props: { focusedRef.current = false; onInputFocusChange?.(false); }} - placeholder="Or type a custom answer" + placeholder={props.question.placeholder ?? "Or type a custom answer"} className="min-h-[54px] rounded-2xl border border-input-border bg-input px-3.5 py-3 font-sans text-base text-foreground" /> diff --git a/apps/server/src/mcp/toolkits/thread/tools.ts b/apps/server/src/mcp/toolkits/thread/tools.ts index 21750cbb6eaae..1bb309592c6c7 100644 --- a/apps/server/src/mcp/toolkits/thread/tools.ts +++ b/apps/server/src/mcp/toolkits/thread/tools.ts @@ -142,6 +142,7 @@ const question = Schema.Struct({ multiSelect: Schema.optional(Schema.Boolean), allowCustomAnswer: Schema.optional(Schema.Boolean), initialAnswer: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), required: Schema.optional(Schema.Boolean), }); const pendingRequest = Schema.Struct({ diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index b74e691ddd139..fa4d1b06e0fc3 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -420,6 +420,7 @@ export interface ChatComposerProps { activeQuestion: { id: string; initialAnswer?: string | undefined; + placeholder?: string | undefined; multiSelect?: boolean | undefined; allowCustomAnswer?: boolean | undefined; } | null; @@ -3805,7 +3806,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) : activePendingProgress ? isChoiceOnlyPendingQuestion ? "Choose an option above" - : "Type your own answer, or leave this blank to use the selected option" + : (activePendingProgress.activeQuestion?.placeholder ?? + "Type your own answer, or leave this blank to use the selected option") : showPlanFollowUpPrompt && activeProposedPlan ? "Add feedback to refine the plan, or leave this blank to implement it" : projectSelectionRequired diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx index bfcfa49050612..994681045c756 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx @@ -236,6 +236,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( { }).pipe(Effect.scoped, Effect.provide(layerTest)), ); + it.effect.each([undefined, "", "1,3; custom answer"])( + "maps native text input without turning its placeholder into an answer, placeholder=%s", + (placeholder) => + Effect.gen(function* () { + const fake = yield* makeFakePi; + const { runtime, takeEvent } = yield* openRuntime(fake); + yield* runtime.ensureThread({ + threadId: THREAD_ID, + modelSelection: modelSelection("default"), + runtimePolicy, + }); + const title = "Where should this run?\n1. Current branch\n2. New worktree"; + for (const [index, answer] of ["", " exact answer\n"].entries()) { + const id = `ui-input-${index}`; + yield* fake.emit({ + type: "extension_ui_request", + id, + method: "input", + title, + ...(placeholder === undefined ? {} : { placeholder }), + }); + const event = yield* takeEvent( + (event) => + event.type === "turn_item.updated" && + event.turnItem.type === "user_input_request" && + event.turnItem.status === "waiting" && + event.turnItem.questions.some((question) => question.id === id), + ); + if (event.type !== "turn_item.updated" || event.turnItem.type !== "user_input_request") { + return assert.fail("expected a user input request"); + } + const [question] = decodeUserInputQuestions(event.turnItem.questions); + assert.equal(question?.header, "Question"); + assert.equal(question?.question, title); + assert.deepEqual(question?.options, []); + assert.equal(question?.initialAnswer, ""); + assert.equal(question?.placeholder, placeholder); + yield* runtime.respondToRuntimeRequest({ + requestId: event.turnItem.requestId, + answers: { [id]: answer }, + }); + const response = yield* fake.takeRequest("extension_ui_response"); + assert.equal(response["value"], answer); + assert.isUndefined(response["cancelled"]); + } + }).pipe(Effect.scoped, Effect.provide(layerTest)), + ); + it.effect("keeps the complete editor prefill editable and returns exact edited text", () => Effect.gen(function* () { const fake = yield* makeFakePi; diff --git a/packages/provider-pi/src/server/adapter.ts b/packages/provider-pi/src/server/adapter.ts index a2a3caf9267e5..eef4310692fb8 100644 --- a/packages/provider-pi/src/server/adapter.ts +++ b/packages/provider-pi/src/server/adapter.ts @@ -3313,21 +3313,22 @@ function piQuestion( description: option || "Empty value", value: option, })) - : method === "editor" - ? [] - : [ - { - label: "Submit empty value", - description: "Send an empty string to the extension.", - value: "", - }, - ]; - const question = recordString(event, "message") ?? recordString(event, "placeholder") ?? title; + : []; + const placeholder = method === "input" ? recordString(event, "placeholder") : undefined; return { id: questionId, - header: title, - question, - ...(method === "editor" ? { initialAnswer: recordString(event, "prefill") ?? "" } : {}), + header: "Question", + question: + title.trim() || + (method === "select" + ? "Choose an option" + : method === "editor" + ? "Edit text" + : "Enter a value"), + ...(method === "select" + ? {} + : { initialAnswer: method === "editor" ? (recordString(event, "prefill") ?? "") : "" }), + ...(placeholder === undefined ? {} : { placeholder }), options, }; } From 1080887b32085eb2a66dbd06257973795db18a1f Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:10:50 +0200 Subject: [PATCH 2/5] ci: retry stalled dependency installation From abb40eb4d3bd1635cdfd4c4b75e07f80146f33f5 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 23:08:18 +0000 Subject: [PATCH 3/5] fix(pi): preserve literal dialog titles and free-text hints --- apps/web/src/components/chat/ChatComposer.tsx | 4 +++- .../provider-pi/src/server/adapter.test.ts | 24 +++++++++++++++---- packages/provider-pi/src/server/adapter.ts | 7 +++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 2f76e8e8d0964..f448d9d7154f4 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -3374,7 +3374,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ? isChoiceOnlyPendingQuestion ? "Choose an option above" : (activePendingProgress.activeQuestion?.placeholder ?? - "Type your own answer, or leave this blank to use the selected option") + (isLiteralPendingAnswer + ? "Type your answer" + : "Type your own answer, or leave this blank to use the selected option")) : showPlanFollowUpPrompt && activeProposedPlan ? "Add feedback to refine the plan, or leave this blank to implement it" : projectSelectionRequired diff --git a/packages/provider-pi/src/server/adapter.test.ts b/packages/provider-pi/src/server/adapter.test.ts index c959c7480280d..73974acd97c96 100644 --- a/packages/provider-pi/src/server/adapter.test.ts +++ b/packages/provider-pi/src/server/adapter.test.ts @@ -3342,9 +3342,21 @@ describe("PiAdapterV2", () => { }).pipe(Effect.scoped, Effect.provide(layerTest)), ); - it.effect.each([undefined, "", "1,3; custom answer"])( + it.effect.each([ + { + placeholder: undefined, + title: "Enter & [name] *exact*", + question: String.raw`Enter <branch> & \[name\] \*exact\*`, + }, + { + placeholder: "", + title: "Where should this run?\n1. Current branch\n2. New worktree", + question: "Where should this run?\n1\\. Current branch\n2\\. New worktree", + }, + { placeholder: "1,3; custom answer", title: "Enter a value", question: "Enter a value" }, + ])( "maps native text input without turning its placeholder into an answer, placeholder=%s", - (placeholder) => + ({ placeholder, title, question: expectedQuestion }) => Effect.gen(function* () { const fake = yield* makeFakePi; const { runtime, takeEvent } = yield* openRuntime(fake); @@ -3353,7 +3365,6 @@ describe("PiAdapterV2", () => { modelSelection: modelSelection("default"), runtimePolicy, }); - const title = "Where should this run?\n1. Current branch\n2. New worktree"; for (const [index, answer] of ["", " exact answer\n"].entries()) { const id = `ui-input-${index}`; yield* fake.emit({ @@ -3375,7 +3386,7 @@ describe("PiAdapterV2", () => { } const [question] = decodeUserInputQuestions(event.turnItem.questions); assert.equal(question?.header, "Question"); - assert.equal(question?.question, title); + assert.equal(question?.question, expectedQuestion); assert.deepEqual(question?.options, []); assert.equal(question?.initialAnswer, ""); assert.equal(question?.placeholder, placeholder); @@ -3414,7 +3425,10 @@ describe("PiAdapterV2", () => { if (event.type !== "turn_item.updated" || event.turnItem.type !== "user_input_request") { return assert.fail("expected a user input request"); } - assert.equal(decodeUserInputQuestions(event.turnItem.questions)[0]?.question, title); + assert.equal( + decodeUserInputQuestions(event.turnItem.questions)[0]?.question, + "Which database?\n1\\. Postgres\n2\\. Type something\\.\nType your answer:", + ); yield* runtime.respondToRuntimeRequest({ requestId: event.turnItem.requestId, answers: { "ui-input": "SQLite" }, diff --git a/packages/provider-pi/src/server/adapter.ts b/packages/provider-pi/src/server/adapter.ts index 1454efe9f39ea..f8b362e0c191b 100644 --- a/packages/provider-pi/src/server/adapter.ts +++ b/packages/provider-pi/src/server/adapter.ts @@ -3330,7 +3330,12 @@ function piQuestion( id: questionId, header: "Question", question: - title.trim() || + title + .trim() + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/[\\`*_[\]{}()#+.!|~-]/g, "\\$&") || (method === "select" ? "Choose an option" : method === "editor" From b4584daf37db274e4c754d404d171e907d5ff68d Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 23:14:09 +0000 Subject: [PATCH 4/5] fix(pi): keep dialog titles literal with math enabled --- packages/provider-pi/src/server/adapter.test.ts | 8 ++++---- packages/provider-pi/src/server/adapter.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/provider-pi/src/server/adapter.test.ts b/packages/provider-pi/src/server/adapter.test.ts index 73974acd97c96..a277c54cac3b2 100644 --- a/packages/provider-pi/src/server/adapter.test.ts +++ b/packages/provider-pi/src/server/adapter.test.ts @@ -3345,13 +3345,13 @@ describe("PiAdapterV2", () => { it.effect.each([ { placeholder: undefined, - title: "Enter & [name] *exact*", - question: String.raw`Enter <branch> & \[name\] \*exact\*`, + title: "Enter & [name] *exact* $PATH$", + question: "Enter <branch> & [name] *exact* $PATH$", }, { placeholder: "", title: "Where should this run?\n1. Current branch\n2. New worktree", - question: "Where should this run?\n1\\. Current branch\n2\\. New worktree", + question: "Where should this run?\n1. Current branch\n2. New worktree", }, { placeholder: "1,3; custom answer", title: "Enter a value", question: "Enter a value" }, ])( @@ -3427,7 +3427,7 @@ describe("PiAdapterV2", () => { } assert.equal( decodeUserInputQuestions(event.turnItem.questions)[0]?.question, - "Which database?\n1\\. Postgres\n2\\. Type something\\.\nType your answer:", + "Which database?\n1. Postgres\n2. Type something.\nType your answer:", ); yield* runtime.respondToRuntimeRequest({ requestId: event.turnItem.requestId, diff --git a/packages/provider-pi/src/server/adapter.ts b/packages/provider-pi/src/server/adapter.ts index f8b362e0c191b..bce915257a1ba 100644 --- a/packages/provider-pi/src/server/adapter.ts +++ b/packages/provider-pi/src/server/adapter.ts @@ -3335,7 +3335,7 @@ function piQuestion( .replace(/&/g, "&") .replace(//g, ">") - .replace(/[\\`*_[\]{}()#+.!|~-]/g, "\\$&") || + .replace(/[\\`*_[\]{}()#+.!|~$-]/g, (character) => `&#${character.charCodeAt(0)};`) || (method === "select" ? "Choose an option" : method === "editor" From 5c581223a4610986c1d4947b6e228fea22c5ac66 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 1 Nov 2026 00:47:49 +0000 Subject: [PATCH 5/5] fix(pi): keep collapsed and multiline dialog titles literal --- .../src/components/chat/ComposerPendingUserInputPanel.tsx | 3 ++- packages/provider-pi/src/server/adapter.test.ts | 5 +++++ packages/provider-pi/src/server/adapter.ts | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx index 18b8c9722a1e4..a23a69865e573 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx @@ -1,4 +1,5 @@ import { type RuntimeRequestId, type ScopedThreadRef } from "@t3tools/contracts"; +import { decodeHTMLStrict } from "entities"; import { memo, use, useEffect, useEffectEvent, useRef, useState } from "react"; import { BackgroundThreadContext } from "./backgroundThread"; import { useStableCallback } from "~/hooks/useStableCallback"; @@ -204,7 +205,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( <> - {activeQuestion.question} + {decodeHTMLStrict(activeQuestion.question)} ) : null} diff --git a/packages/provider-pi/src/server/adapter.test.ts b/packages/provider-pi/src/server/adapter.test.ts index a277c54cac3b2..c3fc89cfa1fcf 100644 --- a/packages/provider-pi/src/server/adapter.test.ts +++ b/packages/provider-pi/src/server/adapter.test.ts @@ -3354,6 +3354,11 @@ describe("PiAdapterV2", () => { question: "Where should this run?\n1. Current branch\n2. New worktree", }, { placeholder: "1,3; custom answer", title: "Enter a value", question: "Enter a value" }, + { + placeholder: "", + title: "Optional release note\n===", + question: "Optional release note\n===", + }, ])( "maps native text input without turning its placeholder into an answer, placeholder=%s", ({ placeholder, title, question: expectedQuestion }) => diff --git a/packages/provider-pi/src/server/adapter.ts b/packages/provider-pi/src/server/adapter.ts index bce915257a1ba..25952f7303635 100644 --- a/packages/provider-pi/src/server/adapter.ts +++ b/packages/provider-pi/src/server/adapter.ts @@ -3335,7 +3335,7 @@ function piQuestion( .replace(/&/g, "&") .replace(//g, ">") - .replace(/[\\`*_[\]{}()#+.!|~$-]/g, (character) => `&#${character.charCodeAt(0)};`) || + .replace(/[\\`*_[\]{}()#+.!|~=$-]/g, (character) => `&#${character.charCodeAt(0)};`) || (method === "select" ? "Choose an option" : method === "editor"