diff --git a/apps/mobile/src/features/threads/PendingUserInputCard.tsx b/apps/mobile/src/features/threads/PendingUserInputCard.tsx index 172eb627645bd66885c62f9195eeb5e990c657e5..27fab480ad7d3cfa98189aaaede0a3f4206bfbc0 100644 --- a/apps/mobile/src/features/threads/PendingUserInputCard.tsx +++ b/apps/mobile/src/features/threads/PendingUserInputCard.tsx @@ -21,6 +21,9 @@ import { SymbolView } from "../../components/AppSymbol"; import { AppText as Text } from "../../components/AppText"; import { ControlPill } from "../../components/ControlPill"; import { cn } from "../../lib/cn"; +import { SelectableMarkdownText } from "../../native/SelectableMarkdownText"; +import { useOpenPullRequest } from "../../native/useOpenPullRequest"; +import { useMarkdownPreviewStyles } from "../files/FileMarkdownPreview"; import { isPendingUserInputOptionSelected, type PendingUserInput, @@ -91,6 +94,19 @@ const EXPANDED_CARD_IS_OVERLAY = Platform.OS === "ios"; const CARD_LAYOUT_TRANSITION = LinearTransition.duration(200); +/** Renders question text as markdown so agent-provided links open on tap. */ +function QuestionText(props: { readonly markdown: string }) { + const styles = useMarkdownPreviewStyles(); + const openLink = useOpenPullRequest(); + return ( + void openLink(href)} + /> + ); +} + export function PendingUserInputCard(props: PendingUserInputCardProps) { const questionCount = props.pendingUserInput.questions.length; // Message responses start a new run and remain available after the provider exits. @@ -277,9 +293,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { {question.header} - - {question.question} - + {question.options.map((option) => { const optionValue = option.value ?? option.label.trim(); diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 64514530b29fb3fc927dd5d9770a46c6630cd2ba..dc46376483cd0b0c4eae993ee37cb042036ec2ad 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -6869,6 +6869,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ) : !isComposerCollapsedMobile && pendingUserInputs.length > 0 ? ( { }; }); +vi.mock("../ChatMarkdown", () => ({ default: ({ text }: { text: string }) =>

{text}

})); + import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel"; +const threadRef = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") }; + const prompt = { requestId: RuntimeRequestId.make("question-navigation"), createdAt: "2026-10-09T00:00:00.000Z", @@ -44,6 +48,8 @@ function Questions({ initialIndex = 0 }: { initialIndex?: number }) { { }; }); +vi.mock("../ChatMarkdown", () => ({ default: ({ text }: { text: string }) =>

{text}

})); + import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel"; +const threadRef = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") }; + it("cancels an answer's pending auto-submit on revocation and resumes after a new grant", async () => { vi.useFakeTimers(); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); @@ -43,6 +47,8 @@ it("cancels an answer's pending auto-submit on revocation and resumes after a ne const panel = (disabled: boolean) => ( { expect(markup).toContain("Incremental"); expect(markup).toContain("Big bang"); }); + + it("links URLs in the question text", () => { + const url = "https://accounts.t3.codes/device?user_code=RTJS-TQGS"; + const question = prompt.questions[0]!; + const markup = renderPanel({ + ...prompt, + questions: [{ ...question, question: `Authorize BeeBox at ${url} and confirm.` }], + }); + + expect(markup).toMatch(new RegExp(`]*href="${url.replace(/[?.]/g, "\\$&")}"`)); + }); }); diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx index 1e8caefac2d405dbc72ab784935a33479bf256d9..88f999974684ff17b9f866b0289b1c155fa624d1 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx @@ -1,4 +1,4 @@ -import { type RuntimeRequestId } from "@t3tools/contracts"; +import { type RuntimeRequestId, type ScopedThreadRef } from "@t3tools/contracts"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { type PendingUserInput } from "../../session-logic"; import { @@ -9,9 +9,13 @@ import { CheckIcon } from "lucide-react"; import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible"; import { cn } from "~/lib/utils"; import { ComposerBanner } from "./ComposerBanner"; +import ChatMarkdown from "../ChatMarkdown"; interface PendingUserInputPanelProps { pendingUserInputs: PendingUserInput[]; + /** Resolves file links in the question text. */ + cwd: string | undefined; + threadRef: ScopedThreadRef; disabled?: boolean; respondingRequestIds: RuntimeRequestId[]; answers: Record; @@ -23,6 +27,8 @@ interface PendingUserInputPanelProps { export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserInputPanel({ pendingUserInputs, + cwd, + threadRef, disabled = false, respondingRequestIds, answers, @@ -39,6 +45,8 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn ; @@ -242,7 +254,12 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( -

{activeQuestion.question}

+ {activeQuestion.multiSelect ? (

Select one or more options.

) : null}