Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
dce0d83
Add AI-generated semantic branch naming for git commits
juliusmarminge Mar 2, 2026
7a86357
optional
juliusmarminge Mar 2, 2026
a4b8a4d
non-dupicate
juliusmarminge Mar 2, 2026
1f73eeb
Extract branch sanitization to shared contracts module
juliusmarminge Mar 2, 2026
f52dc36
Move branch sanitization from contracts to new shared package
juliusmarminge Mar 2, 2026
ad5b315
Document contracts and shared package responsibilities
juliusmarminge Mar 2, 2026
9a0104e
Merge feature-branch checkout into atomic runStackedAction
juliusmarminge Mar 2, 2026
bc3ca03
Fix duplicated branch logic and feature-branch dialog on push-only
juliusmarminge Mar 2, 2026
d13755f
Scope branch checkout effect during auto branch creation
juliusmarminge Mar 3, 2026
4173ad9
Limit default-branch confirmation to direct push
juliusmarminge Mar 3, 2026
bce81ba
Skip commit progress stages when create-PR runs push-only
juliusmarminge Mar 3, 2026
4855d5c
Address PR feedback on git workflows
juliusmarminge Mar 3, 2026
b8b0070
Scope default-branch PR confirmation to full stack
juliusmarminge Mar 3, 2026
7cc24c3
Fix toast follow-up actions to avoid re-branching
juliusmarminge Mar 3, 2026
9a94e35
Restore Open PR toast action after PR creation
juliusmarminge Mar 3, 2026
e1be27a
Fix PR CTAs and quick action after push
juliusmarminge Mar 3, 2026
4f28a87
Keep non-default quick action on full PR flow
juliusmarminge Mar 3, 2026
9b97879
Revert git action button logic toward main behavior
juliusmarminge Mar 3, 2026
3d7c823
Restore Open PR CTA for commit_push with existing PR
juliusmarminge Mar 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ This repository is a VERY EARLY WIP. Proposing sweeping changes that improve lon

If a tradeoff is required, choose correctness and robustness over short-term convenience.

## Maintainability

Long term maintainability is a core priority. If you add new functionality, first check if there are shared logic that can be extracted to a separate module. Duplicate logic across mulitple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem.

## Package Roles

- `apps/server`: Node.js WebSocket server. Wraps Codex app-server (JSON-RPC over stdio), serves the React web app, and manages provider sessions.
- `apps/web`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state. Connects to the server via WebSocket.
- `packages/contracts`: Shared effect/Schema schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types.
- `packages/contracts`: Shared effect/Schema schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. Keep this package schema-only — no runtime logic.
- `packages/shared`: Shared runtime utilities consumed by both server and web. Uses explicit subpath exports (e.g. `@t3tools/shared/git`) — no barrel index.

## Codex App Server (Important)

Expand Down
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@effect/language-service": "0.75.1",
"@effect/vitest": "catalog:",
"@t3tools/contracts": "workspace:*",
"@t3tools/shared": "workspace:*",
Comment thread
juliusmarminge marked this conversation as resolved.
Resolved
"@t3tools/web": "workspace:*",
"@types/bun": "^1.3.9",
"@types/node": "^24.10.13",
Expand Down
31 changes: 30 additions & 1 deletion apps/server/src/git/Layers/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,15 @@ function withFakeCodexEnv<A, E, R>(
const CodexTextGenerationTestLayer = makeCodexTextGenerationTestLayer(process.cwd());

it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => {
it.effect("generates and sanitizes commit messages", () =>
it.effect("generates and sanitizes commit messages without branch by default", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject:
" Add important change to the system with too much detail and a trailing period.\nsecondary line",
body: "\n- added migration\n- updated tests\n",
}),
stdinMustNotContain: "branch must be a short semantic git branch fragment",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;
Expand All @@ -211,6 +212,34 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => {
expect(generated.subject.length).toBeLessThanOrEqual(72);
expect(generated.subject.endsWith(".")).toBe(false);
expect(generated.body).toBe("- added migration\n- updated tests");
expect(generated.branch).toBeUndefined();
}),
),
);

it.effect("generates commit message with branch when includeBranch is true", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject: "Add important change",
body: "",
branch: "fix/important-system-change",
}),
stdinMustContain: "branch must be a short semantic git branch fragment",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/codex-effect",
stagedSummary: "M README.md",
stagedPatch: "diff --git a/README.md b/README.md",
includeBranch: true,
});

