Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c14198f
refactor: extract shared text generation utilities
keyzou Mar 22, 2026
85b874a
contracts: add provider-aware text generation model defaults
keyzou Mar 22, 2026
2ed6e75
feat: add TextGenerationProvider type and provider field to service i…
keyzou Mar 22, 2026
96d1185
feat: add Claude CLI text generation and routing layer
keyzou Mar 22, 2026
783f669
feat: add text generation provider settings UI and frontend wiring
keyzou Mar 22, 2026
9fe39bc
Extract Claude reasoning effort into named constant
keyzou Mar 23, 2026
75fe8da
Extract shared prompt builders and error normalization
keyzou Mar 23, 2026
71e18bc
chore: remove a bit of comment-slop
keyzou Mar 23, 2026
ba74436
Extract provider/model selection to ProviderModelPicker
keyzou Mar 23, 2026
163bc30
Fix type safety loss in buildCommitMessagePrompt output schema
keyzou Mar 24, 2026
f21ef2a
Extract shared toJsonSchemaObject helper for JSON Schema conversion
keyzou Mar 24, 2026
eb88f1e
Fix model picker missing selected model and restore reset button
keyzou Mar 24, 2026
b141993
Fix feature branch step ignoring selected text generation provider
keyzou Mar 24, 2026
ec38c62
chore: format with oxfmt
keyzou Mar 24, 2026
53805a5
Merge branch 'main' into t3code/pr-1323/feat/claude-text-generation
juliusmarminge Mar 25, 2026
8b1fbe3
unify on modelSelection pattern
juliusmarminge Mar 25, 2026
09c4130
Add model selection coverage to git action schema
juliusmarminge Mar 25, 2026
94adce6
forward settings properly
juliusmarminge Mar 25, 2026
9134c00
kewl
juliusmarminge Mar 25, 2026
5807f9b
Add model selection to git action tests
juliusmarminge Mar 25, 2026
839bc38
required
juliusmarminge Mar 25, 2026
ef3a2f7
kewl
juliusmarminge Mar 25, 2026
83a3dbd
Set codex git text generation effort to low
juliusmarminge Mar 25, 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
248 changes: 248 additions & 0 deletions apps/server/src/git/Layers/ClaudeTextGeneration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
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 { ServerConfig } from "../../config.ts";
import { TextGeneration } from "../Services/TextGeneration.ts";
import { ClaudeTextGenerationLive } from "./ClaudeTextGeneration.ts";

const ClaudeTextGenerationTestLayer = ClaudeTextGenerationLive.pipe(
Layer.provideMerge(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3code-claude-text-generation-test-",
}),
),
Layer.provideMerge(NodeServices.layer),
);

function makeFakeClaudeBinary(dir: string) {
return Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const binDir = path.join(dir, "bin");
const claudePath = path.join(binDir, "claude");
yield* fs.makeDirectory(binDir, { recursive: true });

yield* fs.writeFileString(
claudePath,
[
"#!/bin/sh",
'args="$*"',
'stdin_content="$(cat)"',
'if [ -n "$T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN" ]; then',
' printf "%s" "$args" | grep -F -- "$T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN" >/dev/null || {',
' printf "%s\\n" "args missing expected content" >&2',
" exit 2",
" }",
"fi",
'if [ -n "$T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN" ]; then',
' if printf "%s" "$args" | grep -F -- "$T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN" >/dev/null; then',
' printf "%s\\n" "args contained forbidden content" >&2',
" exit 3",
" fi",
"fi",
'if [ -n "$T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN" ]; then',
' printf "%s" "$stdin_content" | grep -F -- "$T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN" >/dev/null || {',
' printf "%s\\n" "stdin missing expected content" >&2',
" exit 4",
" }",
"fi",
'if [ -n "$T3_FAKE_CLAUDE_STDERR" ]; then',
' printf "%s\\n" "$T3_FAKE_CLAUDE_STDERR" >&2',
"fi",
'printf "%s" "$T3_FAKE_CLAUDE_OUTPUT"',
'exit "${T3_FAKE_CLAUDE_EXIT_CODE:-0}"',
"",
].join("\n"),
);
yield* fs.chmod(claudePath, 0o755);
return binDir;
});
}

