Skip to content

Commit 62c88e2

Browse files
authored
Merge branch 'main' into t3android/issue-18978
2 parents afe50db + 85272ee commit 62c88e2

8 files changed

Lines changed: 91 additions & 8 deletions

File tree

‎apps/mobile/src/features/threads/PendingUserInputCard.tsx‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { SymbolView } from "../../components/AppSymbol";
2121
import { AppText as Text } from "../../components/AppText";
2222
import { ControlPill } from "../../components/ControlPill";
2323
import { cn } from "../../lib/cn";
24+
import { SelectableMarkdownText } from "../../native/SelectableMarkdownText";
25+
import { useOpenPullRequest } from "../../native/useOpenPullRequest";
26+
import { useMarkdownPreviewStyles } from "../files/FileMarkdownPreview";
2427
import {
2528
isPendingUserInputOptionSelected,
2629
type PendingUserInput,
@@ -91,6 +94,19 @@ const EXPANDED_CARD_IS_OVERLAY = Platform.OS === "ios";
9194

9295
const CARD_LAYOUT_TRANSITION = LinearTransition.duration(200);
9396

97+
/** Renders question text as markdown so agent-provided links open on tap. */
98+
function QuestionText(props: { readonly markdown: string }) {
99+
const styles = useMarkdownPreviewStyles();
100+
const openLink = useOpenPullRequest();
101+
return (
102+
<SelectableMarkdownText
103+
markdown={props.markdown}
104+
textStyle={styles.nativeTextStyle}
105+
onLinkPress={(href) => void openLink(href)}
106+
/>
107+
);
108+
}
109+
94110
export function PendingUserInputCard(props: PendingUserInputCardProps) {
95111
const questionCount = props.pendingUserInput.questions.length;
96112
// Message responses start a new run and remain available after the provider exits.
@@ -277,9 +293,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
277293
<Text className="font-t3-bold text-xs uppercase tracking-[1px] text-foreground-muted">
278294
{question.header}
279295
</Text>
280-
<Text className="font-sans text-base leading-snug text-foreground">
281-
{question.question}
282-
</Text>
296+
<QuestionText markdown={question.question} />
283297
<View className="gap-2">
284298
{question.options.map((option) => {
285299
const optionValue = option.value ?? option.label.trim();

‎apps/server/src/persistence/Sqlite.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,21 @@ it.effect("collects planner statistics for an existing database on open", () =>
114114
);
115115
});
116116

117+
it.effect("only fsyncs the database file at WAL checkpoints", () => {
118+
const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-sqlite-sync-"));
119+
const dbPath = NodePath.join(tempDir, "state.sqlite");
120+
121+
return Effect.gen(function* () {
122+
const sql = yield* SqlClient.SqlClient;
123+
const rows = yield* sql<{ readonly synchronous: number }>`PRAGMA synchronous`;
124+
// 1 is NORMAL.
125+
assert.equal(rows[0]?.synchronous, 1);
126+
}).pipe(
127+
Effect.provide(SqlitePersistence.layerFromPath(dbPath).pipe(Layer.provide(NodeServices.layer))),
128+
Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),
129+
);
130+
});
131+
117132
it.effect("applies busy_timeout in the shared persistence setup", () =>
118133
Effect.gen(function* () {
119134
const sql = yield* SqlClient.SqlClient;

‎apps/server/src/persistence/Sqlite.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ const layerSetup = Layer.effectDiscard(
1919
yield* sql`PRAGMA busy_timeout = 5000;`;
2020
yield* sql`PRAGMA foreign_keys = ON;`;
2121
yield* sql`PRAGMA journal_mode = WAL;`;
22+
// FULL fsyncs the WAL on every commit, on the main thread. Every provider
23+
// event is its own commit, so a slow or busy disk stalls the event loop
24+
// and clients drop. In WAL mode NORMAL only fsyncs at checkpoints. It
25+
// cannot corrupt the database and an app crash loses nothing; an OS crash
26+
// or power loss can roll back commits since the last checkpoint.
27+
yield* sql`PRAGMA synchronous = NORMAL;`;
2228
// PASSIVE checkpoints never shrink the -wal file, so it otherwise keeps its
2329
// largest size until the last connection closes.
2430
yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`);

‎apps/web/src/components/chat/ChatComposer.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6869,6 +6869,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
68696869
) : !isComposerCollapsedMobile && pendingUserInputs.length > 0 ? (
68706870
<ComposerPendingUserInputPanel
68716871
pendingUserInputs={pendingUserInputs}
6872+
cwd={gitCwd ?? undefined}
6873+
threadRef={routeThreadRef}
68726874
disabled={!canOperateThread}
68736875
respondingRequestIds={
68746876
activePendingIsResponding && activePendingUserInput
@@ -6890,6 +6892,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
68906892
<div data-chat-composer-collapsed-controls="true">
68916893
<ComposerPendingUserInputPanel
68926894
pendingUserInputs={pendingUserInputs}
6895+
cwd={gitCwd ?? undefined}
6896+
threadRef={routeThreadRef}
68936897
disabled={!canOperateThread}
68946898
respondingRequestIds={
68956899
activePendingIsResponding && activePendingUserInput

‎apps/web/src/components/chat/ComposerPendingUserInputPanel.navigation.test.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RuntimeRequestId } from "@t3tools/contracts";
1+
import { EnvironmentId, RuntimeRequestId, ThreadId } from "@t3tools/contracts";
22
import { act, useState, type ReactNode } from "react";
33
import { create, type ReactTestRenderer } from "react-test-renderer";
44
import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
@@ -17,8 +17,12 @@ vi.mock("../ui/collapsible", () => {
1717
};
1818
});
1919

20+
vi.mock("../ChatMarkdown", () => ({ default: ({ text }: { text: string }) => <p>{text}</p> }));
21+
2022
import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";
2123

24+
const threadRef = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") };
25+
2226
const prompt = {
2327
requestId: RuntimeRequestId.make("question-navigation"),
2428
createdAt: "2026-10-09T00:00:00.000Z",
@@ -44,6 +48,8 @@ function Questions({ initialIndex = 0 }: { initialIndex?: number }) {
4448
<button onClick={() => setQuestionIndex(questionIndex - 1)}>Previous</button>
4549
<ComposerPendingUserInputPanel
4650
pendingUserInputs={[prompt]}
51+
cwd="/repo"
52+
threadRef={threadRef}
4753
disabled={false}
4854
respondingRequestIds={[]}
4955
answers={answers}

‎apps/web/src/components/chat/ComposerPendingUserInputPanel.permissions.test.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RuntimeRequestId } from "@t3tools/contracts";
1+
import { EnvironmentId, RuntimeRequestId, ThreadId } from "@t3tools/contracts";
22
import { act, type ReactNode } from "react";
33
import { create, type ReactTestRenderer } from "react-test-renderer";
44
import { expect, it, vi } from "vite-plus/test";
@@ -12,8 +12,12 @@ vi.mock("../ui/collapsible", () => {
1212
};
1313
});
1414

15+
vi.mock("../ChatMarkdown", () => ({ default: ({ text }: { text: string }) => <p>{text}</p> }));
16+
1517
import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";
1618

19+
const threadRef = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") };
20+
1721
it("cancels an answer's pending auto-submit on revocation and resumes after a new grant", async () => {
1822
vi.useFakeTimers();
1923
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
4347
const panel = (disabled: boolean) => (
4448
<ComposerPendingUserInputPanel
4549
pendingUserInputs={[prompt]}
50+
cwd="/repo"
51+
threadRef={threadRef}
4652
disabled={disabled}
4753
respondingRequestIds={[]}
4854
answers={{}}

‎apps/web/src/components/chat/ComposerPendingUserInputPanel.test.tsx‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { RuntimeRequestId } from "@t3tools/contracts";
1+
import { EnvironmentId, RuntimeRequestId, ThreadId } from "@t3tools/contracts";
22
import { renderToStaticMarkup } from "react-dom/server";
33
import { describe, expect, it } from "vite-plus/test";
44

55
import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";
6+
7+
const threadRef = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") };
68
import type { PendingUserInput } from "../../session-logic";
79

810
const prompt: PendingUserInput = {
@@ -28,6 +30,8 @@ function renderPanel(pendingUserInput: PendingUserInput = prompt) {
2830
return renderToStaticMarkup(
2931
<ComposerPendingUserInputPanel
3032
pendingUserInputs={[pendingUserInput]}
33+
cwd="/repo"
34+
threadRef={threadRef}
3135
respondingRequestIds={[]}
3236
answers={{}}
3337
questionIndex={0}
@@ -68,4 +72,15 @@ describe("ComposerPendingUserInputPanel", () => {
6872
expect(markup).toContain("Incremental");
6973
expect(markup).toContain("Big bang");
7074
});
75+
76+
it("links URLs in the question text", () => {
77+
const url = "https://accounts.t3.codes/device?user_code=RTJS-TQGS";
78+
const question = prompt.questions[0]!;
79+
const markup = renderPanel({
80+
...prompt,
81+
questions: [{ ...question, question: `Authorize BeeBox at ${url} and confirm.` }],
82+
});
83+
84+
expect(markup).toMatch(new RegExp(`<a[^>]*href="${url.replace(/[?.]/g, "\\$&")}"`));
85+
});
7186
});

‎apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type RuntimeRequestId } from "@t3tools/contracts";
1+
import { type RuntimeRequestId, type ScopedThreadRef } from "@t3tools/contracts";
22
import { memo, useCallback, useEffect, useRef, useState } from "react";
33
import { type PendingUserInput } from "../../session-logic";
44
import {
@@ -9,9 +9,13 @@ import { CheckIcon } from "lucide-react";
99
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
1010
import { cn } from "~/lib/utils";
1111
import { ComposerBanner } from "./ComposerBanner";
12+
import ChatMarkdown from "../ChatMarkdown";
1213

1314
interface PendingUserInputPanelProps {
1415
pendingUserInputs: PendingUserInput[];
16+
/** Resolves file links in the question text. */
17+
cwd: string | undefined;
18+
threadRef: ScopedThreadRef;
1519
disabled?: boolean;
1620
respondingRequestIds: RuntimeRequestId[];
1721
answers: Record<string, PendingUserInputDraftAnswer>;
@@ -23,6 +27,8 @@ interface PendingUserInputPanelProps {
2327

2428
export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserInputPanel({
2529
pendingUserInputs,
30+
cwd,
31+
threadRef,
2632
disabled = false,
2733
respondingRequestIds,
2834
answers,
@@ -39,6 +45,8 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn
3945
<ComposerPendingUserInputCard
4046
key={activePrompt.requestId}
4147
prompt={activePrompt}
48+
cwd={cwd}
49+
threadRef={threadRef}
4250
disabled={disabled}
4351
isResponding={respondingRequestIds.includes(activePrompt.requestId)}
4452
answers={answers}
@@ -52,6 +60,8 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn
5260

5361
const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard({
5462
prompt,
63+
cwd,
64+
threadRef,
5565
disabled,
5666
isResponding,
5767
answers,
@@ -61,6 +71,8 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(
6171
onDismiss,
6272
}: {
6373
prompt: PendingUserInput;
74+
cwd: string | undefined;
75+
threadRef: ScopedThreadRef;
6476
disabled: boolean;
6577
isResponding: boolean;
6678
answers: Record<string, PendingUserInputDraftAnswer>;
@@ -242,7 +254,12 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(
242254
<CollapsiblePanel>
243255
<ComposerBanner.Scroll>
244256
<ComposerBanner.Body className="pe-1 pb-1 wrap-anywhere">
245-
<p className="text-sm text-foreground/85">{activeQuestion.question}</p>
257+
<ChatMarkdown
258+
text={activeQuestion.question}
259+
cwd={cwd}
260+
threadRef={threadRef}
261+
className="text-foreground/85"
262+
/>
246263
{activeQuestion.multiSelect ? (
247264
<p className="mt-1 text-secondary-label text-xs">Select one or more options.</p>
248265
) : null}

0 commit comments

Comments
 (0)