From e31de23d38d70b6f5d57a20706a8a2d65a92b3f3 Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Mon, 9 Mar 2026 16:00:14 +0530 Subject: [PATCH 01/20] =?UTF-8?q?=E2=9C=A8=20add=20gitmoji-aware=20commit?= =?UTF-8?q?=20message=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Read `t3code.commitMessageStyle` from git config to switch prompt style - Wire `GitCore` into `CodexTextGeneration` runtime/test layers - Add coverage for gitmoji/conventional fallback cases and new feature docs --- .../git/Layers/CodexTextGeneration.test.ts | 260 ++++++++++++++++-- .../src/git/Layers/CodexTextGeneration.ts | 142 ++++++---- apps/server/src/serverLayers.ts | 2 +- docs/features/gitmoji-support.md | 100 +++++++ 4 files changed, 424 insertions(+), 80 deletions(-) create mode 100644 docs/features/gitmoji-support.md diff --git a/apps/server/src/git/Layers/CodexTextGeneration.test.ts b/apps/server/src/git/Layers/CodexTextGeneration.test.ts index 9642f0b06a4..f93110fd30c 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.test.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.test.ts @@ -4,12 +4,15 @@ import { Effect, FileSystem, Layer, Path } from "effect"; import { expect } from "vitest"; import { ServerConfig } from "../../config.ts"; -import { CodexTextGenerationLive } from "./CodexTextGeneration.ts"; import { TextGenerationError } from "../Errors.ts"; import { TextGeneration } from "../Services/TextGeneration.ts"; +import { CodexTextGenerationLive } from "./CodexTextGeneration.ts"; +import { GitCoreLive } from "./GitCore.ts"; +import { GitServiceLive } from "./GitService.ts"; const makeCodexTextGenerationTestLayer = (stateDir: string) => CodexTextGenerationLive.pipe( + Layer.provide(GitCoreLive.pipe(Layer.provideMerge(GitServiceLive))), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), stateDir)), Layer.provideMerge(NodeServices.layer), ); @@ -369,37 +372,37 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { yield* fs.makeDirectory(path.join(process.cwd(), "attachments"), { recursive: true }); yield* fs.writeFile(imagePath, Buffer.from("hello")); - const textGeneration = yield* TextGeneration; - const generated = yield* textGeneration - .generateBranchName({ - cwd: process.cwd(), - message: "Fix layout bug from screenshot.", - attachments: [ - { - type: "image", - id: attachmentId, - name: "bug.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - }) - .pipe( - Effect.tap(() => - fs.stat(imagePath).pipe( - Effect.map((fileInfo) => { - expect(fileInfo.type).toBe("File"); - }), + const textGeneration = yield* TextGeneration; + const generated = yield* textGeneration + .generateBranchName({ + cwd: process.cwd(), + message: "Fix layout bug from screenshot.", + attachments: [ + { + type: "image", + id: attachmentId, + name: "bug.png", + mimeType: "image/png", + sizeBytes: 5, + }, + ], + }) + .pipe( + Effect.tap(() => + fs.stat(imagePath).pipe( + Effect.map((fileInfo) => { + expect(fileInfo.type).toBe("File"); + }), + ), ), - ), - Effect.ensuring( - fs.remove(imagePath).pipe(Effect.catch(() => Effect.void)), - ), - ); + Effect.ensuring( + fs.remove(imagePath).pipe(Effect.catch(() => Effect.void)), + ), + ); - expect(generated.branch).toBe("fix/ui-regression"); - }), - ), + expect(generated.branch).toBe("fix/ui-regression"); + }), + ), ); it.effect("ignores missing attachment ids for codex image inputs", () => @@ -513,4 +516,201 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { }), ), ); + + it.effect( + "includes gitmoji instructions when config is set to 'gitmoji'", + () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "✨ add authentication", + body: "", + }), + stdinMustContain: "subject must start with a gitmoji emoji", + }, + Effect.gen(function* () { + const textGeneration = yield* TextGeneration; + + const generated = yield* textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "feature/auth", + stagedSummary: "M src/auth.ts", + stagedPatch: "+export function authenticate() {}", + }); + + expect(generated.subject).toContain("✨"); + }), + ), + ); + + it.effect( + "uses conventional style when config is set to 'conventional'", + () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "add authentication feature", + body: "", + }), + stdinMustNotContain: "gitmoji", + stdinMustContain: "subject must be imperative", + }, + Effect.gen(function* () { + const textGeneration = yield* TextGeneration; + + const generated = yield* textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "feature/auth", + stagedSummary: "M src/auth.ts", + stagedPatch: "+export function authenticate() {}", + }); + + expect(generated.subject).not.toContain("✨"); + expect(generated.subject).not.toMatch(/^[\p{Emoji}]/u); + }), + ), + ); + + it.effect("defaults to conventional style when config is null", () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "fix memory leak", + body: "", + }), + stdinMustNotContain: "gitmoji", + stdinMustContain: "subject must be imperative", + }, + Effect.gen(function* () { + const textGeneration = yield* TextGeneration; + + const generated = yield* textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "fix/leak", + stagedSummary: "M src/memory.ts", + stagedPatch: "-const leak = []", + }); + + expect(generated.subject).not.toContain("✨"); + }), + ), + ); + + it.effect("handles case-insensitive 'gitmoji' config value", () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "🐛 fix crash on startup", + body: "", + }), + stdinMustContain: "gitmoji", + }, + Effect.gen(function* () { + const textGeneration = yield* TextGeneration; + + const generated = yield* textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "fix/crash", + stagedSummary: "M src/index.ts", + stagedPatch: "+process.on('uncaughtException', logger.error)", + }); + + expect(generated.subject).toMatch(/^[\p{Emoji}]/u); + }), + ), + ); + + it.effect("handles partial match with 'gitmoji' in value", () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "📝 update readme", + body: "", + }), + stdinMustContain: "gitmoji", + }, + Effect.gen(function* () { + const textGeneration = yield* TextGeneration; + + const generated = yield* textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "docs/readme", + stagedSummary: "M README.md", + stagedPatch: "+# Installation", + }); + + expect(generated.subject).toMatch(/^[\p{Emoji}]/u); + }), + ), + ); + + it.effect("ignores invalid config values and defaults to conventional", () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "refactor code structure", + body: "", + }), + stdinMustNotContain: "gitmoji", + }, + Effect.gen(function* () { + const textGeneration = yield* TextGeneration; + + const generated = yield* textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "refactor/structure", + stagedSummary: "M src/index.ts", + stagedPatch: "-export default App", + }); + + expect(generated.subject).not.toMatch(/^[\p{Emoji}]/u); + }), + ), + ); + + it.effect("handles whitespace in config value gracefully", () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "✨ add new endpoints", + body: "", + }), + stdinMustContain: "gitmoji", + }, + Effect.gen(function* () { + const textGeneration = yield* TextGeneration; + + const generated = yield* textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "feature/api", + stagedSummary: "M src/api.ts", + stagedPatch: "+export const handlers = {}", + }); + + expect(generated.subject).toMatch(/^[\p{Emoji}]/u); + }), + ), + ); + + it.effect("includes common gitmoji examples in prompt when enabled", () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "✅ add tests", + body: "", + }), + stdinMustContain: "✨ feat: new feature", + }, + Effect.gen(function* () { + const textGeneration = yield* TextGeneration; + + yield* textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "test/coverage", + stagedSummary: "M test.test.ts", + stagedPatch: "+expect(true).toBe(true)", + }); + }), + ), + ); }); diff --git a/apps/server/src/git/Layers/CodexTextGeneration.ts b/apps/server/src/git/Layers/CodexTextGeneration.ts index 9a8d1d93f0e..afde08d1cb5 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.ts @@ -7,6 +7,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; +import { GitCore } from "../Services/GitCore.ts"; import { TextGenerationError } from "../Errors.ts"; import { type BranchNameGenerationInput, @@ -21,6 +22,8 @@ const CODEX_MODEL = "gpt-5.3-codex"; const CODEX_REASONING_EFFORT = "low"; const CODEX_TIMEOUT_MS = 180_000; +const GITMOJI_CONFIG_KEY = "t3code.commitMessageStyle"; + function toCodexOutputJsonSchema(schema: Schema.Top): unknown { const document = Schema.toJsonSchemaDocument(schema); if (document.definitions && Object.keys(document.definitions).length > 0) { @@ -100,6 +103,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { const path = yield* Path.Path; const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const serverConfig = yield* Effect.service(ServerConfig); + const gitCore = yield* GitCore; type MaterializedImageAttachments = { readonly imagePaths: ReadonlyArray; @@ -313,58 +317,98 @@ const makeCodexTextGeneration = Effect.gen(function* () { }); const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = (input) => { - const wantsBranch = input.includeBranch === true; + return Effect.gen(function* () { + const configValue = yield* gitCore + .readConfigValue(input.cwd, GITMOJI_CONFIG_KEY) + .pipe(Effect.catch(() => Effect.succeed(null))); - 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", - "", - `Branch: ${input.branch ?? "(detached)"}`, - "", - "Staged files:", - limitSection(input.stagedSummary, 6_000), - "", - "Staged patch:", - limitSection(input.stagedPatch, 40_000), - ].join("\n"); + const useGitmoji = configValue === "gitmoji" || + (configValue !== "conventional" && configValue !== null && configValue.toLowerCase().includes("gitmoji")); - const outputSchemaJson = wantsBranch - ? Schema.Struct({ - subject: Schema.String, - body: Schema.String, - branch: Schema.String, - }) - : Schema.Struct({ - subject: Schema.String, - body: Schema.String, - }); + const wantsBranch = input.includeBranch === true; - return runCodexJson({ - operation: "generateCommitMessage", - cwd: input.cwd, - prompt, - outputSchemaJson, - }).pipe( - Effect.map( - (generated) => - ({ - subject: sanitizeCommitSubject(generated.subject), - body: generated.body.trim(), - ...("branch" in generated && typeof generated.branch === "string" - ? { branch: sanitizeFeatureBranchName(generated.branch) } - : {}), - }) satisfies CommitMessageGenerationResult, - ), - ); + const promptSections = [ + "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:", + ]; + + if (useGitmoji) { + promptSections.push( + "- subject must start with a gitmoji emoji (e.g., ✨, 🐛, ♻️, 📝, etc.)", + "- after the emoji, add a space and write the commit message in imperative mood", + "- subject must be <= 72 chars total (including emoji) 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", + "", + "Common gitmoji examples:", + "✨ feat: new feature", + "🐛 fix: bug fix", + "♻️ refactor: code refactoring", + "📝 docs: documentation", + "✅ test: tests", + "🎨 style: formatting", + "⚡ perf: performance", + "🔧 chore: maintenance", + ); + } else { + promptSections.push( + "- 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", + ); + } + + promptSections.push( + "", + `Branch: ${input.branch ?? "(detached)"}`, + "", + "Staged files:", + limitSection(input.stagedSummary, 6_000), + "", + "Staged patch:", + limitSection(input.stagedPatch, 40_000), + ); + + const prompt = promptSections.join("\n"); + + const outputSchemaJson = wantsBranch + ? Schema.Struct({ + subject: Schema.String, + body: Schema.String, + branch: Schema.String, + }) + : Schema.Struct({ + subject: Schema.String, + body: Schema.String, + }); + + return yield* runCodexJson({ + operation: "generateCommitMessage", + cwd: input.cwd, + prompt, + outputSchemaJson, + }).pipe( + Effect.map( + (generated) => + ({ + subject: sanitizeCommitSubject(generated.subject), + body: generated.body.trim(), + ...("branch" in generated && typeof generated.branch === "string" + ? { branch: sanitizeFeatureBranchName(generated.branch) } + : {}), + }) satisfies CommitMessageGenerationResult, + ), + ); + }); }; const generatePrContent: TextGenerationShape["generatePrContent"] = (input) => { diff --git a/apps/server/src/serverLayers.ts b/apps/server/src/serverLayers.ts index b0630a55b95..5e414472c85 100644 --- a/apps/server/src/serverLayers.ts +++ b/apps/server/src/serverLayers.ts @@ -69,7 +69,7 @@ export function makeServerProviderLayer(): Layer.Layer< export function makeServerRuntimeServicesLayer() { const gitCoreLayer = GitCoreLive.pipe(Layer.provideMerge(GitServiceLive)); - const textGenerationLayer = CodexTextGenerationLive; + const textGenerationLayer = CodexTextGenerationLive.pipe(Layer.provide(gitCoreLayer)); const orchestrationLayer = OrchestrationEngineLive.pipe( Layer.provide(OrchestrationProjectionPipelineLive), diff --git a/docs/features/gitmoji-support.md b/docs/features/gitmoji-support.md new file mode 100644 index 00000000000..ede8add9f47 --- /dev/null +++ b/docs/features/gitmoji-support.md @@ -0,0 +1,100 @@ +# Gitmoji Support for Commit Messages + +Automatically generate commit messages with gitmoji emojis. + +## Overview + +T3 Code supports generating git commit messages with [gitmoji](https://gitmoji.dev/) prefixes. This feature helps maintain consistent commit history with visual emoji indicators. + +## Configuration + +### Enable Gitmoji + +To enable gitmoji for all commits in a repository: + +```bash +git config t3code.commitMessageStyle gitmoji +``` + +### Disable Gitmoji + +To use conventional commit style instead: + +```bash +git config t3code.commitMessageStyle conventional +``` + +### Remove Custom Setting + +To revert to default behavior: + +```bash +git config --unset t3code.commitMessageStyle +``` + +## Scope + +Configuration is repository-specific: + +- **Repository-level**: Run the config command in your project root +- **Global**: Add `--global` flag to apply to all your projects + ```bash + git config --global t3code.commitMessageStyle gitmoji + ``` + +## Commit Examples + +### With Gitmoji Enabled + +``` +✨ add user authentication flow +🐛 fix null pointer exception in login +♻️ refactor database connection pool +📝 update API documentation +✅ add unit tests for payment module +🎨 improve button hover animations +⚡ optimize database query performance +🔧 upgrade dependencies to latest versions +``` + +### With Gitmoji Disabled (Conventional) + +``` +feat: add user authentication flow +fix: null pointer exception in login +refactor: database connection pool +docs: update API documentation +test: add unit tests for payment module +style: improve button hover animations +perf: optimize database query performance +chore: upgrade dependencies to latest versions +``` + +## Supported Gitmojis + +The AI model will choose appropriate gitmoji based on your changes: + +| Emoji | Keyword | Description | +| ----- | -------- | ------------------------------- | +| ✨ | feat | New feature | +| 🐛 | fix | Bug fix | +| ♻️ | refactor | Code refactoring | +| 📝 | docs | Documentation changes | +| ✅ | test | Adding or updating tests | +| 🎨 | style | Code style changes (formatting) | +| ⚡ | perf | Performance improvements | +| 🔧 | chore | Maintenance tasks | + +## Platform Availability + +✅ Works on all platforms: + +- Web app (http://localhost:3773) +- Desktop app (Electron) +- Any future clients using the T3 Code backend + +## Notes + +- Commit messages are generated by AI, so gitmoji selection is contextual to your changes +- Generated messages always follow the configured style +- Feature is backend-driven, so it applies automatically when enabled From 330ea17cf0ed52225deaf1ec1eed8596c7d1edf1 Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Mon, 9 Mar 2026 16:25:32 +0530 Subject: [PATCH 02/20] Add git config setup to commit message tests --- .../git/Layers/CodexTextGeneration.test.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/apps/server/src/git/Layers/CodexTextGeneration.test.ts b/apps/server/src/git/Layers/CodexTextGeneration.test.ts index f93110fd30c..364d985b8b4 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.test.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.test.ts @@ -2,6 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; import { Effect, FileSystem, Layer, Path } from "effect"; import { expect } from "vitest"; +import { spawnSync } from "node:child_process"; import { ServerConfig } from "../../config.ts"; import { TextGenerationError } from "../Errors.ts"; @@ -531,6 +532,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { Effect.gen(function* () { const textGeneration = yield* TextGeneration; + yield* Effect.sync(() => { + spawnSync("git", ["config", "t3code.commitMessageStyle", "gitmoji"], { + cwd: process.cwd(), + }); + }); + const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "feature/auth", @@ -539,6 +546,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { }); expect(generated.subject).toContain("✨"); + + yield* Effect.sync(() => { + spawnSync("git", ["config", "--unset", "t3code.commitMessageStyle"], { + cwd: process.cwd(), + }); + }); }), ), ); @@ -558,6 +571,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { Effect.gen(function* () { const textGeneration = yield* TextGeneration; + yield* Effect.sync(() => { + spawnSync("git", ["config", "t3code.commitMessageStyle", "conventional"], { + cwd: process.cwd(), + }); + }); + const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "feature/auth", @@ -567,6 +586,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { expect(generated.subject).not.toContain("✨"); expect(generated.subject).not.toMatch(/^[\p{Emoji}]/u); + + yield* Effect.sync(() => { + spawnSync("git", ["config", "--unset", "t3code.commitMessageStyle"], { + cwd: process.cwd(), + }); + }); }), ), ); @@ -584,6 +609,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { Effect.gen(function* () { const textGeneration = yield* TextGeneration; + yield* Effect.sync(() => { + spawnSync("git", [ "config", "--unset", "t3code.commitMessageStyle"], { + cwd: process.cwd(), + }); + }); + const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "fix/leak", @@ -608,6 +639,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { Effect.gen(function* () { const textGeneration = yield* TextGeneration; + yield* Effect.sync(() => { + spawnSync("git", [ "config", "t3code.commitMessageStyle", "GITMOJI"], { + cwd: process.cwd(), + }); + }); + const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "fix/crash", @@ -616,6 +653,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { }); expect(generated.subject).toMatch(/^[\p{Emoji}]/u); + + yield* Effect.sync(() => { + spawnSync("git", [ "config", "--unset", "t3code.commitMessageStyle"], { + cwd: process.cwd(), + }); + }); }), ), ); @@ -632,6 +675,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { Effect.gen(function* () { const textGeneration = yield* TextGeneration; + yield* Effect.sync(() => { + spawnSync("git", [ "config", "t3code.commitMessageStyle", "use-gitmoji"], { + cwd: process.cwd(), + }); + }); + const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "docs/readme", @@ -640,6 +689,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { }); expect(generated.subject).toMatch(/^[\p{Emoji}]/u); + + yield* Effect.sync(() => { + spawnSync("git", [ "config", "--unset", "t3code.commitMessageStyle"], { + cwd: process.cwd(), + }); + }); }), ), ); @@ -656,6 +711,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { Effect.gen(function* () { const textGeneration = yield* TextGeneration; + yield* Effect.sync(() => { + spawnSync("git", [ "config", "t3code.commitMessageStyle", "invalid-value"], { + cwd: process.cwd(), + }); + }); + const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "refactor/structure", @@ -664,6 +725,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { }); expect(generated.subject).not.toMatch(/^[\p{Emoji}]/u); + + yield* Effect.sync(() => { + spawnSync("git", [ "config", "--unset", "t3code.commitMessageStyle"], { + cwd: process.cwd(), + }); + }); }), ), ); @@ -680,6 +747,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { Effect.gen(function* () { const textGeneration = yield* TextGeneration; + yield* Effect.sync(() => { + spawnSync("git", [ "config", "t3code.commitMessageStyle", " gitmoji "], { + cwd: process.cwd(), + }); + }); + const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "feature/api", @@ -688,6 +761,12 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { }); expect(generated.subject).toMatch(/^[\p{Emoji}]/u); + + yield* Effect.sync(() => { + spawnSync("git", [ "config", "--unset", "t3code.commitMessageStyle"], { + cwd: process.cwd(), + }); + }); }), ), ); @@ -704,12 +783,24 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { Effect.gen(function* () { const textGeneration = yield* TextGeneration; + yield* Effect.sync(() => { + spawnSync("git", ["config", "t3code.commitMessageStyle", "gitmoji"], { + cwd: process.cwd(), + }); + }); + yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "test/coverage", stagedSummary: "M test.test.ts", stagedPatch: "+expect(true).toBe(true)", }); + + yield* Effect.sync(() => { + spawnSync("git", ["config", "--unset", "t3code.commitMessageStyle"], { + cwd: process.cwd(), + }); + }); }), ), ); From 2a01ebbe72459ec63290507e871ced5708311403 Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Mon, 16 Mar 2026 15:39:06 +0530 Subject: [PATCH 03/20] git functionality. Here's the conventional commit message: feat(git): add getRecentCommitMessages function Add method to retrieve recent commit messages from git repository. Supports optional count parameter with default of 5 messages. Excludes merge commits and returns clean message list. - Add interface definition in GitCoreShape - Implement git log command execution with proper error handling - Add comprehensive unit tests for functionality - Follows existing Effect patterns in codebase This commit adds new git functionality that can be used for features like gitmoji suggestions or commit history analysis. --- apps/server/src/git/Layers/GitCore.test.ts | 1 + apps/server/src/git/Layers/GitCore.ts | 16 ++++++++++++++++ apps/server/src/git/Services/GitCore.ts | 8 ++++++++ 3 files changed, 25 insertions(+) diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index 6c98229e8a7..b9a79d2fb44 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -103,6 +103,7 @@ const makeIsolatedGitCore = (gitService: GitServiceShape) => pullCurrentBranch: (cwd) => core.pullCurrentBranch(cwd), readRangeContext: (cwd, baseBranch) => core.readRangeContext(cwd, baseBranch), readConfigValue: (cwd, key) => core.readConfigValue(cwd, key), + getRecentCommitMessages: (cwd, count?) => core.getRecentCommitMessages(cwd, count), listBranches: (input) => core.listBranches(input), createWorktree: (input) => core.createWorktree(input), fetchPullRequestBranch: (input) => core.fetchPullRequestBranch(input), diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index f5b9168abbb..c480d51baa2 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -994,6 +994,21 @@ const makeGitCore = Effect.gen(function* () { Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)), ); + const getRecentCommitMessages: GitCoreShape["getRecentCommitMessages"] = (cwd, count = 5) => + runGitStdout("GitCore.getRecentCommitMessages", cwd, [ + "log", + `-${count}`, + "--pretty=%B%x00", + "--no-merges", + ]).pipe( + Effect.map((stdout) => { + return stdout + .split("\x00") + .map((commit) => commit.trim()) + .filter((commit) => commit.length > 0); + }), + ); + const listBranches: GitCoreShape["listBranches"] = (input) => Effect.gen(function* () { const branchRecencyPromise = readBranchRecency(input.cwd).pipe( @@ -1410,6 +1425,7 @@ const makeGitCore = Effect.gen(function* () { pullCurrentBranch, readRangeContext, readConfigValue, + getRecentCommitMessages, listBranches, createWorktree, fetchPullRequestBranch, diff --git a/apps/server/src/git/Services/GitCore.ts b/apps/server/src/git/Services/GitCore.ts index 879927934ef..699cf7b765f 100644 --- a/apps/server/src/git/Services/GitCore.ts +++ b/apps/server/src/git/Services/GitCore.ts @@ -137,6 +137,14 @@ export interface GitCoreShape { key: string, ) => Effect.Effect; + /** + * Get recent commit messages from the repository. + */ + readonly getRecentCommitMessages: ( + cwd: string, + count?: number, + ) => Effect.Effect; + /** * List local + remote branches and branch metadata. */ From 59e76821a07322bc2d13afe449da3d1adaa905e2 Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Mon, 16 Mar 2026 15:48:04 +0530 Subject: [PATCH 04/20] feat(git): add commit pattern analysis functionality Add intelligent analysis of git commit messages to detect patterns and provide examples. Supports emoji detection, conventional commit types, and scope analysis for AI prompt generation. - Add analyzeCommitPatterns function with comprehensive interface - Implement pattern detection for emojis, types, and scopes with configurable thresholds - Select best commit examples based on pattern completeness - Provide detailed analysis output with detected patterns - Add extensive test coverage (18 test cases) covering all edge cases - Handle various commit formats: one-liners, multi-line, mixed styles - Support Unicode emojis and conventional commit specification - Graceful handling of empty repositories and git command failures This commit adds sophisticated pattern detection that can help AI systems understand and adapt to existing commit styles in repositories. --- .../src/git/analyzeCommitPatterns.test.ts | 673 ++++++++++++++++++ apps/server/src/git/analyzeCommitPatterns.ts | 148 ++++ 2 files changed, 821 insertions(+) create mode 100644 apps/server/src/git/analyzeCommitPatterns.test.ts create mode 100644 apps/server/src/git/analyzeCommitPatterns.ts diff --git a/apps/server/src/git/analyzeCommitPatterns.test.ts b/apps/server/src/git/analyzeCommitPatterns.test.ts new file mode 100644 index 00000000000..ba9115e3b8f --- /dev/null +++ b/apps/server/src/git/analyzeCommitPatterns.test.ts @@ -0,0 +1,673 @@ +import { describe, it, expect } from "vitest"; +import { Effect } from "effect"; + +import { analyzeCommitPatterns } from "./analyzeCommitPatterns"; +import { GitCommandError } from "./Errors.ts"; + +describe("analyzeCommitPatterns", () => { + const mockDependencies = (commits: readonly string[] | Error) => ({ + getRecentCommitMessages: (_cwd: string, _count?: number) => + commits instanceof Error + ? Effect.fail( + new GitCommandError({ + operation: "getRecentCommitMessages", + command: "git log", + cwd: "/fake/cwd", + detail: commits.message, + cause: commits, + }), + ) + : Effect.succeed(commits), + }); + + const runAnalysis = (commits: readonly string[] | Error) => { + const result = Effect.runSync(analyzeCommitPatterns(mockDependencies(commits))("/fake/cwd")); + return result; + }; + + it("detects emoji + scope + type pattern", () => { + const commits = [ + `✨ feat(git): improve commit pattern detection logic + Enhance commit message analysis to detect emojis, scopes and types more accurately. + Improves pattern matching logic for better AI prompt generation. + - Improve regex for emoji detection + - Add validation for commit scope pattern + - Improve detection accuracy for conventional types + - Update tests to cover mixed commit styles + This update ensures the system can correctly detect structured commit formats used in modern repositories.`, + + `🐛 fix: handle empty commit history safely + Fix issue where commit analysis failed when repository had no commit history. + Adds graceful fallback behaviour. + - Handle empty git log response + - Prevent runtime errors when parsing commits + - Add fallback analysis message + This ensures the commit analysis feature works correctly even in newly initialised repositories.`, + + `feat(commit): support emoji detection in commit messages + Add functionality to detect emoji prefixes in commit subjects. + Uses Unicode emoji pattern matching for better accuracy. + - Add emoji regex detection + - Update commit pattern interface + - Add unit tests for emoji commits + This change allows commit analysis to detect gitmoji-style workflows.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.examples[0]).toContain("✨ feat(git): improve commit pattern detection logic"); + expect(result.analysis).toContain("Uses emoji prefixes"); + expect(result.analysis).toContain("Uses scopes"); + expect(result.analysis).toContain("Uses conventional commit types"); + }); + + it("detects emoji + type pattern (no scope)", () => { + const commits = [ + `🐛 fix: handle empty commit history safely + Fix issue where commit analysis failed when repository had no commit history. + Adds graceful fallback behaviour. + - Handle empty git log response + - Prevent runtime errors when parsing commits + - Add fallback analysis message + This ensures the commit analysis feature works correctly even in newly initialised repositories.`, + + `⚡ perf: optimise commit pattern detection performance + Improve efficiency of commit message analysis for large repositories. + Reduces unnecessary regex evaluations. + - Cache compiled regex patterns + - Reduce redundant message parsing + - Add performance benchmarks + This optimisation significantly reduces analysis time for large commit histories.`, + + `📚 update documentation for commit pattern analysis + Improve documentation explaining how commit analysis works internally. + Adds examples and better explanations. + - Add usage examples + - Document regex pattern logic + - Clarify commit analysis workflow + This documentation update helps contributors understand commit analysis behaviour.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.examples[0]).toMatch(/🐛|⚡|📚/); + expect(result.analysis).toContain("Uses emoji prefixes"); + expect(result.analysis).toContain("Uses conventional commit types"); + }); + + it("detects scope + type pattern (no emoji)", () => { + const commits = [ + `feat(commit): support emoji detection in commit messages + Add functionality to detect emoji prefixes in commit subjects. + Uses Unicode emoji pattern matching for better accuracy. + - Add emoji regex detection + - Update commit pattern interface + - Add unit tests for emoji commits + This change allows commit analysis to detect gitmoji-style workflows.`, + + `fix(parser): correctly extract commit subject lines + Fix issue where commit subject parsing failed for multiline messages. + Ensures only the first line is used for pattern detection. + - Improve message splitting logic + - Handle edge cases for newline characters + - Add regression tests + This change improves accuracy of commit pattern detection.`, + + `refactor: simplify commit analysis pipeline + Refactor commit analysis implementation to improve readability and maintainability. + Removes duplicated logic and clarifies intent. + - Extract pattern detection helpers + - Simplify analysis builder logic + - Improve naming for internal variables + This refactor makes the analysis module easier to maintain and extend.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(false); + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.analysis).toContain("Uses scopes"); + expect(result.analysis).toContain("Uses conventional commit types"); + }); + + it("detects type only pattern", () => { + const commits = [ + `refactor: simplify commit analysis pipeline + Refactor commit analysis implementation to improve readability and maintainability. + Removes duplicated logic and clarifies intent. + - Extract pattern detection helpers + - Simplify analysis builder logic + - Improve naming for internal variables + This refactor makes the analysis module easier to maintain and extend.`, + + `test: add coverage for commit pattern detection + Introduce additional tests to verify detection of emojis, scopes and types. + Improves reliability of commit analysis logic. + - Add emoji detection tests + - Add scope detection tests + - Add conventional commit detection tests + These tests ensure the commit analysis behaves correctly across multiple commit styles.`, + + `chore: update dependencies to latest versions + Update project dependencies to their latest stable versions. + Ensures security patches and bug fixes are included. + - Update package.json + - Run npm audit fix + - Update lockfile + This update keeps the project secure and up-to-date.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(false); + expect(result.hasScopes).toBe(false); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.analysis).toContain("Uses conventional commit types"); + }); + + it("detects emoji only pattern", () => { + const commits = [ + `📚 update documentation for commit pattern analysis + Improve documentation explaining how commit analysis works internally. + Adds examples and better explanations. + - Add usage examples + - Document regex pattern logic + - Clarify commit analysis workflow + This documentation update helps contributors understand commit analysis behaviour.`, + + `🎨 improve UI styling for commit viewer + Enhance visual appearance of commit message viewer component. + Uses modern design patterns for better UX. + - Update color scheme + - Improve spacing and typography + - Add hover effects + This visual update makes the commit viewer more pleasant to use.`, + + `✅ add CI pipeline for automated testing + Set up continuous integration pipeline for automated testing. + Ensures code quality and prevents regressions. + - Configure GitHub Actions + - Add automated test runs + - Set up deployment checks + This CI setup improves development workflow and code quality.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasScopes).toBe(false); + expect(result.hasTypes).toBe(false); + expect(result.examples).toHaveLength(1); + expect(result.analysis).toContain("Uses emoji prefixes"); + }); + + it("selects best example when mixed patterns exist", () => { + const commits = [ + `📚 update documentation + Simple doc update with no type or scope.`, + + `✨ feat(git): improve commit pattern detection logic + Enhance commit message analysis to detect emojis, scopes and types more accurately. + Improves pattern matching logic for better AI prompt generation. + - Improve regex for emoji detection + - Add validation for commit scope pattern + - Improve detection accuracy for conventional types + - Update tests to cover mixed commit styles + This update ensures the system can correctly detect structured commit formats used in modern repositories.`, + + `refactor(api): simplify code + Refactor with no emoji but has type and scope.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.examples[0]).toContain("✨ feat(git): improve commit pattern detection logic"); + }); + + it("handles empty repository (no commits)", () => { + const result = runAnalysis([]); + + expect(result.hasEmojis).toBe(false); + expect(result.hasScopes).toBe(false); + expect(result.hasTypes).toBe(false); + expect(result.examples).toEqual([]); + expect(result.analysis).toContain("No previous commits found"); + }); + + it("handles git log failure gracefully", () => { + const result = runAnalysis(new Error("Git command failed")); + + expect(result.hasEmojis).toBe(false); + expect(result.hasScopes).toBe(false); + expect(result.hasTypes).toBe(false); + expect(result.examples).toEqual([]); + expect(result.analysis).toContain("No previous commits found"); + }); + + it("handles commits with only subject lines (no body)", () => { + const commits = [ + "✨ feat: initial commit", + "🐛 fix: bug", + "♻️ refactor: code", + "📝 docs: readme", + "🚀 chore: release", + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.examples[0]).toBe("✨ feat: initial commit"); + }); + + it("detects no pattern when commits are unstructured", () => { + const commits = [ + `Initial commit + Set up the project structure.`, + `Update dependencies + Ran npm update.`, + `Fix bug + Fixed the login issue.`, + `Add feature + Added user profile.`, + `Refactor code + Cleaned up the codebase.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(false); + expect(result.hasScopes).toBe(false); + expect(result.hasTypes).toBe(false); + expect(result.analysis).toContain("No specific pattern detected"); + }); + + it("correctly parses the test commit message from requirements", () => { + const commits = [ + `feat(git): add getRecentCommitMessages function + Add method to retrieve recent commit messages from git repository. + Supports optional count parameter with default of 5 messages. + Excludes merge commits and returns clean message list. + - Add interface definition in GitCoreShape + - Implement git log command execution with proper error handling + - Add comprehensive unit tests for functionality + - Follows existing Effect patterns in codebase + This commit adds new git functionality that can be used for features like gitmoji suggestions or commit history analysis.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(false); + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.examples[0]).toContain("feat(git): add getRecentCommitMessages function"); + expect(result.analysis).toContain("Uses scopes"); + expect(result.analysis).toContain("Uses conventional commit types"); + }); + + it("handles one-liner commits with emoji + type + scope", () => { + const commits = [ + "✨ feat(git): add feature", + "🐛 fix(api): resolve bug", + "🚀 chore(deps): update packages", + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.examples[0]).toBe("✨ feat(git): add feature"); + }); + + it("handles one-liner commits with emoji + type (no scope)", () => { + const commits = ["✨ feat: add feature", "🐛 fix: resolve bug", "🚀 chore: update packages"]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + }); + + it("handles one-liner commits with emoji only (no type)", () => { + const commits = ["✨ add feature", "🐛 resolve bug", "🚀 update packages"]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasScopes).toBe(false); + expect(result.hasTypes).toBe(false); + expect(result.examples).toHaveLength(1); + }); + + it("handles one-liner commits with type only (no emoji)", () => { + const commits = ["feat: add feature", "fix: resolve bug", "chore: update packages"]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(false); + expect(result.hasScopes).toBe(false); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + }); + + it("handles one-liner commits with type + scope (no emoji)", () => { + const commits = [ + "feat(git): add feature", + "fix(api): resolve bug", + "chore(deps): update packages", + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(false); + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + }); + + it("handles one-liner commits with no pattern", () => { + const commits = ["add feature", "resolve bug", "update packages"]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(false); + expect(result.hasScopes).toBe(false); + expect(result.hasTypes).toBe(false); + expect(result.analysis).toContain("No specific pattern detected"); + }); + + it("handles commits with multiple emojis", () => { + const commits = ["✨🚀 feat: add feature", "🐛🔧 fix: resolve bug", "📝🎨 docs: update readme"]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + }); + + it("handles commits with special characters and URLs", () => { + const commits = [ + `feat(api): add OAuth2 authentication +Integrate with GitHub OAuth2 API. +https://github.com/oauth/docs +- Add OAuth2 flow +- Handle tokens +- Fix edge case: API rate-limit (403 errors) +Supports scopes: read:user, repo, admin:repo_hook`, + + `fix(config): resolve CORS issues +Fixed Access-Control-Allow-Origin errors. +See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +- Configure allowed origins +- Set proper headers +- Handle preflight requests`, + + `docs(readme): update installation guide +Updated URLs for new package registry. +https://npmjs.com/package/v2 +- Update install command +- Fix broken links +- Add troubleshooting section`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + }); + + it("handles commits with mixed case types", () => { + const commits = ["Feat: add feature", "FIX: resolve bug", "Chore: update packages"]; + + const result = runAnalysis(commits); + + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + }); + + it("handles commits with colons in subject", () => { + const commits = [ + `feat: add feature: with colon in description +Implement new feature that handles colons.`, + `fix(api): resolve: rate limiting issue +Fixed the API rate:limit problem.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasTypes).toBe(true); + }); + + it("handles commits with trailing whitespace", () => { + const commits = ["feat: add feature ", "fix: resolve bug\t", "chore: update packages \n"]; + + const result = runAnalysis(commits); + + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.examples[0]?.trim()).toBe("feat: add feature"); + }); + + it("handles commits with only body (no proper subject line)", () => { + const commits = [ + `This is a commit with no subject line +Just the body of the commit message. +Describing what was done.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(false); + expect(result.hasScopes).toBe(false); + expect(result.hasTypes).toBe(false); + }); + + it("handles very long commit messages", () => { + const commits = [ + `feat(git): add comprehensive commit message analysis + This is a very long commit message with lots of details. + - Add support for emoji detection + - Add support for scope detection + - Add support for conventional commit types + - Add comprehensive test coverage + - Handle edge cases like one-liners, empty bodies, special characters + - Support mixed commit styles + - Add intelligent example selection + - Ensure proper error handling + - Follow existing code patterns + - Add detailed documentation + - Performance optimizations for large repositories + - Cache compiled regex patterns + - Reduce redundant parsing operations + - Add benchmarks for performance validation + This commit represents a substantial feature addition with comprehensive testing and documentation.`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + }); + + it("handles commits with code blocks and technical content", () => { + const commits = [ + `feat(parser): add commit message parser + Add function to parse commit messages efficiently. + + \`\`\`typescript + function parseCommit(message: string): Commit { + return { + type: extractType(message), + scope: extractScope(message), + subject: extractSubject(message), + }; + } + \`\`\` + + Usage: + \`\`\` + const commit = parseCommit("feat(api): add endpoint"); + // { type: "feat", scope: "api", subject: "add endpoint" } + \`\`\` + + - Support emoji prefixes + - Handle edge cases + - Add unit tests`, + + `fix(types): resolve type inference issue + Fixed issue with generic type constraints. + + Before: + \`\`\`typescript + function parse(input: T): T + \`\`\` + + After: + \`\`\`typescript + function parse(input: T): Commit + \`\`\` + + - Add proper type constraints + - Update all call sites + - Add regression tests`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + }); + + it("handles mixed one-liner and multi-line commits", () => { + const commits = [ + "✨ feat: add feature", + `🐛 fix(api): resolve bug + Fixed null pointer exception in API handler. + - Add null checks + - Add error handling + This prevents crashes on invalid input.`, + "🚀 chore: update packages", + `feat(ui): improve button styling + Better visual feedback for user interactions. + - Add hover states + - Add active states + - Improve accessibility`, + "📝 docs: update readme", + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + }); + + it("handles commits with only bullet points in body", () => { + const commits = [ + `feat(git): add commit analysis + - Support emoji detection + - Support scope detection + - Support type detection + - Add comprehensive tests + - Add documentation`, + + `fix(api): resolve rate limiting + - Add rate limiter + - Configure limits + - Add monitoring + - Add alerts`, + ]; + + const result = runAnalysis(commits); + + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + }); + + it("handles commits with various line endings", () => { + const commits = [ + "feat: add feature\r\n\r\nWith CRLF line endings", + "fix: resolve bug\n\nWith LF line endings", + "chore: update\rWith mixed line endings", + ]; + + const result = runAnalysis(commits); + + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + }); + + it("handles commits with numbers in types", () => { + const commits = ["feat: add feature", "fix2: resolve bug", "v1: initial release"]; + + const result = runAnalysis(commits); + + expect(result.hasTypes).toBe(false); + expect(result.examples).toHaveLength(1); + expect(result.examples[0]).toBe("feat: add feature"); + }); + + it("handles empty strings and whitespace-only commits", () => { + const commits = ["", " ", "\n\n", "feat: actual commit", " \t "]; + + const result = runAnalysis(commits); + + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.examples[0]).toBe("feat: actual commit"); + }); + + it("handles real-world mixed commit styles", () => { + const commits = [ + "✨ feat: add user authentication", + "fix(api): resolve rate limiting", + `feat(ui): improve dashboard + Redesign dashboard with new charts. + - Add pie charts + - Add bar charts + - Add line charts + This improves data visualization.`, + "🐛 fix: memory leak in event handlers", + "refactor(core): optimize data structures", + "📚 docs: update API documentation", + "chore: update dependencies", + `feat(auth): add OAuth2 support + Integrate with GitHub OAuth2. + - Add OAuth2 flow + - Handle tokens + - Store sessions`, + "test: add unit tests for auth module", + "⚡ perf: cache compiled regex patterns", + ]; + + const result = runAnalysis(commits); + + expect(result.hasEmojis).toBe(true); + expect(result.hasScopes).toBe(true); + expect(result.hasTypes).toBe(true); + expect(result.examples).toHaveLength(1); + expect(result.examples[0]).toMatch(/✨ feat:|feat\(auth\):|feat\(ui\):|feat\(git\):/); + }); +}); diff --git a/apps/server/src/git/analyzeCommitPatterns.ts b/apps/server/src/git/analyzeCommitPatterns.ts new file mode 100644 index 00000000000..1bdfaae75e8 --- /dev/null +++ b/apps/server/src/git/analyzeCommitPatterns.ts @@ -0,0 +1,148 @@ +import { Effect } from "effect"; + +import { GitCommandError } from "./Errors.ts"; + +export interface CommitPatternAnalysis { + readonly hasEmojis: boolean; + readonly hasScopes: boolean; + readonly hasTypes: boolean; + readonly examples: readonly string[]; + readonly analysis: string; +} + +/** + * Conventional commit types used across most projects. + * This includes the official specification and common extensions. + */ +const COMMIT_TYPES = [ + "feat", + "fix", + "docs", + "style", + "refactor", + "perf", + "test", + "build", + "ci", + "chore", + "revert", + "security", + "deps", + "config", + "infra", + "adhoc", +] as const; + +/** + * Regex to detect conventional commit messages. + * + * Examples supported: + * feat: add feature + * fix(auth): resolve login issue + * ✨ feat: add feature (with optional emoji prefix) + * ✨🚀 feat: add feature (with multiple emoji prefix) + * Feat: add feature (case-insensitive) + */ +export const TYPE_PATTERN = new RegExp( + `^(?:[\\p{Extended_Pictographic}\\p{Emoji_Presentation}]\\s*)*(${COMMIT_TYPES.join("|")})(\\([^)]+\\))?:`, + "iu", +); + +const SCOPE_PATTERN = /\([^)]+\)/; + +const EMOJI_PATTERN = /^[\p{Extended_Pictographic}\p{Emoji_Presentation}]/u; + +export interface AnalyzeCommitPatternsDependencies { + readonly getRecentCommitMessages: ( + cwd: string, + count?: number, + ) => Effect.Effect; +} + +/** + * Analyzes recent git commit messages to detect patterns and provide examples. + * Returns analysis of emoji usage, scopes, conventional types, and real examples. + * + * Note: Commit messages include full body (subject + body) for better pattern detection + * and more useful examples for AI prompt generation. + */ +export const analyzeCommitPatterns = + (dependencies: AnalyzeCommitPatternsDependencies) => + (cwd: string): Effect.Effect => + Effect.gen(function* () { + const recentMessages = yield* dependencies + .getRecentCommitMessages(cwd, 5) + .pipe(Effect.catch(() => Effect.succeed([]))); + + const validMessages = recentMessages.filter((msg) => msg.trim().length > 0); + + if (validMessages.length === 0) { + return { + hasEmojis: false, + hasScopes: false, + hasTypes: false, + examples: [], + analysis: "No previous commits found. Use conventional commits.", + }; + } + + const subjects = validMessages.map((msg) => { + const firstLine = msg.split(/\r?\n/)[0] ?? ""; + return firstLine.trim(); + }); + + const emojiCount = subjects.filter((subject) => EMOJI_PATTERN.test(subject)).length; + const hasEmojis = emojiCount >= subjects.length * 0.4; + + const scopeCount = subjects.filter((subject) => SCOPE_PATTERN.test(subject)).length; + const hasScopes = scopeCount >= subjects.length * 0.4; + + const typeCount = subjects.filter((subject) => TYPE_PATTERN.test(subject)).length; + const hasTypes = typeCount >= subjects.length * 0.4; + + const bestExample = + validMessages.find((msg) => { + const subject = msg.split(/\r?\n/)[0] ?? ""; + return ( + EMOJI_PATTERN.test(subject) && TYPE_PATTERN.test(subject) && SCOPE_PATTERN.test(subject) + ); + }) ?? + validMessages.find((msg) => { + const subject = msg.split(/\r?\n/)[0] ?? ""; + return ( + (EMOJI_PATTERN.test(subject) && TYPE_PATTERN.test(subject)) || + (TYPE_PATTERN.test(subject) && SCOPE_PATTERN.test(subject)) + ); + }) ?? + validMessages.find((msg) => { + const subject = msg.split(/\r?\n/)[0] ?? ""; + return ( + EMOJI_PATTERN.test(subject) || TYPE_PATTERN.test(subject) || SCOPE_PATTERN.test(subject) + ); + }) ?? + validMessages[0]!; + + const examples: readonly string[] = [bestExample]; + + let analysis = "Detected commit patterns:\n"; + if (hasEmojis) { + analysis += "- Uses emoji prefixes (gitmoji-style)\n"; + } + if (hasScopes) { + analysis += "- Uses scopes like feat(scope): or fix(scope):\n"; + } + if (hasTypes) { + analysis += "- Uses conventional commit types (feat, fix, docs, etc.)\n"; + } + if (!hasEmojis && !hasScopes && !hasTypes) { + analysis += "- No specific pattern detected\n"; + } + + return { + hasEmojis, + hasScopes, + hasTypes, + examples, + analysis, + }; + }); From 8694e8bf55f2991814f57575f35f1ab78f766213 Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Mon, 16 Mar 2026 16:27:49 +0530 Subject: [PATCH 05/20] feat(git): tailor commit prompts to repository commit patterns - Add repository pattern analysis and recent examples for more consistent generated commit messages - Align formatting instructions with detected style for predictable commit output across projects - Expand conventional type guidance and imperative rules for clearer, review-ready commit subjects --- .../git/Layers/CodexTextGeneration.test.ts | 318 +++--------------- .../src/git/Layers/CodexTextGeneration.ts | 83 +++-- docs/features/gitmoji-support.md | 100 ------ 3 files changed, 93 insertions(+), 408 deletions(-) delete mode 100644 docs/features/gitmoji-support.md diff --git a/apps/server/src/git/Layers/CodexTextGeneration.test.ts b/apps/server/src/git/Layers/CodexTextGeneration.test.ts index f1cfeeb274f..d82a1bc64fa 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.test.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.test.ts @@ -2,7 +2,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; import { Effect, FileSystem, Layer, Path } from "effect"; import { expect } from "vitest"; -import { spawnSync } from "node:child_process"; import { ServerConfig } from "../../config.ts"; import { TextGenerationError } from "../Errors.ts"; @@ -372,36 +371,35 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { yield* fs.makeDirectory(path.join(process.cwd(), "attachments"), { recursive: true }); yield* fs.writeFile(imagePath, Buffer.from("hello")); - const textGeneration = yield* TextGeneration; - const generated = yield* textGeneration - .generateBranchName({ - cwd: process.cwd(), - message: "Fix layout bug from screenshot.", - attachments: [ - { - type: "image", - id: attachmentId, - name: "bug.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - }) - .pipe( - Effect.tap(() => - fs.stat(imagePath).pipe( - Effect.map((fileInfo) => { - expect(fileInfo.type).toBe("File"); - }), - ), + const textGeneration = yield* TextGeneration; + const generated = yield* textGeneration + .generateBranchName({ + cwd: process.cwd(), + message: "Fix layout bug from screenshot.", + attachments: [ + { + type: "image", + id: attachmentId, + name: "bug.png", + mimeType: "image/png", + sizeBytes: 5, + }, + ], + }) + .pipe( + Effect.tap(() => + fs.stat(imagePath).pipe( + Effect.map((fileInfo) => { + expect(fileInfo.type).toBe("File"); + }), ), ), Effect.ensuring(fs.remove(imagePath).pipe(Effect.catch(() => Effect.void))), ); - expect(generated.branch).toBe("fix/ui-regression"); - }), - ), + expect(generated.branch).toBe("fix/ui-regression"); + }), + ), ); it.effect("ignores missing attachment ids for codex image inputs", () => @@ -486,7 +484,10 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { it.effect("returns typed TextGenerationError when codex exits non-zero", () => withFakeCodexEnv( { - output: JSON.stringify({ subject: "ignored", body: "" }), + output: JSON.stringify({ + subject: "fix crash on startup", + body: "", + }), exitCode: 1, stderr: "codex execution failed", }, @@ -496,9 +497,9 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { const result = yield* textGeneration .generateCommitMessage({ cwd: process.cwd(), - branch: "feature/codex-error", - stagedSummary: "M README.md", - stagedPatch: "diff --git a/README.md b/README.md", + branch: "fix/crash", + stagedSummary: "M src/index.ts", + stagedPatch: "+process.on('uncaughtException', logger.error)", }) .pipe( Effect.match({ @@ -516,289 +517,76 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { ), ); - it.effect( - "includes gitmoji instructions when config is set to 'gitmoji'", - () => - withFakeCodexEnv( - { - output: JSON.stringify({ - subject: "✨ add authentication", - body: "", - }), - stdinMustContain: "subject must start with a gitmoji emoji", - }, - Effect.gen(function* () { - const textGeneration = yield* TextGeneration; - - yield* Effect.sync(() => { - spawnSync("git", ["config", "t3code.commitMessageStyle", "gitmoji"], { - cwd: process.cwd(), - }); - }); - - const generated = yield* textGeneration.generateCommitMessage({ - cwd: process.cwd(), - branch: "feature/auth", - stagedSummary: "M src/auth.ts", - stagedPatch: "+export function authenticate() {}", - }); - - expect(generated.subject).toContain("✨"); - - yield* Effect.sync(() => { - spawnSync("git", ["config", "--unset", "t3code.commitMessageStyle"], { - cwd: process.cwd(), - }); - }); - }), - ), - ); - - it.effect( - "uses conventional style when config is set to 'conventional'", - () => - withFakeCodexEnv( - { - output: JSON.stringify({ - subject: "add authentication feature", - body: "", - }), - stdinMustNotContain: "gitmoji", - stdinMustContain: "subject must be imperative", - }, - Effect.gen(function* () { - const textGeneration = yield* TextGeneration; - - yield* Effect.sync(() => { - spawnSync("git", ["config", "t3code.commitMessageStyle", "conventional"], { - cwd: process.cwd(), - }); - }); - - const generated = yield* textGeneration.generateCommitMessage({ - cwd: process.cwd(), - branch: "feature/auth", - stagedSummary: "M src/auth.ts", - stagedPatch: "+export function authenticate() {}", - }); - - expect(generated.subject).not.toContain("✨"); - expect(generated.subject).not.toMatch(/^[\p{Emoji}]/u); - - yield* Effect.sync(() => { - spawnSync("git", ["config", "--unset", "t3code.commitMessageStyle"], { - cwd: process.cwd(), - }); - }); - }), - ), - ); - - it.effect("defaults to conventional style when config is null", () => + it.effect("includes pattern analysis and examples in commit message prompt", () => withFakeCodexEnv( { output: JSON.stringify({ - subject: "fix memory leak", + subject: "Add feature", body: "", }), - stdinMustNotContain: "gitmoji", - stdinMustContain: "subject must be imperative", + stdinMustContain: + "Detected patterns:\n\nRecent commit examples from this repository:\n\nFormatting instructions:", }, Effect.gen(function* () { const textGeneration = yield* TextGeneration; - yield* Effect.sync(() => { - spawnSync("git", [ "config", "--unset", "t3code.commitMessageStyle"], { - cwd: process.cwd(), - }); - }); - const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), - branch: "fix/leak", - stagedSummary: "M src/memory.ts", - stagedPatch: "-const leak = []", - }); - - expect(generated.subject).not.toContain("✨"); - }), - ), - ); - - it.effect("handles case-insensitive 'gitmoji' config value", () => - withFakeCodexEnv( - { - output: JSON.stringify({ - subject: "🐛 fix crash on startup", - body: "", - }), - stdinMustContain: "gitmoji", - }, - Effect.gen(function* () { - const textGeneration = yield* TextGeneration; - - yield* Effect.sync(() => { - spawnSync("git", [ "config", "t3code.commitMessageStyle", "GITMOJI"], { - cwd: process.cwd(), - }); - }); - - const generated = yield* textGeneration.generateCommitMessage({ - cwd: process.cwd(), - branch: "fix/crash", - stagedSummary: "M src/index.ts", - stagedPatch: "+process.on('uncaughtException', logger.error)", - }); - - expect(generated.subject).toMatch(/^[\p{Emoji}]/u); - - yield* Effect.sync(() => { - spawnSync("git", [ "config", "--unset", "t3code.commitMessageStyle"], { - cwd: process.cwd(), - }); - }); - }), - ), - ); - - it.effect("handles partial match with 'gitmoji' in value", () => - withFakeCodexEnv( - { - output: JSON.stringify({ - subject: "📝 update readme", - body: "", - }), - stdinMustContain: "gitmoji", - }, - Effect.gen(function* () { - const textGeneration = yield* TextGeneration; - - yield* Effect.sync(() => { - spawnSync("git", [ "config", "t3code.commitMessageStyle", "use-gitmoji"], { - cwd: process.cwd(), - }); - }); - - const generated = yield* textGeneration.generateCommitMessage({ - cwd: process.cwd(), - branch: "docs/readme", + branch: "feature/test", stagedSummary: "M README.md", - stagedPatch: "+# Installation", + stagedPatch: "diff --git a/README.md", }); - expect(generated.subject).toMatch(/^[\p{Emoji}]/u); - - yield* Effect.sync(() => { - spawnSync("git", [ "config", "--unset", "t3code.commitMessageStyle"], { - cwd: process.cwd(), - }); - }); + expect(generated.subject).toBe("Add feature"); }), ), ); - it.effect("ignores invalid config values and defaults to conventional", () => + it.effect("includes expanded conventional commit types in prompt", () => withFakeCodexEnv( { output: JSON.stringify({ - subject: "refactor code structure", + subject: "Add feature", body: "", }), - stdinMustNotContain: "gitmoji", + stdinMustContain: "feat, fix, docs, style, refactor, test, chore, perf, build, ci, revert", }, Effect.gen(function* () { const textGeneration = yield* TextGeneration; - yield* Effect.sync(() => { - spawnSync("git", [ "config", "t3code.commitMessageStyle", "invalid-value"], { - cwd: process.cwd(), - }); - }); - const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), - branch: "refactor/structure", - stagedSummary: "M src/index.ts", - stagedPatch: "-export default App", + branch: "feature/test", + stagedSummary: "M README.md", + stagedPatch: "diff --git a/README.md", }); - expect(generated.subject).not.toMatch(/^[\p{Emoji}]/u); - - yield* Effect.sync(() => { - spawnSync("git", [ "config", "--unset", "t3code.commitMessageStyle"], { - cwd: process.cwd(), - }); - }); + expect(generated.subject).toBe("Add feature"); }), ), ); - it.effect("handles whitespace in config value gracefully", () => + it.effect("includes imperative mood instructions in prompt", () => withFakeCodexEnv( { output: JSON.stringify({ - subject: "✨ add new endpoints", + subject: "Add feature", body: "", }), - stdinMustContain: "gitmoji", + stdinMustContain: + "Subject must be imperative mood (e.g., 'add feature' not 'added feature' or 'adds feature')", }, Effect.gen(function* () { const textGeneration = yield* TextGeneration; - yield* Effect.sync(() => { - spawnSync("git", [ "config", "t3code.commitMessageStyle", " gitmoji "], { - cwd: process.cwd(), - }); - }); - const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), - branch: "feature/api", - stagedSummary: "M src/api.ts", - stagedPatch: "+export const handlers = {}", - }); - - expect(generated.subject).toMatch(/^[\p{Emoji}]/u); - - yield* Effect.sync(() => { - spawnSync("git", [ "config", "--unset", "t3code.commitMessageStyle"], { - cwd: process.cwd(), - }); - }); - }), - ), - ); - - it.effect("includes common gitmoji examples in prompt when enabled", () => - withFakeCodexEnv( - { - output: JSON.stringify({ - subject: "✅ add tests", - body: "", - }), - stdinMustContain: "✨ feat: new feature", - }, - Effect.gen(function* () { - const textGeneration = yield* TextGeneration; - - yield* Effect.sync(() => { - spawnSync("git", ["config", "t3code.commitMessageStyle", "gitmoji"], { - cwd: process.cwd(), - }); - }); - - yield* textGeneration.generateCommitMessage({ - cwd: process.cwd(), - branch: "test/coverage", - stagedSummary: "M test.test.ts", - stagedPatch: "+expect(true).toBe(true)", + branch: "feature/test", + stagedSummary: "M README.md", + stagedPatch: "diff --git a/README.md", }); - yield* Effect.sync(() => { - spawnSync("git", ["config", "--unset", "t3code.commitMessageStyle"], { - cwd: process.cwd(), - }); - }); + expect(generated.subject).toBe("Add feature"); }), ), ); diff --git a/apps/server/src/git/Layers/CodexTextGeneration.ts b/apps/server/src/git/Layers/CodexTextGeneration.ts index afde08d1cb5..82998d75f84 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.ts @@ -9,6 +9,7 @@ import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; import { GitCore } from "../Services/GitCore.ts"; import { TextGenerationError } from "../Errors.ts"; +import { analyzeCommitPatterns as analyzeCommitPatternsUtil } from "../analyzeCommitPatterns.ts"; import { type BranchNameGenerationInput, type BranchNameGenerationResult, @@ -22,8 +23,6 @@ const CODEX_MODEL = "gpt-5.3-codex"; const CODEX_REASONING_EFFORT = "low"; const CODEX_TIMEOUT_MS = 180_000; -const GITMOJI_CONFIG_KEY = "t3code.commitMessageStyle"; - function toCodexOutputJsonSchema(schema: Schema.Top): unknown { const document = Schema.toJsonSchemaDocument(schema); if (document.definitions && Object.keys(document.definitions).length > 0) { @@ -105,6 +104,10 @@ const makeCodexTextGeneration = Effect.gen(function* () { const serverConfig = yield* Effect.service(ServerConfig); const gitCore = yield* GitCore; + const analyzeCommitPatterns = analyzeCommitPatternsUtil({ + getRecentCommitMessages: (cwd, count) => gitCore.getRecentCommitMessages(cwd, count), + }); + type MaterializedImageAttachments = { readonly imagePaths: ReadonlyArray; }; @@ -318,55 +321,49 @@ const makeCodexTextGeneration = Effect.gen(function* () { const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = (input) => { return Effect.gen(function* () { - const configValue = yield* gitCore - .readConfigValue(input.cwd, GITMOJI_CONFIG_KEY) - .pipe(Effect.catch(() => Effect.succeed(null))); - - const useGitmoji = configValue === "gitmoji" || - (configValue !== "conventional" && configValue !== null && configValue.toLowerCase().includes("gitmoji")); - + const patterns = yield* analyzeCommitPatterns(input.cwd); const wantsBranch = input.includeBranch === true; + const exampleLines = + patterns.examples.length > 0 + ? patterns.examples?.slice(0, 2)?.flatMap((ex) => [" Example:", ` ${ex}`]) + : [" (No previous commit examples available)"]; + const promptSections = [ - "You write concise git commit messages.", + "You write concise git commit messages by following the repository's established patterns.", + "", wantsBranch ? "Return a JSON object with keys: subject, body, branch." : "Return a JSON object with keys: subject, body.", - "Rules:", + "", + "Detected patterns:", + patterns.analysis.trim(), + "", + "Recent commit examples from this repository:", + ...exampleLines, + "", + "Formatting instructions:", + ...(patterns.hasEmojis + ? ["- Start with an appropriate emoji if that's the pattern"] + : ["- No emoji prefix (not used in this repository)"]), + ...(patterns.hasScopes + ? ["- Include scope in parentheses like type(scope): when applicable"] + : ["- No scope (not used in this repository)"]), + ...(patterns.hasTypes + ? [ + "- Use conventional commit types: feat, fix, docs, style, refactor, test, chore, perf, build, ci, revert", + ] + : ["- No conventional commit type prefix (not used in this repository)"]), + "- Subject must be imperative mood (e.g., 'add feature' not 'added feature' or 'adds feature')", + "- Subject must be <= 72 characters", + "- No trailing period on subject line", + "- Body should be empty string OR include bullet points with 'for' context (e.g., 'Add user authentication' -> 'for secure access control')", + "- Focus on user-visible or developer-visible impact, not implementation details", + ...(wantsBranch + ? ["- branch must be a short semantic git branch fragment (2-6 words, no punctuation)"] + : []), ]; - if (useGitmoji) { - promptSections.push( - "- subject must start with a gitmoji emoji (e.g., ✨, 🐛, ♻️, 📝, etc.)", - "- after the emoji, add a space and write the commit message in imperative mood", - "- subject must be <= 72 chars total (including emoji) 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", - "", - "Common gitmoji examples:", - "✨ feat: new feature", - "🐛 fix: bug fix", - "♻️ refactor: code refactoring", - "📝 docs: documentation", - "✅ test: tests", - "🎨 style: formatting", - "⚡ perf: performance", - "🔧 chore: maintenance", - ); - } else { - promptSections.push( - "- 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", - ); - } - promptSections.push( "", `Branch: ${input.branch ?? "(detached)"}`, diff --git a/docs/features/gitmoji-support.md b/docs/features/gitmoji-support.md deleted file mode 100644 index ede8add9f47..00000000000 --- a/docs/features/gitmoji-support.md +++ /dev/null @@ -1,100 +0,0 @@ -# Gitmoji Support for Commit Messages - -Automatically generate commit messages with gitmoji emojis. - -## Overview - -T3 Code supports generating git commit messages with [gitmoji](https://gitmoji.dev/) prefixes. This feature helps maintain consistent commit history with visual emoji indicators. - -## Configuration - -### Enable Gitmoji - -To enable gitmoji for all commits in a repository: - -```bash -git config t3code.commitMessageStyle gitmoji -``` - -### Disable Gitmoji - -To use conventional commit style instead: - -```bash -git config t3code.commitMessageStyle conventional -``` - -### Remove Custom Setting - -To revert to default behavior: - -```bash -git config --unset t3code.commitMessageStyle -``` - -## Scope - -Configuration is repository-specific: - -- **Repository-level**: Run the config command in your project root -- **Global**: Add `--global` flag to apply to all your projects - ```bash - git config --global t3code.commitMessageStyle gitmoji - ``` - -## Commit Examples - -### With Gitmoji Enabled - -``` -✨ add user authentication flow -🐛 fix null pointer exception in login -♻️ refactor database connection pool -📝 update API documentation -✅ add unit tests for payment module -🎨 improve button hover animations -⚡ optimize database query performance -🔧 upgrade dependencies to latest versions -``` - -### With Gitmoji Disabled (Conventional) - -``` -feat: add user authentication flow -fix: null pointer exception in login -refactor: database connection pool -docs: update API documentation -test: add unit tests for payment module -style: improve button hover animations -perf: optimize database query performance -chore: upgrade dependencies to latest versions -``` - -## Supported Gitmojis - -The AI model will choose appropriate gitmoji based on your changes: - -| Emoji | Keyword | Description | -| ----- | -------- | ------------------------------- | -| ✨ | feat | New feature | -| 🐛 | fix | Bug fix | -| ♻️ | refactor | Code refactoring | -| 📝 | docs | Documentation changes | -| ✅ | test | Adding or updating tests | -| 🎨 | style | Code style changes (formatting) | -| ⚡ | perf | Performance improvements | -| 🔧 | chore | Maintenance tasks | - -## Platform Availability - -✅ Works on all platforms: - -- Web app (http://localhost:3773) -- Desktop app (Electron) -- Any future clients using the T3 Code backend - -## Notes - -- Commit messages are generated by AI, so gitmoji selection is contextual to your changes -- Generated messages always follow the configured style -- Feature is backend-driven, so it applies automatically when enabled From f71b3207a778acb0547645af8d3186163fb18dbd Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Fri, 20 Mar 2026 13:51:56 +0530 Subject: [PATCH 06/20] feat(git): add commit mode selection support - for conventional, gitmoji, and custom commit message generation - for exposing commit style controls in the commit dialog --- .gitignore | 4 +- .../src/git/Layers/CodexTextGeneration.ts | 91 ++----- apps/server/src/git/Layers/GitManager.ts | 18 ++ .../server/src/git/Services/TextGeneration.ts | 4 + .../web/src/components/CommitModeSelector.tsx | 239 ++++++++++++++++++ apps/web/src/components/GitActionsControl.tsx | 49 +++- apps/web/src/lib/gitReactQuery.ts | 6 + packages/contracts/src/git.ts | 8 + 8 files changed, 343 insertions(+), 76 deletions(-) create mode 100644 apps/web/src/components/CommitModeSelector.tsx diff --git a/.gitignore b/.gitignore index 3e8d287755b..f8affc6330e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ release/ apps/web/.playwright apps/web/playwright-report apps/web/src/components/__screenshots__ -.vitest-* \ No newline at end of file +.vitest-* +tasks/ +.opencode/ diff --git a/apps/server/src/git/Layers/CodexTextGeneration.ts b/apps/server/src/git/Layers/CodexTextGeneration.ts index 982c210bb01..47ca4fdc064 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.ts @@ -1,21 +1,10 @@ import { randomUUID } from "node:crypto"; -import { - Effect, - FileSystem, - Layer, - Option, - Path, - Schema, - Stream, -} from "effect"; +import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { DEFAULT_GIT_TEXT_GENERATION_MODEL } from "@t3tools/contracts"; -import { - sanitizeBranchFragment, - sanitizeFeatureBranchName, -} from "@t3tools/shared/git"; +import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; @@ -116,8 +105,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { const gitCore = yield* GitCore; const analyzeCommitPatterns = analyzeCommitPatternsUtil({ - getRecentCommitMessages: (cwd, count) => - gitCore.getRecentCommitMessages(cwd, count), + getRecentCommitMessages: (cwd, count) => gitCore.getRecentCommitMessages(cwd, count), }); type MaterializedImageAttachments = { @@ -136,28 +124,20 @@ const makeCodexTextGeneration = Effect.gen(function* () { }), ).pipe( Effect.mapError((cause) => - normalizeCodexError( - operation, - cause, - "Failed to collect process output", - ), + normalizeCodexError(operation, cause, "Failed to collect process output"), ), ); return text; }); - const tempDir = - process.env.TMPDIR ?? process.env.TEMP ?? process.env.TMP ?? "/tmp"; + const tempDir = process.env.TMPDIR ?? process.env.TEMP ?? process.env.TMP ?? "/tmp"; const writeTempFile = ( operation: string, prefix: string, content: string, ): Effect.Effect => { - const filePath = path.join( - tempDir, - `t3code-${prefix}-${process.pid}-${randomUUID()}.tmp`, - ); + const filePath = path.join(tempDir, `t3code-${prefix}-${process.pid}-${randomUUID()}.tmp`); return fileSystem.writeFileString(filePath, content).pipe( Effect.mapError( (cause) => @@ -175,10 +155,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { fileSystem.remove(filePath).pipe(Effect.catch(() => Effect.void)); const materializeImageAttachments = ( - _operation: - | "generateCommitMessage" - | "generatePrContent" - | "generateBranchName", + _operation: "generateCommitMessage" | "generatePrContent" | "generateBranchName", attachments: BranchNameGenerationInput["attachments"], ): Effect.Effect => Effect.gen(function* () { @@ -219,10 +196,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { cleanupPaths = [], model, }: { - operation: - | "generateCommitMessage" - | "generatePrContent" - | "generateBranchName"; + operation: "generateCommitMessage" | "generatePrContent" | "generateBranchName"; cwd: string; prompt: string; outputSchemaJson: S; @@ -270,11 +244,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { .spawn(command) .pipe( Effect.mapError((cause) => - normalizeCodexError( - operation, - cause, - "Failed to spawn Codex CLI process", - ), + normalizeCodexError(operation, cause, "Failed to spawn Codex CLI process"), ), ); @@ -285,11 +255,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { child.exitCode.pipe( Effect.map((value) => Number(value)), Effect.mapError((cause) => - normalizeCodexError( - operation, - cause, - "Failed to read Codex CLI exit code", - ), + normalizeCodexError(operation, cause, "Failed to read Codex CLI exit code"), ), ), ], @@ -311,9 +277,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { }); const cleanup = Effect.all( - [schemaPath, outputPath, ...cleanupPaths].map((filePath) => - safeUnlink(filePath), - ), + [schemaPath, outputPath, ...cleanupPaths].map((filePath) => safeUnlink(filePath)), { concurrency: "unbounded", }, @@ -346,9 +310,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { cause, }), ), - Effect.flatMap( - Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson)), - ), + Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson))), Effect.catchTag("SchemaError", (cause) => Effect.fail( new TextGenerationError({ @@ -362,18 +324,14 @@ const makeCodexTextGeneration = Effect.gen(function* () { }).pipe(Effect.ensuring(cleanup)); }); - const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = ( - input, - ) => { + const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = (input) => { return Effect.gen(function* () { const patterns = yield* analyzeCommitPatterns(input.cwd); const wantsBranch = input.includeBranch === true; const exampleLines = patterns.examples.length > 0 - ? patterns.examples - ?.slice(0, 2) - ?.flatMap((ex) => [" Example:", ` ${ex}`]) + ? patterns.examples?.slice(0, 2)?.flatMap((ex) => [" Example:", ` ${ex}`]) : [" (No previous commit examples available)"]; const promptSections = [ @@ -400,18 +358,14 @@ const makeCodexTextGeneration = Effect.gen(function* () { ? [ "- Use conventional commit types: feat, fix, docs, style, refactor, test, chore, perf, build, ci, revert", ] - : [ - "- No conventional commit type prefix (not used in this repository)", - ]), + : ["- No conventional commit type prefix (not used in this repository)"]), "- Subject must be imperative mood (e.g., 'add feature' not 'added feature' or 'adds feature')", "- Subject must be <= 72 characters", "- No trailing period on subject line", "- Body should be empty string OR include bullet points with 'for' context (e.g., 'Add user authentication' -> 'for secure access control')", "- Focus on user-visible or developer-visible impact, not implementation details", ...(wantsBranch - ? [ - "- branch must be a short semantic git branch fragment (2-6 words, no punctuation)", - ] + ? ["- branch must be a short semantic git branch fragment (2-6 words, no punctuation)"] : []), ]; @@ -460,9 +414,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { }); }; - const generatePrContent: TextGenerationShape["generatePrContent"] = ( - input, - ) => { + const generatePrContent: TextGenerationShape["generatePrContent"] = (input) => { const prompt = [ "You write GitHub pull request content.", "Return a JSON object with keys: title, body.", @@ -505,9 +457,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { ); }; - const generateBranchName: TextGenerationShape["generateBranchName"] = ( - input, - ) => { + const generateBranchName: TextGenerationShape["generateBranchName"] = (input) => { return Effect.gen(function* () { const { imagePaths } = yield* materializeImageAttachments( "generateBranchName", @@ -563,7 +513,4 @@ const makeCodexTextGeneration = Effect.gen(function* () { } satisfies TextGenerationShape; }); -export const CodexTextGenerationLive = Layer.effect( - TextGeneration, - makeCodexTextGeneration, -); +export const CodexTextGenerationLive = Layer.effect(TextGeneration, makeCodexTextGeneration); diff --git a/apps/server/src/git/Layers/GitManager.ts b/apps/server/src/git/Layers/GitManager.ts index 1a3cf2bb35b..2414157a486 100644 --- a/apps/server/src/git/Layers/GitManager.ts +++ b/apps/server/src/git/Layers/GitManager.ts @@ -640,6 +640,8 @@ export const makeGitManager = Effect.gen(function* () { includeBranch?: boolean; filePaths?: readonly string[]; model?: string; + commitMessageMode?: "auto" | "gitmoji" | "standard" | "custom"; + commitMessageCustomInstructions?: string; }) => Effect.gen(function* () { const context = yield* gitCore.prepareCommitContext(input.cwd, input.filePaths); @@ -667,6 +669,10 @@ export const makeGitManager = Effect.gen(function* () { stagedPatch: limitContext(context.stagedPatch, 50_000), ...(input.includeBranch ? { includeBranch: true } : {}), ...(input.model ? { model: input.model } : {}), + ...(input.commitMessageMode ? { commitMessageMode: input.commitMessageMode } : {}), + ...(input.commitMessageCustomInstructions + ? { commitMessageCustomInstructions: input.commitMessageCustomInstructions } + : {}), }) .pipe(Effect.map((result) => sanitizeCommitMessage(result))); @@ -685,6 +691,8 @@ export const makeGitManager = Effect.gen(function* () { preResolvedSuggestion?: CommitAndBranchSuggestion, filePaths?: readonly string[], model?: string, + commitMessageMode?: "auto" | "gitmoji" | "standard" | "custom", + commitMessageCustomInstructions?: string, ) => Effect.gen(function* () { const suggestion = @@ -695,6 +703,8 @@ export const makeGitManager = Effect.gen(function* () { ...(commitMessage ? { commitMessage } : {}), ...(filePaths ? { filePaths } : {}), ...(model ? { model } : {}), + ...(commitMessageMode ? { commitMessageMode } : {}), + ...(commitMessageCustomInstructions ? { commitMessageCustomInstructions } : {}), })); if (!suggestion) { return { status: "skipped_no_changes" as const }; @@ -978,6 +988,8 @@ export const makeGitManager = Effect.gen(function* () { commitMessage?: string, filePaths?: readonly string[], model?: string, + commitMessageMode?: "auto" | "gitmoji" | "standard" | "custom", + commitMessageCustomInstructions?: string, ) => Effect.gen(function* () { const suggestion = yield* resolveCommitAndBranchSuggestion({ @@ -987,6 +999,8 @@ export const makeGitManager = Effect.gen(function* () { ...(filePaths ? { filePaths } : {}), includeBranch: true, ...(model ? { model } : {}), + ...(commitMessageMode ? { commitMessageMode } : {}), + ...(commitMessageCustomInstructions ? { commitMessageCustomInstructions } : {}), }); if (!suggestion) { return yield* gitManagerError( @@ -1036,6 +1050,8 @@ export const makeGitManager = Effect.gen(function* () { input.commitMessage, input.filePaths, input.textGenerationModel, + input.commitMessageMode, + input.commitMessageCustomInstructions, ); branchStep = result.branchStep; commitMessageForStep = result.resolvedCommitMessage; @@ -1053,6 +1069,8 @@ export const makeGitManager = Effect.gen(function* () { preResolvedCommitSuggestion, input.filePaths, input.textGenerationModel, + input.commitMessageMode, + input.commitMessageCustomInstructions, ); const push = wantsPush diff --git a/apps/server/src/git/Services/TextGeneration.ts b/apps/server/src/git/Services/TextGeneration.ts index b4650ed5707..99e96e5f638 100644 --- a/apps/server/src/git/Services/TextGeneration.ts +++ b/apps/server/src/git/Services/TextGeneration.ts @@ -21,6 +21,10 @@ export interface CommitMessageGenerationInput { includeBranch?: boolean; /** Model to use for generation. Defaults to gpt-5.4-mini if not specified. */ model?: string; + /** Commit message generation mode. Defaults to 'auto'. */ + commitMessageMode?: "auto" | "gitmoji" | "standard" | "custom"; + /** Custom instructions for commit message generation (only used when mode is 'custom'). */ + commitMessageCustomInstructions?: string; } export interface CommitMessageGenerationResult { diff --git a/apps/web/src/components/CommitModeSelector.tsx b/apps/web/src/components/CommitModeSelector.tsx new file mode 100644 index 00000000000..4159831c14e --- /dev/null +++ b/apps/web/src/components/CommitModeSelector.tsx @@ -0,0 +1,239 @@ +import type { GitCommitMessageMode } from "@t3tools/contracts"; +import { + CheckIcon, + ChevronRightIcon, + GitBranchIcon, + InfoIcon, + Settings2Icon, + SparklesIcon, + SmileIcon, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { Button } from "~/components/ui/button"; +import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; +import { cn } from "~/lib/utils"; + +const COMMIT_MODES: ReadonlyArray<{ + value: GitCommitMessageMode; + label: string; + summary: string; + description: string; + icon: React.ComponentType<{ className?: string }>; +}> = [ + { + value: "standard", + label: "Conventional", + summary: "Conventional format", + description: + "Follows strict conventional commit format with type, optional scope, and description", + icon: GitBranchIcon, + }, + { + value: "auto", + label: "Auto", + summary: "Infer from repo", + description: + "Analyzes past commit messages, selects the best example, and adapts to your repo's existing patterns", + icon: SparklesIcon, + }, + { + value: "gitmoji", + label: "Gitmoji", + summary: "Emoji + conventional", + description: + "Uses Gitmoji emoji prefixes while following conventional commit structure for expressive commits", + icon: SmileIcon, + }, + { + value: "custom", + label: "Custom", + summary: "Your own rules", + description: "Use a predefined template or provide your own instructions for custom formatting", + icon: Settings2Icon, + }, +] as const; + +const CUSTOM_TEMPLATES: ReadonlyArray<{ + id: string; + label: string; + prompt: string; + description: string; + example: string; +}> = [ + { + id: "standard", + label: "Conventional", + prompt: "Conventional Commits: '(): '", + description: "Most common machine-readable format for changelogs and release tooling", + example: "feat(auth): add login validation", + }, + { + id: "conventional-ticket", + label: "Conventional + Ticket", + prompt: + "Conventional Commits with ticket reference in footer: '(): ' plus footer like 'Refs: PROJ-123'", + description: "Best fit when you need issue tracking without breaking conventional tooling", + example: "feat(auth): add login validation", + }, + { + id: "simple", + label: "Simple", + prompt: "Simple git message: imperative subject only, no prefix", + description: "Plain readable fallback when you do not need automated parsing", + example: "add login validation", + }, +] as const; + +interface CommitModeSelectorProps { + value: GitCommitMessageMode; + onChange: (value: GitCommitMessageMode) => void; + customInstructions: string; + onCustomInstructionsChange: (value: string) => void; + commitMessage: string; + onCommitMessageChange: (value: string) => void; + disabled?: boolean; +} + +export function CommitModeSelector({ + value, + onChange, + customInstructions, + onCustomInstructionsChange, + commitMessage, + onCommitMessageChange, + disabled = false, +}: CommitModeSelectorProps) { + const [popoverOpen, setPopoverOpen] = useState(false); + const [templatePopoverOpen, setTemplatePopoverOpen] = useState(false); + + const selectedMode = COMMIT_MODES.find((mode) => mode.value === value); + + useEffect(() => { + if (value !== "custom" && customInstructions.trim()) { + onCustomInstructionsChange(""); + } + }, [value, customInstructions, onCustomInstructionsChange]); + + const handleTemplateSelect = (template: (typeof CUSTOM_TEMPLATES)[number]) => { + onCommitMessageChange(template.prompt); + setTemplatePopoverOpen(false); + }; + + return ( +
+
+ Commit style + + + + + } + /> + +
+

How commit messages are generated

+
    + {COMMIT_MODES.map((mode) => ( +
  • +
    + + {mode.label}: + {mode.summary} +
    +

    {mode.description}

    +
  • + ))} +
+
+
+
+ + {selectedMode && ( +
+ ( + + {selectedMode.summary} + ) +
+ )} +
+
+ {COMMIT_MODES.map((mode) => { + const Icon = mode.icon; + return ( + + ); + })} +
+ + {value === "custom" && ( +
+

Select a template or enter custom instructions

+ + + + {commitMessage.trim() ? "✓ Template applied" : "Choose a template..."} + + + + } + /> + +
+

+ PREDEFINED TEMPLATES +

+ {CUSTOM_TEMPLATES.map((template) => ( + + ))} +
+
+
+ {commitMessage && ( +

+ 💡 Template applied to commit message above. Edit to customize. +

+ )} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 07718751356..d5382ec2778 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -1,7 +1,13 @@ -import type { GitStackedAction, GitStatusResult, ThreadId } from "@t3tools/contracts"; +import type { + GitCommitMessageMode, + GitStackedAction, + GitStatusResult, + ThreadId, +} from "@t3tools/contracts"; import { useIsMutating, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon, InfoIcon } from "lucide-react"; +import { CommitModeSelector } from "./CommitModeSelector"; import { GitHubIcon } from "./Icons"; import { buildGitActionProgressStages, @@ -163,6 +169,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions const queryClient = useQueryClient(); const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false); const [dialogCommitMessage, setDialogCommitMessage] = useState(""); + const [commitMessageMode, setCommitMessageMode] = useState("standard"); + const [customCommitInstructions, setCustomCommitInstructions] = useState(""); const [excludedFiles, setExcludedFiles] = useState>(new Set()); const [isEditingFiles, setIsEditingFiles] = useState(false); const [pendingDefaultBranchAction, setPendingDefaultBranchAction] = @@ -265,6 +273,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions async ({ action, commitMessage, + commitMessageMode: mode = "auto", + commitMessageCustomInstructions = "", forcePushOnlyProgress = false, onConfirmed, skipDefaultBranchPrompt = false, @@ -276,6 +286,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions }: { action: GitStackedAction; commitMessage?: string; + commitMessageMode?: GitCommitMessageMode; + commitMessageCustomInstructions?: string; forcePushOnlyProgress?: boolean; onConfirmed?: () => void; skipDefaultBranchPrompt?: boolean; @@ -355,6 +367,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions const promise = runImmediateGitActionMutation.mutateAsync({ action, ...(commitMessage ? { commitMessage } : {}), + ...(mode ? { commitMessageMode: mode } : {}), + ...(commitMessageCustomInstructions ? { commitMessageCustomInstructions } : {}), ...(featureBranch ? { featureBranch } : {}), ...(filePaths ? { filePaths } : {}), }); @@ -464,12 +478,19 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions void runGitActionWithToast({ action, ...(commitMessage ? { commitMessage } : {}), + commitMessageMode, + commitMessageCustomInstructions: customCommitInstructions, forcePushOnlyProgress, ...(onConfirmed ? { onConfirmed } : {}), ...(filePaths ? { filePaths } : {}), skipDefaultBranchPrompt: true, }); - }, [pendingDefaultBranchAction, runGitActionWithToast]); + }, [ + pendingDefaultBranchAction, + runGitActionWithToast, + commitMessageMode, + customCommitInstructions, + ]); const checkoutNewBranchAndRunAction = useCallback( (actionParams: { @@ -481,11 +502,13 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions }) => { void runGitActionWithToast({ ...actionParams, + commitMessageMode, + commitMessageCustomInstructions: customCommitInstructions, featureBranch: true, skipDefaultBranchPrompt: true, }); }, - [runGitActionWithToast], + [runGitActionWithToast, commitMessageMode, customCommitInstructions], ); const checkoutFeatureBranchAndContinuePendingAction = useCallback(() => { @@ -508,6 +531,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions setIsCommitDialogOpen(false); setDialogCommitMessage(""); + setCommitMessageMode("auto"); + setCustomCommitInstructions(""); setExcludedFiles(new Set()); setIsEditingFiles(false); @@ -591,11 +616,15 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions const commitMessage = dialogCommitMessage.trim(); setIsCommitDialogOpen(false); setDialogCommitMessage(""); + setCommitMessageMode("auto"); + setCustomCommitInstructions(""); setExcludedFiles(new Set()); setIsEditingFiles(false); void runGitActionWithToast({ action: "commit", ...(commitMessage ? { commitMessage } : {}), + commitMessageMode, + commitMessageCustomInstructions: customCommitInstructions, ...(!allSelected ? { filePaths: selectedFiles.map((f) => f.path) } : {}), }); }, [ @@ -604,6 +633,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions isCommitDialogOpen, runGitActionWithToast, selectedFiles, + commitMessageMode, + customCommitInstructions, setDialogCommitMessage, setIsCommitDialogOpen, ]); @@ -768,6 +799,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions if (!open) { setIsCommitDialogOpen(false); setDialogCommitMessage(""); + setCommitMessageMode("auto"); + setCustomCommitInstructions(""); setExcludedFiles(new Set()); setIsEditingFiles(false); } @@ -891,6 +924,14 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions )} +

Commit message (optional)