function withFakeClaudeEnv<A, E, R>(
input: {
output: string;
exitCode?: number;
stderr?: string;
argsMustContain?: string;
argsMustNotContain?: string;
stdinMustContain?: string;
},
effect: Effect.Effect<A, E, R>,
) {
return Effect.acquireUseRelease(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-claude-text-" });
const binDir = yield* makeFakeClaudeBinary(tempDir);
const previousPath = process.env.PATH;
const previousOutput = process.env.T3_FAKE_CLAUDE_OUTPUT;
const previousExitCode = process.env.T3_FAKE_CLAUDE_EXIT_CODE;
const previousStderr = process.env.T3_FAKE_CLAUDE_STDERR;
const previousArgsMustContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN;
const previousArgsMustNotContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN;
const previousStdinMustContain = process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN;

yield* Effect.sync(() => {
process.env.PATH = `${binDir}:${previousPath ?? ""}`;
process.env.T3_FAKE_CLAUDE_OUTPUT = input.output;

if (input.exitCode !== undefined) {
process.env.T3_FAKE_CLAUDE_EXIT_CODE = String(input.exitCode);
} else {
delete process.env.T3_FAKE_CLAUDE_EXIT_CODE;
}

if (input.stderr !== undefined) {
process.env.T3_FAKE_CLAUDE_STDERR = input.stderr;
} else {
delete process.env.T3_FAKE_CLAUDE_STDERR;
}

if (input.argsMustContain !== undefined) {
process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN = input.argsMustContain;
} else {
delete process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN;
}

if (input.argsMustNotContain !== undefined) {
process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN = input.argsMustNotContain;
} else {
delete process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN;
}

if (input.stdinMustContain !== undefined) {
process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN = input.stdinMustContain;
} else {
delete process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN;
}
});

return {
previousPath,
previousOutput,
previousExitCode,
previousStderr,
previousArgsMustContain,
previousArgsMustNotContain,
previousStdinMustContain,
};
}),
() => effect,
(previous) =>
Effect.sync(() => {
process.env.PATH = previous.previousPath;

if (previous.previousOutput === undefined) {
delete process.env.T3_FAKE_CLAUDE_OUTPUT;
} else {
process.env.T3_FAKE_CLAUDE_OUTPUT = previous.previousOutput;
}

if (previous.previousExitCode === undefined) {
delete process.env.T3_FAKE_CLAUDE_EXIT_CODE;
} else {
process.env.T3_FAKE_CLAUDE_EXIT_CODE = previous.previousExitCode;
}

if (previous.previousStderr === undefined) {
delete process.env.T3_FAKE_CLAUDE_STDERR;
} else {
process.env.T3_FAKE_CLAUDE_STDERR = previous.previousStderr;
}

if (previous.previousArgsMustContain === undefined) {
delete process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN;
} else {
process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN = previous.previousArgsMustContain;
}

if (previous.previousArgsMustNotContain === undefined) {
delete process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN;
} else {
process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN = previous.previousArgsMustNotContain;
}

if (previous.previousStdinMustContain === undefined) {
delete process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN;
} else {
process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN = previous.previousStdinMustContain;
}
}),
);
}

it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGenerationLive", (it) => {
it.effect("forwards Claude thinking settings for Haiku without passing effort", () =>
withFakeClaudeEnv(
{
output: JSON.stringify({
structured_output: {
subject: "Add important change",
body: "",
},
}),
argsMustContain: '--settings {"alwaysThinkingEnabled":false}',
argsMustNotContain: "--effort",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/claude-effect",
stagedSummary: "M README.md",
stagedPatch: "diff --git a/README.md b/README.md",
modelSelection: {
provider: "claudeAgent",
model: "claude-haiku-4-5",
options: {
thinking: false,
effort: "high",
},
},
});

expect(generated.subject).toBe("Add important change");
}),
),
);

it.effect("forwards Claude fast mode and supported effort", () =>
withFakeClaudeEnv(
{
output: JSON.stringify({
structured_output: {
title: "Improve orchestration flow",
body: "Body",
},
}),
argsMustContain: '--effort max --settings {"fastMode":true}',
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generatePrContent({
cwd: process.cwd(),
baseBranch: "main",
headBranch: "feature/claude-effect",
commitSummary: "Improve orchestration",
diffSummary: "1 file changed",
diffPatch: "diff --git a/README.md b/README.md",
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-6",
options: {
effort: "max",
fastMode: true,
},
},
});

expect(generated.title).toBe("Improve orchestration flow");
}),
),
);
});
Loading