Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
GitActionProgressEvent,
GitPreparePullRequestThreadInput,
ModelSelection,
SourceControlProviderInfo,
ThreadId,
} from "@t3tools/contracts";

Expand Down Expand Up @@ -649,6 +650,8 @@ function preparePullRequestThread(

function makeManager(input?: {
ghScenario?: FakeGhScenario;
sourceControlProviderContext?: SourceControlProviderRegistry.SourceControlProviderHandle["context"];
sourceControlProviderContextSource?: SourceControlProviderRegistry.SourceControlProviderHandle["contextSource"];
textGeneration?: Partial<FakeGitTextGeneration>;
setupScriptRunner?: ProjectSetupScriptRunnerShape;
}) {
Expand All @@ -671,7 +674,12 @@ function makeManager(input?: {
Effect.map((provider) =>
SourceControlProviderRegistry.SourceControlProviderRegistry.of({
get: () => Effect.succeed(provider),
resolveHandle: () => Effect.succeed({ provider, context: null }),
resolveHandle: () =>
Effect.succeed({
provider,
context: input?.sourceControlProviderContext ?? null,
contextSource: input?.sourceControlProviderContextSource ?? null,
}),
resolve: () => Effect.succeed(provider),
discover: Effect.succeed([]),
}),
Expand Down Expand Up @@ -706,6 +714,18 @@ const GitManagerTestLayer = GitVcsDriver.layer.pipe(
Layer.provideMerge(NodeServices.layer),
);

const githubProvider = {
kind: "github",
name: "GitHub",
baseUrl: "https://github.com",
} satisfies SourceControlProviderInfo;

const gitlabProvider = {
kind: "gitlab",
name: "GitLab",
baseUrl: "https://gitlab.com",
} satisfies SourceControlProviderInfo;

it.layer(GitManagerTestLayer)("GitManager", (it) => {
it.effect("status includes PR metadata when branch already has an open PR", () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -749,6 +769,30 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
}),
);

it.effect("status prefers branch remote over detected provider context", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
yield* runGit(repoDir, ["remote", "add", "origin", "git@gitlab.com:pingdotgg/t3code.git"]);
yield* runGit(repoDir, ["remote", "add", "upstream", "git@github.com:pingdotgg/t3code.git"]);
yield* runGit(repoDir, ["checkout", "-b", "branch-remote"]);
yield* runGit(repoDir, ["config", "branch.branch-remote.remote", "upstream"]);

const { manager } = yield* makeManager({
sourceControlProviderContext: {
provider: gitlabProvider,
remoteName: "origin",
remoteUrl: "git@gitlab.com:pingdotgg/t3code.git",
},
sourceControlProviderContextSource: "detected",
});

const status = yield* manager.localStatus({ cwd: repoDir });

expect(status.sourceControlProvider).toEqual(githubProvider);
}),
);

it.effect("status trims PR metadata returned by gh before publishing it", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
Expand Down
21 changes: 20 additions & 1 deletion apps/server/src/git/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,18 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
cwd: string,
branch: string | null,
) {
const providerHandle = yield* sourceControlProviders.resolveHandle({ cwd }).pipe(
Effect.catch(() =>
Effect.succeed({
context: null,
contextSource: null,
}),
),
);
if (providerHandle.contextSource === "override" && providerHandle.context) {
return providerHandle.context.provider;
}
Comment thread
shivamhwp marked this conversation as resolved.

const preferredRemoteName =
branch === null
? "origin"
Expand All @@ -796,7 +808,14 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
(yield* readConfigValueNullable(cwd, `remote.${preferredRemoteName}.url`)) ??
(yield* readConfigValueNullable(cwd, "remote.origin.url"));

return remoteUrl ? detectSourceControlProviderFromGitRemoteUrl(remoteUrl) : null;
const providerFromBranchRemote = remoteUrl
? detectSourceControlProviderFromGitRemoteUrl(remoteUrl)
: null;
if (providerFromBranchRemote) {
return providerFromBranchRemote;
}

return providerHandle.context?.provider ?? null;
});

const resolveRemoteRepositoryContext = Effect.fn("resolveRemoteRepositoryContext")(function* (
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/project/Layers/ProjectSetupScriptRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Option from "effect/Option";
import { describe, expect, it, vi } from "vitest";

import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { TerminalManager } from "../../terminal/Services/Manager.ts";
import { ProjectSetupScriptRunner } from "../Services/ProjectSetupScriptRunner.ts";
import { ProjectSetupScriptRunnerLive } from "./ProjectSetupScriptRunner.ts";
Expand Down Expand Up @@ -50,6 +51,7 @@ describe("ProjectSetupScriptRunner", () => {
Effect.provide(
ProjectSetupScriptRunnerLive.pipe(
Layer.provideMerge(makeProjectionSnapshotQueryLayer(project)),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(
Layer.succeed(TerminalManager, {
open,
Expand Down Expand Up @@ -109,6 +111,20 @@ describe("ProjectSetupScriptRunner", () => {
Effect.provide(
ProjectSetupScriptRunnerLive.pipe(
Layer.provideMerge(makeProjectionSnapshotQueryLayer(project)),
Layer.provideMerge(
ServerSettingsService.layerTest({
projectSettings: {
[project.id]: {
remoteOverride: null,
automaticGitFetchInterval: null,
actionEnvironment: {
API_BASE_URL: "https://api.example.test",
},
disabledProviderInstanceIds: [],
},
},
}),
),
Layer.provideMerge(
Layer.succeed(TerminalManager, {
open,
Expand Down Expand Up @@ -146,6 +162,7 @@ describe("ProjectSetupScriptRunner", () => {
cwd: "/repo/worktrees/a",
worktreePath: "/repo/worktrees/a",
env: {
API_BASE_URL: "https://api.example.test",
T3CODE_PROJECT_ROOT: "/repo/project",
T3CODE_WORKTREE_PATH: "/repo/worktrees/a",
},
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/project/Layers/ProjectSetupScriptRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Layer from "effect/Layer";
import * as Option from "effect/Option";

import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { TerminalManager } from "../../terminal/Services/Manager.ts";
import {
type ProjectSetupScriptRunnerShape,
Expand All @@ -14,6 +15,7 @@ import {

const makeProjectSetupScriptRunner = Effect.gen(function* () {
const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
const serverSettings = yield* ServerSettingsService;
const terminalManager = yield* TerminalManager;

const runForThread: ProjectSetupScriptRunnerShape["runForThread"] = (input) =>
Expand Down Expand Up @@ -46,9 +48,12 @@ const makeProjectSetupScriptRunner = Effect.gen(function* () {

const terminalId = input.preferredTerminalId ?? `setup-${script.id}`;
const cwd = input.worktreePath;
const settings = yield* serverSettings.getSettings;
const actionEnvironment = settings.projectSettings[project.id]?.actionEnvironment ?? {};
const env = projectScriptRuntimeEnv({
project: { cwd: project.workspaceRoot },
worktreePath: input.worktreePath,
extraEnv: actionEnvironment,
});

yield* terminalManager.open({
Expand Down
47 changes: 38 additions & 9 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type ServerProvider,
type ServerProviderSlashCommand,
type ServerSettings as ContractServerSettings,
type ServerSettingsPatch,
} from "@t3tools/contracts";
import * as PlatformError from "effect/PlatformError";
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
Expand Down Expand Up @@ -252,19 +253,47 @@ function makeMutableServerSettingsService(
const settingsRef = yield* Ref.make(initial);
const changes = yield* PubSub.unbounded<ContractServerSettings>();

const commitSettings = (makePatch: (current: ContractServerSettings) => ServerSettingsPatch) =>
Effect.gen(function* () {
const current = yield* Ref.get(settingsRef);
const next = decodeServerSettings(
encodeServerSettings(applyServerSettingsPatch(current, makePatch(current))),
);
yield* Ref.set(settingsRef, next);
yield* PubSub.publish(changes, next);
return next;
});
Comment thread
cursor[bot] marked this conversation as resolved.

return {
start: Effect.void,
ready: Effect.void,
getSettings: Ref.get(settingsRef),
updateSettings: (patch) =>
Effect.gen(function* () {
const current = yield* Ref.get(settingsRef);
const next = applyServerSettingsPatch(current, patch);
encodeServerSettings(next);
yield* Ref.set(settingsRef, next);
yield* PubSub.publish(changes, next);
return next;
}),
updateSettings: (patch) => commitSettings(() => patch),
updateProjectSettings: (projectId, patch) =>
commitSettings((settings) => ({
projectSettings: {
...settings.projectSettings,
[projectId]: {
...(settings.projectSettings[projectId] ?? {
remoteOverride: null,
automaticGitFetchInterval: null,
actionEnvironment: {},
disabledProviderInstanceIds: [],
}),
...patch,
},
},
})).pipe(
Effect.map(
(settings) =>
settings.projectSettings[projectId] ?? {
remoteOverride: null,
automaticGitFetchInterval: null,
actionEnvironment: {},
disabledProviderInstanceIds: [],
},
),
),
get streamChanges() {
return Stream.fromPubSub(changes);
},
Expand Down
7 changes: 7 additions & 0 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,13 @@ const buildAppUnderTest = (options?: {
ready: Effect.void,
getSettings: Effect.succeed(DEFAULT_SERVER_SETTINGS),
updateSettings: () => Effect.succeed(DEFAULT_SERVER_SETTINGS),
updateProjectSettings: () =>
Effect.succeed({
remoteOverride: null,
automaticGitFetchInterval: null,
actionEnvironment: {},
disabledProviderInstanceIds: [],
}),
streamChanges: Stream.empty,
...options?.layers?.serverSettings,
}),
Expand Down
8 changes: 3 additions & 5 deletions apps/server/src/serverRuntimeStartup.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { DEFAULT_MODEL, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
import { ProjectId, ThreadId } from "@t3tools/contracts";
import { createDefaultModelSelection } from "@t3tools/shared/model";
import { assert, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
Expand All @@ -25,10 +26,7 @@ import {
} from "./serverRuntimeStartup.ts";

it("uses the canonical Codex default for auto-bootstrapped model selection", () => {
assert.deepStrictEqual(getAutoBootstrapDefaultModelSelection(), {
instanceId: ProviderInstanceId.make("codex"),
model: DEFAULT_MODEL,
});
assert.deepStrictEqual(getAutoBootstrapDefaultModelSelection(), createDefaultModelSelection());
});

it.effect("enqueueCommand waits for readiness and then drains queued work", () =>
Expand Down
9 changes: 3 additions & 6 deletions apps/server/src/serverRuntimeStartup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {
CommandId,
DEFAULT_MODEL,
DEFAULT_PROVIDER_INTERACTION_MODE,
type ModelSelection,
ProjectId,
ProviderInstanceId,
ThreadId,
} from "@t3tools/contracts";
import { createDefaultModelSelection } from "@t3tools/shared/model";
import * as Data from "effect/Data";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -153,10 +152,8 @@ export const launchStartupHeartbeat = recordStartupHeartbeat.pipe(
Effect.asVoid,
);

export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({
instanceId: ProviderInstanceId.make("codex"),
model: DEFAULT_MODEL,
});
export const getAutoBootstrapDefaultModelSelection = (): ModelSelection =>
createDefaultModelSelection();

export const resolveWelcomeBase = Effect.gen(function* () {
const serverConfig = yield* ServerConfig;
Expand Down
25 changes: 25 additions & 0 deletions apps/server/src/serverSettings.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import {
DEFAULT_SERVER_SETTINGS,
ProjectId,
ProviderDriverKind,
ProviderInstanceId,
ServerSettings,
Expand Down Expand Up @@ -141,6 +142,30 @@ it.layer(NodeServices.layer)("server settings", (it) => {
}).pipe(Effect.provide(makeServerSettingsLayer())),
);

it.effect("updates project settings from the latest persisted snapshot", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
const firstProjectId = ProjectId.make("project-1");
const secondProjectId = ProjectId.make("project-2");

yield* Effect.all(
[
serverSettings.updateProjectSettings(firstProjectId, {
actionEnvironment: { FIRST: "1" },
}),
serverSettings.updateProjectSettings(secondProjectId, {
actionEnvironment: { SECOND: "2" },
}),
],
{ concurrency: "unbounded" },
);

const next = yield* serverSettings.getSettings;
assert.deepEqual(next.projectSettings[firstProjectId]?.actionEnvironment, { FIRST: "1" });
assert.deepEqual(next.projectSettings[secondProjectId]?.actionEnvironment, { SECOND: "2" });
}).pipe(Effect.provide(makeServerSettingsLayer())),
);

it.effect("preserves model when switching providers via textGenerationModelSelection", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
Expand Down
Loading
Loading