diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index bee861677a5..cc7340f965a 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -20,27 +20,16 @@ import type { } from "@t3tools/contracts"; import { GitCommandError, TextGenerationError } from "@t3tools/contracts"; -import { type GitManagerShape } from "./GitManager.ts"; -import { - GitHubCliError, - type GitHubCliShape, - type GitHubPullRequestSummary, - GitHubCli, -} from "../sourceControl/GitHubCli.ts"; -import { type TextGenerationShape, TextGeneration } from "../textGeneration/TextGeneration.ts"; +import * as GitManager from "./GitManager.ts"; +import * as GitHubCli from "../sourceControl/GitHubCli.ts"; +import * as TextGeneration from "../textGeneration/TextGeneration.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as GitHubSourceControlProvider from "../sourceControl/GitHubSourceControlProvider.ts"; import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; -import { makeGitManager } from "./GitManager.ts"; -import { ServerConfig } from "../config.ts"; -import { ServerSettingsService } from "../serverSettings.ts"; -import { - ProjectSetupScriptRunner, - ProjectSetupScriptRunnerError, - type ProjectSetupScriptRunnerInput, - type ProjectSetupScriptRunnerShape, -} from "../project/Services/ProjectSetupScriptRunner.ts"; +import * as ServerConfig from "../config.ts"; +import * as ServerSettings from "../serverSettings.ts"; +import * as ProjectSetupScriptRunner from "../project/Services/ProjectSetupScriptRunner.ts"; interface FakeGhScenario { prListSequence?: string[]; @@ -60,7 +49,7 @@ interface FakeGhScenario { headRepositoryOwnerLogin?: string | null; }; repositoryCloneUrls?: Record; - failWith?: GitHubCliError; + failWith?: GitHubCli.GitHubCliError; } function fakeGhOutput(stdout: string): VcsProcess.VcsProcessOutput { @@ -108,7 +97,7 @@ interface FakeGitTextGeneration { type FakePullRequest = NonNullable; -function normalizeFakePullRequestSummary(raw: unknown): GitHubPullRequestSummary | null { +function normalizeFakePullRequestSummary(raw: unknown): GitHubCli.GitHubPullRequestSummary | null { if (!raw || typeof raw !== "object") { return null; } @@ -182,13 +171,13 @@ function runGitSyncForFakeGh(cwd: string, args: readonly string[]): void { if (result.status === 0) { return; } - throw new GitHubCliError({ + throw new GitHubCli.GitHubCliError({ operation: "execute", detail: `Failed to simulate gh checkout with git ${args.join(" ")}: ${result.stderr?.trim() || "unknown error"}`, }); } -function isGitHubCliError(error: unknown): error is GitHubCliError { +function isGitHubCliError(error: unknown): error is GitHubCli.GitHubCliError { return ( typeof error === "object" && error !== null && @@ -312,7 +301,9 @@ function configureVisibleRemoteUrlWithLocalRewrite( }); } -function createTextGeneration(overrides: Partial = {}): TextGenerationShape { +function createTextGeneration( + overrides: Partial = {}, +): TextGeneration.TextGeneration["Service"] { const implementation: FakeGitTextGeneration = { generateCommitMessage: (input) => Effect.succeed({ @@ -385,7 +376,7 @@ function createTextGeneration(overrides: Partial = {}): T } function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { - service: GitHubCliShape; + service: GitHubCli.GitHubCliShape; ghCalls: string[]; } { const prListQueue = [...(scenario.prListSequence ?? [])]; @@ -397,7 +388,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { ); const ghCalls: string[] = []; - const execute: GitHubCliShape["execute"] = (input) => { + const execute: GitHubCli.GitHubCliShape["execute"] = (input) => { const args = [...input.args]; ghCalls.push(args.join(" ")); @@ -487,7 +478,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { catch: (error) => isGitHubCliError(error) ? error - : new GitHubCliError({ + : new GitHubCli.GitHubCliError({ operation: "execute", detail: error instanceof Error @@ -503,7 +494,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { const cloneUrls = scenario.repositoryCloneUrls?.[repository]; if (!cloneUrls) { return Effect.fail( - new GitHubCliError({ + new GitHubCli.GitHubCliError({ operation: "execute", detail: `Unexpected repository lookup: ${repository}`, }), @@ -523,7 +514,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { } return Effect.fail( - new GitHubCliError({ + new GitHubCli.GitHubCliError({ operation: "execute", detail: `Unexpected gh command: ${args.join(" ")}`, }), @@ -553,7 +544,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { Effect.map((raw) => raw .map((entry) => normalizeFakePullRequestSummary(entry)) - .filter((entry): entry is GitHubPullRequestSummary => entry !== null), + .filter((entry): entry is GitHubCli.GitHubPullRequestSummary => entry !== null), ), ), createPullRequest: (input) => @@ -592,7 +583,9 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { "--json", "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner", ], - }).pipe(Effect.map((result) => JSON.parse(result.stdout) as GitHubPullRequestSummary)), + }).pipe( + Effect.map((result) => JSON.parse(result.stdout) as GitHubCli.GitHubPullRequestSummary), + ), getRepositoryCloneUrls: (input) => execute({ cwd: input.cwd, @@ -600,7 +593,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { }).pipe(Effect.map((result) => JSON.parse(result.stdout))), createRepository: (input) => Effect.fail( - new GitHubCliError({ + new GitHubCli.GitHubCliError({ operation: "createRepository", detail: `Unexpected repository create: ${input.repository}`, }), @@ -616,7 +609,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { } function runStackedAction( - manager: GitManagerShape, + manager: GitManager.GitManagerShape, input: { cwd: string; action: "commit" | "push" | "create_pr" | "commit_push" | "commit_push_pr"; @@ -625,7 +618,7 @@ function runStackedAction( featureBranch?: boolean; filePaths?: readonly string[]; }, - options?: Parameters[1], + options?: Parameters[1], ) { return manager.runStackedAction( { @@ -636,12 +629,15 @@ function runStackedAction( ); } -function resolvePullRequest(manager: GitManagerShape, input: { cwd: string; reference: string }) { +function resolvePullRequest( + manager: GitManager.GitManagerShape, + input: { cwd: string; reference: string }, +) { return manager.resolvePullRequest(input); } function preparePullRequestThread( - manager: GitManagerShape, + manager: GitManager.GitManagerShape, input: GitPreparePullRequestThreadInput, ) { return manager.preparePullRequestThread(input); @@ -650,20 +646,20 @@ function preparePullRequestThread( function makeManager(input?: { ghScenario?: FakeGhScenario; textGeneration?: Partial; - setupScriptRunner?: ProjectSetupScriptRunnerShape; + setupScriptRunner?: ProjectSetupScriptRunner.ProjectSetupScriptRunnerShape; }) { const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario); const textGeneration = createTextGeneration(input?.textGeneration); - const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { + const serverConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-", }); - const serverSettingsLayer = ServerSettingsService.layerTest(); + const serverSettingsLayer = ServerSettings.ServerSettingsService.layerTest(); const vcsDriverLayer = GitVcsDriver.layer.pipe( Layer.provideMerge(VcsProcess.layer), Layer.provideMerge(NodeServices.layer), - Layer.provideMerge(ServerConfigLayer), + Layer.provideMerge(serverConfigLayer), ); const sourceControlRegistryLayer = Layer.effect( SourceControlProviderRegistry.SourceControlProviderRegistry, @@ -676,14 +672,14 @@ function makeManager(input?: { discover: Effect.succeed([]), }), ), - Effect.provide(Layer.succeed(GitHubCli, gitHubCli)), + Effect.provide(Layer.succeed(GitHubCli.GitHubCli, gitHubCli)), ), ); const managerLayer = Layer.mergeAll( - Layer.succeed(TextGeneration, textGeneration), + Layer.succeed(TextGeneration.TextGeneration, textGeneration), Layer.succeed( - ProjectSetupScriptRunner, + ProjectSetupScriptRunner.ProjectSetupScriptRunner, input?.setupScriptRunner ?? { runForThread: () => Effect.succeed({ status: "no-script" as const }), }, @@ -692,7 +688,7 @@ function makeManager(input?: { serverSettingsLayer, ).pipe(Layer.provideMerge(sourceControlRegistryLayer), Layer.provideMerge(NodeServices.layer)); - return makeGitManager().pipe( + return GitManager.makeGitManager().pipe( Effect.provide(managerLayer), Effect.map((manager) => ({ manager, ghCalls })), ); @@ -701,7 +697,9 @@ function makeManager(input?: { const asThreadId = (threadId: string) => threadId as ThreadId; const GitManagerTestLayer = GitVcsDriver.layer.pipe( - Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" })), + Layer.provide( + ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" }), + ), Layer.provideMerge(VcsProcess.layer), Layer.provideMerge(NodeServices.layer), ); @@ -1335,7 +1333,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const { manager } = yield* makeManager({ ghScenario: { - failWith: new GitHubCliError({ + failWith: new GitHubCli.GitHubCliError({ operation: "execute", detail: "GitHub CLI (`gh`) is required but not available on PATH.", }), @@ -2417,7 +2415,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const { manager } = yield* makeManager({ ghScenario: { - failWith: new GitHubCliError({ + failWith: new GitHubCli.GitHubCliError({ operation: "execute", detail: "GitHub CLI (`gh`) is required but not available on PATH.", }), @@ -2446,7 +2444,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const { manager } = yield* makeManager({ ghScenario: { - failWith: new GitHubCliError({ + failWith: new GitHubCli.GitHubCliError({ operation: "execute", detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.", }), @@ -2702,7 +2700,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { yield* runGit(repoDir, ["push", "origin", "HEAD:refs/pull/177/head"]); yield* runGit(repoDir, ["checkout", "main"]); - const setupCalls: ProjectSetupScriptRunnerInput[] = []; + const setupCalls: ProjectSetupScriptRunner.ProjectSetupScriptRunnerInput[] = []; const { manager } = yield* makeManager({ ghScenario: { pullRequest: { @@ -2924,7 +2922,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const worktreePath = path.join(repoDir, "..", `pr-existing-${path.basename(repoDir)}`); yield* runGit(repoDir, ["worktree", "add", worktreePath, "feature/pr-existing-worktree"]); - const setupCalls: ProjectSetupScriptRunnerInput[] = []; + const setupCalls: ProjectSetupScriptRunner.ProjectSetupScriptRunnerInput[] = []; const { manager } = yield* makeManager({ ghScenario: { pullRequest: { @@ -3164,7 +3162,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { }, setupScriptRunner: { runForThread: () => - Effect.fail(new ProjectSetupScriptRunnerError({ message: "terminal start failed" })), + Effect.fail( + new ProjectSetupScriptRunner.ProjectSetupScriptRunnerError({ + message: "terminal start failed", + }), + ), }, }); diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts index 7fb545b2bed..c4145ecf1a0 100644 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts @@ -10,16 +10,16 @@ import * as Layer from "effect/Layer"; import * as PubSub from "effect/PubSub"; import * as Stream from "effect/Stream"; -import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; -import type { CodexAdapterShape } from "../Services/CodexAdapter.ts"; -import type { CursorAdapterShape } from "../Services/CursorAdapter.ts"; -import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; -import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; -import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts"; +import type * as ClaudeAdapter from "../Services/ClaudeAdapter.ts"; +import type * as CodexAdapter from "../Services/CodexAdapter.ts"; +import type * as CursorAdapter from "../Services/CursorAdapter.ts"; +import type * as OpenCodeAdapter from "../Services/OpenCodeAdapter.ts"; +import * as ProviderAdapterRegistry from "../Services/ProviderAdapterRegistry.ts"; +import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts"; import type { ProviderInstance } from "../ProviderDriver.ts"; import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts"; -import type { TextGenerationShape } from "../../textGeneration/TextGeneration.ts"; -import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts"; +import type * as TextGeneration from "../../textGeneration/TextGeneration.ts"; +import * as ProviderAdapterRegistryLayer from "./ProviderAdapterRegistry.ts"; import * as NodeServices from "@effect/platform-node/NodeServices"; const CODEX_DRIVER = ProviderDriverKind.make("codex"); @@ -27,7 +27,7 @@ const CLAUDE_AGENT_DRIVER = ProviderDriverKind.make("claudeAgent"); const OPENCODE_DRIVER = ProviderDriverKind.make("opencode"); const CURSOR_DRIVER = ProviderDriverKind.make("cursor"); -const fakeCodexAdapter: CodexAdapterShape = { +const fakeCodexAdapter: CodexAdapter.CodexAdapterShape = { provider: CODEX_DRIVER, capabilities: { sessionModelSwitch: "in-session" }, startSession: vi.fn(), @@ -44,7 +44,7 @@ const fakeCodexAdapter: CodexAdapterShape = { streamEvents: Stream.empty, }; -const fakeClaudeAdapter: ClaudeAdapterShape = { +const fakeClaudeAdapter: ClaudeAdapter.ClaudeAdapterShape = { provider: CLAUDE_AGENT_DRIVER, capabilities: { sessionModelSwitch: "in-session" }, startSession: vi.fn(), @@ -61,7 +61,7 @@ const fakeClaudeAdapter: ClaudeAdapterShape = { streamEvents: Stream.empty, }; -const fakeOpenCodeAdapter: OpenCodeAdapterShape = { +const fakeOpenCodeAdapter: OpenCodeAdapter.OpenCodeAdapterShape = { provider: OPENCODE_DRIVER, capabilities: { sessionModelSwitch: "in-session" }, startSession: vi.fn(), @@ -78,7 +78,7 @@ const fakeOpenCodeAdapter: OpenCodeAdapterShape = { streamEvents: Stream.empty, }; -const fakeCursorAdapter: CursorAdapterShape = { +const fakeCursorAdapter: CursorAdapter.CursorAdapterShape = { provider: CURSOR_DRIVER, capabilities: { sessionModelSwitch: "in-session" }, startSession: vi.fn(), @@ -124,7 +124,7 @@ const makeFakeInstance = ( streamChanges: Stream.empty, }, adapter, - textGeneration: {} as unknown as TextGenerationShape, + textGeneration: {} as unknown as TextGeneration.TextGeneration["Service"], }; }; @@ -135,7 +135,7 @@ const fakeInstances: ReadonlyArray = [ makeFakeInstance("cursor", fakeCursorAdapter), ]; -const fakeInstanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, { +const fakeInstanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, { getInstance: (instanceId) => Effect.succeed(fakeInstances.find((instance) => instance.instanceId === instanceId)), listInstances: Effect.succeed(fakeInstances), @@ -147,14 +147,17 @@ const fakeInstanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, { }); const layer = Layer.mergeAll( - Layer.provide(ProviderAdapterRegistryLive, fakeInstanceRegistryLayer), + Layer.provide( + ProviderAdapterRegistryLayer.ProviderAdapterRegistryLive, + fakeInstanceRegistryLayer, + ), NodeServices.layer, ); it.layer(layer)("ProviderAdapterRegistryLive", (it) => { it("resolves adapters and routing metadata from provider instances", () => Effect.gen(function* () { - const registry = yield* ProviderAdapterRegistry; + const registry = yield* ProviderAdapterRegistry.ProviderAdapterRegistry; const claudeInstanceId = defaultInstanceIdForDriver(CLAUDE_AGENT_DRIVER); const adapter = yield* registry.getByInstance(claudeInstanceId); diff --git a/apps/server/src/provider/ProviderDriver.ts b/apps/server/src/provider/ProviderDriver.ts index 3a57f374de4..c738882c23a 100644 --- a/apps/server/src/provider/ProviderDriver.ts +++ b/apps/server/src/provider/ProviderDriver.ts @@ -30,7 +30,7 @@ import type * as Effect from "effect/Effect"; import type * as Schema from "effect/Schema"; import type * as Scope from "effect/Scope"; -import type { TextGenerationShape } from "../textGeneration/TextGeneration.ts"; +import type * as TextGeneration from "../textGeneration/TextGeneration.ts"; import type { ProviderAdapterError, ProviderDriverError } from "./Errors.ts"; import type { ProviderAdapterShape } from "./Services/ProviderAdapter.ts"; import type { ServerProviderShape } from "./Services/ServerProvider.ts"; @@ -70,7 +70,7 @@ export interface ProviderInstance { readonly enabled: boolean; readonly snapshot: ServerProviderShape; readonly adapter: ProviderAdapterShape; - readonly textGeneration: TextGenerationShape; + readonly textGeneration: TextGeneration.TextGeneration["Service"]; } export interface ProviderContinuationIdentity { diff --git a/apps/server/src/review/ReviewService.ts b/apps/server/src/review/ReviewService.ts index 63f1d133213..3f222bd520f 100644 --- a/apps/server/src/review/ReviewService.ts +++ b/apps/server/src/review/ReviewService.ts @@ -13,22 +13,21 @@ import { type ReviewDiffPreviewResult, } from "@t3tools/contracts"; -import { ServerConfig } from "../config.ts"; +import * as ServerConfig from "../config.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; -export interface ReviewServiceShape { - readonly getDiffPreview: ( - input: ReviewDiffPreviewInput, - ) => Effect.Effect; -} - -export class ReviewService extends Context.Service()( - "t3/review/ReviewService", -) {} - -export const make = Effect.fn("makeReviewService")(function* () { - const config = yield* ServerConfig; +export class ReviewService extends Context.Service< + ReviewService, + { + readonly getDiffPreview: ( + input: ReviewDiffPreviewInput, + ) => Effect.Effect; + } +>()("t3/review/ReviewService") {} + +export const make = Effect.gen(function* () { + const config = yield* ServerConfig.ServerConfig; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; @@ -62,7 +61,7 @@ export const make = Effect.fn("makeReviewService")(function* () { }); }); - const getDiffPreview: ReviewServiceShape["getDiffPreview"] = Effect.fn( + const getDiffPreview: ReviewService["Service"]["getDiffPreview"] = Effect.fn( "ReviewService.getDiffPreview", )(function* (input) { yield* assertWorkspaceBoundCwd(input.cwd); @@ -96,4 +95,4 @@ export const make = Effect.fn("makeReviewService")(function* () { }); }); -export const layer = Layer.effect(ReviewService, make()); +export const layer = Layer.effect(ReviewService, make); diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts index 0c53dbecea0..c8fe4ead3be 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts @@ -9,13 +9,13 @@ import * as Schema from "effect/Schema"; import { createModelSelection } from "@t3tools/shared/model"; import { expect } from "vite-plus/test"; -import { ServerConfig } from "../config.ts"; -import { type TextGenerationShape } from "./TextGeneration.ts"; +import * as ServerConfig from "../config.ts"; +import * as TextGeneration from "./TextGeneration.ts"; import { sanitizeThreadTitle } from "./TextGenerationUtils.ts"; import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts"; const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); -const ClaudeTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), { +const ClaudeTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3code-claude-text-generation-test-", }).pipe(Layer.provideMerge(NodeServices.layer)); @@ -79,7 +79,7 @@ function withFakeClaudeEnv( homeMustBe?: string; claudeConfig?: Partial; }, - effectFn: (textGeneration: TextGenerationShape) => Effect.Effect, + effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect, ) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.ts index 91ad90b786e..872bf936cb1 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.ts @@ -1,7 +1,7 @@ /** * ClaudeTextGeneration – Text generation layer using the Claude CLI. * - * Implements the same TextGenerationShape contract as CodexTextGeneration but + * Implements the same TextGeneration service contract as CodexTextGeneration but * delegates to the `claude` CLI (`claude -p`) with structured JSON output * instead of the `codex exec` CLI. * @@ -18,7 +18,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { TextGenerationError } from "@t3tools/contracts"; -import { type TextGenerationShape } from "./TextGeneration.ts"; +import * as TextGeneration from "./TextGeneration.ts"; import { buildBranchNamePrompt, buildCommitMessagePrompt, @@ -260,107 +260,103 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu }); // --------------------------------------------------------------------------- - // TextGenerationShape methods + // TextGeneration service methods // --------------------------------------------------------------------------- - const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn( - "ClaudeTextGeneration.generateCommitMessage", - )(function* (input) { - const { prompt, outputSchema } = buildCommitMessagePrompt({ - branch: input.branch, - stagedSummary: input.stagedSummary, - stagedPatch: input.stagedPatch, - includeBranch: input.includeBranch === true, - }); + const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] = + Effect.fn("ClaudeTextGeneration.generateCommitMessage")(function* (input) { + const { prompt, outputSchema } = buildCommitMessagePrompt({ + branch: input.branch, + stagedSummary: input.stagedSummary, + stagedPatch: input.stagedPatch, + includeBranch: input.includeBranch === true, + }); + + const generated = yield* runClaudeJson({ + operation: "generateCommitMessage", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runClaudeJson({ - operation: "generateCommitMessage", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + subject: sanitizeCommitSubject(generated.subject), + body: generated.body.trim(), + ...("branch" in generated && typeof generated.branch === "string" + ? { branch: sanitizeFeatureBranchName(generated.branch) } + : {}), + }; }); - return { - subject: sanitizeCommitSubject(generated.subject), - body: generated.body.trim(), - ...("branch" in generated && typeof generated.branch === "string" - ? { branch: sanitizeFeatureBranchName(generated.branch) } - : {}), - }; - }); + const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] = + Effect.fn("ClaudeTextGeneration.generatePrContent")(function* (input) { + const { prompt, outputSchema } = buildPrContentPrompt({ + baseBranch: input.baseBranch, + headBranch: input.headBranch, + commitSummary: input.commitSummary, + diffSummary: input.diffSummary, + diffPatch: input.diffPatch, + }); - const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn( - "ClaudeTextGeneration.generatePrContent", - )(function* (input) { - const { prompt, outputSchema } = buildPrContentPrompt({ - baseBranch: input.baseBranch, - headBranch: input.headBranch, - commitSummary: input.commitSummary, - diffSummary: input.diffSummary, - diffPatch: input.diffPatch, - }); + const generated = yield* runClaudeJson({ + operation: "generatePrContent", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runClaudeJson({ - operation: "generatePrContent", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + title: sanitizePrTitle(generated.title), + body: generated.body.trim(), + }; }); - return { - title: sanitizePrTitle(generated.title), - body: generated.body.trim(), - }; - }); + const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] = + Effect.fn("ClaudeTextGeneration.generateBranchName")(function* (input) { + const { prompt, outputSchema } = buildBranchNamePrompt({ + message: input.message, + attachments: input.attachments, + }); - const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn( - "ClaudeTextGeneration.generateBranchName", - )(function* (input) { - const { prompt, outputSchema } = buildBranchNamePrompt({ - message: input.message, - attachments: input.attachments, - }); + const generated = yield* runClaudeJson({ + operation: "generateBranchName", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runClaudeJson({ - operation: "generateBranchName", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + branch: sanitizeBranchFragment(generated.branch), + }; }); - return { - branch: sanitizeBranchFragment(generated.branch), - }; - }); + const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] = + Effect.fn("ClaudeTextGeneration.generateThreadTitle")(function* (input) { + const { prompt, outputSchema } = buildThreadTitlePrompt({ + message: input.message, + attachments: input.attachments, + }); - const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn( - "ClaudeTextGeneration.generateThreadTitle", - )(function* (input) { - const { prompt, outputSchema } = buildThreadTitlePrompt({ - message: input.message, - attachments: input.attachments, - }); + const generated = yield* runClaudeJson({ + operation: "generateThreadTitle", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runClaudeJson({ - operation: "generateThreadTitle", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + title: sanitizeThreadTitle(generated.title), + }; }); - return { - title: sanitizeThreadTitle(generated.title), - }; - }); - return { generateCommitMessage, generatePrContent, generateBranchName, generateThreadTitle, - } satisfies TextGenerationShape; + } satisfies TextGeneration.TextGeneration["Service"]; }); diff --git a/apps/server/src/textGeneration/CodexTextGeneration.test.ts b/apps/server/src/textGeneration/CodexTextGeneration.test.ts index cf0ad7d5781..24054a95870 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.test.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.test.ts @@ -11,8 +11,8 @@ import { expect } from "vite-plus/test"; import { CodexSettings, ProviderInstanceId, TextGenerationError } from "@t3tools/contracts"; -import { ServerConfig } from "../config.ts"; -import { type TextGenerationShape } from "./TextGeneration.ts"; +import * as ServerConfig from "../config.ts"; +import * as TextGeneration from "./TextGeneration.ts"; import { makeCodexTextGeneration } from "./CodexTextGeneration.ts"; const decodeCodexSettings = Schema.decodeSync(CodexSettings); @@ -21,7 +21,7 @@ const DEFAULT_TEST_MODEL_SELECTION = createModelSelection( "gpt-5.4-mini", ); -const CodexTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), { +const CodexTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3code-codex-text-generation-test-", }).pipe(Layer.provideMerge(NodeServices.layer)); @@ -169,7 +169,7 @@ function withFakeCodexEnv( stdinMustContain?: string; stdinMustNotContain?: string; }, - effectFn: (textGeneration: TextGenerationShape) => Effect.Effect, + effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect, ) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -427,7 +427,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const { attachmentsDir } = yield* ServerConfig; + const { attachmentsDir } = yield* ServerConfig.ServerConfig; const attachmentId = "thread-branch-image-attachment"; const attachmentPath = path.join(attachmentsDir, `${attachmentId}.png`); yield* fs.makeDirectory(attachmentsDir, { recursive: true }); @@ -465,7 +465,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const { attachmentsDir } = yield* ServerConfig; + const { attachmentsDir } = yield* ServerConfig.ServerConfig; const attachmentId = "thread-1-attachment"; const imagePath = path.join(attachmentsDir, `${attachmentId}.png`); yield* fs.makeDirectory(attachmentsDir, { recursive: true }); @@ -514,7 +514,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const { attachmentsDir } = yield* ServerConfig; + const { attachmentsDir } = yield* ServerConfig.ServerConfig; const missingAttachmentId = "thread-missing-attachment"; const missingPath = path.join(attachmentsDir, `${missingAttachmentId}.png`); yield* fs.remove(missingPath).pipe(Effect.catch(() => Effect.void)); diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index 80b39af2584..95783b06cca 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -12,14 +12,10 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { resolveAttachmentPath } from "../attachmentStore.ts"; -import { ServerConfig } from "../config.ts"; +import * as ServerConfig from "../config.ts"; import { expandHomePath } from "../pathExpansion.ts"; import { TextGenerationError } from "@t3tools/contracts"; -import { - type BranchNameGenerationInput, - type ThreadTitleGenerationResult, - type TextGenerationShape, -} from "./TextGeneration.ts"; +import * as TextGeneration from "./TextGeneration.ts"; import { buildBranchNamePrompt, buildCommitMessagePrompt, @@ -50,7 +46,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const serverConfig = yield* Effect.service(ServerConfig); + const serverConfig = yield* Effect.service(ServerConfig.ServerConfig); const resolvedEnvironment = environment ?? process.env; type MaterializedImageAttachments = { @@ -121,7 +117,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func | "generatePrContent" | "generateBranchName" | "generateThreadTitle", - attachments: BranchNameGenerationInput["attachments"], + attachments: TextGeneration.BranchNameGenerationInput["attachments"], ): Effect.fn.Return { if (!attachments || attachments.length === 0) { return { imagePaths: [] }; @@ -298,114 +294,110 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func }).pipe(Effect.ensuring(cleanup)); }); - const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn( - "CodexTextGeneration.generateCommitMessage", - )(function* (input) { - const { prompt, outputSchema } = buildCommitMessagePrompt({ - branch: input.branch, - stagedSummary: input.stagedSummary, - stagedPatch: input.stagedPatch, - includeBranch: input.includeBranch === true, - }); + const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] = + Effect.fn("CodexTextGeneration.generateCommitMessage")(function* (input) { + const { prompt, outputSchema } = buildCommitMessagePrompt({ + branch: input.branch, + stagedSummary: input.stagedSummary, + stagedPatch: input.stagedPatch, + includeBranch: input.includeBranch === true, + }); - const generated = yield* runCodexJson({ - operation: "generateCommitMessage", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + const generated = yield* runCodexJson({ + operation: "generateCommitMessage", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + subject: sanitizeCommitSubject(generated.subject), + body: generated.body.trim(), + ...("branch" in generated && typeof generated.branch === "string" + ? { branch: sanitizeFeatureBranchName(generated.branch) } + : {}), + }; }); - return { - subject: sanitizeCommitSubject(generated.subject), - body: generated.body.trim(), - ...("branch" in generated && typeof generated.branch === "string" - ? { branch: sanitizeFeatureBranchName(generated.branch) } - : {}), - }; - }); + const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] = + Effect.fn("CodexTextGeneration.generatePrContent")(function* (input) { + const { prompt, outputSchema } = buildPrContentPrompt({ + baseBranch: input.baseBranch, + headBranch: input.headBranch, + commitSummary: input.commitSummary, + diffSummary: input.diffSummary, + diffPatch: input.diffPatch, + }); - const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn( - "CodexTextGeneration.generatePrContent", - )(function* (input) { - const { prompt, outputSchema } = buildPrContentPrompt({ - baseBranch: input.baseBranch, - headBranch: input.headBranch, - commitSummary: input.commitSummary, - diffSummary: input.diffSummary, - diffPatch: input.diffPatch, - }); + const generated = yield* runCodexJson({ + operation: "generatePrContent", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runCodexJson({ - operation: "generatePrContent", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + title: sanitizePrTitle(generated.title), + body: generated.body.trim(), + }; }); - return { - title: sanitizePrTitle(generated.title), - body: generated.body.trim(), - }; - }); + const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] = + Effect.fn("CodexTextGeneration.generateBranchName")(function* (input) { + const { imagePaths } = yield* materializeImageAttachments( + "generateBranchName", + input.attachments, + ); + const { prompt, outputSchema } = buildBranchNamePrompt({ + message: input.message, + attachments: input.attachments, + }); - const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn( - "CodexTextGeneration.generateBranchName", - )(function* (input) { - const { imagePaths } = yield* materializeImageAttachments( - "generateBranchName", - input.attachments, - ); - const { prompt, outputSchema } = buildBranchNamePrompt({ - message: input.message, - attachments: input.attachments, - }); + const generated = yield* runCodexJson({ + operation: "generateBranchName", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + imagePaths, + modelSelection: input.modelSelection, + }); - const generated = yield* runCodexJson({ - operation: "generateBranchName", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - imagePaths, - modelSelection: input.modelSelection, + return { + branch: sanitizeBranchFragment(generated.branch), + }; }); - return { - branch: sanitizeBranchFragment(generated.branch), - }; - }); + const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] = + Effect.fn("CodexTextGeneration.generateThreadTitle")(function* (input) { + const { imagePaths } = yield* materializeImageAttachments( + "generateThreadTitle", + input.attachments, + ); + const { prompt, outputSchema } = buildThreadTitlePrompt({ + message: input.message, + attachments: input.attachments, + }); - const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn( - "CodexTextGeneration.generateThreadTitle", - )(function* (input) { - const { imagePaths } = yield* materializeImageAttachments( - "generateThreadTitle", - input.attachments, - ); - const { prompt, outputSchema } = buildThreadTitlePrompt({ - message: input.message, - attachments: input.attachments, - }); + const generated = yield* runCodexJson({ + operation: "generateThreadTitle", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + imagePaths, + modelSelection: input.modelSelection, + }); - const generated = yield* runCodexJson({ - operation: "generateThreadTitle", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - imagePaths, - modelSelection: input.modelSelection, + return { + title: sanitizeThreadTitle(generated.title), + } satisfies TextGeneration.ThreadTitleGenerationResult; }); - return { - title: sanitizeThreadTitle(generated.title), - } satisfies ThreadTitleGenerationResult; - }); - return { generateCommitMessage, generatePrContent, generateBranchName, generateThreadTitle, - } satisfies TextGenerationShape; + } satisfies TextGeneration.TextGeneration["Service"]; }); diff --git a/apps/server/src/textGeneration/CursorTextGeneration.test.ts b/apps/server/src/textGeneration/CursorTextGeneration.test.ts index c7ca9f7086e..5365d920471 100644 --- a/apps/server/src/textGeneration/CursorTextGeneration.test.ts +++ b/apps/server/src/textGeneration/CursorTextGeneration.test.ts @@ -16,8 +16,8 @@ import { expect } from "vite-plus/test"; import { CursorSettings, ProviderInstanceId } from "@t3tools/contracts"; -import { ServerConfig } from "../config.ts"; -import { type TextGenerationShape } from "./TextGeneration.ts"; +import * as ServerConfig from "../config.ts"; +import * as TextGeneration from "./TextGeneration.ts"; import { makeCursorTextGeneration } from "./CursorTextGeneration.ts"; const decodeCursorSettings = Schema.decodeSync(CursorSettings); @@ -28,7 +28,7 @@ function shellSingleQuote(value: string): string { return `'${value.replaceAll("'", `'"'"'`)}'`; } -const CursorTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), { +const CursorTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3code-cursor-text-generation-test-", }).pipe(Layer.provideMerge(NodeServices.layer)); @@ -56,7 +56,7 @@ function makeAcpAgentWrapper(dir: string, env: Record): string { function withFakeAcpAgent( env: Record, - effectFn: (textGeneration: TextGenerationShape) => Effect.Effect, + effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect, ) { return Effect.gen(function* () { const tempDir = mkdtempSync(path.join(os.tmpdir(), "t3code-cursor-text-acp-")); diff --git a/apps/server/src/textGeneration/CursorTextGeneration.ts b/apps/server/src/textGeneration/CursorTextGeneration.ts index 6d72178b8ae..24676789b05 100644 --- a/apps/server/src/textGeneration/CursorTextGeneration.ts +++ b/apps/server/src/textGeneration/CursorTextGeneration.ts @@ -9,7 +9,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar import { extractJsonObject } from "@t3tools/shared/schemaJson"; import { TextGenerationError } from "@t3tools/contracts"; -import { type ThreadTitleGenerationResult, type TextGenerationShape } from "./TextGeneration.ts"; +import * as TextGeneration from "./TextGeneration.ts"; import { buildBranchNamePrompt, buildCommitMessagePrompt, @@ -176,104 +176,100 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu Effect.scoped, ); - const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn( - "CursorTextGeneration.generateCommitMessage", - )(function* (input) { - const { prompt, outputSchema } = buildCommitMessagePrompt({ - branch: input.branch, - stagedSummary: input.stagedSummary, - stagedPatch: input.stagedPatch, - includeBranch: input.includeBranch === true, - }); + const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] = + Effect.fn("CursorTextGeneration.generateCommitMessage")(function* (input) { + const { prompt, outputSchema } = buildCommitMessagePrompt({ + branch: input.branch, + stagedSummary: input.stagedSummary, + stagedPatch: input.stagedPatch, + includeBranch: input.includeBranch === true, + }); - const generated = yield* runCursorJson({ - operation: "generateCommitMessage", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + const generated = yield* runCursorJson({ + operation: "generateCommitMessage", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + subject: sanitizeCommitSubject(generated.subject), + body: generated.body.trim(), + ...("branch" in generated && typeof generated.branch === "string" + ? { branch: sanitizeFeatureBranchName(generated.branch) } + : {}), + }; }); - return { - subject: sanitizeCommitSubject(generated.subject), - body: generated.body.trim(), - ...("branch" in generated && typeof generated.branch === "string" - ? { branch: sanitizeFeatureBranchName(generated.branch) } - : {}), - }; - }); + const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] = + Effect.fn("CursorTextGeneration.generatePrContent")(function* (input) { + const { prompt, outputSchema } = buildPrContentPrompt({ + baseBranch: input.baseBranch, + headBranch: input.headBranch, + commitSummary: input.commitSummary, + diffSummary: input.diffSummary, + diffPatch: input.diffPatch, + }); - const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn( - "CursorTextGeneration.generatePrContent", - )(function* (input) { - const { prompt, outputSchema } = buildPrContentPrompt({ - baseBranch: input.baseBranch, - headBranch: input.headBranch, - commitSummary: input.commitSummary, - diffSummary: input.diffSummary, - diffPatch: input.diffPatch, - }); + const generated = yield* runCursorJson({ + operation: "generatePrContent", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runCursorJson({ - operation: "generatePrContent", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + title: sanitizePrTitle(generated.title), + body: generated.body.trim(), + }; }); - return { - title: sanitizePrTitle(generated.title), - body: generated.body.trim(), - }; - }); + const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] = + Effect.fn("CursorTextGeneration.generateBranchName")(function* (input) { + const { prompt, outputSchema } = buildBranchNamePrompt({ + message: input.message, + attachments: input.attachments, + }); - const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn( - "CursorTextGeneration.generateBranchName", - )(function* (input) { - const { prompt, outputSchema } = buildBranchNamePrompt({ - message: input.message, - attachments: input.attachments, - }); + const generated = yield* runCursorJson({ + operation: "generateBranchName", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runCursorJson({ - operation: "generateBranchName", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + branch: sanitizeBranchFragment(generated.branch), + }; }); - return { - branch: sanitizeBranchFragment(generated.branch), - }; - }); + const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] = + Effect.fn("CursorTextGeneration.generateThreadTitle")(function* (input) { + const { prompt, outputSchema } = buildThreadTitlePrompt({ + message: input.message, + attachments: input.attachments, + }); - const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn( - "CursorTextGeneration.generateThreadTitle", - )(function* (input) { - const { prompt, outputSchema } = buildThreadTitlePrompt({ - message: input.message, - attachments: input.attachments, - }); + const generated = yield* runCursorJson({ + operation: "generateThreadTitle", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runCursorJson({ - operation: "generateThreadTitle", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + title: sanitizeThreadTitle(generated.title), + } satisfies TextGeneration.ThreadTitleGenerationResult; }); - return { - title: sanitizeThreadTitle(generated.title), - } satisfies ThreadTitleGenerationResult; - }); - return { generateCommitMessage, generatePrContent, generateBranchName, generateThreadTitle, - } satisfies TextGenerationShape; + } satisfies TextGeneration.TextGeneration["Service"]; }); diff --git a/apps/server/src/textGeneration/GrokTextGeneration.test.ts b/apps/server/src/textGeneration/GrokTextGeneration.test.ts index 58ce165752c..5df012cca85 100644 --- a/apps/server/src/textGeneration/GrokTextGeneration.test.ts +++ b/apps/server/src/textGeneration/GrokTextGeneration.test.ts @@ -13,8 +13,8 @@ import { createModelSelection } from "@t3tools/shared/model"; import { expect } from "vite-plus/test"; import { GrokSettings, ProviderInstanceId } from "@t3tools/contracts"; -import { ServerConfig } from "../config.ts"; -import { type TextGenerationShape } from "./TextGeneration.ts"; +import * as ServerConfig from "../config.ts"; +import * as TextGeneration from "./TextGeneration.ts"; import { makeGrokTextGeneration } from "./GrokTextGeneration.ts"; const decodeGrokSettings = Schema.decodeSync(GrokSettings); @@ -25,7 +25,7 @@ function shellSingleQuote(value: string): string { return `'${value.replaceAll("'", `'"'"'`)}'`; } -const GrokTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), { +const GrokTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3code-grok-text-generation-test-", }).pipe(Layer.provideMerge(NodeServices.layer)); @@ -53,7 +53,7 @@ function makeAcpGrokWrapper(dir: string, env: Record): string { function withFakeAcpGrok( env: Record, - effectFn: (textGeneration: TextGenerationShape) => Effect.Effect, + effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect, ) { return Effect.gen(function* () { const tempDir = mkdtempSync(path.join(os.tmpdir(), "t3code-grok-text-acp-")); diff --git a/apps/server/src/textGeneration/GrokTextGeneration.ts b/apps/server/src/textGeneration/GrokTextGeneration.ts index 6d7ff8e872d..ab52efb1116 100644 --- a/apps/server/src/textGeneration/GrokTextGeneration.ts +++ b/apps/server/src/textGeneration/GrokTextGeneration.ts @@ -10,7 +10,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar import { extractJsonObject } from "@t3tools/shared/schemaJson"; import { TextGenerationError } from "@t3tools/contracts"; -import { type ThreadTitleGenerationResult, type TextGenerationShape } from "./TextGeneration.ts"; +import * as TextGeneration from "./TextGeneration.ts"; import { buildBranchNamePrompt, buildCommitMessagePrompt, @@ -169,104 +169,100 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi Effect.scoped, ); - const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn( - "GrokTextGeneration.generateCommitMessage", - )(function* (input) { - const { prompt, outputSchema } = buildCommitMessagePrompt({ - branch: input.branch, - stagedSummary: input.stagedSummary, - stagedPatch: input.stagedPatch, - includeBranch: input.includeBranch === true, - }); + const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] = + Effect.fn("GrokTextGeneration.generateCommitMessage")(function* (input) { + const { prompt, outputSchema } = buildCommitMessagePrompt({ + branch: input.branch, + stagedSummary: input.stagedSummary, + stagedPatch: input.stagedPatch, + includeBranch: input.includeBranch === true, + }); - const generated = yield* runGrokJson({ - operation: "generateCommitMessage", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + const generated = yield* runGrokJson({ + operation: "generateCommitMessage", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + subject: sanitizeCommitSubject(generated.subject), + body: generated.body.trim(), + ...("branch" in generated && typeof generated.branch === "string" + ? { branch: sanitizeFeatureBranchName(generated.branch) } + : {}), + }; }); - return { - subject: sanitizeCommitSubject(generated.subject), - body: generated.body.trim(), - ...("branch" in generated && typeof generated.branch === "string" - ? { branch: sanitizeFeatureBranchName(generated.branch) } - : {}), - }; - }); + const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] = + Effect.fn("GrokTextGeneration.generatePrContent")(function* (input) { + const { prompt, outputSchema } = buildPrContentPrompt({ + baseBranch: input.baseBranch, + headBranch: input.headBranch, + commitSummary: input.commitSummary, + diffSummary: input.diffSummary, + diffPatch: input.diffPatch, + }); - const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn( - "GrokTextGeneration.generatePrContent", - )(function* (input) { - const { prompt, outputSchema } = buildPrContentPrompt({ - baseBranch: input.baseBranch, - headBranch: input.headBranch, - commitSummary: input.commitSummary, - diffSummary: input.diffSummary, - diffPatch: input.diffPatch, - }); + const generated = yield* runGrokJson({ + operation: "generatePrContent", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runGrokJson({ - operation: "generatePrContent", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + title: sanitizePrTitle(generated.title), + body: generated.body.trim(), + }; }); - return { - title: sanitizePrTitle(generated.title), - body: generated.body.trim(), - }; - }); + const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] = + Effect.fn("GrokTextGeneration.generateBranchName")(function* (input) { + const { prompt, outputSchema } = buildBranchNamePrompt({ + message: input.message, + attachments: input.attachments, + }); - const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn( - "GrokTextGeneration.generateBranchName", - )(function* (input) { - const { prompt, outputSchema } = buildBranchNamePrompt({ - message: input.message, - attachments: input.attachments, - }); + const generated = yield* runGrokJson({ + operation: "generateBranchName", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runGrokJson({ - operation: "generateBranchName", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + branch: sanitizeBranchFragment(generated.branch), + }; }); - return { - branch: sanitizeBranchFragment(generated.branch), - }; - }); + const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] = + Effect.fn("GrokTextGeneration.generateThreadTitle")(function* (input) { + const { prompt, outputSchema } = buildThreadTitlePrompt({ + message: input.message, + attachments: input.attachments, + }); - const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn( - "GrokTextGeneration.generateThreadTitle", - )(function* (input) { - const { prompt, outputSchema } = buildThreadTitlePrompt({ - message: input.message, - attachments: input.attachments, - }); + const generated = yield* runGrokJson({ + operation: "generateThreadTitle", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generated = yield* runGrokJson({ - operation: "generateThreadTitle", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + title: sanitizeThreadTitle(generated.title), + } satisfies TextGeneration.ThreadTitleGenerationResult; }); - return { - title: sanitizeThreadTitle(generated.title), - } satisfies ThreadTitleGenerationResult; - }); - return { generateCommitMessage, generatePrContent, generateBranchName, generateThreadTitle, - } satisfies TextGenerationShape; + } satisfies TextGeneration.TextGeneration["Service"]; }); diff --git a/apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts b/apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts index ba1f3a0435c..f6d9c133f38 100644 --- a/apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts +++ b/apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts @@ -9,13 +9,9 @@ import * as TestClock from "effect/testing/TestClock"; import * as NetService from "@t3tools/shared/Net"; import { beforeEach, expect } from "vite-plus/test"; -import { ServerConfig } from "../config.ts"; -import { - OpenCodeRuntime, - OpenCodeRuntimeError, - type OpenCodeRuntimeShape, -} from "../provider/opencodeRuntime.ts"; -import { type TextGenerationShape } from "./TextGeneration.ts"; +import * as ServerConfig from "../config.ts"; +import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts"; +import * as TextGeneration from "./TextGeneration.ts"; import { makeOpenCodeTextGeneration } from "./OpenCodeTextGeneration.ts"; const runtimeMock = { @@ -37,7 +33,7 @@ const runtimeMock = { }, }; -const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { +const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { startOpenCodeServerProcess: ({ binaryPath }) => Effect.gen(function* () { const index = runtimeMock.state.startCalls.length + 1; @@ -88,10 +84,10 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { ); }, }, - }) as unknown as ReturnType, + }) as unknown as ReturnType, loadOpenCodeInventory: () => Effect.fail( - new OpenCodeRuntimeError({ + new OpenCodeRuntime.OpenCodeRuntimeError({ operation: "loadOpenCodeInventory", detail: "OpenCodeRuntimeTestDouble.loadOpenCodeInventory not used in this test", cause: null, @@ -107,11 +103,11 @@ const DEFAULT_TEST_MODEL_SELECTION = { const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000; const OpenCodeTextGenerationTestLayer = Layer.succeed( - OpenCodeRuntime, + OpenCodeRuntime.OpenCodeRuntime, OpenCodeRuntimeTestDouble, ).pipe( Layer.provideMerge( - ServerConfig.layerTest(process.cwd(), { + ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3code-opencode-text-generation-test-", }), ), @@ -120,11 +116,11 @@ const OpenCodeTextGenerationTestLayer = Layer.succeed( ); const OpenCodeTextGenerationExistingServerTestLayer = Layer.succeed( - OpenCodeRuntime, + OpenCodeRuntime.OpenCodeRuntime, OpenCodeRuntimeTestDouble, ).pipe( Layer.provideMerge( - ServerConfig.layerTest(process.cwd(), { + ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3code-opencode-text-generation-existing-server-test-", }), ), @@ -143,7 +139,7 @@ const EXISTING_SERVER_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({ function withOpenCodeTextGeneration( settings: OpenCodeSettings, - effectFn: (textGeneration: TextGenerationShape) => Effect.Effect, + effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect, ) { return Effect.gen(function* () { const textGeneration = yield* makeOpenCodeTextGeneration(settings); diff --git a/apps/server/src/textGeneration/OpenCodeTextGeneration.ts b/apps/server/src/textGeneration/OpenCodeTextGeneration.ts index 65d3854e945..0ba7726d68c 100644 --- a/apps/server/src/textGeneration/OpenCodeTextGeneration.ts +++ b/apps/server/src/textGeneration/OpenCodeTextGeneration.ts @@ -15,7 +15,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { extractJsonObject } from "@t3tools/shared/schemaJson"; -import { ServerConfig } from "../config.ts"; +import * as ServerConfig from "../config.ts"; import { resolveAttachmentPath } from "../attachmentStore.ts"; import { buildBranchNamePrompt, @@ -23,20 +23,13 @@ import { buildPrContentPrompt, buildThreadTitlePrompt, } from "./TextGenerationPrompts.ts"; -import { type TextGenerationShape } from "./TextGeneration.ts"; +import * as TextGeneration from "./TextGeneration.ts"; import { sanitizeCommitSubject, sanitizePrTitle, sanitizeThreadTitle, } from "./TextGenerationUtils.ts"; -import { - OpenCodeRuntime, - type OpenCodeServerConnection, - type OpenCodeServerProcess, - openCodeRuntimeErrorDetail, - parseOpenCodeModelSlug, - toOpenCodeFileParts, -} from "../provider/opencodeRuntime.ts"; +import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts"; const OPENCODE_TEXT_GENERATION_IDLE_TTL = "30 seconds"; @@ -84,7 +77,7 @@ function getOpenCodeTextResponse(parts: ReadonlyArray | undefined): str } interface SharedOpenCodeTextGenerationServerState { - server: OpenCodeServerProcess | null; + server: OpenCodeRuntime.OpenCodeServerProcess | null; /** * The scope that owns the shared server's lifetime. Closing this scope * terminates the OpenCode child process and interrupts any fibers the @@ -101,8 +94,8 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" openCodeSettings: OpenCodeSettings, environment?: NodeJS.ProcessEnv, ) { - const serverConfig = yield* ServerConfig; - const openCodeRuntime = yield* OpenCodeRuntime; + const serverConfig = yield* ServerConfig.ServerConfig; + const openCodeRuntime = yield* OpenCodeRuntime.OpenCodeRuntime; const resolvedEnvironment = environment ?? process.env; const idleFiberScope = yield* Effect.acquireRelease(Scope.make(), (scope) => Scope.close(scope, Exit.void), @@ -135,7 +128,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" }); const scheduleIdleClose = Effect.fn("scheduleIdleClose")(function* ( - server: OpenCodeServerProcess, + server: OpenCodeRuntime.OpenCodeServerProcess, ) { yield* cancelIdleCloseFiber(); const fiber = yield* Effect.sleep(OPENCODE_TEXT_GENERATION_IDLE_TTL).pipe( @@ -217,7 +210,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" (cause) => new TextGenerationError({ operation: input.operation, - detail: openCodeRuntimeErrorDetail(cause), + detail: OpenCodeRuntime.openCodeRuntimeErrorDetail(cause), cause, }), ), @@ -240,7 +233,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" }), ); - const releaseSharedServer = (server: OpenCodeServerProcess) => + const releaseSharedServer = (server: OpenCodeRuntime.OpenCodeServerProcess) => sharedServerMutex.withPermit( Effect.gen(function* () { if (sharedServerState.server !== server) { @@ -278,7 +271,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" readonly modelSelection: ModelSelection; readonly attachments?: ReadonlyArray | undefined; }) { - const parsedModel = parseOpenCodeModelSlug(input.modelSelection.model); + const parsedModel = OpenCodeRuntime.parseOpenCodeModelSlug(input.modelSelection.model); if (!parsedModel) { return yield* new TextGenerationError({ operation: input.operation, @@ -286,13 +279,13 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" }); } - const fileParts = toOpenCodeFileParts({ + const fileParts = OpenCodeRuntime.toOpenCodeFileParts({ attachments: input.attachments, resolveAttachmentPath: (attachment) => resolveAttachmentPath({ attachmentsDir: serverConfig.attachmentsDir, attachment }), }); - const runAgainstServer = (server: Pick) => + const runAgainstServer = (server: Pick) => Effect.tryPromise({ try: async () => { const client = openCodeRuntime.createOpenCodeSdkClient({ @@ -336,7 +329,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" catch: (cause) => new TextGenerationError({ operation: input.operation, - detail: openCodeRuntimeErrorDetail(cause), + detail: OpenCodeRuntime.openCodeRuntimeErrorDetail(cause), cause, }), }); @@ -367,102 +360,98 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" ); }); - const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn( - "OpenCodeTextGeneration.generateCommitMessage", - )(function* (input) { - const { prompt, outputSchema } = buildCommitMessagePrompt({ - branch: input.branch, - stagedSummary: input.stagedSummary, - stagedPatch: input.stagedPatch, - includeBranch: input.includeBranch === true, - }); - const generated = yield* runOpenCodeJson({ - operation: "generateCommitMessage", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] = + Effect.fn("OpenCodeTextGeneration.generateCommitMessage")(function* (input) { + const { prompt, outputSchema } = buildCommitMessagePrompt({ + branch: input.branch, + stagedSummary: input.stagedSummary, + stagedPatch: input.stagedPatch, + includeBranch: input.includeBranch === true, + }); + const generated = yield* runOpenCodeJson({ + operation: "generateCommitMessage", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + subject: sanitizeCommitSubject(generated.subject), + body: generated.body.trim(), + ...("branch" in generated && typeof generated.branch === "string" + ? { branch: sanitizeFeatureBranchName(generated.branch) } + : {}), + }; }); - return { - subject: sanitizeCommitSubject(generated.subject), - body: generated.body.trim(), - ...("branch" in generated && typeof generated.branch === "string" - ? { branch: sanitizeFeatureBranchName(generated.branch) } - : {}), - }; - }); + const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] = + Effect.fn("OpenCodeTextGeneration.generatePrContent")(function* (input) { + const { prompt, outputSchema } = buildPrContentPrompt({ + baseBranch: input.baseBranch, + headBranch: input.headBranch, + commitSummary: input.commitSummary, + diffSummary: input.diffSummary, + diffPatch: input.diffPatch, + }); + const generated = yield* runOpenCodeJson({ + operation: "generatePrContent", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); - const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn( - "OpenCodeTextGeneration.generatePrContent", - )(function* (input) { - const { prompt, outputSchema } = buildPrContentPrompt({ - baseBranch: input.baseBranch, - headBranch: input.headBranch, - commitSummary: input.commitSummary, - diffSummary: input.diffSummary, - diffPatch: input.diffPatch, - }); - const generated = yield* runOpenCodeJson({ - operation: "generatePrContent", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, + return { + title: sanitizePrTitle(generated.title), + body: generated.body.trim(), + }; }); - return { - title: sanitizePrTitle(generated.title), - body: generated.body.trim(), - }; - }); + const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] = + Effect.fn("OpenCodeTextGeneration.generateBranchName")(function* (input) { + const { prompt, outputSchema } = buildBranchNamePrompt({ + message: input.message, + attachments: input.attachments, + }); + const generated = yield* runOpenCodeJson({ + operation: "generateBranchName", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + attachments: input.attachments, + }); - const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn( - "OpenCodeTextGeneration.generateBranchName", - )(function* (input) { - const { prompt, outputSchema } = buildBranchNamePrompt({ - message: input.message, - attachments: input.attachments, - }); - const generated = yield* runOpenCodeJson({ - operation: "generateBranchName", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, - attachments: input.attachments, + return { + branch: sanitizeBranchFragment(generated.branch), + }; }); - return { - branch: sanitizeBranchFragment(generated.branch), - }; - }); + const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] = + Effect.fn("OpenCodeTextGeneration.generateThreadTitle")(function* (input) { + const { prompt, outputSchema } = buildThreadTitlePrompt({ + message: input.message, + attachments: input.attachments, + }); + const generated = yield* runOpenCodeJson({ + operation: "generateThreadTitle", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + attachments: input.attachments, + }); - const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn( - "OpenCodeTextGeneration.generateThreadTitle", - )(function* (input) { - const { prompt, outputSchema } = buildThreadTitlePrompt({ - message: input.message, - attachments: input.attachments, - }); - const generated = yield* runOpenCodeJson({ - operation: "generateThreadTitle", - cwd: input.cwd, - prompt, - outputSchemaJson: outputSchema, - modelSelection: input.modelSelection, - attachments: input.attachments, + return { + title: sanitizeThreadTitle(generated.title), + }; }); - return { - title: sanitizeThreadTitle(generated.title), - }; - }); - return { generateCommitMessage, generatePrContent, generateBranchName, generateThreadTitle, - } satisfies TextGenerationShape; + } satisfies TextGeneration.TextGeneration["Service"]; }); diff --git a/apps/server/src/textGeneration/TextGeneration.test.ts b/apps/server/src/textGeneration/TextGeneration.test.ts index f186d934e52..9bccb9c1fc5 100644 --- a/apps/server/src/textGeneration/TextGeneration.test.ts +++ b/apps/server/src/textGeneration/TextGeneration.test.ts @@ -9,23 +9,24 @@ import { ProviderInstanceId } from "@t3tools/contracts"; import { createModelSelection } from "@t3tools/shared/model"; import type { ProviderInstance } from "../provider/ProviderDriver.ts"; -import type { ProviderInstanceRegistryShape } from "../provider/Services/ProviderInstanceRegistry.ts"; -import type { TextGenerationShape } from "./TextGeneration.ts"; +import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts"; +import * as TextGeneration from "./TextGeneration.ts"; -import { makeTextGenerationFromRegistry } from "./TextGeneration.ts"; - -const makeStubTextGeneration = (overrides: Partial): TextGenerationShape => ({ - generateCommitMessage: () => - Effect.die("generateCommitMessage stub not configured for this test"), - generatePrContent: () => Effect.die("generatePrContent stub not configured for this test"), - generateBranchName: () => Effect.die("generateBranchName stub not configured for this test"), - generateThreadTitle: () => Effect.die("generateThreadTitle stub not configured for this test"), - ...overrides, -}); +const makeStubTextGeneration = ( + overrides: Partial, +): TextGeneration.TextGeneration["Service"] => + TextGeneration.TextGeneration.of({ + generateCommitMessage: () => + Effect.die("generateCommitMessage stub not configured for this test"), + generatePrContent: () => Effect.die("generatePrContent stub not configured for this test"), + generateBranchName: () => Effect.die("generateBranchName stub not configured for this test"), + generateThreadTitle: () => Effect.die("generateThreadTitle stub not configured for this test"), + ...overrides, + }); const makeStubInstance = ( instanceId: ProviderInstanceId, - textGeneration: TextGenerationShape, + textGeneration: TextGeneration.TextGeneration["Service"], ): ProviderInstance => ({ instanceId, @@ -43,7 +44,7 @@ const makeStubInstance = ( const makeStubRegistry = ( instances: ReadonlyArray, -): ProviderInstanceRegistryShape => { +): ProviderInstanceRegistry.ProviderInstanceRegistry["Service"] => { const byId = new Map(instances.map((instance) => [instance.instanceId, instance] as const)); return { getInstance: (id) => Effect.succeed(byId.get(id)), @@ -81,7 +82,7 @@ describe("makeTextGenerationFromRegistry", () => { }), ); - const tg = makeTextGenerationFromRegistry(makeStubRegistry([personal, work])); + const tg = TextGeneration.makeTextGenerationFromRegistry(makeStubRegistry([personal, work])); const result = yield* tg.generateBranchName({ cwd: process.cwd(), @@ -96,7 +97,7 @@ describe("makeTextGenerationFromRegistry", () => { it.effect("fails with TextGenerationError when the instance is unknown", () => Effect.gen(function* () { - const tg = makeTextGenerationFromRegistry(makeStubRegistry([])); + const tg = TextGeneration.makeTextGenerationFromRegistry(makeStubRegistry([])); const result = yield* tg .generateBranchName({ diff --git a/apps/server/src/textGeneration/TextGeneration.ts b/apps/server/src/textGeneration/TextGeneration.ts index d5d28e638ed..e62a79afe78 100644 --- a/apps/server/src/textGeneration/TextGeneration.ts +++ b/apps/server/src/textGeneration/TextGeneration.ts @@ -4,10 +4,7 @@ import * as Layer from "effect/Layer"; import type { ChatAttachment, ModelSelection, ProviderInstanceId } from "@t3tools/contracts"; import { TextGenerationError } from "@t3tools/contracts"; -import { - ProviderInstanceRegistry, - type ProviderInstanceRegistryShape, -} from "../provider/Services/ProviderInstanceRegistry.ts"; +import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts"; import type { ProviderInstance } from "../provider/ProviderDriver.ts"; export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "grok" | "opencode"; @@ -79,45 +76,44 @@ export interface TextGenerationService { generateThreadTitle(input: ThreadTitleGenerationInput): Promise; } -/** - * TextGenerationShape - Service API for commit/PR text generation. - */ -export interface TextGenerationShape { - /** - * Generate a commit message from staged change context. - */ - readonly generateCommitMessage: ( - input: CommitMessageGenerationInput, - ) => Effect.Effect; - - /** - * Generate pull request title/body from branch and diff context. - */ - readonly generatePrContent: ( - input: PrContentGenerationInput, - ) => Effect.Effect; - - /** - * Generate a concise branch name from a user message. - */ - readonly generateBranchName: ( - input: BranchNameGenerationInput, - ) => Effect.Effect; - - /** - * Generate a concise thread title from a user's first message. - */ - readonly generateThreadTitle: ( - input: ThreadTitleGenerationInput, - ) => Effect.Effect; -} - /** * TextGeneration - Service tag for commit and PR text generation. */ -export class TextGeneration extends Context.Service()( - "t3/textGeneration/TextGeneration", -) {} +export class TextGeneration extends Context.Service< + TextGeneration, + { + /** + * Generate a commit message from staged change context. + */ + readonly generateCommitMessage: ( + input: CommitMessageGenerationInput, + ) => Effect.Effect; + + /** + * Generate pull request title/body from branch and diff context. + */ + readonly generatePrContent: ( + input: PrContentGenerationInput, + ) => Effect.Effect; + + /** + * Generate a concise branch name from a user message. + */ + readonly generateBranchName: ( + input: BranchNameGenerationInput, + ) => Effect.Effect; + + /** + * Generate a concise thread title from a user's first message. + */ + readonly generateThreadTitle: ( + input: ThreadTitleGenerationInput, + ) => Effect.Effect; + } +>()("t3/textGeneration/TextGeneration") {} + +/** @deprecated Use `TextGeneration["Service"]`. */ +export type TextGenerationShape = TextGeneration["Service"]; type TextGenerationOp = | "generateCommitMessage" @@ -126,7 +122,7 @@ type TextGenerationOp = | "generateThreadTitle"; const resolveInstance = ( - registry: ProviderInstanceRegistryShape, + registry: ProviderInstanceRegistry.ProviderInstanceRegistry["Service"], operation: TextGenerationOp, instanceId: ProviderInstanceId, ): Effect.Effect => @@ -144,30 +140,30 @@ const resolveInstance = ( ); export const makeTextGenerationFromRegistry = ( - registry: ProviderInstanceRegistryShape, -): TextGenerationShape => ({ - generateCommitMessage: (input) => - resolveInstance(registry, "generateCommitMessage", input.modelSelection.instanceId).pipe( - Effect.flatMap((textGeneration) => textGeneration.generateCommitMessage(input)), - ), - generatePrContent: (input) => - resolveInstance(registry, "generatePrContent", input.modelSelection.instanceId).pipe( - Effect.flatMap((textGeneration) => textGeneration.generatePrContent(input)), - ), - generateBranchName: (input) => - resolveInstance(registry, "generateBranchName", input.modelSelection.instanceId).pipe( - Effect.flatMap((textGeneration) => textGeneration.generateBranchName(input)), - ), - generateThreadTitle: (input) => - resolveInstance(registry, "generateThreadTitle", input.modelSelection.instanceId).pipe( - Effect.flatMap((textGeneration) => textGeneration.generateThreadTitle(input)), - ), + registry: ProviderInstanceRegistry.ProviderInstanceRegistry["Service"], +): TextGeneration["Service"] => + TextGeneration.of({ + generateCommitMessage: (input) => + resolveInstance(registry, "generateCommitMessage", input.modelSelection.instanceId).pipe( + Effect.flatMap((textGeneration) => textGeneration.generateCommitMessage(input)), + ), + generatePrContent: (input) => + resolveInstance(registry, "generatePrContent", input.modelSelection.instanceId).pipe( + Effect.flatMap((textGeneration) => textGeneration.generatePrContent(input)), + ), + generateBranchName: (input) => + resolveInstance(registry, "generateBranchName", input.modelSelection.instanceId).pipe( + Effect.flatMap((textGeneration) => textGeneration.generateBranchName(input)), + ), + generateThreadTitle: (input) => + resolveInstance(registry, "generateThreadTitle", input.modelSelection.instanceId).pipe( + Effect.flatMap((textGeneration) => textGeneration.generateThreadTitle(input)), + ), + }); + +export const make = Effect.gen(function* () { + const registry = yield* ProviderInstanceRegistry.ProviderInstanceRegistry; + return makeTextGenerationFromRegistry(registry); }); -export const layer = Layer.effect( - TextGeneration, - Effect.gen(function* () { - const registry = yield* ProviderInstanceRegistry; - return makeTextGenerationFromRegistry(registry); - }), -); +export const layer = Layer.effect(TextGeneration, make);