authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-19 21:04:13-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 21:04:13-07:00
logb6302784f8e058de4fb20dae931b4df29e9302fe
tree11ac8eeb52331ea3ba270b402d02c36ca978bfb8
parent9544e72d08755dfc5d9014efd87243f4654f239d
signature Signed by PGP key B5690EEEBB952194

[codex] Refactor review and text generation services (#3196)

Co-authored-by: codex <codex@users.noreply.github.com>

16 files changed, 609 insertions(+), 643 deletions(-)

apps/server/src/git/GitManager.test.ts+51-49
......@@ -20,27 +20,16 @@ import type {
2020} from "@t3tools/contracts";
2121
2222import { GitCommandError, TextGenerationError } from "@t3tools/contracts";
23import { type GitManagerShape } from "./GitManager.ts";
24import {
25 GitHubCliError,
26 type GitHubCliShape,
27 type GitHubPullRequestSummary,
28 GitHubCli,
29} from "../sourceControl/GitHubCli.ts";
30import { type TextGenerationShape, TextGeneration } from "../textGeneration/TextGeneration.ts";
23import * as GitManager from "./GitManager.ts";
24import * as GitHubCli from "../sourceControl/GitHubCli.ts";
25import * as TextGeneration from "../textGeneration/TextGeneration.ts";
3126import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
3227import * as VcsProcess from "../vcs/VcsProcess.ts";
3328import * as GitHubSourceControlProvider from "../sourceControl/GitHubSourceControlProvider.ts";
3429import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
35import { makeGitManager } from "./GitManager.ts";
36import { ServerConfig } from "../config.ts";
37import { ServerSettingsService } from "../serverSettings.ts";
38import {
39 ProjectSetupScriptRunner,
40 ProjectSetupScriptRunnerError,
41 type ProjectSetupScriptRunnerInput,
42 type ProjectSetupScriptRunnerShape,
43} from "../project/Services/ProjectSetupScriptRunner.ts";
30import * as ServerConfig from "../config.ts";
31import * as ServerSettings from "../serverSettings.ts";
32import * as ProjectSetupScriptRunner from "../project/Services/ProjectSetupScriptRunner.ts";
4433
4534interface FakeGhScenario {
4635 prListSequence?: string[];
......@@ -60,7 +49,7 @@ interface FakeGhScenario {
6049 headRepositoryOwnerLogin?: string | null;
6150 };
6251 repositoryCloneUrls?: Record<string, { url: string; sshUrl: string }>;
63 failWith?: GitHubCliError;
52 failWith?: GitHubCli.GitHubCliError;
6453}
6554
6655function fakeGhOutput(stdout: string): VcsProcess.VcsProcessOutput {
......@@ -108,7 +97,7 @@ interface FakeGitTextGeneration {
10897
10998type FakePullRequest = NonNullable<FakeGhScenario["pullRequest"]>;
11099
111function normalizeFakePullRequestSummary(raw: unknown): GitHubPullRequestSummary | null {
100function normalizeFakePullRequestSummary(raw: unknown): GitHubCli.GitHubPullRequestSummary | null {
112101 if (!raw || typeof raw !== "object") {
113102 return null;
114103 }
......@@ -182,13 +171,13 @@ function runGitSyncForFakeGh(cwd: string, args: readonly string[]): void {
182171 if (result.status === 0) {
183172 return;
184173 }
185 throw new GitHubCliError({
174 throw new GitHubCli.GitHubCliError({
186175 operation: "execute",
187176 detail: `Failed to simulate gh checkout with git ${args.join(" ")}: ${result.stderr?.trim() || "unknown error"}`,
188177 });
189178}
190179
191function isGitHubCliError(error: unknown): error is GitHubCliError {
180function isGitHubCliError(error: unknown): error is GitHubCli.GitHubCliError {
192181 return (
193182 typeof error === "object" &&
194183 error !== null &&
......@@ -312,7 +301,9 @@ function configureVisibleRemoteUrlWithLocalRewrite(
312301 });
313302}
314303
315function createTextGeneration(overrides: Partial<FakeGitTextGeneration> = {}): TextGenerationShape {
304function createTextGeneration(
305 overrides: Partial<FakeGitTextGeneration> = {},
306): TextGeneration.TextGeneration["Service"] {
316307 const implementation: FakeGitTextGeneration = {
317308 generateCommitMessage: (input) =>
318309 Effect.succeed({
......@@ -385,7 +376,7 @@ function createTextGeneration(overrides: Partial<FakeGitTextGeneration> = {}): T
385376}
386377
387378function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
388 service: GitHubCliShape;
379 service: GitHubCli.GitHubCliShape;
389380 ghCalls: string[];
390381} {
391382 const prListQueue = [...(scenario.prListSequence ?? [])];
......@@ -397,7 +388,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
397388 );
398389 const ghCalls: string[] = [];
399390
400 const execute: GitHubCliShape["execute"] = (input) => {
391 const execute: GitHubCli.GitHubCliShape["execute"] = (input) => {
401392 const args = [...input.args];
402393 ghCalls.push(args.join(" "));
403394
......@@ -487,7 +478,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
487478 catch: (error) =>
488479 isGitHubCliError(error)
489480 ? error
490 : new GitHubCliError({
481 : new GitHubCli.GitHubCliError({
491482 operation: "execute",
492483 detail:
493484 error instanceof Error
......@@ -503,7 +494,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
503494 const cloneUrls = scenario.repositoryCloneUrls?.[repository];
504495 if (!cloneUrls) {
505496 return Effect.fail(
506 new GitHubCliError({
497 new GitHubCli.GitHubCliError({
507498 operation: "execute",
508499 detail: `Unexpected repository lookup: ${repository}`,
509500 }),
......@@ -523,7 +514,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
523514 }
524515
525516 return Effect.fail(
526 new GitHubCliError({
517 new GitHubCli.GitHubCliError({
527518 operation: "execute",
528519 detail: `Unexpected gh command: ${args.join(" ")}`,
529520 }),
......@@ -553,7 +544,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
553544 Effect.map((raw) =>
554545 raw
555546 .map((entry) => normalizeFakePullRequestSummary(entry))
556 .filter((entry): entry is GitHubPullRequestSummary => entry !== null),
547 .filter((entry): entry is GitHubCli.GitHubPullRequestSummary => entry !== null),
557548 ),
558549 ),
559550 createPullRequest: (input) =>
......@@ -592,7 +583,9 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
592583 "--json",
593584 "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner",
594585 ],
595 }).pipe(Effect.map((result) => JSON.parse(result.stdout) as GitHubPullRequestSummary)),
586 }).pipe(
587 Effect.map((result) => JSON.parse(result.stdout) as GitHubCli.GitHubPullRequestSummary),
588 ),
596589 getRepositoryCloneUrls: (input) =>
597590 execute({
598591 cwd: input.cwd,
......@@ -600,7 +593,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
600593 }).pipe(Effect.map((result) => JSON.parse(result.stdout))),
601594 createRepository: (input) =>
602595 Effect.fail(
603 new GitHubCliError({
596 new GitHubCli.GitHubCliError({
604597 operation: "createRepository",
605598 detail: `Unexpected repository create: ${input.repository}`,
606599 }),
......@@ -616,7 +609,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
616609}
617610
618611function runStackedAction(
619 manager: GitManagerShape,
612 manager: GitManager.GitManagerShape,
620613 input: {
621614 cwd: string;
622615 action: "commit" | "push" | "create_pr" | "commit_push" | "commit_push_pr";
......@@ -625,7 +618,7 @@ function runStackedAction(
625618 featureBranch?: boolean;
626619 filePaths?: readonly string[];
627620 },
628 options?: Parameters<GitManagerShape["runStackedAction"]>[1],
621 options?: Parameters<GitManager.GitManagerShape["runStackedAction"]>[1],
629622) {
630623 return manager.runStackedAction(
631624 {
......@@ -636,12 +629,15 @@ function runStackedAction(
636629 );
637630}
638631
639function resolvePullRequest(manager: GitManagerShape, input: { cwd: string; reference: string }) {
632function resolvePullRequest(
633 manager: GitManager.GitManagerShape,
634 input: { cwd: string; reference: string },
635) {
640636 return manager.resolvePullRequest(input);
641637}
642638
643639function preparePullRequestThread(
644 manager: GitManagerShape,
640 manager: GitManager.GitManagerShape,
645641 input: GitPreparePullRequestThreadInput,
646642) {
647643 return manager.preparePullRequestThread(input);
......@@ -650,20 +646,20 @@ function preparePullRequestThread(
650646function makeManager(input?: {
651647 ghScenario?: FakeGhScenario;
652648 textGeneration?: Partial<FakeGitTextGeneration>;
653 setupScriptRunner?: ProjectSetupScriptRunnerShape;
649 setupScriptRunner?: ProjectSetupScriptRunner.ProjectSetupScriptRunnerShape;
654650}) {
655651 const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario);
656652 const textGeneration = createTextGeneration(input?.textGeneration);
657 const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
653 const serverConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {
658654 prefix: "t3-git-manager-test-",
659655 });
660656
661 const serverSettingsLayer = ServerSettingsService.layerTest();
657 const serverSettingsLayer = ServerSettings.ServerSettingsService.layerTest();
662658
663659 const vcsDriverLayer = GitVcsDriver.layer.pipe(
664660 Layer.provideMerge(VcsProcess.layer),
665661 Layer.provideMerge(NodeServices.layer),
666 Layer.provideMerge(ServerConfigLayer),
662 Layer.provideMerge(serverConfigLayer),
667663 );
668664 const sourceControlRegistryLayer = Layer.effect(
669665 SourceControlProviderRegistry.SourceControlProviderRegistry,
......@@ -676,14 +672,14 @@ function makeManager(input?: {
676672 discover: Effect.succeed([]),
677673 }),
678674 ),
679 Effect.provide(Layer.succeed(GitHubCli, gitHubCli)),
675 Effect.provide(Layer.succeed(GitHubCli.GitHubCli, gitHubCli)),
680676 ),
681677 );
682678
683679 const managerLayer = Layer.mergeAll(
684 Layer.succeed(TextGeneration, textGeneration),
680 Layer.succeed(TextGeneration.TextGeneration, textGeneration),
685681 Layer.succeed(
686 ProjectSetupScriptRunner,
682 ProjectSetupScriptRunner.ProjectSetupScriptRunner,
687683 input?.setupScriptRunner ?? {
688684 runForThread: () => Effect.succeed({ status: "no-script" as const }),
689685 },
......@@ -692,7 +688,7 @@ function makeManager(input?: {
692688 serverSettingsLayer,
693689 ).pipe(Layer.provideMerge(sourceControlRegistryLayer), Layer.provideMerge(NodeServices.layer));
694690
695 return makeGitManager().pipe(
691 return GitManager.makeGitManager().pipe(
696692 Effect.provide(managerLayer),
697693 Effect.map((manager) => ({ manager, ghCalls })),
698694 );
......@@ -701,7 +697,9 @@ function makeManager(input?: {
701697const asThreadId = (threadId: string) => threadId as ThreadId;
702698
703699const GitManagerTestLayer = GitVcsDriver.layer.pipe(
704 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" })),
700 Layer.provide(
701 ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" }),
702 ),
705703 Layer.provideMerge(VcsProcess.layer),
706704 Layer.provideMerge(NodeServices.layer),
707705);
......@@ -1335,7 +1333,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
13351333
13361334 const { manager } = yield* makeManager({
13371335 ghScenario: {
1338 failWith: new GitHubCliError({
1336 failWith: new GitHubCli.GitHubCliError({
13391337 operation: "execute",
13401338 detail: "GitHub CLI (`gh`) is required but not available on PATH.",
13411339 }),
......@@ -2417,7 +2415,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
24172415
24182416 const { manager } = yield* makeManager({
24192417 ghScenario: {
2420 failWith: new GitHubCliError({
2418 failWith: new GitHubCli.GitHubCliError({
24212419 operation: "execute",
24222420 detail: "GitHub CLI (`gh`) is required but not available on PATH.",
24232421 }),
......@@ -2446,7 +2444,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
24462444
24472445 const { manager } = yield* makeManager({
24482446 ghScenario: {
2449 failWith: new GitHubCliError({
2447 failWith: new GitHubCli.GitHubCliError({
24502448 operation: "execute",
24512449 detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.",
24522450 }),
......@@ -2702,7 +2700,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
27022700 yield* runGit(repoDir, ["push", "origin", "HEAD:refs/pull/177/head"]);
27032701 yield* runGit(repoDir, ["checkout", "main"]);
27042702
2705 const setupCalls: ProjectSetupScriptRunnerInput[] = [];
2703 const setupCalls: ProjectSetupScriptRunner.ProjectSetupScriptRunnerInput[] = [];
27062704 const { manager } = yield* makeManager({
27072705 ghScenario: {
27082706 pullRequest: {
......@@ -2924,7 +2922,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
29242922 const worktreePath = path.join(repoDir, "..", `pr-existing-${path.basename(repoDir)}`);
29252923 yield* runGit(repoDir, ["worktree", "add", worktreePath, "feature/pr-existing-worktree"]);
29262924
2927 const setupCalls: ProjectSetupScriptRunnerInput[] = [];
2925 const setupCalls: ProjectSetupScriptRunner.ProjectSetupScriptRunnerInput[] = [];
29282926 const { manager } = yield* makeManager({
29292927 ghScenario: {
29302928 pullRequest: {
......@@ -3164,7 +3162,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
31643162 },
31653163 setupScriptRunner: {
31663164 runForThread: () =>
3167 Effect.fail(new ProjectSetupScriptRunnerError({ message: "terminal start failed" })),
3165 Effect.fail(
3166 new ProjectSetupScriptRunner.ProjectSetupScriptRunnerError({
3167 message: "terminal start failed",
3168 }),
3169 ),
31683170 },
31693171 });
31703172
apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts+19-16
......@@ -10,16 +10,16 @@ import * as Layer from "effect/Layer";
1010import * as PubSub from "effect/PubSub";
1111import * as Stream from "effect/Stream";
1212
13import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
14import type { CodexAdapterShape } from "../Services/CodexAdapter.ts";
15import type { CursorAdapterShape } from "../Services/CursorAdapter.ts";
16import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts";
17import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
18import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";
13import type * as ClaudeAdapter from "../Services/ClaudeAdapter.ts";
14import type * as CodexAdapter from "../Services/CodexAdapter.ts";
15import type * as CursorAdapter from "../Services/CursorAdapter.ts";
16import type * as OpenCodeAdapter from "../Services/OpenCodeAdapter.ts";
17import * as ProviderAdapterRegistry from "../Services/ProviderAdapterRegistry.ts";
18import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";
1919import type { ProviderInstance } from "../ProviderDriver.ts";
2020import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
21import type { TextGenerationShape } from "../../textGeneration/TextGeneration.ts";
22import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
21import type * as TextGeneration from "../../textGeneration/TextGeneration.ts";
22import * as ProviderAdapterRegistryLayer from "./ProviderAdapterRegistry.ts";
2323import * as NodeServices from "@effect/platform-node/NodeServices";
2424
2525const CODEX_DRIVER = ProviderDriverKind.make("codex");
......@@ -27,7 +27,7 @@ const CLAUDE_AGENT_DRIVER = ProviderDriverKind.make("claudeAgent");
2727const OPENCODE_DRIVER = ProviderDriverKind.make("opencode");
2828const CURSOR_DRIVER = ProviderDriverKind.make("cursor");
2929
30const fakeCodexAdapter: CodexAdapterShape = {
30const fakeCodexAdapter: CodexAdapter.CodexAdapterShape = {
3131 provider: CODEX_DRIVER,
3232 capabilities: { sessionModelSwitch: "in-session" },
3333 startSession: vi.fn(),
......@@ -44,7 +44,7 @@ const fakeCodexAdapter: CodexAdapterShape = {
4444 streamEvents: Stream.empty,
4545};
4646
47const fakeClaudeAdapter: ClaudeAdapterShape = {
47const fakeClaudeAdapter: ClaudeAdapter.ClaudeAdapterShape = {
4848 provider: CLAUDE_AGENT_DRIVER,
4949 capabilities: { sessionModelSwitch: "in-session" },
5050 startSession: vi.fn(),
......@@ -61,7 +61,7 @@ const fakeClaudeAdapter: ClaudeAdapterShape = {
6161 streamEvents: Stream.empty,
6262};
6363
64const fakeOpenCodeAdapter: OpenCodeAdapterShape = {
64const fakeOpenCodeAdapter: OpenCodeAdapter.OpenCodeAdapterShape = {
6565 provider: OPENCODE_DRIVER,
6666 capabilities: { sessionModelSwitch: "in-session" },
6767 startSession: vi.fn(),
......@@ -78,7 +78,7 @@ const fakeOpenCodeAdapter: OpenCodeAdapterShape = {
7878 streamEvents: Stream.empty,
7979};
8080
81const fakeCursorAdapter: CursorAdapterShape = {
81const fakeCursorAdapter: CursorAdapter.CursorAdapterShape = {
8282 provider: CURSOR_DRIVER,
8383 capabilities: { sessionModelSwitch: "in-session" },
8484 startSession: vi.fn(),
......@@ -124,7 +124,7 @@ const makeFakeInstance = (
124124 streamChanges: Stream.empty,
125125 },
126126 adapter,
127 textGeneration: {} as unknown as TextGenerationShape,
127 textGeneration: {} as unknown as TextGeneration.TextGeneration["Service"],
128128 };
129129};
130130
......@@ -135,7 +135,7 @@ const fakeInstances: ReadonlyArray<ProviderInstance> = [
135135 makeFakeInstance("cursor", fakeCursorAdapter),
136136];
137137
138const fakeInstanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {
138const fakeInstanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, {
139139 getInstance: (instanceId) =>
140140 Effect.succeed(fakeInstances.find((instance) => instance.instanceId === instanceId)),
141141 listInstances: Effect.succeed(fakeInstances),
......@@ -147,14 +147,17 @@ const fakeInstanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {
147147});
148148
149149const layer = Layer.mergeAll(
150 Layer.provide(ProviderAdapterRegistryLive, fakeInstanceRegistryLayer),
150 Layer.provide(
151 ProviderAdapterRegistryLayer.ProviderAdapterRegistryLive,
152 fakeInstanceRegistryLayer,
153 ),
151154 NodeServices.layer,
152155);
153156
154157it.layer(layer)("ProviderAdapterRegistryLive", (it) => {
155158 it("resolves adapters and routing metadata from provider instances", () =>
156159 Effect.gen(function* () {
157 const registry = yield* ProviderAdapterRegistry;
160 const registry = yield* ProviderAdapterRegistry.ProviderAdapterRegistry;
158161 const claudeInstanceId = defaultInstanceIdForDriver(CLAUDE_AGENT_DRIVER);
159162
160163 const adapter = yield* registry.getByInstance(claudeInstanceId);
apps/server/src/provider/ProviderDriver.ts+2-2
......@@ -30,7 +30,7 @@ import type * as Effect from "effect/Effect";
3030import type * as Schema from "effect/Schema";
3131import type * as Scope from "effect/Scope";
3232
33import type { TextGenerationShape } from "../textGeneration/TextGeneration.ts";
33import type * as TextGeneration from "../textGeneration/TextGeneration.ts";
3434import type { ProviderAdapterError, ProviderDriverError } from "./Errors.ts";
3535import type { ProviderAdapterShape } from "./Services/ProviderAdapter.ts";
3636import type { ServerProviderShape } from "./Services/ServerProvider.ts";
......@@ -70,7 +70,7 @@ export interface ProviderInstance {
7070 readonly enabled: boolean;
7171 readonly snapshot: ServerProviderShape;
7272 readonly adapter: ProviderAdapterShape<ProviderAdapterError>;
73 readonly textGeneration: TextGenerationShape;
73 readonly textGeneration: TextGeneration.TextGeneration["Service"];
7474}
7575
7676export interface ProviderContinuationIdentity {
apps/server/src/review/ReviewService.ts+14-15
......@@ -13,22 +13,21 @@ import {
1313 type ReviewDiffPreviewResult,
1414} from "@t3tools/contracts";
1515
16import { ServerConfig } from "../config.ts";
16import * as ServerConfig from "../config.ts";
1717import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
1818import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
1919
20export interface ReviewServiceShape {
21 readonly getDiffPreview: (
22 input: ReviewDiffPreviewInput,
23 ) => Effect.Effect<ReviewDiffPreviewResult, ReviewDiffPreviewError>;
24}
25
26export class ReviewService extends Context.Service<ReviewService, ReviewServiceShape>()(
27 "t3/review/ReviewService",
28) {}
29
30export const make = Effect.fn("makeReviewService")(function* () {
31 const config = yield* ServerConfig;
20export class ReviewService extends Context.Service<
21 ReviewService,
22 {
23 readonly getDiffPreview: (
24 input: ReviewDiffPreviewInput,
25 ) => Effect.Effect<ReviewDiffPreviewResult, ReviewDiffPreviewError>;
26 }
27>()("t3/review/ReviewService") {}
28
29export const make = Effect.gen(function* () {
30 const config = yield* ServerConfig.ServerConfig;
3231 const fileSystem = yield* FileSystem.FileSystem;
3332 const path = yield* Path.Path;
3433 const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry;
......@@ -62,7 +61,7 @@ export const make = Effect.fn("makeReviewService")(function* () {
6261 });
6362 });
6463
65 const getDiffPreview: ReviewServiceShape["getDiffPreview"] = Effect.fn(
64 const getDiffPreview: ReviewService["Service"]["getDiffPreview"] = Effect.fn(
6665 "ReviewService.getDiffPreview",
6766 )(function* (input) {
6867 yield* assertWorkspaceBoundCwd(input.cwd);
......@@ -96,4 +95,4 @@ export const make = Effect.fn("makeReviewService")(function* () {
9695 });
9796});
9897
99export const layer = Layer.effect(ReviewService, make());
98export const layer = Layer.effect(ReviewService, make);
apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+4-4
......@@ -9,13 +9,13 @@ import * as Schema from "effect/Schema";
99import { createModelSelection } from "@t3tools/shared/model";
1010import { expect } from "vite-plus/test";
1111
12import { ServerConfig } from "../config.ts";
13import { type TextGenerationShape } from "./TextGeneration.ts";
12import * as ServerConfig from "../config.ts";
13import * as TextGeneration from "./TextGeneration.ts";
1414import { sanitizeThreadTitle } from "./TextGenerationUtils.ts";
1515import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts";
1616const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
1717
18const ClaudeTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), {
18const ClaudeTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {
1919 prefix: "t3code-claude-text-generation-test-",
2020}).pipe(Layer.provideMerge(NodeServices.layer));
2121
......@@ -79,7 +79,7 @@ function withFakeClaudeEnv<A, E, R>(
7979 homeMustBe?: string;
8080 claudeConfig?: Partial<ClaudeSettings>;
8181 },
82 effectFn: (textGeneration: TextGenerationShape) => Effect.Effect<A, E, R>,
82 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
8383) {
8484 return Effect.gen(function* () {
8585 const fs = yield* FileSystem.FileSystem;
apps/server/src/textGeneration/ClaudeTextGeneration.ts+79-83
......@@ -1,7 +1,7 @@
11/**
22 * ClaudeTextGeneration – Text generation layer using the Claude CLI.
33 *
4 * Implements the same TextGenerationShape contract as CodexTextGeneration but
4 * Implements the same TextGeneration service contract as CodexTextGeneration but
55 * delegates to the `claude` CLI (`claude -p`) with structured JSON output
66 * instead of the `codex exec` CLI.
77 *
......@@ -18,7 +18,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar
1818import { resolveSpawnCommand } from "@t3tools/shared/shell";
1919
2020import { TextGenerationError } from "@t3tools/contracts";
21import { type TextGenerationShape } from "./TextGeneration.ts";
21import * as TextGeneration from "./TextGeneration.ts";
2222import {
2323 buildBranchNamePrompt,
2424 buildCommitMessagePrompt,
......@@ -260,107 +260,103 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
260260 });
261261
262262 // ---------------------------------------------------------------------------
263 // TextGenerationShape methods
263 // TextGeneration service methods
264264 // ---------------------------------------------------------------------------
265265
266 const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn(
267 "ClaudeTextGeneration.generateCommitMessage",
268 )(function* (input) {
269 const { prompt, outputSchema } = buildCommitMessagePrompt({
270 branch: input.branch,
271 stagedSummary: input.stagedSummary,
272 stagedPatch: input.stagedPatch,
273 includeBranch: input.includeBranch === true,
274 });
266 const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] =
267 Effect.fn("ClaudeTextGeneration.generateCommitMessage")(function* (input) {
268 const { prompt, outputSchema } = buildCommitMessagePrompt({
269 branch: input.branch,
270 stagedSummary: input.stagedSummary,
271 stagedPatch: input.stagedPatch,
272 includeBranch: input.includeBranch === true,
273 });
274
275 const generated = yield* runClaudeJson({
276 operation: "generateCommitMessage",
277 cwd: input.cwd,
278 prompt,
279 outputSchemaJson: outputSchema,
280 modelSelection: input.modelSelection,
281 });
275282
276 const generated = yield* runClaudeJson({
277 operation: "generateCommitMessage",
278 cwd: input.cwd,
279 prompt,
280 outputSchemaJson: outputSchema,
281 modelSelection: input.modelSelection,
283 return {
284 subject: sanitizeCommitSubject(generated.subject),
285 body: generated.body.trim(),
286 ...("branch" in generated && typeof generated.branch === "string"
287 ? { branch: sanitizeFeatureBranchName(generated.branch) }
288 : {}),
289 };
282290 });
283291
284 return {
285 subject: sanitizeCommitSubject(generated.subject),
286 body: generated.body.trim(),
287 ...("branch" in generated && typeof generated.branch === "string"
288 ? { branch: sanitizeFeatureBranchName(generated.branch) }
289 : {}),
290 };
291 });
292 const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] =
293 Effect.fn("ClaudeTextGeneration.generatePrContent")(function* (input) {
294 const { prompt, outputSchema } = buildPrContentPrompt({
295 baseBranch: input.baseBranch,
296 headBranch: input.headBranch,
297 commitSummary: input.commitSummary,
298 diffSummary: input.diffSummary,
299 diffPatch: input.diffPatch,
300 });
292301
293 const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn(
294 "ClaudeTextGeneration.generatePrContent",
295 )(function* (input) {
296 const { prompt, outputSchema } = buildPrContentPrompt({
297 baseBranch: input.baseBranch,
298 headBranch: input.headBranch,
299 commitSummary: input.commitSummary,
300 diffSummary: input.diffSummary,
301 diffPatch: input.diffPatch,
302 });
302 const generated = yield* runClaudeJson({
303 operation: "generatePrContent",
304 cwd: input.cwd,
305 prompt,
306 outputSchemaJson: outputSchema,
307 modelSelection: input.modelSelection,
308 });
303309
304 const generated = yield* runClaudeJson({
305 operation: "generatePrContent",
306 cwd: input.cwd,
307 prompt,
308 outputSchemaJson: outputSchema,
309 modelSelection: input.modelSelection,
310 return {
311 title: sanitizePrTitle(generated.title),
312 body: generated.body.trim(),
313 };
310314 });
311315
312 return {
313 title: sanitizePrTitle(generated.title),
314 body: generated.body.trim(),
315 };
316 });
316 const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] =
317 Effect.fn("ClaudeTextGeneration.generateBranchName")(function* (input) {
318 const { prompt, outputSchema } = buildBranchNamePrompt({
319 message: input.message,
320 attachments: input.attachments,
321 });
317322
318 const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn(
319 "ClaudeTextGeneration.generateBranchName",
320 )(function* (input) {
321 const { prompt, outputSchema } = buildBranchNamePrompt({
322 message: input.message,
323 attachments: input.attachments,
324 });
323 const generated = yield* runClaudeJson({
324 operation: "generateBranchName",
325 cwd: input.cwd,
326 prompt,
327 outputSchemaJson: outputSchema,
328 modelSelection: input.modelSelection,
329 });
325330
326 const generated = yield* runClaudeJson({
327 operation: "generateBranchName",
328 cwd: input.cwd,
329 prompt,
330 outputSchemaJson: outputSchema,
331 modelSelection: input.modelSelection,
331 return {
332 branch: sanitizeBranchFragment(generated.branch),
333 };
332334 });
333335
334 return {
335 branch: sanitizeBranchFragment(generated.branch),
336 };
337 });
336 const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] =
337 Effect.fn("ClaudeTextGeneration.generateThreadTitle")(function* (input) {
338 const { prompt, outputSchema } = buildThreadTitlePrompt({
339 message: input.message,
340 attachments: input.attachments,
341 });
338342
339 const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn(
340 "ClaudeTextGeneration.generateThreadTitle",
341 )(function* (input) {
342 const { prompt, outputSchema } = buildThreadTitlePrompt({
343 message: input.message,
344 attachments: input.attachments,
345 });
343 const generated = yield* runClaudeJson({
344 operation: "generateThreadTitle",
345 cwd: input.cwd,
346 prompt,
347 outputSchemaJson: outputSchema,
348 modelSelection: input.modelSelection,
349 });
346350
347 const generated = yield* runClaudeJson({
348 operation: "generateThreadTitle",
349 cwd: input.cwd,
350 prompt,
351 outputSchemaJson: outputSchema,
352 modelSelection: input.modelSelection,
351 return {
352 title: sanitizeThreadTitle(generated.title),
353 };
353354 });
354355
355 return {
356 title: sanitizeThreadTitle(generated.title),
357 };
358 });
359
360356 return {
361357 generateCommitMessage,
362358 generatePrContent,
363359 generateBranchName,
364360 generateThreadTitle,
365 } satisfies TextGenerationShape;
361 } satisfies TextGeneration.TextGeneration["Service"];
366362});
apps/server/src/textGeneration/CodexTextGeneration.test.ts+7-7
......@@ -11,8 +11,8 @@ import { expect } from "vite-plus/test";
1111
1212import { CodexSettings, ProviderInstanceId, TextGenerationError } from "@t3tools/contracts";
1313
14import { ServerConfig } from "../config.ts";
15import { type TextGenerationShape } from "./TextGeneration.ts";
14import * as ServerConfig from "../config.ts";
15import * as TextGeneration from "./TextGeneration.ts";
1616import { makeCodexTextGeneration } from "./CodexTextGeneration.ts";
1717const decodeCodexSettings = Schema.decodeSync(CodexSettings);
1818
......@@ -21,7 +21,7 @@ const DEFAULT_TEST_MODEL_SELECTION = createModelSelection(
2121 "gpt-5.4-mini",
2222);
2323
24const CodexTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), {
24const CodexTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {
2525 prefix: "t3code-codex-text-generation-test-",
2626}).pipe(Layer.provideMerge(NodeServices.layer));
2727
......@@ -169,7 +169,7 @@ function withFakeCodexEnv<A, E, R>(
169169 stdinMustContain?: string;
170170 stdinMustNotContain?: string;
171171 },
172 effectFn: (textGeneration: TextGenerationShape) => Effect.Effect<A, E, R>,
172 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
173173) {
174174 return Effect.gen(function* () {
175175 const fs = yield* FileSystem.FileSystem;
......@@ -427,7 +427,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
427427 Effect.gen(function* () {
428428 const fs = yield* FileSystem.FileSystem;
429429 const path = yield* Path.Path;
430 const { attachmentsDir } = yield* ServerConfig;
430 const { attachmentsDir } = yield* ServerConfig.ServerConfig;
431431 const attachmentId = "thread-branch-image-attachment";
432432 const attachmentPath = path.join(attachmentsDir, `${attachmentId}.png`);
433433 yield* fs.makeDirectory(attachmentsDir, { recursive: true });
......@@ -465,7 +465,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
465465 Effect.gen(function* () {
466466 const fs = yield* FileSystem.FileSystem;
467467 const path = yield* Path.Path;
468 const { attachmentsDir } = yield* ServerConfig;
468 const { attachmentsDir } = yield* ServerConfig.ServerConfig;
469469 const attachmentId = "thread-1-attachment";
470470 const imagePath = path.join(attachmentsDir, `${attachmentId}.png`);
471471 yield* fs.makeDirectory(attachmentsDir, { recursive: true });
......@@ -514,7 +514,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
514514 Effect.gen(function* () {
515515 const fs = yield* FileSystem.FileSystem;
516516 const path = yield* Path.Path;
517 const { attachmentsDir } = yield* ServerConfig;
517 const { attachmentsDir } = yield* ServerConfig.ServerConfig;
518518 const missingAttachmentId = "thread-missing-attachment";
519519 const missingPath = path.join(attachmentsDir, `${missingAttachmentId}.png`);
520520 yield* fs.remove(missingPath).pipe(Effect.catch(() => Effect.void));
apps/server/src/textGeneration/CodexTextGeneration.ts+90-98
......@@ -12,14 +12,10 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar
1212import { resolveSpawnCommand } from "@t3tools/shared/shell";
1313
1414import { resolveAttachmentPath } from "../attachmentStore.ts";
15import { ServerConfig } from "../config.ts";
15import * as ServerConfig from "../config.ts";
1616import { expandHomePath } from "../pathExpansion.ts";
1717import { TextGenerationError } from "@t3tools/contracts";
18import {
19 type BranchNameGenerationInput,
20 type ThreadTitleGenerationResult,
21 type TextGenerationShape,
22} from "./TextGeneration.ts";
18import * as TextGeneration from "./TextGeneration.ts";
2319import {
2420 buildBranchNamePrompt,
2521 buildCommitMessagePrompt,
......@@ -50,7 +46,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
5046 const fileSystem = yield* FileSystem.FileSystem;
5147 const path = yield* Path.Path;
5248 const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
53 const serverConfig = yield* Effect.service(ServerConfig);
49 const serverConfig = yield* Effect.service(ServerConfig.ServerConfig);
5450 const resolvedEnvironment = environment ?? process.env;
5551
5652 type MaterializedImageAttachments = {
......@@ -121,7 +117,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
121117 | "generatePrContent"
122118 | "generateBranchName"
123119 | "generateThreadTitle",
124 attachments: BranchNameGenerationInput["attachments"],
120 attachments: TextGeneration.BranchNameGenerationInput["attachments"],
125121 ): Effect.fn.Return<MaterializedImageAttachments, TextGenerationError> {
126122 if (!attachments || attachments.length === 0) {
127123 return { imagePaths: [] };
......@@ -298,114 +294,110 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
298294 }).pipe(Effect.ensuring(cleanup));
299295 });
300296
301 const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn(
302 "CodexTextGeneration.generateCommitMessage",
303 )(function* (input) {
304 const { prompt, outputSchema } = buildCommitMessagePrompt({
305 branch: input.branch,
306 stagedSummary: input.stagedSummary,
307 stagedPatch: input.stagedPatch,
308 includeBranch: input.includeBranch === true,
309 });
297 const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] =
298 Effect.fn("CodexTextGeneration.generateCommitMessage")(function* (input) {
299 const { prompt, outputSchema } = buildCommitMessagePrompt({
300 branch: input.branch,
301 stagedSummary: input.stagedSummary,
302 stagedPatch: input.stagedPatch,
303 includeBranch: input.includeBranch === true,
304 });
310305
311 const generated = yield* runCodexJson({
312 operation: "generateCommitMessage",
313 cwd: input.cwd,
314 prompt,
315 outputSchemaJson: outputSchema,
316 modelSelection: input.modelSelection,
306 const generated = yield* runCodexJson({
307 operation: "generateCommitMessage",
308 cwd: input.cwd,
309 prompt,
310 outputSchemaJson: outputSchema,
311 modelSelection: input.modelSelection,
312 });
313
314 return {
315 subject: sanitizeCommitSubject(generated.subject),
316 body: generated.body.trim(),
317 ...("branch" in generated && typeof generated.branch === "string"
318 ? { branch: sanitizeFeatureBranchName(generated.branch) }
319 : {}),
320 };
317321 });
318322
319 return {
320 subject: sanitizeCommitSubject(generated.subject),
321 body: generated.body.trim(),
322 ...("branch" in generated && typeof generated.branch === "string"
323 ? { branch: sanitizeFeatureBranchName(generated.branch) }
324 : {}),
325 };
326 });
323 const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] =
324 Effect.fn("CodexTextGeneration.generatePrContent")(function* (input) {
325 const { prompt, outputSchema } = buildPrContentPrompt({
326 baseBranch: input.baseBranch,
327 headBranch: input.headBranch,
328 commitSummary: input.commitSummary,
329 diffSummary: input.diffSummary,
330 diffPatch: input.diffPatch,
331 });
327332
328 const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn(
329 "CodexTextGeneration.generatePrContent",
330 )(function* (input) {
331 const { prompt, outputSchema } = buildPrContentPrompt({
332 baseBranch: input.baseBranch,
333 headBranch: input.headBranch,
334 commitSummary: input.commitSummary,
335 diffSummary: input.diffSummary,
336 diffPatch: input.diffPatch,
337 });
333 const generated = yield* runCodexJson({
334 operation: "generatePrContent",
335 cwd: input.cwd,
336 prompt,
337 outputSchemaJson: outputSchema,
338 modelSelection: input.modelSelection,
339 });
338340
339 const generated = yield* runCodexJson({
340 operation: "generatePrContent",
341 cwd: input.cwd,
342 prompt,
343 outputSchemaJson: outputSchema,
344 modelSelection: input.modelSelection,
341 return {
342 title: sanitizePrTitle(generated.title),
343 body: generated.body.trim(),
344 };
345345 });
346346
347 return {
348 title: sanitizePrTitle(generated.title),
349 body: generated.body.trim(),
350 };
351 });
347 const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] =
348 Effect.fn("CodexTextGeneration.generateBranchName")(function* (input) {
349 const { imagePaths } = yield* materializeImageAttachments(
350 "generateBranchName",
351 input.attachments,
352 );
353 const { prompt, outputSchema } = buildBranchNamePrompt({
354 message: input.message,
355 attachments: input.attachments,
356 });
352357
353 const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn(
354 "CodexTextGeneration.generateBranchName",
355 )(function* (input) {
356 const { imagePaths } = yield* materializeImageAttachments(
357 "generateBranchName",
358 input.attachments,
359 );
360 const { prompt, outputSchema } = buildBranchNamePrompt({
361 message: input.message,
362 attachments: input.attachments,
363 });
358 const generated = yield* runCodexJson({
359 operation: "generateBranchName",
360 cwd: input.cwd,
361 prompt,
362 outputSchemaJson: outputSchema,
363 imagePaths,
364 modelSelection: input.modelSelection,
365 });
364366
365 const generated = yield* runCodexJson({
366 operation: "generateBranchName",
367 cwd: input.cwd,
368 prompt,
369 outputSchemaJson: outputSchema,
370 imagePaths,
371 modelSelection: input.modelSelection,
367 return {
368 branch: sanitizeBranchFragment(generated.branch),
369 };
372370 });
373371
374 return {
375 branch: sanitizeBranchFragment(generated.branch),
376 };
377 });
372 const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] =
373 Effect.fn("CodexTextGeneration.generateThreadTitle")(function* (input) {
374 const { imagePaths } = yield* materializeImageAttachments(
375 "generateThreadTitle",
376 input.attachments,
377 );
378 const { prompt, outputSchema } = buildThreadTitlePrompt({
379 message: input.message,
380 attachments: input.attachments,
381 });
378382
379 const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn(
380 "CodexTextGeneration.generateThreadTitle",
381 )(function* (input) {
382 const { imagePaths } = yield* materializeImageAttachments(
383 "generateThreadTitle",
384 input.attachments,
385 );
386 const { prompt, outputSchema } = buildThreadTitlePrompt({
387 message: input.message,
388 attachments: input.attachments,
389 });
383 const generated = yield* runCodexJson({
384 operation: "generateThreadTitle",
385 cwd: input.cwd,
386 prompt,
387 outputSchemaJson: outputSchema,
388 imagePaths,
389 modelSelection: input.modelSelection,
390 });
390391
391 const generated = yield* runCodexJson({
392 operation: "generateThreadTitle",
393 cwd: input.cwd,
394 prompt,
395 outputSchemaJson: outputSchema,
396 imagePaths,
397 modelSelection: input.modelSelection,
392 return {
393 title: sanitizeThreadTitle(generated.title),
394 } satisfies TextGeneration.ThreadTitleGenerationResult;
398395 });
399396
400 return {
401 title: sanitizeThreadTitle(generated.title),
402 } satisfies ThreadTitleGenerationResult;
403 });
404
405397 return {
406398 generateCommitMessage,
407399 generatePrContent,
408400 generateBranchName,
409401 generateThreadTitle,
410 } satisfies TextGenerationShape;
402 } satisfies TextGeneration.TextGeneration["Service"];
411403});
apps/server/src/textGeneration/CursorTextGeneration.test.ts+4-4
......@@ -16,8 +16,8 @@ import { expect } from "vite-plus/test";
1616
1717import { CursorSettings, ProviderInstanceId } from "@t3tools/contracts";
1818
19import { ServerConfig } from "../config.ts";
20import { type TextGenerationShape } from "./TextGeneration.ts";
19import * as ServerConfig from "../config.ts";
20import * as TextGeneration from "./TextGeneration.ts";
2121import { makeCursorTextGeneration } from "./CursorTextGeneration.ts";
2222const decodeCursorSettings = Schema.decodeSync(CursorSettings);
2323
......@@ -28,7 +28,7 @@ function shellSingleQuote(value: string): string {
2828 return `'${value.replaceAll("'", `'"'"'`)}'`;
2929}
3030
31const CursorTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), {
31const CursorTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {
3232 prefix: "t3code-cursor-text-generation-test-",
3333}).pipe(Layer.provideMerge(NodeServices.layer));
3434
......@@ -56,7 +56,7 @@ function makeAcpAgentWrapper(dir: string, env: Record<string, string>): string {
5656
5757function withFakeAcpAgent<A, E, R>(
5858 env: Record<string, string>,
59 effectFn: (textGeneration: TextGenerationShape) => Effect.Effect<A, E, R>,
59 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
6060) {
6161 return Effect.gen(function* () {
6262 const tempDir = mkdtempSync(path.join(os.tmpdir(), "t3code-cursor-text-acp-"));
apps/server/src/textGeneration/CursorTextGeneration.ts+77-81
......@@ -9,7 +9,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar
99import { extractJsonObject } from "@t3tools/shared/schemaJson";
1010
1111import { TextGenerationError } from "@t3tools/contracts";
12import { type ThreadTitleGenerationResult, type TextGenerationShape } from "./TextGeneration.ts";
12import * as TextGeneration from "./TextGeneration.ts";
1313import {
1414 buildBranchNamePrompt,
1515 buildCommitMessagePrompt,
......@@ -176,104 +176,100 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu
176176 Effect.scoped,
177177 );
178178
179 const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn(
180 "CursorTextGeneration.generateCommitMessage",
181 )(function* (input) {
182 const { prompt, outputSchema } = buildCommitMessagePrompt({
183 branch: input.branch,
184 stagedSummary: input.stagedSummary,
185 stagedPatch: input.stagedPatch,
186 includeBranch: input.includeBranch === true,
187 });
179 const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] =
180 Effect.fn("CursorTextGeneration.generateCommitMessage")(function* (input) {
181 const { prompt, outputSchema } = buildCommitMessagePrompt({
182 branch: input.branch,
183 stagedSummary: input.stagedSummary,
184 stagedPatch: input.stagedPatch,
185 includeBranch: input.includeBranch === true,
186 });
188187
189 const generated = yield* runCursorJson({
190 operation: "generateCommitMessage",
191 cwd: input.cwd,
192 prompt,
193 outputSchemaJson: outputSchema,
194 modelSelection: input.modelSelection,
188 const generated = yield* runCursorJson({
189 operation: "generateCommitMessage",
190 cwd: input.cwd,
191 prompt,
192 outputSchemaJson: outputSchema,
193 modelSelection: input.modelSelection,
194 });
195
196 return {
197 subject: sanitizeCommitSubject(generated.subject),
198 body: generated.body.trim(),
199 ...("branch" in generated && typeof generated.branch === "string"
200 ? { branch: sanitizeFeatureBranchName(generated.branch) }
201 : {}),
202 };
195203 });
196204
197 return {
198 subject: sanitizeCommitSubject(generated.subject),
199 body: generated.body.trim(),
200 ...("branch" in generated && typeof generated.branch === "string"
201 ? { branch: sanitizeFeatureBranchName(generated.branch) }
202 : {}),
203 };
204 });
205 const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] =
206 Effect.fn("CursorTextGeneration.generatePrContent")(function* (input) {
207 const { prompt, outputSchema } = buildPrContentPrompt({
208 baseBranch: input.baseBranch,
209 headBranch: input.headBranch,
210 commitSummary: input.commitSummary,
211 diffSummary: input.diffSummary,
212 diffPatch: input.diffPatch,
213 });
205214
206 const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn(
207 "CursorTextGeneration.generatePrContent",
208 )(function* (input) {
209 const { prompt, outputSchema } = buildPrContentPrompt({
210 baseBranch: input.baseBranch,
211 headBranch: input.headBranch,
212 commitSummary: input.commitSummary,
213 diffSummary: input.diffSummary,
214 diffPatch: input.diffPatch,
215 });
215 const generated = yield* runCursorJson({
216 operation: "generatePrContent",
217 cwd: input.cwd,
218 prompt,
219 outputSchemaJson: outputSchema,
220 modelSelection: input.modelSelection,
221 });
216222
217 const generated = yield* runCursorJson({
218 operation: "generatePrContent",
219 cwd: input.cwd,
220 prompt,
221 outputSchemaJson: outputSchema,
222 modelSelection: input.modelSelection,
223 return {
224 title: sanitizePrTitle(generated.title),
225 body: generated.body.trim(),
226 };
223227 });
224228
225 return {
226 title: sanitizePrTitle(generated.title),
227 body: generated.body.trim(),
228 };
229 });
229 const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] =
230 Effect.fn("CursorTextGeneration.generateBranchName")(function* (input) {
231 const { prompt, outputSchema } = buildBranchNamePrompt({
232 message: input.message,
233 attachments: input.attachments,
234 });
230235
231 const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn(
232 "CursorTextGeneration.generateBranchName",
233 )(function* (input) {
234 const { prompt, outputSchema } = buildBranchNamePrompt({
235 message: input.message,
236 attachments: input.attachments,
237 });
236 const generated = yield* runCursorJson({
237 operation: "generateBranchName",
238 cwd: input.cwd,
239 prompt,
240 outputSchemaJson: outputSchema,
241 modelSelection: input.modelSelection,
242 });
238243
239 const generated = yield* runCursorJson({
240 operation: "generateBranchName",
241 cwd: input.cwd,
242 prompt,
243 outputSchemaJson: outputSchema,
244 modelSelection: input.modelSelection,
244 return {
245 branch: sanitizeBranchFragment(generated.branch),
246 };
245247 });
246248
247 return {
248 branch: sanitizeBranchFragment(generated.branch),
249 };
250 });
249 const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] =
250 Effect.fn("CursorTextGeneration.generateThreadTitle")(function* (input) {
251 const { prompt, outputSchema } = buildThreadTitlePrompt({
252 message: input.message,
253 attachments: input.attachments,
254 });
251255
252 const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn(
253 "CursorTextGeneration.generateThreadTitle",
254 )(function* (input) {
255 const { prompt, outputSchema } = buildThreadTitlePrompt({
256 message: input.message,
257 attachments: input.attachments,
258 });
256 const generated = yield* runCursorJson({
257 operation: "generateThreadTitle",
258 cwd: input.cwd,
259 prompt,
260 outputSchemaJson: outputSchema,
261 modelSelection: input.modelSelection,
262 });
259263
260 const generated = yield* runCursorJson({
261 operation: "generateThreadTitle",
262 cwd: input.cwd,
263 prompt,
264 outputSchemaJson: outputSchema,
265 modelSelection: input.modelSelection,
264 return {
265 title: sanitizeThreadTitle(generated.title),
266 } satisfies TextGeneration.ThreadTitleGenerationResult;
266267 });
267268
268 return {
269 title: sanitizeThreadTitle(generated.title),
270 } satisfies ThreadTitleGenerationResult;
271 });
272
273269 return {
274270 generateCommitMessage,
275271 generatePrContent,
276272 generateBranchName,
277273 generateThreadTitle,
278 } satisfies TextGenerationShape;
274 } satisfies TextGeneration.TextGeneration["Service"];
279275});
apps/server/src/textGeneration/GrokTextGeneration.test.ts+4-4
......@@ -13,8 +13,8 @@ import { createModelSelection } from "@t3tools/shared/model";
1313import { expect } from "vite-plus/test";
1414import { GrokSettings, ProviderInstanceId } from "@t3tools/contracts";
1515
16import { ServerConfig } from "../config.ts";
17import { type TextGenerationShape } from "./TextGeneration.ts";
16import * as ServerConfig from "../config.ts";
17import * as TextGeneration from "./TextGeneration.ts";
1818import { makeGrokTextGeneration } from "./GrokTextGeneration.ts";
1919const decodeGrokSettings = Schema.decodeSync(GrokSettings);
2020
......@@ -25,7 +25,7 @@ function shellSingleQuote(value: string): string {
2525 return `'${value.replaceAll("'", `'"'"'`)}'`;
2626}
2727
28const GrokTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), {
28const GrokTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {
2929 prefix: "t3code-grok-text-generation-test-",
3030}).pipe(Layer.provideMerge(NodeServices.layer));
3131
......@@ -53,7 +53,7 @@ function makeAcpGrokWrapper(dir: string, env: Record<string, string>): string {
5353
5454function withFakeAcpGrok<A, E, R>(
5555 env: Record<string, string>,
56 effectFn: (textGeneration: TextGenerationShape) => Effect.Effect<A, E, R>,
56 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
5757) {
5858 return Effect.gen(function* () {
5959 const tempDir = mkdtempSync(path.join(os.tmpdir(), "t3code-grok-text-acp-"));
apps/server/src/textGeneration/GrokTextGeneration.ts+77-81
......@@ -10,7 +10,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar
1010import { extractJsonObject } from "@t3tools/shared/schemaJson";
1111
1212import { TextGenerationError } from "@t3tools/contracts";
13import { type ThreadTitleGenerationResult, type TextGenerationShape } from "./TextGeneration.ts";
13import * as TextGeneration from "./TextGeneration.ts";
1414import {
1515 buildBranchNamePrompt,
1616 buildCommitMessagePrompt,
......@@ -169,104 +169,100 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi
169169 Effect.scoped,
170170 );
171171
172 const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn(
173 "GrokTextGeneration.generateCommitMessage",
174 )(function* (input) {
175 const { prompt, outputSchema } = buildCommitMessagePrompt({
176 branch: input.branch,
177 stagedSummary: input.stagedSummary,
178 stagedPatch: input.stagedPatch,
179 includeBranch: input.includeBranch === true,
180 });
172 const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] =
173 Effect.fn("GrokTextGeneration.generateCommitMessage")(function* (input) {
174 const { prompt, outputSchema } = buildCommitMessagePrompt({
175 branch: input.branch,
176 stagedSummary: input.stagedSummary,
177 stagedPatch: input.stagedPatch,
178 includeBranch: input.includeBranch === true,
179 });
181180
182 const generated = yield* runGrokJson({
183 operation: "generateCommitMessage",
184 cwd: input.cwd,
185 prompt,
186 outputSchemaJson: outputSchema,
187 modelSelection: input.modelSelection,
181 const generated = yield* runGrokJson({
182 operation: "generateCommitMessage",
183 cwd: input.cwd,
184 prompt,
185 outputSchemaJson: outputSchema,
186 modelSelection: input.modelSelection,
187 });
188
189 return {
190 subject: sanitizeCommitSubject(generated.subject),
191 body: generated.body.trim(),
192 ...("branch" in generated && typeof generated.branch === "string"
193 ? { branch: sanitizeFeatureBranchName(generated.branch) }
194 : {}),
195 };
188196 });
189197
190 return {
191 subject: sanitizeCommitSubject(generated.subject),
192 body: generated.body.trim(),
193 ...("branch" in generated && typeof generated.branch === "string"
194 ? { branch: sanitizeFeatureBranchName(generated.branch) }
195 : {}),
196 };
197 });
198 const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] =
199 Effect.fn("GrokTextGeneration.generatePrContent")(function* (input) {
200 const { prompt, outputSchema } = buildPrContentPrompt({
201 baseBranch: input.baseBranch,
202 headBranch: input.headBranch,
203 commitSummary: input.commitSummary,
204 diffSummary: input.diffSummary,
205 diffPatch: input.diffPatch,
206 });
198207
199 const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn(
200 "GrokTextGeneration.generatePrContent",
201 )(function* (input) {
202 const { prompt, outputSchema } = buildPrContentPrompt({
203 baseBranch: input.baseBranch,
204 headBranch: input.headBranch,
205 commitSummary: input.commitSummary,
206 diffSummary: input.diffSummary,
207 diffPatch: input.diffPatch,
208 });
208 const generated = yield* runGrokJson({
209 operation: "generatePrContent",
210 cwd: input.cwd,
211 prompt,
212 outputSchemaJson: outputSchema,
213 modelSelection: input.modelSelection,
214 });
209215
210 const generated = yield* runGrokJson({
211 operation: "generatePrContent",
212 cwd: input.cwd,
213 prompt,
214 outputSchemaJson: outputSchema,
215 modelSelection: input.modelSelection,
216 return {
217 title: sanitizePrTitle(generated.title),
218 body: generated.body.trim(),
219 };
216220 });
217221
218 return {
219 title: sanitizePrTitle(generated.title),
220 body: generated.body.trim(),
221 };
222 });
222 const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] =
223 Effect.fn("GrokTextGeneration.generateBranchName")(function* (input) {
224 const { prompt, outputSchema } = buildBranchNamePrompt({
225 message: input.message,
226 attachments: input.attachments,
227 });
223228
224 const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn(
225 "GrokTextGeneration.generateBranchName",
226 )(function* (input) {
227 const { prompt, outputSchema } = buildBranchNamePrompt({
228 message: input.message,
229 attachments: input.attachments,
230 });
229 const generated = yield* runGrokJson({
230 operation: "generateBranchName",
231 cwd: input.cwd,
232 prompt,
233 outputSchemaJson: outputSchema,
234 modelSelection: input.modelSelection,
235 });
231236
232 const generated = yield* runGrokJson({
233 operation: "generateBranchName",
234 cwd: input.cwd,
235 prompt,
236 outputSchemaJson: outputSchema,
237 modelSelection: input.modelSelection,
237 return {
238 branch: sanitizeBranchFragment(generated.branch),
239 };
238240 });
239241
240 return {
241 branch: sanitizeBranchFragment(generated.branch),
242 };
243 });
242 const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] =
243 Effect.fn("GrokTextGeneration.generateThreadTitle")(function* (input) {
244 const { prompt, outputSchema } = buildThreadTitlePrompt({
245 message: input.message,
246 attachments: input.attachments,
247 });
244248
245 const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn(
246 "GrokTextGeneration.generateThreadTitle",
247 )(function* (input) {
248 const { prompt, outputSchema } = buildThreadTitlePrompt({
249 message: input.message,
250 attachments: input.attachments,
251 });
249 const generated = yield* runGrokJson({
250 operation: "generateThreadTitle",
251 cwd: input.cwd,
252 prompt,
253 outputSchemaJson: outputSchema,
254 modelSelection: input.modelSelection,
255 });
252256
253 const generated = yield* runGrokJson({
254 operation: "generateThreadTitle",
255 cwd: input.cwd,
256 prompt,
257 outputSchemaJson: outputSchema,
258 modelSelection: input.modelSelection,
257 return {
258 title: sanitizeThreadTitle(generated.title),
259 } satisfies TextGeneration.ThreadTitleGenerationResult;
259260 });
260261
261 return {
262 title: sanitizeThreadTitle(generated.title),
263 } satisfies ThreadTitleGenerationResult;
264 });
265
266262 return {
267263 generateCommitMessage,
268264 generatePrContent,
269265 generateBranchName,
270266 generateThreadTitle,
271 } satisfies TextGenerationShape;
267 } satisfies TextGeneration.TextGeneration["Service"];
272268});
apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts+11-15
......@@ -9,13 +9,9 @@ import * as TestClock from "effect/testing/TestClock";
99import * as NetService from "@t3tools/shared/Net";
1010import { beforeEach, expect } from "vite-plus/test";
1111
12import { ServerConfig } from "../config.ts";
13import {
14 OpenCodeRuntime,
15 OpenCodeRuntimeError,
16 type OpenCodeRuntimeShape,
17} from "../provider/opencodeRuntime.ts";
18import { type TextGenerationShape } from "./TextGeneration.ts";
12import * as ServerConfig from "../config.ts";
13import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";
14import * as TextGeneration from "./TextGeneration.ts";
1915import { makeOpenCodeTextGeneration } from "./OpenCodeTextGeneration.ts";
2016
2117const runtimeMock = {
......@@ -37,7 +33,7 @@ const runtimeMock = {
3733 },
3834};
3935
40const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {
36const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = {
4137 startOpenCodeServerProcess: ({ binaryPath }) =>
4238 Effect.gen(function* () {
4339 const index = runtimeMock.state.startCalls.length + 1;
......@@ -88,10 +84,10 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {
8884 );
8985 },
9086 },
91 }) as unknown as ReturnType<OpenCodeRuntimeShape["createOpenCodeSdkClient"]>,
87 }) as unknown as ReturnType<OpenCodeRuntime.OpenCodeRuntimeShape["createOpenCodeSdkClient"]>,
9288 loadOpenCodeInventory: () =>
9389 Effect.fail(
94 new OpenCodeRuntimeError({
90 new OpenCodeRuntime.OpenCodeRuntimeError({
9591 operation: "loadOpenCodeInventory",
9692 detail: "OpenCodeRuntimeTestDouble.loadOpenCodeInventory not used in this test",
9793 cause: null,
......@@ -107,11 +103,11 @@ const DEFAULT_TEST_MODEL_SELECTION = {
107103const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000;
108104
109105const OpenCodeTextGenerationTestLayer = Layer.succeed(
110 OpenCodeRuntime,
106 OpenCodeRuntime.OpenCodeRuntime,
111107 OpenCodeRuntimeTestDouble,
112108).pipe(
113109 Layer.provideMerge(
114 ServerConfig.layerTest(process.cwd(), {
110 ServerConfig.ServerConfig.layerTest(process.cwd(), {
115111 prefix: "t3code-opencode-text-generation-test-",
116112 }),
117113 ),
......@@ -120,11 +116,11 @@ const OpenCodeTextGenerationTestLayer = Layer.succeed(
120116);
121117
122118const OpenCodeTextGenerationExistingServerTestLayer = Layer.succeed(
123 OpenCodeRuntime,
119 OpenCodeRuntime.OpenCodeRuntime,
124120 OpenCodeRuntimeTestDouble,
125121).pipe(
126122 Layer.provideMerge(
127 ServerConfig.layerTest(process.cwd(), {
123 ServerConfig.ServerConfig.layerTest(process.cwd(), {
128124 prefix: "t3code-opencode-text-generation-existing-server-test-",
129125 }),
130126 ),
......@@ -143,7 +139,7 @@ const EXISTING_SERVER_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({
143139
144140function withOpenCodeTextGeneration<A, E, R>(
145141 settings: OpenCodeSettings,
146 effectFn: (textGeneration: TextGenerationShape) => Effect.Effect<A, E, R>,
142 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
147143) {
148144 return Effect.gen(function* () {
149145 const textGeneration = yield* makeOpenCodeTextGeneration(settings);
apps/server/src/textGeneration/OpenCodeTextGeneration.ts+91-102
......@@ -15,7 +15,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar
1515import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
1616import { extractJsonObject } from "@t3tools/shared/schemaJson";
1717
18import { ServerConfig } from "../config.ts";
18import * as ServerConfig from "../config.ts";
1919import { resolveAttachmentPath } from "../attachmentStore.ts";
2020import {
2121 buildBranchNamePrompt,
......@@ -23,20 +23,13 @@ import {
2323 buildPrContentPrompt,
2424 buildThreadTitlePrompt,
2525} from "./TextGenerationPrompts.ts";
26import { type TextGenerationShape } from "./TextGeneration.ts";
26import * as TextGeneration from "./TextGeneration.ts";
2727import {
2828 sanitizeCommitSubject,
2929 sanitizePrTitle,
3030 sanitizeThreadTitle,
3131} from "./TextGenerationUtils.ts";
32import {
33 OpenCodeRuntime,
34 type OpenCodeServerConnection,
35 type OpenCodeServerProcess,
36 openCodeRuntimeErrorDetail,
37 parseOpenCodeModelSlug,
38 toOpenCodeFileParts,
39} from "../provider/opencodeRuntime.ts";
32import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";
4033
4134const OPENCODE_TEXT_GENERATION_IDLE_TTL = "30 seconds";
4235
......@@ -84,7 +77,7 @@ function getOpenCodeTextResponse(parts: ReadonlyArray<unknown> | undefined): str
8477}
8578
8679interface SharedOpenCodeTextGenerationServerState {
87 server: OpenCodeServerProcess | null;
80 server: OpenCodeRuntime.OpenCodeServerProcess | null;
8881 /**
8982 * The scope that owns the shared server's lifetime. Closing this scope
9083 * terminates the OpenCode child process and interrupts any fibers the
......@@ -101,8 +94,8 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
10194 openCodeSettings: OpenCodeSettings,
10295 environment?: NodeJS.ProcessEnv,
10396) {
104 const serverConfig = yield* ServerConfig;
105 const openCodeRuntime = yield* OpenCodeRuntime;
97 const serverConfig = yield* ServerConfig.ServerConfig;
98 const openCodeRuntime = yield* OpenCodeRuntime.OpenCodeRuntime;
10699 const resolvedEnvironment = environment ?? process.env;
107100 const idleFiberScope = yield* Effect.acquireRelease(Scope.make(), (scope) =>
108101 Scope.close(scope, Exit.void),
......@@ -135,7 +128,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
135128 });
136129
137130 const scheduleIdleClose = Effect.fn("scheduleIdleClose")(function* (
138 server: OpenCodeServerProcess,
131 server: OpenCodeRuntime.OpenCodeServerProcess,
139132 ) {
140133 yield* cancelIdleCloseFiber();
141134 const fiber = yield* Effect.sleep(OPENCODE_TEXT_GENERATION_IDLE_TTL).pipe(
......@@ -217,7 +210,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
217210 (cause) =>
218211 new TextGenerationError({
219212 operation: input.operation,
220 detail: openCodeRuntimeErrorDetail(cause),
213 detail: OpenCodeRuntime.openCodeRuntimeErrorDetail(cause),
221214 cause,
222215 }),
223216 ),
......@@ -240,7 +233,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
240233 }),
241234 );
242235
243 const releaseSharedServer = (server: OpenCodeServerProcess) =>
236 const releaseSharedServer = (server: OpenCodeRuntime.OpenCodeServerProcess) =>
244237 sharedServerMutex.withPermit(
245238 Effect.gen(function* () {
246239 if (sharedServerState.server !== server) {
......@@ -278,7 +271,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
278271 readonly modelSelection: ModelSelection;
279272 readonly attachments?: ReadonlyArray<ChatAttachment> | undefined;
280273 }) {
281 const parsedModel = parseOpenCodeModelSlug(input.modelSelection.model);
274 const parsedModel = OpenCodeRuntime.parseOpenCodeModelSlug(input.modelSelection.model);
282275 if (!parsedModel) {
283276 return yield* new TextGenerationError({
284277 operation: input.operation,
......@@ -286,13 +279,13 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
286279 });
287280 }
288281
289 const fileParts = toOpenCodeFileParts({
282 const fileParts = OpenCodeRuntime.toOpenCodeFileParts({
290283 attachments: input.attachments,
291284 resolveAttachmentPath: (attachment) =>
292285 resolveAttachmentPath({ attachmentsDir: serverConfig.attachmentsDir, attachment }),
293286 });
294287
295 const runAgainstServer = (server: Pick<OpenCodeServerConnection, "url">) =>
288 const runAgainstServer = (server: Pick<OpenCodeRuntime.OpenCodeServerConnection, "url">) =>
296289 Effect.tryPromise({
297290 try: async () => {
298291 const client = openCodeRuntime.createOpenCodeSdkClient({
......@@ -336,7 +329,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
336329 catch: (cause) =>
337330 new TextGenerationError({
338331 operation: input.operation,
339 detail: openCodeRuntimeErrorDetail(cause),
332 detail: OpenCodeRuntime.openCodeRuntimeErrorDetail(cause),
340333 cause,
341334 }),
342335 });
......@@ -367,102 +360,98 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
367360 );
368361 });
369362
370 const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn(
371 "OpenCodeTextGeneration.generateCommitMessage",
372 )(function* (input) {
373 const { prompt, outputSchema } = buildCommitMessagePrompt({
374 branch: input.branch,
375 stagedSummary: input.stagedSummary,
376 stagedPatch: input.stagedPatch,
377 includeBranch: input.includeBranch === true,
378 });
379 const generated = yield* runOpenCodeJson({
380 operation: "generateCommitMessage",
381 cwd: input.cwd,
382 prompt,
383 outputSchemaJson: outputSchema,
384 modelSelection: input.modelSelection,
363 const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] =
364 Effect.fn("OpenCodeTextGeneration.generateCommitMessage")(function* (input) {
365 const { prompt, outputSchema } = buildCommitMessagePrompt({
366 branch: input.branch,
367 stagedSummary: input.stagedSummary,
368 stagedPatch: input.stagedPatch,
369 includeBranch: input.includeBranch === true,
370 });
371 const generated = yield* runOpenCodeJson({
372 operation: "generateCommitMessage",
373 cwd: input.cwd,
374 prompt,
375 outputSchemaJson: outputSchema,
376 modelSelection: input.modelSelection,
377 });
378
379 return {
380 subject: sanitizeCommitSubject(generated.subject),
381 body: generated.body.trim(),
382 ...("branch" in generated && typeof generated.branch === "string"
383 ? { branch: sanitizeFeatureBranchName(generated.branch) }
384 : {}),
385 };
385386 });
386387
387 return {
388 subject: sanitizeCommitSubject(generated.subject),
389 body: generated.body.trim(),
390 ...("branch" in generated && typeof generated.branch === "string"
391 ? { branch: sanitizeFeatureBranchName(generated.branch) }
392 : {}),
393 };
394 });
388 const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] =
389 Effect.fn("OpenCodeTextGeneration.generatePrContent")(function* (input) {
390 const { prompt, outputSchema } = buildPrContentPrompt({
391 baseBranch: input.baseBranch,
392 headBranch: input.headBranch,
393 commitSummary: input.commitSummary,
394 diffSummary: input.diffSummary,
395 diffPatch: input.diffPatch,
396 });
397 const generated = yield* runOpenCodeJson({
398 operation: "generatePrContent",
399 cwd: input.cwd,
400 prompt,
401 outputSchemaJson: outputSchema,
402 modelSelection: input.modelSelection,
403 });
395404
396 const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn(
397 "OpenCodeTextGeneration.generatePrContent",
398 )(function* (input) {
399 const { prompt, outputSchema } = buildPrContentPrompt({
400 baseBranch: input.baseBranch,
401 headBranch: input.headBranch,
402 commitSummary: input.commitSummary,
403 diffSummary: input.diffSummary,
404 diffPatch: input.diffPatch,
405 });
406 const generated = yield* runOpenCodeJson({
407 operation: "generatePrContent",
408 cwd: input.cwd,
409 prompt,
410 outputSchemaJson: outputSchema,
411 modelSelection: input.modelSelection,
405 return {
406 title: sanitizePrTitle(generated.title),
407 body: generated.body.trim(),
408 };
412409 });
413410
414 return {
415 title: sanitizePrTitle(generated.title),
416 body: generated.body.trim(),
417 };
418 });
411 const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] =
412 Effect.fn("OpenCodeTextGeneration.generateBranchName")(function* (input) {
413 const { prompt, outputSchema } = buildBranchNamePrompt({
414 message: input.message,
415 attachments: input.attachments,
416 });
417 const generated = yield* runOpenCodeJson({
418 operation: "generateBranchName",
419 cwd: input.cwd,
420 prompt,
421 outputSchemaJson: outputSchema,
422 modelSelection: input.modelSelection,
423 attachments: input.attachments,
424 });
419425
420 const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn(
421 "OpenCodeTextGeneration.generateBranchName",
422 )(function* (input) {
423 const { prompt, outputSchema } = buildBranchNamePrompt({
424 message: input.message,
425 attachments: input.attachments,
426 });
427 const generated = yield* runOpenCodeJson({
428 operation: "generateBranchName",
429 cwd: input.cwd,
430 prompt,
431 outputSchemaJson: outputSchema,
432 modelSelection: input.modelSelection,
433 attachments: input.attachments,
426 return {
427 branch: sanitizeBranchFragment(generated.branch),
428 };
434429 });
435430
436 return {
437 branch: sanitizeBranchFragment(generated.branch),
438 };
439 });
431 const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] =
432 Effect.fn("OpenCodeTextGeneration.generateThreadTitle")(function* (input) {
433 const { prompt, outputSchema } = buildThreadTitlePrompt({
434 message: input.message,
435 attachments: input.attachments,
436 });
437 const generated = yield* runOpenCodeJson({
438 operation: "generateThreadTitle",
439 cwd: input.cwd,
440 prompt,
441 outputSchemaJson: outputSchema,
442 modelSelection: input.modelSelection,
443 attachments: input.attachments,
444 });
440445
441 const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn(
442 "OpenCodeTextGeneration.generateThreadTitle",
443 )(function* (input) {
444 const { prompt, outputSchema } = buildThreadTitlePrompt({
445 message: input.message,
446 attachments: input.attachments,
447 });
448 const generated = yield* runOpenCodeJson({
449 operation: "generateThreadTitle",
450 cwd: input.cwd,
451 prompt,
452 outputSchemaJson: outputSchema,
453 modelSelection: input.modelSelection,
454 attachments: input.attachments,
446 return {
447 title: sanitizeThreadTitle(generated.title),
448 };
455449 });
456450
457 return {
458 title: sanitizeThreadTitle(generated.title),
459 };
460 });
461
462451 return {
463452 generateCommitMessage,
464453 generatePrContent,
465454 generateBranchName,
466455 generateThreadTitle,
467 } satisfies TextGenerationShape;
456 } satisfies TextGeneration.TextGeneration["Service"];
468457});
apps/server/src/textGeneration/TextGeneration.test.ts+17-16
......@@ -9,23 +9,24 @@ import { ProviderInstanceId } from "@t3tools/contracts";
99import { createModelSelection } from "@t3tools/shared/model";
1010
1111import type { ProviderInstance } from "../provider/ProviderDriver.ts";
12import type { ProviderInstanceRegistryShape } from "../provider/Services/ProviderInstanceRegistry.ts";
13import type { TextGenerationShape } from "./TextGeneration.ts";
12import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts";
13import * as TextGeneration from "./TextGeneration.ts";
1414
15import { makeTextGenerationFromRegistry } from "./TextGeneration.ts";
16
17const makeStubTextGeneration = (overrides: Partial<TextGenerationShape>): TextGenerationShape => ({
18 generateCommitMessage: () =>
19 Effect.die("generateCommitMessage stub not configured for this test"),
20 generatePrContent: () => Effect.die("generatePrContent stub not configured for this test"),
21 generateBranchName: () => Effect.die("generateBranchName stub not configured for this test"),
22 generateThreadTitle: () => Effect.die("generateThreadTitle stub not configured for this test"),
23 ...overrides,
24});
15const makeStubTextGeneration = (
16 overrides: Partial<TextGeneration.TextGeneration["Service"]>,
17): TextGeneration.TextGeneration["Service"] =>
18 TextGeneration.TextGeneration.of({
19 generateCommitMessage: () =>
20 Effect.die("generateCommitMessage stub not configured for this test"),
21 generatePrContent: () => Effect.die("generatePrContent stub not configured for this test"),
22 generateBranchName: () => Effect.die("generateBranchName stub not configured for this test"),
23 generateThreadTitle: () => Effect.die("generateThreadTitle stub not configured for this test"),
24 ...overrides,
25 });
2526
2627const makeStubInstance = (
2728 instanceId: ProviderInstanceId,
28 textGeneration: TextGenerationShape,
29 textGeneration: TextGeneration.TextGeneration["Service"],
2930): ProviderInstance =>
3031 ({
3132 instanceId,
......@@ -43,7 +44,7 @@ const makeStubInstance = (
4344
4445const makeStubRegistry = (
4546 instances: ReadonlyArray<ProviderInstance>,
46): ProviderInstanceRegistryShape => {
47): ProviderInstanceRegistry.ProviderInstanceRegistry["Service"] => {
4748 const byId = new Map(instances.map((instance) => [instance.instanceId, instance] as const));
4849 return {
4950 getInstance: (id) => Effect.succeed(byId.get(id)),
......@@ -81,7 +82,7 @@ describe("makeTextGenerationFromRegistry", () => {
8182 }),
8283 );
8384
84 const tg = makeTextGenerationFromRegistry(makeStubRegistry([personal, work]));
85 const tg = TextGeneration.makeTextGenerationFromRegistry(makeStubRegistry([personal, work]));
8586
8687 const result = yield* tg.generateBranchName({
8788 cwd: process.cwd(),
......@@ -96,7 +97,7 @@ describe("makeTextGenerationFromRegistry", () => {
9697
9798 it.effect("fails with TextGenerationError when the instance is unknown", () =>
9899 Effect.gen(function* () {
99 const tg = makeTextGenerationFromRegistry(makeStubRegistry([]));
100 const tg = TextGeneration.makeTextGenerationFromRegistry(makeStubRegistry([]));
100101
101102 const result = yield* tg
102103 .generateBranchName({
apps/server/src/textGeneration/TextGeneration.ts+62-66
......@@ -4,10 +4,7 @@ import * as Layer from "effect/Layer";
44import type { ChatAttachment, ModelSelection, ProviderInstanceId } from "@t3tools/contracts";
55import { TextGenerationError } from "@t3tools/contracts";
66
7import {
8 ProviderInstanceRegistry,
9 type ProviderInstanceRegistryShape,
10} from "../provider/Services/ProviderInstanceRegistry.ts";
7import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts";
118import type { ProviderInstance } from "../provider/ProviderDriver.ts";
129
1310export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "grok" | "opencode";
......@@ -79,45 +76,44 @@ export interface TextGenerationService {
7976 generateThreadTitle(input: ThreadTitleGenerationInput): Promise<ThreadTitleGenerationResult>;
8077}
8178
82/**
83 * TextGenerationShape - Service API for commit/PR text generation.
84 */
85export interface TextGenerationShape {
86 /**
87 * Generate a commit message from staged change context.
88 */
89 readonly generateCommitMessage: (
90 input: CommitMessageGenerationInput,
91 ) => Effect.Effect<CommitMessageGenerationResult, TextGenerationError>;
92
93 /**
94 * Generate pull request title/body from branch and diff context.
95 */
96 readonly generatePrContent: (
97 input: PrContentGenerationInput,
98 ) => Effect.Effect<PrContentGenerationResult, TextGenerationError>;
99
100 /**
101 * Generate a concise branch name from a user message.
102 */
103 readonly generateBranchName: (
104 input: BranchNameGenerationInput,
105 ) => Effect.Effect<BranchNameGenerationResult, TextGenerationError>;
106
107 /**
108 * Generate a concise thread title from a user's first message.
109 */
110 readonly generateThreadTitle: (
111 input: ThreadTitleGenerationInput,
112 ) => Effect.Effect<ThreadTitleGenerationResult, TextGenerationError>;
113}
114
11579/**
11680 * TextGeneration - Service tag for commit and PR text generation.
11781 */
118export class TextGeneration extends Context.Service<TextGeneration, TextGenerationShape>()(
119 "t3/textGeneration/TextGeneration",
120) {}
82export class TextGeneration extends Context.Service<
83 TextGeneration,
84 {
85 /**
86 * Generate a commit message from staged change context.
87 */
88 readonly generateCommitMessage: (
89 input: CommitMessageGenerationInput,
90 ) => Effect.Effect<CommitMessageGenerationResult, TextGenerationError>;
91
92 /**
93 * Generate pull request title/body from branch and diff context.
94 */
95 readonly generatePrContent: (
96 input: PrContentGenerationInput,
97 ) => Effect.Effect<PrContentGenerationResult, TextGenerationError>;
98
99 /**
100 * Generate a concise branch name from a user message.
101 */
102 readonly generateBranchName: (
103 input: BranchNameGenerationInput,
104 ) => Effect.Effect<BranchNameGenerationResult, TextGenerationError>;
105
106 /**
107 * Generate a concise thread title from a user's first message.
108 */
109 readonly generateThreadTitle: (
110 input: ThreadTitleGenerationInput,
111 ) => Effect.Effect<ThreadTitleGenerationResult, TextGenerationError>;
112 }
113>()("t3/textGeneration/TextGeneration") {}
114
115/** @deprecated Use `TextGeneration["Service"]`. */
116export type TextGenerationShape = TextGeneration["Service"];
121117
122118type TextGenerationOp =
123119 | "generateCommitMessage"
......@@ -126,7 +122,7 @@ type TextGenerationOp =
126122 | "generateThreadTitle";
127123
128124const resolveInstance = (
129 registry: ProviderInstanceRegistryShape,
125 registry: ProviderInstanceRegistry.ProviderInstanceRegistry["Service"],
130126 operation: TextGenerationOp,
131127 instanceId: ProviderInstanceId,
132128): Effect.Effect<ProviderInstance["textGeneration"], TextGenerationError> =>
......@@ -144,30 +140,30 @@ const resolveInstance = (
144140 );
145141
146142export const makeTextGenerationFromRegistry = (
147 registry: ProviderInstanceRegistryShape,
148): TextGenerationShape => ({
149 generateCommitMessage: (input) =>
150 resolveInstance(registry, "generateCommitMessage", input.modelSelection.instanceId).pipe(
151 Effect.flatMap((textGeneration) => textGeneration.generateCommitMessage(input)),
152 ),
153 generatePrContent: (input) =>
154 resolveInstance(registry, "generatePrContent", input.modelSelection.instanceId).pipe(
155 Effect.flatMap((textGeneration) => textGeneration.generatePrContent(input)),
156 ),
157 generateBranchName: (input) =>
158 resolveInstance(registry, "generateBranchName", input.modelSelection.instanceId).pipe(
159 Effect.flatMap((textGeneration) => textGeneration.generateBranchName(input)),
160 ),
161 generateThreadTitle: (input) =>
162 resolveInstance(registry, "generateThreadTitle", input.modelSelection.instanceId).pipe(
163 Effect.flatMap((textGeneration) => textGeneration.generateThreadTitle(input)),
164 ),
143 registry: ProviderInstanceRegistry.ProviderInstanceRegistry["Service"],
144): TextGeneration["Service"] =>
145 TextGeneration.of({
146 generateCommitMessage: (input) =>
147 resolveInstance(registry, "generateCommitMessage", input.modelSelection.instanceId).pipe(
148 Effect.flatMap((textGeneration) => textGeneration.generateCommitMessage(input)),
149 ),
150 generatePrContent: (input) =>
151 resolveInstance(registry, "generatePrContent", input.modelSelection.instanceId).pipe(
152 Effect.flatMap((textGeneration) => textGeneration.generatePrContent(input)),
153 ),
154 generateBranchName: (input) =>
155 resolveInstance(registry, "generateBranchName", input.modelSelection.instanceId).pipe(
156 Effect.flatMap((textGeneration) => textGeneration.generateBranchName(input)),
157 ),
158 generateThreadTitle: (input) =>
159 resolveInstance(registry, "generateThreadTitle", input.modelSelection.instanceId).pipe(
160 Effect.flatMap((textGeneration) => textGeneration.generateThreadTitle(input)),
161 ),
162 });
163
164export const make = Effect.gen(function* () {
165 const registry = yield* ProviderInstanceRegistry.ProviderInstanceRegistry;
166 return makeTextGenerationFromRegistry(registry);
165167});
166168
167export const layer = Layer.effect(
168 TextGeneration,
169 Effect.gen(function* () {
170 const registry = yield* ProviderInstanceRegistry;
171 return makeTextGenerationFromRegistry(registry);
172 }),
173);
169export const layer = Layer.effect(TextGeneration, make);