expect(generated.subject).toBe("Add important change");
expect(generated.branch).toBe("feature/fix/important-system-change");
}),
),
);
Expand Down
55 changes: 30 additions & 25 deletions apps/server/src/git/Layers/CodexTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { randomUUID } from "node:crypto";
import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";

import { resolveAttachmentPath } from "../../attachmentStore.ts";
import { ServerConfig } from "../../config.ts";
import { TextGenerationError } from "../Errors.ts";
Expand Down Expand Up @@ -93,24 +95,6 @@ function sanitizePrTitle(raw: string): string {
return "Update project changes";
}

function sanitizeBranchName(raw: string): string {
const normalized = raw
.trim()
.toLowerCase()
.replace(/['"`]/g, "")
.replace(/^[./\s_-]+|[./\s_-]+$/g, "");

const branchFragment = normalized
.replace(/[^a-z0-9/_-]+/g, "-")
.replace(/\/+/g, "/")
.replace(/-+/g, "-")
.replace(/^[./_-]+|[./_-]+$/g, "")
.slice(0, 64)
.replace(/[./_-]+$/g, "");

return branchFragment.length > 0 ? branchFragment : "update";
}

const makeCodexTextGeneration = Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
Expand Down Expand Up @@ -328,12 +312,19 @@ const makeCodexTextGeneration = Effect.gen(function* () {
});

const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = (input) => {
const wantsBranch = input.includeBranch === true;

const prompt = [
"You write concise git commit messages.",
"Return a JSON object with keys: subject, body.",
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)"}`,
Expand All @@ -345,20 +336,31 @@ const makeCodexTextGeneration = Effect.gen(function* () {
limitSection(input.stagedPatch, 40_000),
].join("\n");

const outputSchemaJson = wantsBranch
? Schema.Struct({
subject: Schema.String,
body: Schema.String,
branch: Schema.String,
})
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
: Schema.Struct({
subject: Schema.String,
body: Schema.String,
});

return runCodexJson({
operation: "generateCommitMessage",
cwd: input.cwd,
prompt,
outputSchemaJson: Schema.Struct({
subject: Schema.String,
body: Schema.String,
}),
outputSchemaJson,
}).pipe(
Effect.map(
(generated) =>
({
subject: sanitizeCommitSubject(generated.subject),
body: generated.body.trim(),
...("branch" in generated && typeof generated.branch === "string"
? { branch: sanitizeFeatureBranchName(generated.branch) }
: {}),
Comment thread
juliusmarminge marked this conversation as resolved.
Resolved
}) satisfies CommitMessageGenerationResult,
),
);
Expand Down Expand Up @@ -408,7 +410,10 @@ const makeCodexTextGeneration = Effect.gen(function* () {

const generateBranchName: TextGenerationShape["generateBranchName"] = (input) => {
return Effect.gen(function* () {
const { imagePaths } = yield* materializeImageAttachments("generateBranchName", input.attachments);
const { imagePaths } = yield* materializeImageAttachments(
"generateBranchName",
input.attachments,
);
const attachmentLines = (input.attachments ?? []).map(
(attachment) =>
`- ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes)`,
Expand Down Expand Up @@ -446,7 +451,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {
});

return {
branch: sanitizeBranchName(generated.branch),
branch: sanitizeBranchFragment(generated.branch),
} satisfies BranchNameGenerationResult;
});
};
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/git/Layers/GitCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const makeIsolatedGitCore = (gitService: GitServiceShape) =>
createBranch: (input) => core.createBranch(input),
checkoutBranch: (input) => core.checkoutBranch(input),
initRepo: (input) => core.initRepo(input),
listLocalBranchNames: (cwd) => core.listLocalBranchNames(cwd),
} satisfies GitCoreShape;
});

Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/git/Layers/GitCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,20 @@ const makeGitCore = Effect.gen(function* () {
fallbackErrorMessage: "git init failed",
}).pipe(Effect.asVoid);

const listLocalBranchNames: GitCoreShape["listLocalBranchNames"] = (cwd) =>
runGitStdout("GitCore.listLocalBranchNames", cwd, [
"branch",
"--list",
"--format=%(refname:short)",
]).pipe(
Effect.map((stdout) =>
stdout
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0),
),
);

return {
status,
statusDetails,
Expand All @@ -1188,6 +1202,7 @@ const makeGitCore = Effect.gen(function* () {
createBranch,
checkoutBranch,
initRepo,
listLocalBranchNames,
} satisfies GitCoreShape;
});

Expand Down
Loading