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