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
15 changes: 15 additions & 0 deletions apps/web/src/components/ChatView.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
dismissBranchMismatchForSession,
getStartedThreadModelChangeBlockReason,
hasServerAcknowledgedLocalDispatch,
insertReviewPromptIntoDraft,
isBranchMismatchDismissedForSession,
reconcileMountedTerminalThreadIds,
reconcileRetainedMountedThreadIds,
Expand All @@ -36,6 +37,20 @@ const projectId = ProjectId.make("project-1");
const threadId = ThreadId.make("thread-1");
const now = "2026-03-29T00:00:00.000Z";

describe("insertReviewPromptIntoDraft", () => {
it("inserts into an empty composer", () => {
expect(insertReviewPromptIntoDraft("", " Review these changes. ")).toBe(
"Review these changes.",
);
});

it("appends without overwriting an existing draft", () => {
expect(insertReviewPromptIntoDraft("Keep this context. \n", "Review these changes.")).toBe(
"Keep this context.\n\nReview these changes.",
);
});
});

function makeThread(overrides: Partial<Thread> = {}): Thread {
return {
id: threadId,
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,3 +564,9 @@ export function hasServerAcknowledgedLocalDispatch(input: {
input.localDispatch.sessionUpdatedAt !== (session?.updatedAt ?? null)
);
}

export function insertReviewPromptIntoDraft(currentPrompt: string, reviewPrompt: string): string {
const current = currentPrompt.trimEnd();
const review = reviewPrompt.trim();
return current.length > 0 ? `${current}\n\n${review}` : review;
}
23 changes: 23 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ import {
deriveComposerSendState,
dismissBranchMismatchForSession,
hasServerAcknowledgedLocalDispatch,
insertReviewPromptIntoDraft,
isBranchMismatchDismissedForSession,
shouldShowBranchMismatchBanner,
getStartedThreadModelChangeBlockReason,
Expand Down Expand Up @@ -3068,6 +3069,25 @@ function ChatViewContent(props: ChatViewProps) {
onDiffPanelOpen,
planSidebarOpen,
]);
const openBranchChanges = useCallback(() => {
if (!activeThreadRef || !isServerThread || !isGitRepo) return;
useDiffPanelStore.getState().selectGitScope(activeThreadRef, "branch");
addDiffSurface();
}, [activeThreadRef, addDiffSurface, isGitRepo, isServerThread]);
const insertReviewPrompt = useCallback(() => {
const currentPrompt =
useComposerDraftStore.getState().getComposerDraft(composerDraftTarget)?.prompt ?? "";
setComposerDraftPrompt(
composerDraftTarget,
insertReviewPromptIntoDraft(currentPrompt, primaryServerSettings.reviewPrompt),
);
scheduleComposerFocus();
}, [
composerDraftTarget,
primaryServerSettings.reviewPrompt,
scheduleComposerFocus,
setComposerDraftPrompt,
]);
const addFilesSurface = useCallback(() => {
if (!activeThreadRef || !activeProject) return;
useRightPanelStore.getState().open(activeThreadRef, "files");
Expand Down Expand Up @@ -5716,6 +5736,9 @@ function ChatViewContent(props: ChatViewProps) {
availableEditors={availableEditors}
rightPanelOpen={rightPanelOpen}
gitCwd={gitCwd}
canOpenChanges={isServerThread && isGitRepo}
onOpenChanges={openBranchChanges}
onReview={insertReviewPrompt}
onNewThreadInProject={handleNewThreadInActiveProject}
onRunProjectScript={runProjectScript}
onAddProjectScript={saveProjectScript}
Expand Down
53 changes: 53 additions & 0 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type ThreadId,
} from "@t3tools/contracts";
import { scopeThreadRef } from "@t3tools/client-runtime/environment";
import { FileDiffIcon, ScanSearchIcon } from "lucide-react";
import { memo } from "react";
import GitActionsControl from "../GitActionsControl";
import { type DraftId } from "~/composerDraftStore";
Expand All @@ -19,6 +20,7 @@ import { usePrimaryEnvironmentId } from "../../state/environments";
import { useT3ProjectFileScripts } from "~/hooks/useT3ProjectFileScripts";
import { ProjectFavicon } from "../ProjectFavicon";
import { cn } from "~/lib/utils";
import { Button } from "../ui/button";

interface ChatHeaderProps {
activeThreadEnvironmentId: EnvironmentId;
Expand All @@ -34,6 +36,9 @@ interface ChatHeaderProps {
availableEditors: ReadonlyArray<EditorId>;
rightPanelOpen: boolean;
gitCwd: string | null;
canOpenChanges: boolean;
onOpenChanges: () => void;
onReview: () => void;
onNewThreadInProject: () => void;
onRunProjectScript: (script: ProjectScript) => void;
onAddProjectScript: (input: NewProjectScriptInput) => Promise<ProjectScriptActionResult>;
Expand Down Expand Up @@ -70,6 +75,9 @@ export const ChatHeader = memo(function ChatHeader({
availableEditors,
rightPanelOpen,
gitCwd,
canOpenChanges,
onOpenChanges,
onReview,
onNewThreadInProject,
onRunProjectScript,
onAddProjectScript,
Expand Down Expand Up @@ -163,6 +171,51 @@ export const ChatHeader = memo(function ChatHeader({
: null
}
/>
{activeProjectName ? (
<>
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
size="xs"
variant="outline"
disabled={!canOpenChanges}
onClick={onOpenChanges}
aria-label="View branch changes"
/>
}
>
<FileDiffIcon className="size-3.5" aria-hidden />
<span className="sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5">
Changes
</span>
</TooltipTrigger>
<TooltipPopup side="top">
{canOpenChanges
? "View files changed on this branch"
: "Changes are available after this thread starts"}
</TooltipPopup>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
size="xs"
variant="outline"
onClick={onReview}
aria-label="Insert review prompt"
/>
}
>
<ScanSearchIcon className="size-3.5" aria-hidden />
<span className="ml-0.5">Review</span>
</TooltipTrigger>
<TooltipPopup side="top">Insert the configured review prompt</TooltipPopup>
</Tooltip>
</>
) : null}
{activeProjectName && (
<GitActionsControl
gitCwd={gitCwd}
Expand Down
34 changes: 34 additions & 0 deletions apps/web/src/components/settings/SourceControlWritingSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,40 @@ export function SourceControlWritingSettingsSection() {
</div>
}
/>

<SettingsRow
title="Review prompt"
description="Text inserted into the composer when you choose Review beside the Git actions."
resetAction={
settings.reviewPrompt !== DEFAULT_UNIFIED_SETTINGS.reviewPrompt ? (
<SettingResetButton
label="review prompt"
onClick={() =>
updateSettings({ reviewPrompt: DEFAULT_UNIFIED_SETTINGS.reviewPrompt })
}
/>
) : null
}
>
<div className="mt-3 max-w-2xl pb-3.5">
<Textarea
key={settings.reviewPrompt}
defaultValue={settings.reviewPrompt}
onBlur={(event) => {
const reviewPrompt = event.target.value.trim();
if (reviewPrompt.length === 0) {
event.target.value = settings.reviewPrompt;
return;
}
if (reviewPrompt !== settings.reviewPrompt) {
updateSettings({ reviewPrompt });
}
}}
rows={4}
aria-label="Review prompt"
/>
</div>
</SettingsRow>
</SettingsSection>
);
}
5 changes: 5 additions & 0 deletions docs/integrations/source-control-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ T3 Code works with the platforms your team already uses:
- T3 Code can suggest titles and descriptions based on your commits
- Supports GitHub Pull Requests, GitLab Merge Requests, and Bitbucket Pull Requests

**Review branch changes without leaving the thread**

- **Changes** beside the Git actions opens the branch diff against its base, including after a pull request has been created
- **Review** inserts a configurable review prompt into the composer; edit it under **Settings → Source Control → Text generation**

**Stay on top of open reviews**

- See if your current branch already has an open PR/MR
Expand Down
14 changes: 14 additions & 0 deletions packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ProviderInstanceId } from "./providerInstance.ts";
import {
ClientSettingsSchema,
ClientSettingsPatch,
DEFAULT_REVIEW_PROMPT,
DEFAULT_SERVER_SETTINGS,
ServerSettings,
ServerSettingsPatch,
Expand Down Expand Up @@ -206,6 +207,19 @@ describe("ServerSettings.sourceControlWritingStyle", () => {
});
});

describe("ServerSettings.reviewPrompt", () => {
it("provides a useful default for legacy configs", () => {
expect(decodeServerSettings({}).reviewPrompt).toBe(DEFAULT_REVIEW_PROMPT);
});

it("trims review prompt updates and rejects empty prompts", () => {
expect(
decodeServerSettingsPatch({ reviewPrompt: " Review this carefully. " }).reviewPrompt,
).toBe("Review this carefully.");
expect(() => decodeServerSettingsPatch({ reviewPrompt: " " })).toThrow();
});
});

describe("ServerSettingsPatch.providerInstances", () => {
it("treats providerInstances as an optional whole-map replacement", () => {
const patch = decodeServerSettingsPatch({});
Expand Down
7 changes: 7 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export const DiffTheme = Schema.Literals([
export type DiffTheme = typeof DiffTheme.Type;
export const DEFAULT_DIFF_THEME: DiffTheme = "pierre-dark";

export const DEFAULT_REVIEW_PROMPT =
"Review the current changes. Focus on correctness, regressions, security, performance, and missing tests. Report findings by severity with file and line references.";

export const ClientSettingsSchema = Schema.Struct({
autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
Expand Down Expand Up @@ -538,6 +541,9 @@ export const ServerSettings = Schema.Struct({
sourceControlWriterModelSelection: Schema.NullOr(ModelSelection).pipe(
Schema.withDecodingDefault(Effect.succeed(null)),
),
reviewPrompt: TrimmedNonEmptyString.pipe(
Schema.withDecodingDefault(Effect.succeed(DEFAULT_REVIEW_PROMPT)),
),

// Legacy single-instance-per-driver settings. Continues to be the source
// of truth until `providerInstances` (below) lands per-driver migration
Expand Down Expand Up @@ -682,6 +688,7 @@ export const ServerSettingsPatch = Schema.Struct({
}),
),
sourceControlWriterModelSelection: Schema.optionalKey(Schema.NullOr(ModelSelection)),
reviewPrompt: Schema.optionalKey(TrimmedNonEmptyString),
observability: Schema.optionalKey(
Schema.Struct({
otlpTracesUrl: Schema.optionalKey(TrimmedString),
Expand Down