| author | |
| committer | |
| log | b6302784f8e058de4fb20dae931b4df29e9302fe |
| tree | 11ac8eeb52331ea3ba270b402d02c36ca978bfb8 |
| parent | 9544e72d08755dfc5d9014efd87243f4654f239d |
| signature | Signed by PGP key B5690EEEBB952194 |
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 { |
| 20 | 20 | } from "@t3tools/contracts"; |
| 21 | 21 | |
| 22 | 22 | import { GitCommandError, TextGenerationError } from "@t3tools/contracts"; |
| 23 | import { type GitManagerShape } from "./GitManager.ts"; | |
| 24 | import { | |
| 25 | GitHubCliError, | |
| 26 | type GitHubCliShape, | |
| 27 | type GitHubPullRequestSummary, | |
| 28 | GitHubCli, | |
| 29 | } from "../sourceControl/GitHubCli.ts"; | |
| 30 | import { type TextGenerationShape, TextGeneration } from "../textGeneration/TextGeneration.ts"; | |
| 23 | import * as GitManager from "./GitManager.ts"; | |
| 24 | import * as GitHubCli from "../sourceControl/GitHubCli.ts"; | |
| 25 | import * as TextGeneration from "../textGeneration/TextGeneration.ts"; | |
| 31 | 26 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 32 | 27 | import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| 33 | 28 | import * as GitHubSourceControlProvider from "../sourceControl/GitHubSourceControlProvider.ts"; |
| 34 | 29 | import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; |
| 35 | import { makeGitManager } from "./GitManager.ts"; | |
| 36 | import { ServerConfig } from "../config.ts"; | |
| 37 | import { ServerSettingsService } from "../serverSettings.ts"; | |
| 38 | import { | |
| 39 | ProjectSetupScriptRunner, | |
| 40 | ProjectSetupScriptRunnerError, | |
| 41 | type ProjectSetupScriptRunnerInput, | |
| 42 | type ProjectSetupScriptRunnerShape, | |
| 43 | } from "../project/Services/ProjectSetupScriptRunner.ts"; | |
| 30 | import * as ServerConfig from "../config.ts"; | |
| 31 | import * as ServerSettings from "../serverSettings.ts"; | |
| 32 | import * as ProjectSetupScriptRunner from "../project/Services/ProjectSetupScriptRunner.ts"; | |
| 44 | 33 | |
| 45 | 34 | interface FakeGhScenario { |
| 46 | 35 | prListSequence?: string[]; |
| ... | ... | @@ -60,7 +49,7 @@ interface FakeGhScenario { |
| 60 | 49 | headRepositoryOwnerLogin?: string | null; |
| 61 | 50 | }; |
| 62 | 51 | repositoryCloneUrls?: Record<string, { url: string; sshUrl: string }>; |
| 63 | failWith?: GitHubCliError; | |
| 52 | failWith?: GitHubCli.GitHubCliError; | |
| 64 | 53 | } |
| 65 | 54 | |
| 66 | 55 | function fakeGhOutput(stdout: string): VcsProcess.VcsProcessOutput { |
| ... | ... | @@ -108,7 +97,7 @@ interface FakeGitTextGeneration { |
| 108 | 97 | |
| 109 | 98 | type FakePullRequest = NonNullable<FakeGhScenario["pullRequest"]>; |
| 110 | 99 | |
| 111 | function normalizeFakePullRequestSummary(raw: unknown): GitHubPullRequestSummary | null { | |
| 100 | function normalizeFakePullRequestSummary(raw: unknown): GitHubCli.GitHubPullRequestSummary | null { | |
| 112 | 101 | if (!raw || typeof raw !== "object") { |
| 113 | 102 | return null; |
| 114 | 103 | } |
| ... | ... | @@ -182,13 +171,13 @@ function runGitSyncForFakeGh(cwd: string, args: readonly string[]): void { |
| 182 | 171 | if (result.status === 0) { |
| 183 | 172 | return; |
| 184 | 173 | } |
| 185 | throw new GitHubCliError({ | |
| 174 | throw new GitHubCli.GitHubCliError({ | |
| 186 | 175 | operation: "execute", |
| 187 | 176 | detail: `Failed to simulate gh checkout with git ${args.join(" ")}: ${result.stderr?.trim() || "unknown error"}`, |
| 188 | 177 | }); |
| 189 | 178 | } |
| 190 | 179 | |
| 191 | function isGitHubCliError(error: unknown): error is GitHubCliError { | |
| 180 | function isGitHubCliError(error: unknown): error is GitHubCli.GitHubCliError { | |
| 192 | 181 | return ( |
| 193 | 182 | typeof error === "object" && |
| 194 | 183 | error !== null && |
| ... | ... | @@ -312,7 +301,9 @@ function configureVisibleRemoteUrlWithLocalRewrite( |
| 312 | 301 | }); |
| 313 | 302 | } |
| 314 | 303 | |
| 315 | function createTextGeneration(overrides: Partial<FakeGitTextGeneration> = {}): TextGenerationShape { | |
| 304 | function createTextGeneration( | |
| 305 | overrides: Partial<FakeGitTextGeneration> = {}, | |
| 306 | ): TextGeneration.TextGeneration["Service"] { | |
| 316 | 307 | const implementation: FakeGitTextGeneration = { |
| 317 | 308 | generateCommitMessage: (input) => |
| 318 | 309 | Effect.succeed({ |
| ... | ... | @@ -385,7 +376,7 @@ function createTextGeneration(overrides: Partial<FakeGitTextGeneration> = {}): T |
| 385 | 376 | } |
| 386 | 377 | |
| 387 | 378 | function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 388 | service: GitHubCliShape; | |
| 379 | service: GitHubCli.GitHubCliShape; | |
| 389 | 380 | ghCalls: string[]; |
| 390 | 381 | } { |
| 391 | 382 | const prListQueue = [...(scenario.prListSequence ?? [])]; |
| ... | ... | @@ -397,7 +388,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 397 | 388 | ); |
| 398 | 389 | const ghCalls: string[] = []; |
| 399 | 390 | |
| 400 | const execute: GitHubCliShape["execute"] = (input) => { | |
| 391 | const execute: GitHubCli.GitHubCliShape["execute"] = (input) => { | |
| 401 | 392 | const args = [...input.args]; |
| 402 | 393 | ghCalls.push(args.join(" ")); |
| 403 | 394 | |
| ... | ... | @@ -487,7 +478,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 487 | 478 | catch: (error) => |
| 488 | 479 | isGitHubCliError(error) |
| 489 | 480 | ? error |
| 490 | : new GitHubCliError({ | |
| 481 | : new GitHubCli.GitHubCliError({ | |
| 491 | 482 | operation: "execute", |
| 492 | 483 | detail: |
| 493 | 484 | error instanceof Error |
| ... | ... | @@ -503,7 +494,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 503 | 494 | const cloneUrls = scenario.repositoryCloneUrls?.[repository]; |
| 504 | 495 | if (!cloneUrls) { |
| 505 | 496 | return Effect.fail( |
| 506 | new GitHubCliError({ | |
| 497 | new GitHubCli.GitHubCliError({ | |
| 507 | 498 | operation: "execute", |
| 508 | 499 | detail: `Unexpected repository lookup: ${repository}`, |
| 509 | 500 | }), |
| ... | ... | @@ -523,7 +514,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 523 | 514 | } |
| 524 | 515 | |
| 525 | 516 | return Effect.fail( |
| 526 | new GitHubCliError({ | |
| 517 | new GitHubCli.GitHubCliError({ | |
| 527 | 518 | operation: "execute", |
| 528 | 519 | detail: `Unexpected gh command: ${args.join(" ")}`, |
| 529 | 520 | }), |
| ... | ... | @@ -553,7 +544,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 553 | 544 | Effect.map((raw) => |
| 554 | 545 | raw |
| 555 | 546 | .map((entry) => normalizeFakePullRequestSummary(entry)) |
| 556 | .filter((entry): entry is GitHubPullRequestSummary => entry !== null), | |
| 547 | .filter((entry): entry is GitHubCli.GitHubPullRequestSummary => entry !== null), | |
| 557 | 548 | ), |
| 558 | 549 | ), |
| 559 | 550 | createPullRequest: (input) => |
| ... | ... | @@ -592,7 +583,9 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 592 | 583 | "--json", |
| 593 | 584 | "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner", |
| 594 | 585 | ], |
| 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 | ), | |
| 596 | 589 | getRepositoryCloneUrls: (input) => |
| 597 | 590 | execute({ |
| 598 | 591 | cwd: input.cwd, |
| ... | ... | @@ -600,7 +593,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 600 | 593 | }).pipe(Effect.map((result) => JSON.parse(result.stdout))), |
| 601 | 594 | createRepository: (input) => |
| 602 | 595 | Effect.fail( |
| 603 | new GitHubCliError({ | |
| 596 | new GitHubCli.GitHubCliError({ | |
| 604 | 597 | operation: "createRepository", |
| 605 | 598 | detail: `Unexpected repository create: ${input.repository}`, |
| 606 | 599 | }), |
| ... | ... | @@ -616,7 +609,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 616 | 609 | } |
| 617 | 610 | |
| 618 | 611 | function runStackedAction( |
| 619 | manager: GitManagerShape, | |
| 612 | manager: GitManager.GitManagerShape, | |
| 620 | 613 | input: { |
| 621 | 614 | cwd: string; |
| 622 | 615 | action: "commit" | "push" | "create_pr" | "commit_push" | "commit_push_pr"; |
| ... | ... | @@ -625,7 +618,7 @@ function runStackedAction( |
| 625 | 618 | featureBranch?: boolean; |
| 626 | 619 | filePaths?: readonly string[]; |
| 627 | 620 | }, |
| 628 | options?: Parameters<GitManagerShape["runStackedAction"]>[1], | |
| 621 | options?: Parameters<GitManager.GitManagerShape["runStackedAction"]>[1], | |
| 629 | 622 | ) { |
| 630 | 623 | return manager.runStackedAction( |
| 631 | 624 | { |
| ... | ... | @@ -636,12 +629,15 @@ function runStackedAction( |
| 636 | 629 | ); |
| 637 | 630 | } |
| 638 | 631 | |
| 639 | function resolvePullRequest(manager: GitManagerShape, input: { cwd: string; reference: string }) { | |
| 632 | function resolvePullRequest( | |
| 633 | manager: GitManager.GitManagerShape, | |
| 634 | input: { cwd: string; reference: string }, | |
| 635 | ) { | |
| 640 | 636 | return manager.resolvePullRequest(input); |
| 641 | 637 | } |
| 642 | 638 | |
| 643 | 639 | function preparePullRequestThread( |
| 644 | manager: GitManagerShape, | |
| 640 | manager: GitManager.GitManagerShape, | |
| 645 | 641 | input: GitPreparePullRequestThreadInput, |
| 646 | 642 | ) { |
| 647 | 643 | return manager.preparePullRequestThread(input); |
| ... | ... | @@ -650,20 +646,20 @@ function preparePullRequestThread( |
| 650 | 646 | function makeManager(input?: { |
| 651 | 647 | ghScenario?: FakeGhScenario; |
| 652 | 648 | textGeneration?: Partial<FakeGitTextGeneration>; |
| 653 | setupScriptRunner?: ProjectSetupScriptRunnerShape; | |
| 649 | setupScriptRunner?: ProjectSetupScriptRunner.ProjectSetupScriptRunnerShape; | |
| 654 | 650 | }) { |
| 655 | 651 | const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario); |
| 656 | 652 | const textGeneration = createTextGeneration(input?.textGeneration); |
| 657 | const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { | |
| 653 | const serverConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { | |
| 658 | 654 | prefix: "t3-git-manager-test-", |
| 659 | 655 | }); |
| 660 | 656 | |
| 661 | const serverSettingsLayer = ServerSettingsService.layerTest(); | |
| 657 | const serverSettingsLayer = ServerSettings.ServerSettingsService.layerTest(); | |
| 662 | 658 | |
| 663 | 659 | const vcsDriverLayer = GitVcsDriver.layer.pipe( |
| 664 | 660 | Layer.provideMerge(VcsProcess.layer), |
| 665 | 661 | Layer.provideMerge(NodeServices.layer), |
| 666 | Layer.provideMerge(ServerConfigLayer), | |
| 662 | Layer.provideMerge(serverConfigLayer), | |
| 667 | 663 | ); |
| 668 | 664 | const sourceControlRegistryLayer = Layer.effect( |
| 669 | 665 | SourceControlProviderRegistry.SourceControlProviderRegistry, |
| ... | ... | @@ -676,14 +672,14 @@ function makeManager(input?: { |
| 676 | 672 | discover: Effect.succeed([]), |
| 677 | 673 | }), |
| 678 | 674 | ), |
| 679 | Effect.provide(Layer.succeed(GitHubCli, gitHubCli)), | |
| 675 | Effect.provide(Layer.succeed(GitHubCli.GitHubCli, gitHubCli)), | |
| 680 | 676 | ), |
| 681 | 677 | ); |
| 682 | 678 | |
| 683 | 679 | const managerLayer = Layer.mergeAll( |
| 684 | Layer.succeed(TextGeneration, textGeneration), | |
| 680 | Layer.succeed(TextGeneration.TextGeneration, textGeneration), | |
| 685 | 681 | Layer.succeed( |
| 686 | ProjectSetupScriptRunner, | |
| 682 | ProjectSetupScriptRunner.ProjectSetupScriptRunner, | |
| 687 | 683 | input?.setupScriptRunner ?? { |
| 688 | 684 | runForThread: () => Effect.succeed({ status: "no-script" as const }), |
| 689 | 685 | }, |
| ... | ... | @@ -692,7 +688,7 @@ function makeManager(input?: { |
| 692 | 688 | serverSettingsLayer, |
| 693 | 689 | ).pipe(Layer.provideMerge(sourceControlRegistryLayer), Layer.provideMerge(NodeServices.layer)); |
| 694 | 690 | |
| 695 | return makeGitManager().pipe( | |
| 691 | return GitManager.makeGitManager().pipe( | |
| 696 | 692 | Effect.provide(managerLayer), |
| 697 | 693 | Effect.map((manager) => ({ manager, ghCalls })), |
| 698 | 694 | ); |
| ... | ... | @@ -701,7 +697,9 @@ function makeManager(input?: { |
| 701 | 697 | const asThreadId = (threadId: string) => threadId as ThreadId; |
| 702 | 698 | |
| 703 | 699 | const 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 | ), | |
| 705 | 703 | Layer.provideMerge(VcsProcess.layer), |
| 706 | 704 | Layer.provideMerge(NodeServices.layer), |
| 707 | 705 | ); |
| ... | ... | @@ -1335,7 +1333,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { |
| 1335 | 1333 | |
| 1336 | 1334 | const { manager } = yield* makeManager({ |
| 1337 | 1335 | ghScenario: { |
| 1338 | failWith: new GitHubCliError({ | |
| 1336 | failWith: new GitHubCli.GitHubCliError({ | |
| 1339 | 1337 | operation: "execute", |
| 1340 | 1338 | detail: "GitHub CLI (`gh`) is required but not available on PATH.", |
| 1341 | 1339 | }), |
| ... | ... | @@ -2417,7 +2415,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { |
| 2417 | 2415 | |
| 2418 | 2416 | const { manager } = yield* makeManager({ |
| 2419 | 2417 | ghScenario: { |
| 2420 | failWith: new GitHubCliError({ | |
| 2418 | failWith: new GitHubCli.GitHubCliError({ | |
| 2421 | 2419 | operation: "execute", |
| 2422 | 2420 | detail: "GitHub CLI (`gh`) is required but not available on PATH.", |
| 2423 | 2421 | }), |
| ... | ... | @@ -2446,7 +2444,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { |
| 2446 | 2444 | |
| 2447 | 2445 | const { manager } = yield* makeManager({ |
| 2448 | 2446 | ghScenario: { |
| 2449 | failWith: new GitHubCliError({ | |
| 2447 | failWith: new GitHubCli.GitHubCliError({ | |
| 2450 | 2448 | operation: "execute", |
| 2451 | 2449 | detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.", |
| 2452 | 2450 | }), |
| ... | ... | @@ -2702,7 +2700,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { |
| 2702 | 2700 | yield* runGit(repoDir, ["push", "origin", "HEAD:refs/pull/177/head"]); |
| 2703 | 2701 | yield* runGit(repoDir, ["checkout", "main"]); |
| 2704 | 2702 | |
| 2705 | const setupCalls: ProjectSetupScriptRunnerInput[] = []; | |
| 2703 | const setupCalls: ProjectSetupScriptRunner.ProjectSetupScriptRunnerInput[] = []; | |
| 2706 | 2704 | const { manager } = yield* makeManager({ |
| 2707 | 2705 | ghScenario: { |
| 2708 | 2706 | pullRequest: { |
| ... | ... | @@ -2924,7 +2922,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { |
| 2924 | 2922 | const worktreePath = path.join(repoDir, "..", `pr-existing-${path.basename(repoDir)}`); |
| 2925 | 2923 | yield* runGit(repoDir, ["worktree", "add", worktreePath, "feature/pr-existing-worktree"]); |
| 2926 | 2924 | |
| 2927 | const setupCalls: ProjectSetupScriptRunnerInput[] = []; | |
| 2925 | const setupCalls: ProjectSetupScriptRunner.ProjectSetupScriptRunnerInput[] = []; | |
| 2928 | 2926 | const { manager } = yield* makeManager({ |
| 2929 | 2927 | ghScenario: { |
| 2930 | 2928 | pullRequest: { |
| ... | ... | @@ -3164,7 +3162,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { |
| 3164 | 3162 | }, |
| 3165 | 3163 | setupScriptRunner: { |
| 3166 | 3164 | runForThread: () => |
| 3167 | Effect.fail(new ProjectSetupScriptRunnerError({ message: "terminal start failed" })), | |
| 3165 | Effect.fail( | |
| 3166 | new ProjectSetupScriptRunner.ProjectSetupScriptRunnerError({ | |
| 3167 | message: "terminal start failed", | |
| 3168 | }), | |
| 3169 | ), | |
| 3168 | 3170 | }, |
| 3169 | 3171 | }); |
| 3170 | 3172 |
apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts+19-16| ... | ... | @@ -10,16 +10,16 @@ import * as Layer from "effect/Layer"; |
| 10 | 10 | import * as PubSub from "effect/PubSub"; |
| 11 | 11 | import * as Stream from "effect/Stream"; |
| 12 | 12 | |
| 13 | import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; | |
| 14 | import type { CodexAdapterShape } from "../Services/CodexAdapter.ts"; | |
| 15 | import type { CursorAdapterShape } from "../Services/CursorAdapter.ts"; | |
| 16 | import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; | |
| 17 | import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; | |
| 18 | import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts"; | |
| 13 | import type * as ClaudeAdapter from "../Services/ClaudeAdapter.ts"; | |
| 14 | import type * as CodexAdapter from "../Services/CodexAdapter.ts"; | |
| 15 | import type * as CursorAdapter from "../Services/CursorAdapter.ts"; | |
| 16 | import type * as OpenCodeAdapter from "../Services/OpenCodeAdapter.ts"; | |
| 17 | import * as ProviderAdapterRegistry from "../Services/ProviderAdapterRegistry.ts"; | |
| 18 | import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts"; | |
| 19 | 19 | import type { ProviderInstance } from "../ProviderDriver.ts"; |
| 20 | 20 | import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts"; |
| 21 | import type { TextGenerationShape } from "../../textGeneration/TextGeneration.ts"; | |
| 22 | import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts"; | |
| 21 | import type * as TextGeneration from "../../textGeneration/TextGeneration.ts"; | |
| 22 | import * as ProviderAdapterRegistryLayer from "./ProviderAdapterRegistry.ts"; | |
| 23 | 23 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 24 | 24 | |
| 25 | 25 | const CODEX_DRIVER = ProviderDriverKind.make("codex"); |
| ... | ... | @@ -27,7 +27,7 @@ const CLAUDE_AGENT_DRIVER = ProviderDriverKind.make("claudeAgent"); |
| 27 | 27 | const OPENCODE_DRIVER = ProviderDriverKind.make("opencode"); |
| 28 | 28 | const CURSOR_DRIVER = ProviderDriverKind.make("cursor"); |
| 29 | 29 | |
| 30 | const fakeCodexAdapter: CodexAdapterShape = { | |
| 30 | const fakeCodexAdapter: CodexAdapter.CodexAdapterShape = { | |
| 31 | 31 | provider: CODEX_DRIVER, |
| 32 | 32 | capabilities: { sessionModelSwitch: "in-session" }, |
| 33 | 33 | startSession: vi.fn(), |
| ... | ... | @@ -44,7 +44,7 @@ const fakeCodexAdapter: CodexAdapterShape = { |
| 44 | 44 | streamEvents: Stream.empty, |
| 45 | 45 | }; |
| 46 | 46 | |
| 47 | const fakeClaudeAdapter: ClaudeAdapterShape = { | |
| 47 | const fakeClaudeAdapter: ClaudeAdapter.ClaudeAdapterShape = { | |
| 48 | 48 | provider: CLAUDE_AGENT_DRIVER, |
| 49 | 49 | capabilities: { sessionModelSwitch: "in-session" }, |
| 50 | 50 | startSession: vi.fn(), |
| ... | ... | @@ -61,7 +61,7 @@ const fakeClaudeAdapter: ClaudeAdapterShape = { |
| 61 | 61 | streamEvents: Stream.empty, |
| 62 | 62 | }; |
| 63 | 63 | |
| 64 | const fakeOpenCodeAdapter: OpenCodeAdapterShape = { | |
| 64 | const fakeOpenCodeAdapter: OpenCodeAdapter.OpenCodeAdapterShape = { | |
| 65 | 65 | provider: OPENCODE_DRIVER, |
| 66 | 66 | capabilities: { sessionModelSwitch: "in-session" }, |
| 67 | 67 | startSession: vi.fn(), |
| ... | ... | @@ -78,7 +78,7 @@ const fakeOpenCodeAdapter: OpenCodeAdapterShape = { |
| 78 | 78 | streamEvents: Stream.empty, |
| 79 | 79 | }; |
| 80 | 80 | |
| 81 | const fakeCursorAdapter: CursorAdapterShape = { | |
| 81 | const fakeCursorAdapter: CursorAdapter.CursorAdapterShape = { | |
| 82 | 82 | provider: CURSOR_DRIVER, |
| 83 | 83 | capabilities: { sessionModelSwitch: "in-session" }, |
| 84 | 84 | startSession: vi.fn(), |
| ... | ... | @@ -124,7 +124,7 @@ const makeFakeInstance = ( |
| 124 | 124 | streamChanges: Stream.empty, |
| 125 | 125 | }, |
| 126 | 126 | adapter, |
| 127 | textGeneration: {} as unknown as TextGenerationShape, | |
| 127 | textGeneration: {} as unknown as TextGeneration.TextGeneration["Service"], | |
| 128 | 128 | }; |
| 129 | 129 | }; |
| 130 | 130 | |
| ... | ... | @@ -135,7 +135,7 @@ const fakeInstances: ReadonlyArray<ProviderInstance> = [ |
| 135 | 135 | makeFakeInstance("cursor", fakeCursorAdapter), |
| 136 | 136 | ]; |
| 137 | 137 | |
| 138 | const fakeInstanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, { | |
| 138 | const fakeInstanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, { | |
| 139 | 139 | getInstance: (instanceId) => |
| 140 | 140 | Effect.succeed(fakeInstances.find((instance) => instance.instanceId === instanceId)), |
| 141 | 141 | listInstances: Effect.succeed(fakeInstances), |
| ... | ... | @@ -147,14 +147,17 @@ const fakeInstanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, { |
| 147 | 147 | }); |
| 148 | 148 | |
| 149 | 149 | const layer = Layer.mergeAll( |
| 150 | Layer.provide(ProviderAdapterRegistryLive, fakeInstanceRegistryLayer), | |
| 150 | Layer.provide( | |
| 151 | ProviderAdapterRegistryLayer.ProviderAdapterRegistryLive, | |
| 152 | fakeInstanceRegistryLayer, | |
| 153 | ), | |
| 151 | 154 | NodeServices.layer, |
| 152 | 155 | ); |
| 153 | 156 | |
| 154 | 157 | it.layer(layer)("ProviderAdapterRegistryLive", (it) => { |
| 155 | 158 | it("resolves adapters and routing metadata from provider instances", () => |
| 156 | 159 | Effect.gen(function* () { |
| 157 | const registry = yield* ProviderAdapterRegistry; | |
| 160 | const registry = yield* ProviderAdapterRegistry.ProviderAdapterRegistry; | |
| 158 | 161 | const claudeInstanceId = defaultInstanceIdForDriver(CLAUDE_AGENT_DRIVER); |
| 159 | 162 | |
| 160 | 163 | 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"; |
| 30 | 30 | import type * as Schema from "effect/Schema"; |
| 31 | 31 | import type * as Scope from "effect/Scope"; |
| 32 | 32 | |
| 33 | import type { TextGenerationShape } from "../textGeneration/TextGeneration.ts"; | |
| 33 | import type * as TextGeneration from "../textGeneration/TextGeneration.ts"; | |
| 34 | 34 | import type { ProviderAdapterError, ProviderDriverError } from "./Errors.ts"; |
| 35 | 35 | import type { ProviderAdapterShape } from "./Services/ProviderAdapter.ts"; |
| 36 | 36 | import type { ServerProviderShape } from "./Services/ServerProvider.ts"; |
| ... | ... | @@ -70,7 +70,7 @@ export interface ProviderInstance { |
| 70 | 70 | readonly enabled: boolean; |
| 71 | 71 | readonly snapshot: ServerProviderShape; |
| 72 | 72 | readonly adapter: ProviderAdapterShape<ProviderAdapterError>; |
| 73 | readonly textGeneration: TextGenerationShape; | |
| 73 | readonly textGeneration: TextGeneration.TextGeneration["Service"]; | |
| 74 | 74 | } |
| 75 | 75 | |
| 76 | 76 | export interface ProviderContinuationIdentity { |
apps/server/src/review/ReviewService.ts+14-15| ... | ... | @@ -13,22 +13,21 @@ import { |
| 13 | 13 | type ReviewDiffPreviewResult, |
| 14 | 14 | } from "@t3tools/contracts"; |
| 15 | 15 | |
| 16 | import { ServerConfig } from "../config.ts"; | |
| 16 | import * as ServerConfig from "../config.ts"; | |
| 17 | 17 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 18 | 18 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; |
| 19 | 19 | |
| 20 | export interface ReviewServiceShape { | |
| 21 | readonly getDiffPreview: ( | |
| 22 | input: ReviewDiffPreviewInput, | |
| 23 | ) => Effect.Effect<ReviewDiffPreviewResult, ReviewDiffPreviewError>; | |
| 24 | } | |
| 25 | ||
| 26 | export class ReviewService extends Context.Service<ReviewService, ReviewServiceShape>()( | |
| 27 | "t3/review/ReviewService", | |
| 28 | ) {} | |
| 29 | ||
| 30 | export const make = Effect.fn("makeReviewService")(function* () { | |
| 31 | const config = yield* ServerConfig; | |
| 20 | export 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 | ||
| 29 | export const make = Effect.gen(function* () { | |
| 30 | const config = yield* ServerConfig.ServerConfig; | |
| 32 | 31 | const fileSystem = yield* FileSystem.FileSystem; |
| 33 | 32 | const path = yield* Path.Path; |
| 34 | 33 | const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; |
| ... | ... | @@ -62,7 +61,7 @@ export const make = Effect.fn("makeReviewService")(function* () { |
| 62 | 61 | }); |
| 63 | 62 | }); |
| 64 | 63 | |
| 65 | const getDiffPreview: ReviewServiceShape["getDiffPreview"] = Effect.fn( | |
| 64 | const getDiffPreview: ReviewService["Service"]["getDiffPreview"] = Effect.fn( | |
| 66 | 65 | "ReviewService.getDiffPreview", |
| 67 | 66 | )(function* (input) { |
| 68 | 67 | yield* assertWorkspaceBoundCwd(input.cwd); |
| ... | ... | @@ -96,4 +95,4 @@ export const make = Effect.fn("makeReviewService")(function* () { |
| 96 | 95 | }); |
| 97 | 96 | }); |
| 98 | 97 | |
| 99 | export const layer = Layer.effect(ReviewService, make()); | |
| 98 | export 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"; |
| 9 | 9 | import { createModelSelection } from "@t3tools/shared/model"; |
| 10 | 10 | import { expect } from "vite-plus/test"; |
| 11 | 11 | |
| 12 | import { ServerConfig } from "../config.ts"; | |
| 13 | import { type TextGenerationShape } from "./TextGeneration.ts"; | |
| 12 | import * as ServerConfig from "../config.ts"; | |
| 13 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 14 | 14 | import { sanitizeThreadTitle } from "./TextGenerationUtils.ts"; |
| 15 | 15 | import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts"; |
| 16 | 16 | const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); |
| 17 | 17 | |
| 18 | const ClaudeTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), { | |
| 18 | const ClaudeTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { | |
| 19 | 19 | prefix: "t3code-claude-text-generation-test-", |
| 20 | 20 | }).pipe(Layer.provideMerge(NodeServices.layer)); |
| 21 | 21 | |
| ... | ... | @@ -79,7 +79,7 @@ function withFakeClaudeEnv<A, E, R>( |
| 79 | 79 | homeMustBe?: string; |
| 80 | 80 | claudeConfig?: Partial<ClaudeSettings>; |
| 81 | 81 | }, |
| 82 | effectFn: (textGeneration: TextGenerationShape) => Effect.Effect<A, E, R>, | |
| 82 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, | |
| 83 | 83 | ) { |
| 84 | 84 | return Effect.gen(function* () { |
| 85 | 85 | const fs = yield* FileSystem.FileSystem; |
apps/server/src/textGeneration/ClaudeTextGeneration.ts+79-83| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | /** |
| 2 | 2 | * ClaudeTextGeneration – Text generation layer using the Claude CLI. |
| 3 | 3 | * |
| 4 | * Implements the same TextGenerationShape contract as CodexTextGeneration but | |
| 4 | * Implements the same TextGeneration service contract as CodexTextGeneration but | |
| 5 | 5 | * delegates to the `claude` CLI (`claude -p`) with structured JSON output |
| 6 | 6 | * instead of the `codex exec` CLI. |
| 7 | 7 | * |
| ... | ... | @@ -18,7 +18,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar |
| 18 | 18 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 19 | 19 | |
| 20 | 20 | import { TextGenerationError } from "@t3tools/contracts"; |
| 21 | import { type TextGenerationShape } from "./TextGeneration.ts"; | |
| 21 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 22 | 22 | import { |
| 23 | 23 | buildBranchNamePrompt, |
| 24 | 24 | buildCommitMessagePrompt, |
| ... | ... | @@ -260,107 +260,103 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu |
| 260 | 260 | }); |
| 261 | 261 | |
| 262 | 262 | // --------------------------------------------------------------------------- |
| 263 | // TextGenerationShape methods | |
| 263 | // TextGeneration service methods | |
| 264 | 264 | // --------------------------------------------------------------------------- |
| 265 | 265 | |
| 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 | }); | |
| 275 | 282 | |
| 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 | }; | |
| 282 | 290 | }); |
| 283 | 291 | |
| 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 | }); | |
| 292 | 301 | |
| 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 | }); | |
| 303 | 309 | |
| 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 | }; | |
| 310 | 314 | }); |
| 311 | 315 | |
| 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 | }); | |
| 317 | 322 | |
| 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 | }); | |
| 325 | 330 | |
| 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 | }; | |
| 332 | 334 | }); |
| 333 | 335 | |
| 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 | }); | |
| 338 | 342 | |
| 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 | }); | |
| 346 | 350 | |
| 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 | }; | |
| 353 | 354 | }); |
| 354 | 355 | |
| 355 | return { | |
| 356 | title: sanitizeThreadTitle(generated.title), | |
| 357 | }; | |
| 358 | }); | |
| 359 | ||
| 360 | 356 | return { |
| 361 | 357 | generateCommitMessage, |
| 362 | 358 | generatePrContent, |
| 363 | 359 | generateBranchName, |
| 364 | 360 | generateThreadTitle, |
| 365 | } satisfies TextGenerationShape; | |
| 361 | } satisfies TextGeneration.TextGeneration["Service"]; | |
| 366 | 362 | }); |
apps/server/src/textGeneration/CodexTextGeneration.test.ts+7-7| ... | ... | @@ -11,8 +11,8 @@ import { expect } from "vite-plus/test"; |
| 11 | 11 | |
| 12 | 12 | import { CodexSettings, ProviderInstanceId, TextGenerationError } from "@t3tools/contracts"; |
| 13 | 13 | |
| 14 | import { ServerConfig } from "../config.ts"; | |
| 15 | import { type TextGenerationShape } from "./TextGeneration.ts"; | |
| 14 | import * as ServerConfig from "../config.ts"; | |
| 15 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 16 | 16 | import { makeCodexTextGeneration } from "./CodexTextGeneration.ts"; |
| 17 | 17 | const decodeCodexSettings = Schema.decodeSync(CodexSettings); |
| 18 | 18 | |
| ... | ... | @@ -21,7 +21,7 @@ const DEFAULT_TEST_MODEL_SELECTION = createModelSelection( |
| 21 | 21 | "gpt-5.4-mini", |
| 22 | 22 | ); |
| 23 | 23 | |
| 24 | const CodexTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), { | |
| 24 | const CodexTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { | |
| 25 | 25 | prefix: "t3code-codex-text-generation-test-", |
| 26 | 26 | }).pipe(Layer.provideMerge(NodeServices.layer)); |
| 27 | 27 | |
| ... | ... | @@ -169,7 +169,7 @@ function withFakeCodexEnv<A, E, R>( |
| 169 | 169 | stdinMustContain?: string; |
| 170 | 170 | stdinMustNotContain?: string; |
| 171 | 171 | }, |
| 172 | effectFn: (textGeneration: TextGenerationShape) => Effect.Effect<A, E, R>, | |
| 172 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, | |
| 173 | 173 | ) { |
| 174 | 174 | return Effect.gen(function* () { |
| 175 | 175 | const fs = yield* FileSystem.FileSystem; |
| ... | ... | @@ -427,7 +427,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { |
| 427 | 427 | Effect.gen(function* () { |
| 428 | 428 | const fs = yield* FileSystem.FileSystem; |
| 429 | 429 | const path = yield* Path.Path; |
| 430 | const { attachmentsDir } = yield* ServerConfig; | |
| 430 | const { attachmentsDir } = yield* ServerConfig.ServerConfig; | |
| 431 | 431 | const attachmentId = "thread-branch-image-attachment"; |
| 432 | 432 | const attachmentPath = path.join(attachmentsDir, `${attachmentId}.png`); |
| 433 | 433 | yield* fs.makeDirectory(attachmentsDir, { recursive: true }); |
| ... | ... | @@ -465,7 +465,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { |
| 465 | 465 | Effect.gen(function* () { |
| 466 | 466 | const fs = yield* FileSystem.FileSystem; |
| 467 | 467 | const path = yield* Path.Path; |
| 468 | const { attachmentsDir } = yield* ServerConfig; | |
| 468 | const { attachmentsDir } = yield* ServerConfig.ServerConfig; | |
| 469 | 469 | const attachmentId = "thread-1-attachment"; |
| 470 | 470 | const imagePath = path.join(attachmentsDir, `${attachmentId}.png`); |
| 471 | 471 | yield* fs.makeDirectory(attachmentsDir, { recursive: true }); |
| ... | ... | @@ -514,7 +514,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { |
| 514 | 514 | Effect.gen(function* () { |
| 515 | 515 | const fs = yield* FileSystem.FileSystem; |
| 516 | 516 | const path = yield* Path.Path; |
| 517 | const { attachmentsDir } = yield* ServerConfig; | |
| 517 | const { attachmentsDir } = yield* ServerConfig.ServerConfig; | |
| 518 | 518 | const missingAttachmentId = "thread-missing-attachment"; |
| 519 | 519 | const missingPath = path.join(attachmentsDir, `${missingAttachmentId}.png`); |
| 520 | 520 | 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 |
| 12 | 12 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 13 | 13 | |
| 14 | 14 | import { resolveAttachmentPath } from "../attachmentStore.ts"; |
| 15 | import { ServerConfig } from "../config.ts"; | |
| 15 | import * as ServerConfig from "../config.ts"; | |
| 16 | 16 | import { expandHomePath } from "../pathExpansion.ts"; |
| 17 | 17 | import { TextGenerationError } from "@t3tools/contracts"; |
| 18 | import { | |
| 19 | type BranchNameGenerationInput, | |
| 20 | type ThreadTitleGenerationResult, | |
| 21 | type TextGenerationShape, | |
| 22 | } from "./TextGeneration.ts"; | |
| 18 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 23 | 19 | import { |
| 24 | 20 | buildBranchNamePrompt, |
| 25 | 21 | buildCommitMessagePrompt, |
| ... | ... | @@ -50,7 +46,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 50 | 46 | const fileSystem = yield* FileSystem.FileSystem; |
| 51 | 47 | const path = yield* Path.Path; |
| 52 | 48 | const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 53 | const serverConfig = yield* Effect.service(ServerConfig); | |
| 49 | const serverConfig = yield* Effect.service(ServerConfig.ServerConfig); | |
| 54 | 50 | const resolvedEnvironment = environment ?? process.env; |
| 55 | 51 | |
| 56 | 52 | type MaterializedImageAttachments = { |
| ... | ... | @@ -121,7 +117,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 121 | 117 | | "generatePrContent" |
| 122 | 118 | | "generateBranchName" |
| 123 | 119 | | "generateThreadTitle", |
| 124 | attachments: BranchNameGenerationInput["attachments"], | |
| 120 | attachments: TextGeneration.BranchNameGenerationInput["attachments"], | |
| 125 | 121 | ): Effect.fn.Return<MaterializedImageAttachments, TextGenerationError> { |
| 126 | 122 | if (!attachments || attachments.length === 0) { |
| 127 | 123 | return { imagePaths: [] }; |
| ... | ... | @@ -298,114 +294,110 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 298 | 294 | }).pipe(Effect.ensuring(cleanup)); |
| 299 | 295 | }); |
| 300 | 296 | |
| 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 | }); | |
| 310 | 305 | |
| 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 | }; | |
| 317 | 321 | }); |
| 318 | 322 | |
| 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 | }); | |
| 327 | 332 | |
| 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 | }); | |
| 338 | 340 | |
| 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 | }; | |
| 345 | 345 | }); |
| 346 | 346 | |
| 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 | }); | |
| 352 | 357 | |
| 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 | }); | |
| 364 | 366 | |
| 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 | }; | |
| 372 | 370 | }); |
| 373 | 371 | |
| 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 | }); | |
| 378 | 382 | |
| 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 | }); | |
| 390 | 391 | |
| 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; | |
| 398 | 395 | }); |
| 399 | 396 | |
| 400 | return { | |
| 401 | title: sanitizeThreadTitle(generated.title), | |
| 402 | } satisfies ThreadTitleGenerationResult; | |
| 403 | }); | |
| 404 | ||
| 405 | 397 | return { |
| 406 | 398 | generateCommitMessage, |
| 407 | 399 | generatePrContent, |
| 408 | 400 | generateBranchName, |
| 409 | 401 | generateThreadTitle, |
| 410 | } satisfies TextGenerationShape; | |
| 402 | } satisfies TextGeneration.TextGeneration["Service"]; | |
| 411 | 403 | }); |
apps/server/src/textGeneration/CursorTextGeneration.test.ts+4-4| ... | ... | @@ -16,8 +16,8 @@ import { expect } from "vite-plus/test"; |
| 16 | 16 | |
| 17 | 17 | import { CursorSettings, ProviderInstanceId } from "@t3tools/contracts"; |
| 18 | 18 | |
| 19 | import { ServerConfig } from "../config.ts"; | |
| 20 | import { type TextGenerationShape } from "./TextGeneration.ts"; | |
| 19 | import * as ServerConfig from "../config.ts"; | |
| 20 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 21 | 21 | import { makeCursorTextGeneration } from "./CursorTextGeneration.ts"; |
| 22 | 22 | const decodeCursorSettings = Schema.decodeSync(CursorSettings); |
| 23 | 23 | |
| ... | ... | @@ -28,7 +28,7 @@ function shellSingleQuote(value: string): string { |
| 28 | 28 | return `'${value.replaceAll("'", `'"'"'`)}'`; |
| 29 | 29 | } |
| 30 | 30 | |
| 31 | const CursorTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), { | |
| 31 | const CursorTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { | |
| 32 | 32 | prefix: "t3code-cursor-text-generation-test-", |
| 33 | 33 | }).pipe(Layer.provideMerge(NodeServices.layer)); |
| 34 | 34 | |
| ... | ... | @@ -56,7 +56,7 @@ function makeAcpAgentWrapper(dir: string, env: Record<string, string>): string { |
| 56 | 56 | |
| 57 | 57 | function withFakeAcpAgent<A, E, R>( |
| 58 | 58 | env: Record<string, string>, |
| 59 | effectFn: (textGeneration: TextGenerationShape) => Effect.Effect<A, E, R>, | |
| 59 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, | |
| 60 | 60 | ) { |
| 61 | 61 | return Effect.gen(function* () { |
| 62 | 62 | 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 |
| 9 | 9 | import { extractJsonObject } from "@t3tools/shared/schemaJson"; |
| 10 | 10 | |
| 11 | 11 | import { TextGenerationError } from "@t3tools/contracts"; |
| 12 | import { type ThreadTitleGenerationResult, type TextGenerationShape } from "./TextGeneration.ts"; | |
| 12 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 13 | 13 | import { |
| 14 | 14 | buildBranchNamePrompt, |
| 15 | 15 | buildCommitMessagePrompt, |
| ... | ... | @@ -176,104 +176,100 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu |
| 176 | 176 | Effect.scoped, |
| 177 | 177 | ); |
| 178 | 178 | |
| 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 | }); | |
| 188 | 187 | |
| 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 | }; | |
| 195 | 203 | }); |
| 196 | 204 | |
| 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 | }); | |
| 205 | 214 | |
| 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 | }); | |
| 216 | 222 | |
| 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 | }; | |
| 223 | 227 | }); |
| 224 | 228 | |
| 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 | }); | |
| 230 | 235 | |
| 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 | }); | |
| 238 | 243 | |
| 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 | }; | |
| 245 | 247 | }); |
| 246 | 248 | |
| 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 | }); | |
| 251 | 255 | |
| 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 | }); | |
| 259 | 263 | |
| 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; | |
| 266 | 267 | }); |
| 267 | 268 | |
| 268 | return { | |
| 269 | title: sanitizeThreadTitle(generated.title), | |
| 270 | } satisfies ThreadTitleGenerationResult; | |
| 271 | }); | |
| 272 | ||
| 273 | 269 | return { |
| 274 | 270 | generateCommitMessage, |
| 275 | 271 | generatePrContent, |
| 276 | 272 | generateBranchName, |
| 277 | 273 | generateThreadTitle, |
| 278 | } satisfies TextGenerationShape; | |
| 274 | } satisfies TextGeneration.TextGeneration["Service"]; | |
| 279 | 275 | }); |
apps/server/src/textGeneration/GrokTextGeneration.test.ts+4-4| ... | ... | @@ -13,8 +13,8 @@ import { createModelSelection } from "@t3tools/shared/model"; |
| 13 | 13 | import { expect } from "vite-plus/test"; |
| 14 | 14 | import { GrokSettings, ProviderInstanceId } from "@t3tools/contracts"; |
| 15 | 15 | |
| 16 | import { ServerConfig } from "../config.ts"; | |
| 17 | import { type TextGenerationShape } from "./TextGeneration.ts"; | |
| 16 | import * as ServerConfig from "../config.ts"; | |
| 17 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 18 | 18 | import { makeGrokTextGeneration } from "./GrokTextGeneration.ts"; |
| 19 | 19 | const decodeGrokSettings = Schema.decodeSync(GrokSettings); |
| 20 | 20 | |
| ... | ... | @@ -25,7 +25,7 @@ function shellSingleQuote(value: string): string { |
| 25 | 25 | return `'${value.replaceAll("'", `'"'"'`)}'`; |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | const GrokTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), { | |
| 28 | const GrokTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { | |
| 29 | 29 | prefix: "t3code-grok-text-generation-test-", |
| 30 | 30 | }).pipe(Layer.provideMerge(NodeServices.layer)); |
| 31 | 31 | |
| ... | ... | @@ -53,7 +53,7 @@ function makeAcpGrokWrapper(dir: string, env: Record<string, string>): string { |
| 53 | 53 | |
| 54 | 54 | function withFakeAcpGrok<A, E, R>( |
| 55 | 55 | env: Record<string, string>, |
| 56 | effectFn: (textGeneration: TextGenerationShape) => Effect.Effect<A, E, R>, | |
| 56 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, | |
| 57 | 57 | ) { |
| 58 | 58 | return Effect.gen(function* () { |
| 59 | 59 | 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 |
| 10 | 10 | import { extractJsonObject } from "@t3tools/shared/schemaJson"; |
| 11 | 11 | |
| 12 | 12 | import { TextGenerationError } from "@t3tools/contracts"; |
| 13 | import { type ThreadTitleGenerationResult, type TextGenerationShape } from "./TextGeneration.ts"; | |
| 13 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 14 | 14 | import { |
| 15 | 15 | buildBranchNamePrompt, |
| 16 | 16 | buildCommitMessagePrompt, |
| ... | ... | @@ -169,104 +169,100 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi |
| 169 | 169 | Effect.scoped, |
| 170 | 170 | ); |
| 171 | 171 | |
| 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 | }); | |
| 181 | 180 | |
| 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 | }; | |
| 188 | 196 | }); |
| 189 | 197 | |
| 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 | }); | |
| 198 | 207 | |
| 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 | }); | |
| 209 | 215 | |
| 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 | }; | |
| 216 | 220 | }); |
| 217 | 221 | |
| 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 | }); | |
| 223 | 228 | |
| 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 | }); | |
| 231 | 236 | |
| 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 | }; | |
| 238 | 240 | }); |
| 239 | 241 | |
| 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 | }); | |
| 244 | 248 | |
| 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 | }); | |
| 252 | 256 | |
| 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; | |
| 259 | 260 | }); |
| 260 | 261 | |
| 261 | return { | |
| 262 | title: sanitizeThreadTitle(generated.title), | |
| 263 | } satisfies ThreadTitleGenerationResult; | |
| 264 | }); | |
| 265 | ||
| 266 | 262 | return { |
| 267 | 263 | generateCommitMessage, |
| 268 | 264 | generatePrContent, |
| 269 | 265 | generateBranchName, |
| 270 | 266 | generateThreadTitle, |
| 271 | } satisfies TextGenerationShape; | |
| 267 | } satisfies TextGeneration.TextGeneration["Service"]; | |
| 272 | 268 | }); |
apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts+11-15| ... | ... | @@ -9,13 +9,9 @@ import * as TestClock from "effect/testing/TestClock"; |
| 9 | 9 | import * as NetService from "@t3tools/shared/Net"; |
| 10 | 10 | import { beforeEach, expect } from "vite-plus/test"; |
| 11 | 11 | |
| 12 | import { ServerConfig } from "../config.ts"; | |
| 13 | import { | |
| 14 | OpenCodeRuntime, | |
| 15 | OpenCodeRuntimeError, | |
| 16 | type OpenCodeRuntimeShape, | |
| 17 | } from "../provider/opencodeRuntime.ts"; | |
| 18 | import { type TextGenerationShape } from "./TextGeneration.ts"; | |
| 12 | import * as ServerConfig from "../config.ts"; | |
| 13 | import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts"; | |
| 14 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 19 | 15 | import { makeOpenCodeTextGeneration } from "./OpenCodeTextGeneration.ts"; |
| 20 | 16 | |
| 21 | 17 | const runtimeMock = { |
| ... | ... | @@ -37,7 +33,7 @@ const runtimeMock = { |
| 37 | 33 | }, |
| 38 | 34 | }; |
| 39 | 35 | |
| 40 | const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { | |
| 36 | const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { | |
| 41 | 37 | startOpenCodeServerProcess: ({ binaryPath }) => |
| 42 | 38 | Effect.gen(function* () { |
| 43 | 39 | const index = runtimeMock.state.startCalls.length + 1; |
| ... | ... | @@ -88,10 +84,10 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { |
| 88 | 84 | ); |
| 89 | 85 | }, |
| 90 | 86 | }, |
| 91 | }) as unknown as ReturnType<OpenCodeRuntimeShape["createOpenCodeSdkClient"]>, | |
| 87 | }) as unknown as ReturnType<OpenCodeRuntime.OpenCodeRuntimeShape["createOpenCodeSdkClient"]>, | |
| 92 | 88 | loadOpenCodeInventory: () => |
| 93 | 89 | Effect.fail( |
| 94 | new OpenCodeRuntimeError({ | |
| 90 | new OpenCodeRuntime.OpenCodeRuntimeError({ | |
| 95 | 91 | operation: "loadOpenCodeInventory", |
| 96 | 92 | detail: "OpenCodeRuntimeTestDouble.loadOpenCodeInventory not used in this test", |
| 97 | 93 | cause: null, |
| ... | ... | @@ -107,11 +103,11 @@ const DEFAULT_TEST_MODEL_SELECTION = { |
| 107 | 103 | const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000; |
| 108 | 104 | |
| 109 | 105 | const OpenCodeTextGenerationTestLayer = Layer.succeed( |
| 110 | OpenCodeRuntime, | |
| 106 | OpenCodeRuntime.OpenCodeRuntime, | |
| 111 | 107 | OpenCodeRuntimeTestDouble, |
| 112 | 108 | ).pipe( |
| 113 | 109 | Layer.provideMerge( |
| 114 | ServerConfig.layerTest(process.cwd(), { | |
| 110 | ServerConfig.ServerConfig.layerTest(process.cwd(), { | |
| 115 | 111 | prefix: "t3code-opencode-text-generation-test-", |
| 116 | 112 | }), |
| 117 | 113 | ), |
| ... | ... | @@ -120,11 +116,11 @@ const OpenCodeTextGenerationTestLayer = Layer.succeed( |
| 120 | 116 | ); |
| 121 | 117 | |
| 122 | 118 | const OpenCodeTextGenerationExistingServerTestLayer = Layer.succeed( |
| 123 | OpenCodeRuntime, | |
| 119 | OpenCodeRuntime.OpenCodeRuntime, | |
| 124 | 120 | OpenCodeRuntimeTestDouble, |
| 125 | 121 | ).pipe( |
| 126 | 122 | Layer.provideMerge( |
| 127 | ServerConfig.layerTest(process.cwd(), { | |
| 123 | ServerConfig.ServerConfig.layerTest(process.cwd(), { | |
| 128 | 124 | prefix: "t3code-opencode-text-generation-existing-server-test-", |
| 129 | 125 | }), |
| 130 | 126 | ), |
| ... | ... | @@ -143,7 +139,7 @@ const EXISTING_SERVER_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettings)({ |
| 143 | 139 | |
| 144 | 140 | function withOpenCodeTextGeneration<A, E, R>( |
| 145 | 141 | settings: OpenCodeSettings, |
| 146 | effectFn: (textGeneration: TextGenerationShape) => Effect.Effect<A, E, R>, | |
| 142 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, | |
| 147 | 143 | ) { |
| 148 | 144 | return Effect.gen(function* () { |
| 149 | 145 | const textGeneration = yield* makeOpenCodeTextGeneration(settings); |
apps/server/src/textGeneration/OpenCodeTextGeneration.ts+91-102| ... | ... | @@ -15,7 +15,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar |
| 15 | 15 | import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; |
| 16 | 16 | import { extractJsonObject } from "@t3tools/shared/schemaJson"; |
| 17 | 17 | |
| 18 | import { ServerConfig } from "../config.ts"; | |
| 18 | import * as ServerConfig from "../config.ts"; | |
| 19 | 19 | import { resolveAttachmentPath } from "../attachmentStore.ts"; |
| 20 | 20 | import { |
| 21 | 21 | buildBranchNamePrompt, |
| ... | ... | @@ -23,20 +23,13 @@ import { |
| 23 | 23 | buildPrContentPrompt, |
| 24 | 24 | buildThreadTitlePrompt, |
| 25 | 25 | } from "./TextGenerationPrompts.ts"; |
| 26 | import { type TextGenerationShape } from "./TextGeneration.ts"; | |
| 26 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 27 | 27 | import { |
| 28 | 28 | sanitizeCommitSubject, |
| 29 | 29 | sanitizePrTitle, |
| 30 | 30 | sanitizeThreadTitle, |
| 31 | 31 | } from "./TextGenerationUtils.ts"; |
| 32 | import { | |
| 33 | OpenCodeRuntime, | |
| 34 | type OpenCodeServerConnection, | |
| 35 | type OpenCodeServerProcess, | |
| 36 | openCodeRuntimeErrorDetail, | |
| 37 | parseOpenCodeModelSlug, | |
| 38 | toOpenCodeFileParts, | |
| 39 | } from "../provider/opencodeRuntime.ts"; | |
| 32 | import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts"; | |
| 40 | 33 | |
| 41 | 34 | const OPENCODE_TEXT_GENERATION_IDLE_TTL = "30 seconds"; |
| 42 | 35 | |
| ... | ... | @@ -84,7 +77,7 @@ function getOpenCodeTextResponse(parts: ReadonlyArray<unknown> | undefined): str |
| 84 | 77 | } |
| 85 | 78 | |
| 86 | 79 | interface SharedOpenCodeTextGenerationServerState { |
| 87 | server: OpenCodeServerProcess | null; | |
| 80 | server: OpenCodeRuntime.OpenCodeServerProcess | null; | |
| 88 | 81 | /** |
| 89 | 82 | * The scope that owns the shared server's lifetime. Closing this scope |
| 90 | 83 | * terminates the OpenCode child process and interrupts any fibers the |
| ... | ... | @@ -101,8 +94,8 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" |
| 101 | 94 | openCodeSettings: OpenCodeSettings, |
| 102 | 95 | environment?: NodeJS.ProcessEnv, |
| 103 | 96 | ) { |
| 104 | const serverConfig = yield* ServerConfig; | |
| 105 | const openCodeRuntime = yield* OpenCodeRuntime; | |
| 97 | const serverConfig = yield* ServerConfig.ServerConfig; | |
| 98 | const openCodeRuntime = yield* OpenCodeRuntime.OpenCodeRuntime; | |
| 106 | 99 | const resolvedEnvironment = environment ?? process.env; |
| 107 | 100 | const idleFiberScope = yield* Effect.acquireRelease(Scope.make(), (scope) => |
| 108 | 101 | Scope.close(scope, Exit.void), |
| ... | ... | @@ -135,7 +128,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" |
| 135 | 128 | }); |
| 136 | 129 | |
| 137 | 130 | const scheduleIdleClose = Effect.fn("scheduleIdleClose")(function* ( |
| 138 | server: OpenCodeServerProcess, | |
| 131 | server: OpenCodeRuntime.OpenCodeServerProcess, | |
| 139 | 132 | ) { |
| 140 | 133 | yield* cancelIdleCloseFiber(); |
| 141 | 134 | const fiber = yield* Effect.sleep(OPENCODE_TEXT_GENERATION_IDLE_TTL).pipe( |
| ... | ... | @@ -217,7 +210,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" |
| 217 | 210 | (cause) => |
| 218 | 211 | new TextGenerationError({ |
| 219 | 212 | operation: input.operation, |
| 220 | detail: openCodeRuntimeErrorDetail(cause), | |
| 213 | detail: OpenCodeRuntime.openCodeRuntimeErrorDetail(cause), | |
| 221 | 214 | cause, |
| 222 | 215 | }), |
| 223 | 216 | ), |
| ... | ... | @@ -240,7 +233,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" |
| 240 | 233 | }), |
| 241 | 234 | ); |
| 242 | 235 | |
| 243 | const releaseSharedServer = (server: OpenCodeServerProcess) => | |
| 236 | const releaseSharedServer = (server: OpenCodeRuntime.OpenCodeServerProcess) => | |
| 244 | 237 | sharedServerMutex.withPermit( |
| 245 | 238 | Effect.gen(function* () { |
| 246 | 239 | if (sharedServerState.server !== server) { |
| ... | ... | @@ -278,7 +271,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" |
| 278 | 271 | readonly modelSelection: ModelSelection; |
| 279 | 272 | readonly attachments?: ReadonlyArray<ChatAttachment> | undefined; |
| 280 | 273 | }) { |
| 281 | const parsedModel = parseOpenCodeModelSlug(input.modelSelection.model); | |
| 274 | const parsedModel = OpenCodeRuntime.parseOpenCodeModelSlug(input.modelSelection.model); | |
| 282 | 275 | if (!parsedModel) { |
| 283 | 276 | return yield* new TextGenerationError({ |
| 284 | 277 | operation: input.operation, |
| ... | ... | @@ -286,13 +279,13 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" |
| 286 | 279 | }); |
| 287 | 280 | } |
| 288 | 281 | |
| 289 | const fileParts = toOpenCodeFileParts({ | |
| 282 | const fileParts = OpenCodeRuntime.toOpenCodeFileParts({ | |
| 290 | 283 | attachments: input.attachments, |
| 291 | 284 | resolveAttachmentPath: (attachment) => |
| 292 | 285 | resolveAttachmentPath({ attachmentsDir: serverConfig.attachmentsDir, attachment }), |
| 293 | 286 | }); |
| 294 | 287 | |
| 295 | const runAgainstServer = (server: Pick<OpenCodeServerConnection, "url">) => | |
| 288 | const runAgainstServer = (server: Pick<OpenCodeRuntime.OpenCodeServerConnection, "url">) => | |
| 296 | 289 | Effect.tryPromise({ |
| 297 | 290 | try: async () => { |
| 298 | 291 | const client = openCodeRuntime.createOpenCodeSdkClient({ |
| ... | ... | @@ -336,7 +329,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" |
| 336 | 329 | catch: (cause) => |
| 337 | 330 | new TextGenerationError({ |
| 338 | 331 | operation: input.operation, |
| 339 | detail: openCodeRuntimeErrorDetail(cause), | |
| 332 | detail: OpenCodeRuntime.openCodeRuntimeErrorDetail(cause), | |
| 340 | 333 | cause, |
| 341 | 334 | }), |
| 342 | 335 | }); |
| ... | ... | @@ -367,102 +360,98 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" |
| 367 | 360 | ); |
| 368 | 361 | }); |
| 369 | 362 | |
| 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 | }; | |
| 385 | 386 | }); |
| 386 | 387 | |
| 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 | }); | |
| 395 | 404 | |
| 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 | }; | |
| 412 | 409 | }); |
| 413 | 410 | |
| 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 | }); | |
| 419 | 425 | |
| 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 | }; | |
| 434 | 429 | }); |
| 435 | 430 | |
| 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 | }); | |
| 440 | 445 | |
| 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 | }; | |
| 455 | 449 | }); |
| 456 | 450 | |
| 457 | return { | |
| 458 | title: sanitizeThreadTitle(generated.title), | |
| 459 | }; | |
| 460 | }); | |
| 461 | ||
| 462 | 451 | return { |
| 463 | 452 | generateCommitMessage, |
| 464 | 453 | generatePrContent, |
| 465 | 454 | generateBranchName, |
| 466 | 455 | generateThreadTitle, |
| 467 | } satisfies TextGenerationShape; | |
| 456 | } satisfies TextGeneration.TextGeneration["Service"]; | |
| 468 | 457 | }); |
apps/server/src/textGeneration/TextGeneration.test.ts+17-16| ... | ... | @@ -9,23 +9,24 @@ import { ProviderInstanceId } from "@t3tools/contracts"; |
| 9 | 9 | import { createModelSelection } from "@t3tools/shared/model"; |
| 10 | 10 | |
| 11 | 11 | import type { ProviderInstance } from "../provider/ProviderDriver.ts"; |
| 12 | import type { ProviderInstanceRegistryShape } from "../provider/Services/ProviderInstanceRegistry.ts"; | |
| 13 | import type { TextGenerationShape } from "./TextGeneration.ts"; | |
| 12 | import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts"; | |
| 13 | import * as TextGeneration from "./TextGeneration.ts"; | |
| 14 | 14 | |
| 15 | import { makeTextGenerationFromRegistry } from "./TextGeneration.ts"; | |
| 16 | ||
| 17 | const 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 | }); | |
| 15 | const 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 | }); | |
| 25 | 26 | |
| 26 | 27 | const makeStubInstance = ( |
| 27 | 28 | instanceId: ProviderInstanceId, |
| 28 | textGeneration: TextGenerationShape, | |
| 29 | textGeneration: TextGeneration.TextGeneration["Service"], | |
| 29 | 30 | ): ProviderInstance => |
| 30 | 31 | ({ |
| 31 | 32 | instanceId, |
| ... | ... | @@ -43,7 +44,7 @@ const makeStubInstance = ( |
| 43 | 44 | |
| 44 | 45 | const makeStubRegistry = ( |
| 45 | 46 | instances: ReadonlyArray<ProviderInstance>, |
| 46 | ): ProviderInstanceRegistryShape => { | |
| 47 | ): ProviderInstanceRegistry.ProviderInstanceRegistry["Service"] => { | |
| 47 | 48 | const byId = new Map(instances.map((instance) => [instance.instanceId, instance] as const)); |
| 48 | 49 | return { |
| 49 | 50 | getInstance: (id) => Effect.succeed(byId.get(id)), |
| ... | ... | @@ -81,7 +82,7 @@ describe("makeTextGenerationFromRegistry", () => { |
| 81 | 82 | }), |
| 82 | 83 | ); |
| 83 | 84 | |
| 84 | const tg = makeTextGenerationFromRegistry(makeStubRegistry([personal, work])); | |
| 85 | const tg = TextGeneration.makeTextGenerationFromRegistry(makeStubRegistry([personal, work])); | |
| 85 | 86 | |
| 86 | 87 | const result = yield* tg.generateBranchName({ |
| 87 | 88 | cwd: process.cwd(), |
| ... | ... | @@ -96,7 +97,7 @@ describe("makeTextGenerationFromRegistry", () => { |
| 96 | 97 | |
| 97 | 98 | it.effect("fails with TextGenerationError when the instance is unknown", () => |
| 98 | 99 | Effect.gen(function* () { |
| 99 | const tg = makeTextGenerationFromRegistry(makeStubRegistry([])); | |
| 100 | const tg = TextGeneration.makeTextGenerationFromRegistry(makeStubRegistry([])); | |
| 100 | 101 | |
| 101 | 102 | const result = yield* tg |
| 102 | 103 | .generateBranchName({ |
apps/server/src/textGeneration/TextGeneration.ts+62-66| ... | ... | @@ -4,10 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | 4 | import type { ChatAttachment, ModelSelection, ProviderInstanceId } from "@t3tools/contracts"; |
| 5 | 5 | import { TextGenerationError } from "@t3tools/contracts"; |
| 6 | 6 | |
| 7 | import { | |
| 8 | ProviderInstanceRegistry, | |
| 9 | type ProviderInstanceRegistryShape, | |
| 10 | } from "../provider/Services/ProviderInstanceRegistry.ts"; | |
| 7 | import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts"; | |
| 11 | 8 | import type { ProviderInstance } from "../provider/ProviderDriver.ts"; |
| 12 | 9 | |
| 13 | 10 | export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "grok" | "opencode"; |
| ... | ... | @@ -79,45 +76,44 @@ export interface TextGenerationService { |
| 79 | 76 | generateThreadTitle(input: ThreadTitleGenerationInput): Promise<ThreadTitleGenerationResult>; |
| 80 | 77 | } |
| 81 | 78 | |
| 82 | /** | |
| 83 | * TextGenerationShape - Service API for commit/PR text generation. | |
| 84 | */ | |
| 85 | export 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 | ||
| 115 | 79 | /** |
| 116 | 80 | * TextGeneration - Service tag for commit and PR text generation. |
| 117 | 81 | */ |
| 118 | export class TextGeneration extends Context.Service<TextGeneration, TextGenerationShape>()( | |
| 119 | "t3/textGeneration/TextGeneration", | |
| 120 | ) {} | |
| 82 | export 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"]`. */ | |
| 116 | export type TextGenerationShape = TextGeneration["Service"]; | |
| 121 | 117 | |
| 122 | 118 | type TextGenerationOp = |
| 123 | 119 | | "generateCommitMessage" |
| ... | ... | @@ -126,7 +122,7 @@ type TextGenerationOp = |
| 126 | 122 | | "generateThreadTitle"; |
| 127 | 123 | |
| 128 | 124 | const resolveInstance = ( |
| 129 | registry: ProviderInstanceRegistryShape, | |
| 125 | registry: ProviderInstanceRegistry.ProviderInstanceRegistry["Service"], | |
| 130 | 126 | operation: TextGenerationOp, |
| 131 | 127 | instanceId: ProviderInstanceId, |
| 132 | 128 | ): Effect.Effect<ProviderInstance["textGeneration"], TextGenerationError> => |
| ... | ... | @@ -144,30 +140,30 @@ const resolveInstance = ( |
| 144 | 140 | ); |
| 145 | 141 | |
| 146 | 142 | export 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 | ||
| 164 | export const make = Effect.gen(function* () { | |
| 165 | const registry = yield* ProviderInstanceRegistry.ProviderInstanceRegistry; | |
| 166 | return makeTextGenerationFromRegistry(registry); | |
| 165 | 167 | }); |
| 166 | 168 | |
| 167 | export const layer = Layer.effect( | |
| 168 | TextGeneration, | |
| 169 | Effect.gen(function* () { | |
| 170 | const registry = yield* ProviderInstanceRegistry; | |
| 171 | return makeTextGenerationFromRegistry(registry); | |
| 172 | }), | |
| 173 | ); | |
| 169 | export const layer = Layer.effect(TextGeneration, make); |