Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions apps/mobile/src/features/threads/PendingUserInputCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<SelectableMarkdownText
markdown={props.markdown}
textStyle={styles.nativeTextStyle}
onLinkPress={(href) => 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.
Expand Down Expand Up @@ -277,9 +293,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
<Text className="font-t3-bold text-xs uppercase tracking-[1px] text-foreground-muted">
{question.header}
</Text>
<Text className="font-sans text-base leading-snug text-foreground">
{question.question}
</Text>
<QuestionText markdown={question.question} />
<View className="gap-2">
{question.options.map((option) => {
const optionValue = option.value ?? option.label.trim();
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6869,6 +6869,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
) : !isComposerCollapsedMobile && pendingUserInputs.length > 0 ? (
<ComposerPendingUserInputPanel
pendingUserInputs={pendingUserInputs}
cwd={gitCwd ?? undefined}
threadRef={routeThreadRef}
disabled={!canOperateThread}
respondingRequestIds={
activePendingIsResponding && activePendingUserInput
Expand All @@ -6890,6 +6892,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
<div data-chat-composer-collapsed-controls="true">
<ComposerPendingUserInputPanel
pendingUserInputs={pendingUserInputs}
cwd={gitCwd ?? undefined}
threadRef={routeThreadRef}
disabled={!canOperateThread}
respondingRequestIds={
activePendingIsResponding && activePendingUserInput
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RuntimeRequestId } from "@t3tools/contracts";
import { EnvironmentId, RuntimeRequestId, ThreadId } from "@t3tools/contracts";
import { act, useState, type ReactNode } from "react";
import { create, type ReactTestRenderer } from "react-test-renderer";
import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
Expand All @@ -17,8 +17,12 @@ vi.mock("../ui/collapsible", () => {
};
});

vi.mock("../ChatMarkdown", () => ({ default: ({ text }: { text: string }) => <p>{text}</p> }));

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",
Expand All @@ -44,6 +48,8 @@ function Questions({ initialIndex = 0 }: { initialIndex?: number }) {
<button onClick={() => setQuestionIndex(questionIndex - 1)}>Previous</button>
<ComposerPendingUserInputPanel
pendingUserInputs={[prompt]}
cwd="/repo"
threadRef={threadRef}
disabled={false}
respondingRequestIds={[]}
answers={answers}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RuntimeRequestId } from "@t3tools/contracts";
import { EnvironmentId, RuntimeRequestId, ThreadId } from "@t3tools/contracts";
import { act, type ReactNode } from "react";
import { create, type ReactTestRenderer } from "react-test-renderer";
import { expect, it, vi } from "vite-plus/test";
Expand All @@ -12,8 +12,12 @@ vi.mock("../ui/collapsible", () => {
};
});

vi.mock("../ChatMarkdown", () => ({ default: ({ text }: { text: string }) => <p>{text}</p> }));

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);
Expand Down Expand Up @@ -43,6 +47,8 @@ it("cancels an answer's pending auto-submit on revocation and resumes after a ne
const panel = (disabled: boolean) => (
<ComposerPendingUserInputPanel
pendingUserInputs={[prompt]}
cwd="/repo"
threadRef={threadRef}
disabled={disabled}
respondingRequestIds={[]}
answers={{}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { RuntimeRequestId } from "@t3tools/contracts";
import { EnvironmentId, RuntimeRequestId, ThreadId } from "@t3tools/contracts";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vite-plus/test";

import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";

const threadRef = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") };
import type { PendingUserInput } from "../../session-logic";

const prompt: PendingUserInput = {
Expand All @@ -28,6 +30,8 @@ function renderPanel(pendingUserInput: PendingUserInput = prompt) {
return renderToStaticMarkup(
<ComposerPendingUserInputPanel
pendingUserInputs={[pendingUserInput]}
cwd="/repo"
threadRef={threadRef}
respondingRequestIds={[]}
answers={{}}
questionIndex={0}
Expand Down Expand Up @@ -68,4 +72,15 @@ describe("ComposerPendingUserInputPanel", () => {
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(`<a[^>]*href="${url.replace(/[?.]/g, "\\$&")}"`));
});
});
21 changes: 19 additions & 2 deletions apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<string, PendingUserInputDraftAnswer>;
Expand All @@ -23,6 +27,8 @@ interface PendingUserInputPanelProps {

export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserInputPanel({
pendingUserInputs,
cwd,
threadRef,
disabled = false,
respondingRequestIds,
answers,
Expand All @@ -39,6 +45,8 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn
<ComposerPendingUserInputCard
key={activePrompt.requestId}
prompt={activePrompt}
cwd={cwd}
threadRef={threadRef}
disabled={disabled}
isResponding={respondingRequestIds.includes(activePrompt.requestId)}
answers={answers}
Expand All @@ -52,6 +60,8 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn

const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard({
prompt,
cwd,
threadRef,
disabled,
isResponding,
answers,
Expand All @@ -61,6 +71,8 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(
onDismiss,
}: {
prompt: PendingUserInput;
cwd: string | undefined;
threadRef: ScopedThreadRef;
disabled: boolean;
isResponding: boolean;
answers: Record<string, PendingUserInputDraftAnswer>;
Expand Down Expand Up @@ -242,7 +254,12 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(
<CollapsiblePanel>
<ComposerBanner.Scroll>
<ComposerBanner.Body className="pe-1 pb-1 wrap-anywhere">
<p className="text-sm text-foreground/85">{activeQuestion.question}</p>
<ChatMarkdown
text={activeQuestion.question}
cwd={cwd}
threadRef={threadRef}
className="text-foreground/85"
/>
{activeQuestion.multiSelect ? (
<p className="mt-1 text-secondary-label text-xs">Select one or more options.</p>
) : null}
Expand Down
Loading