/** * Shared prompt builders for text generation providers. * * Extracts the prompt construction logic that is identical across * Codex, Claude, and any future CLI-based text generation backends. * * @module textGenerationPrompts */import * as Schema from "effect/Schema";import type { ChatAttachment } from "@t3tools/contracts"; import { limitSection } from "./TextGenerationUtils.ts";import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts"; function policyInstruction(instruction: string | undefined): ReadonlyArray<string> { const trimmed = instruction?.trim(); return trimmed ? ["", "Additional instructions:", limitSection(trimmed, 4_000)] : [];} // ---------------------------------------------------------------------------// Commit message// --------------------------------------------------------------------------- export interface CommitMessagePromptInput { branch: string | null; stagedSummary: string; stagedPatch: string; includeBranch: boolean; policy?: TextGenerationPolicy | undefined;} export function buildCommitMessagePrompt(input: CommitMessagePromptInput) { const wantsBranch = input.includeBranch; const prompt = [ "You write concise git commit messages.", wantsBranch ? "Return a JSON object with keys: subject, body, branch." : "Return a JSON object with keys: subject, body.", "Rules:", "- subject must be imperative, <= 72 chars, and no trailing period", "- body can be empty string or short bullet points", ...(wantsBranch ? ["- branch must be a short semantic git branch fragment for this change"] : []), "- capture the primary user-visible or developer-visible change", ...policyInstruction(input.policy?.commitInstructions), "", `Branch: ${input.branch ?? "(detached)"}`, "", "Staged files:", limitSection(input.stagedSummary, 6_000), "", "Staged patch:", limitSection(input.stagedPatch, 40_000), ].join("\n"); if (wantsBranch) { return { prompt, outputSchema: Schema.Struct({ subject: Schema.String, body: Schema.String, branch: Schema.String, }), }; } return { prompt, outputSchema: Schema.Struct({ subject: Schema.String, body: Schema.String, }), };} // ---------------------------------------------------------------------------// PR content// --------------------------------------------------------------------------- export interface PrContentPromptInput { baseBranch: string; headBranch: string; commitSummary: string; diffSummary: string; diffPatch: string; policy?: TextGenerationPolicy | undefined;} export function buildPrContentPrompt(input: PrContentPromptInput) { const prompt = [ "You write GitHub pull request content.", "Return a JSON object with keys: title, body.", "Rules:", "- title should be concise and specific", "- body must be markdown and include headings '## Summary' and '## Testing'", "- under Summary, provide short bullet points", "- under Testing, include bullet points with concrete checks or 'Not run' where appropriate", ...policyInstruction(input.policy?.changeRequestInstructions), "", `Base branch: ${input.baseBranch}`, `Head branch: ${input.headBranch}`, "", "Commits:", limitSection(input.commitSummary, 12_000), "", "Diff stat:", limitSection(input.diffSummary, 12_000), "", "Diff patch:", limitSection(input.diffPatch, 40_000), ].join("\n"); const outputSchema = Schema.Struct({ title: Schema.String, body: Schema.String, }); return { prompt, outputSchema };} // ---------------------------------------------------------------------------// Branch name// --------------------------------------------------------------------------- export interface BranchNamePromptInput { message: string; attachments?: ReadonlyArray<ChatAttachment> | undefined; policy?: TextGenerationPolicy | undefined;} interface PromptFromMessageInput { instruction: string; responseShape: string; rules: ReadonlyArray<string>; message: string; attachments?: ReadonlyArray<ChatAttachment> | undefined; additionalInstructions?: string | undefined;} function buildPromptFromMessage(input: PromptFromMessageInput): string { const attachmentLines = (input.attachments ?? []).map( (attachment) => `- ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes)`, ); const promptSections = [ input.instruction, input.responseShape, "Rules:", ...input.rules.map((rule) => `- ${rule}`), "", "User message:", limitSection(input.message, 8_000), ...policyInstruction(input.additionalInstructions), ]; if (attachmentLines.length > 0) { promptSections.push( "", "Attachment metadata:", limitSection(attachmentLines.join("\n"), 4_000), ); } return promptSections.join("\n");} export function buildBranchNamePrompt(input: BranchNamePromptInput) { const prompt = buildPromptFromMessage({ instruction: "You generate concise git branch names.", responseShape: "Return a JSON object with key: branch.", rules: [ "Branch should describe the requested work from the user message.", "Keep it short and specific (2-6 words).", "Use plain words only, no issue prefixes and no punctuation-heavy text.", "If images are attached, use them as primary context for visual/UI issues.", ], message: input.message, attachments: input.attachments, additionalInstructions: input.policy?.branchInstructions, }); const outputSchema = Schema.Struct({ branch: Schema.String, }); return { prompt, outputSchema };} // ---------------------------------------------------------------------------// Thread title// --------------------------------------------------------------------------- export interface ThreadTitlePromptInput { message: string; attachments?: ReadonlyArray<ChatAttachment> | undefined; policy?: TextGenerationPolicy | undefined;} export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) { const prompt = buildPromptFromMessage({ instruction: "You write concise thread titles for coding conversations.", responseShape: "Return a JSON object with key: title.", rules: [ "Title should summarize the user's request, not restate it verbatim.", "Keep it short and specific (3-8 words).", "Avoid quotes, filler, prefixes, and trailing punctuation.", "If images are attached, use them as primary context for visual/UI issues.", ], message: input.message, attachments: input.attachments, additionalInstructions: input.policy?.threadTitleInstructions, }); const outputSchema = Schema.Struct({ title: Schema.String, }); return { prompt, outputSchema };}
/**
* Shared prompt builders for text generation providers.
*
* Extracts the prompt construction logic that is identical across
* Codex, Claude, and any future CLI-based text generation backends.
*
* @module textGenerationPrompts
*/
import * as Schema from "effect/Schema";
import type { ChatAttachment } from "@t3tools/contracts";
import { limitSection } from "./TextGenerationUtils.ts";
import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts";
function policyInstruction(instruction: string | undefined): ReadonlyArray<string> {
const trimmed = instruction?.trim();
return trimmed ? ["", "Additional instructions:", limitSection(trimmed, 4_000)] : [];
}
// ---------------------------------------------------------------------------
// Commit message
// ---------------------------------------------------------------------------
export interface CommitMessagePromptInput {
branch: string | null;
stagedSummary: string;
stagedPatch: string;
includeBranch: boolean;
policy?: TextGenerationPolicy | undefined;
}
export function buildCommitMessagePrompt(input: CommitMessagePromptInput) {
const wantsBranch = input.includeBranch;
const prompt = [
"You write concise git commit messages.",
wantsBranch
? "Return a JSON object with keys: subject, body, branch."
: "Return a JSON object with keys: subject, body.",
"Rules:",
"- subject must be imperative, <= 72 chars, and no trailing period",
"- body can be empty string or short bullet points",
...(wantsBranch
? ["- branch must be a short semantic git branch fragment for this change"]
: []),
"- capture the primary user-visible or developer-visible change",
...policyInstruction(input.policy?.commitInstructions),
"",
`Branch: ${input.branch ?? "(detached)"}`,
"",
"Staged files:",
limitSection(input.stagedSummary, 6_000),
"",
"Staged patch:",
limitSection(input.stagedPatch, 40_000),
].join("\n");
if (wantsBranch) {
return {
prompt,
outputSchema: Schema.Struct({
subject: Schema.String,
body: Schema.String,
branch: Schema.String,
}),
};
}
return {
prompt,
outputSchema: Schema.Struct({
subject: Schema.String,
body: Schema.String,
}),
};
}
// ---------------------------------------------------------------------------
// PR content
// ---------------------------------------------------------------------------
export interface PrContentPromptInput {
baseBranch: string;
headBranch: string;
commitSummary: string;
diffSummary: string;
diffPatch: string;
policy?: TextGenerationPolicy | undefined;
}
export function buildPrContentPrompt(input: PrContentPromptInput) {
const prompt = [
"You write GitHub pull request content.",
"Return a JSON object with keys: title, body.",
"Rules:",
"- title should be concise and specific",
"- body must be markdown and include headings '## Summary' and '## Testing'",
"- under Summary, provide short bullet points",
"- under Testing, include bullet points with concrete checks or 'Not run' where appropriate",
...policyInstruction(input.policy?.changeRequestInstructions),
"",
`Base branch: ${input.baseBranch}`,
`Head branch: ${input.headBranch}`,
"",
"Commits:",
limitSection(input.commitSummary, 12_000),
"",
"Diff stat:",
limitSection(input.diffSummary, 12_000),
"",
"Diff patch:",
limitSection(input.diffPatch, 40_000),
].join("\n");
const outputSchema = Schema.Struct({
title: Schema.String,
body: Schema.String,
});
return { prompt, outputSchema };
}
// ---------------------------------------------------------------------------
// Branch name
// ---------------------------------------------------------------------------
export interface BranchNamePromptInput {
message: string;
attachments?: ReadonlyArray<ChatAttachment> | undefined;
policy?: TextGenerationPolicy | undefined;
}
interface PromptFromMessageInput {
instruction: string;
responseShape: string;
rules: ReadonlyArray<string>;
message: string;
attachments?: ReadonlyArray<ChatAttachment> | undefined;
additionalInstructions?: string | undefined;
}
function buildPromptFromMessage(input: PromptFromMessageInput): string {
const attachmentLines = (input.attachments ?? []).map(
(attachment) => `- ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes)`,
);
const promptSections = [
input.instruction,
input.responseShape,
"Rules:",
...input.rules.map((rule) => `- ${rule}`),
"",
"User message:",
limitSection(input.message, 8_000),
...policyInstruction(input.additionalInstructions),
];
if (attachmentLines.length > 0) {
promptSections.push(
"",
"Attachment metadata:",
limitSection(attachmentLines.join("\n"), 4_000),
);
}
return promptSections.join("\n");
}
export function buildBranchNamePrompt(input: BranchNamePromptInput) {
const prompt = buildPromptFromMessage({
instruction: "You generate concise git branch names.",
responseShape: "Return a JSON object with key: branch.",
rules: [
"Branch should describe the requested work from the user message.",
"Keep it short and specific (2-6 words).",
"Use plain words only, no issue prefixes and no punctuation-heavy text.",
"If images are attached, use them as primary context for visual/UI issues.",
],
message: input.message,
attachments: input.attachments,
additionalInstructions: input.policy?.branchInstructions,
});
const outputSchema = Schema.Struct({
branch: Schema.String,
});
return { prompt, outputSchema };
}
// ---------------------------------------------------------------------------
// Thread title
// ---------------------------------------------------------------------------
export interface ThreadTitlePromptInput {
message: string;
attachments?: ReadonlyArray<ChatAttachment> | undefined;
policy?: TextGenerationPolicy | undefined;
}
export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) {
const prompt = buildPromptFromMessage({
instruction: "You write concise thread titles for coding conversations.",
responseShape: "Return a JSON object with key: title.",
rules: [
"Title should summarize the user's request, not restate it verbatim.",
"Keep it short and specific (3-8 words).",
"Avoid quotes, filler, prefixes, and trailing punctuation.",
"If images are attached, use them as primary context for visual/UI issues.",
],
message: input.message,
attachments: input.attachments,
additionalInstructions: input.policy?.threadTitleInstructions,
});
const outputSchema = Schema.Struct({
title: Schema.String,
});
return { prompt, outputSchema };
}