Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a8204e1
feat(orchestration): plumb text generation model through turn starts
maria-rcks Mar 24, 2026
fc6e443
feat(server): generate first-turn thread titles
maria-rcks Mar 24, 2026
22cd903
test(server): fix thread title expectation
maria-rcks Mar 24, 2026
84d83c8
fix(threads): address review follow-ups
maria-rcks Mar 24, 2026
afd3ab5
fix(server): handle blank normalized thread titles
maria-rcks Mar 24, 2026
e3fd816
fix(server): trim thread titles after quote removal
maria-rcks Mar 25, 2026
b2f39a2
refactor(web): centralize text generation model selection
maria-rcks Mar 25, 2026
942b736
chore(rebase): resolve main conflicts
maria-rcks Mar 26, 2026
a7a8fa5
test(server): fix branch naming model assertion
maria-rcks Mar 26, 2026
69dfef8
chore(rebase): refresh main conflict resolution
maria-rcks Mar 26, 2026
618bccb
fix(server): catch title generation settings failures
maria-rcks Mar 26, 2026
1fdb983
fix(web): only send non-default provider options
maria-rcks Mar 26, 2026
3494af0
style(web): format provider options condition
maria-rcks Mar 26, 2026
4ac6114
refactor(server): route thread titles through prompt backends
maria-rcks Mar 28, 2026
32d768d
refactor(orchestration): drop unused provider start options
maria-rcks Mar 28, 2026
451f55b
fix(orchestration): preserve custom first-turn thread titles
maria-rcks Mar 28, 2026
359ce3e
refactor(shared): share truncate title utility
maria-rcks Mar 28, 2026
81c3c98
fix(server): close duplicated bootstrap fd fallback
maria-rcks Mar 28, 2026
cb2929e
fix(threads): preserve auto-title matching after prompt formatting
maria-rcks Mar 28, 2026
5c0dd5d
fix(ci): align typecheck with pinned effect betas
maria-rcks Mar 28, 2026
4cfbe74
fix(threads): declare title seed in title generation input
maria-rcks Mar 28, 2026
c3f8be4
fix(marketing): restore layout description copy
maria-rcks Mar 28, 2026
9af0a8e
refactor(threads): remove unrelated client plumbing
maria-rcks Mar 28, 2026
f366028
refactor(threads): remove unrelated changes
maria-rcks Mar 28, 2026
f8fd462
refactor(shared): rename truncate helper
maria-rcks Mar 28, 2026
83105bd
test(server): use layer mock for text generation
maria-rcks Mar 28, 2026
4e07295
refactor(threads): simplify title seed matching
maria-rcks Mar 28, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,8 @@ export const makeOrchestrationIntegrationHarness = (
Effect.succeed({ branch: input.newBranch }),
} as unknown as GitCoreShape);
const textGenerationLayer = Layer.succeed(TextGeneration, {
generateBranchName: () => Effect.succeed({ branch: null }),
generateBranchName: () => Effect.succeed({ branch: "update" }),
generateThreadTitle: () => Effect.succeed({ title: "New thread" }),
} as unknown as TextGenerationShape);
const providerCommandReactorLayer = ProviderCommandReactorLive.pipe(
Layer.provideMerge(runtimeServicesLayer),
Expand Down
59 changes: 59 additions & 0 deletions apps/server/src/git/Layers/ClaudeTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { expect } from "vitest";

import { ServerConfig } from "../../config.ts";
import { TextGeneration } from "../Services/TextGeneration.ts";
import { sanitizeThreadTitle } from "../Utils.ts";
import { ClaudeTextGenerationLive } from "./ClaudeTextGeneration.ts";
import { ServerSettingsService } from "../../serverSettings.ts";

Expand Down Expand Up @@ -247,4 +248,62 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGenerationLive", (it) => {
}),
),
);

it.effect("generates thread titles through the Claude provider", () =>
withFakeClaudeEnv(
{
output: JSON.stringify({
structured_output: {
title:
' "Reconnect failures after restart because the session state does not recover" ',
},
}),
stdinMustContain: "You write concise thread titles for coding conversations.",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateThreadTitle({
cwd: process.cwd(),
message: "Please investigate reconnect failures after restarting the session.",
modelSelection: {
provider: "claudeAgent",
model: "claude-sonnet-4-6",
},
});

expect(generated.title).toBe(
sanitizeThreadTitle(
'"Reconnect failures after restart because the session state does not recover"',
),
);
}),
),
);

it.effect("falls back when Claude thread title normalization becomes whitespace-only", () =>
withFakeClaudeEnv(
{
output: JSON.stringify({
structured_output: {
title: ' """ """ ',
},
}),
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateThreadTitle({
cwd: process.cwd(),
message: "Name this thread.",
modelSelection: {
provider: "claudeAgent",
model: "claude-sonnet-4-6",
},
});

expect(generated.title).toBe("New thread");
}),
),
);
});
37 changes: 36 additions & 1 deletion apps/server/src/git/Layers/ClaudeTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import {
buildBranchNamePrompt,
buildCommitMessagePrompt,
buildPrContentPrompt,
buildThreadTitlePrompt,
} from "../Prompts.ts";
import {
normalizeCliError,
sanitizeCommitSubject,
sanitizePrTitle,
sanitizeThreadTitle,
toJsonSchemaObject,
} from "../Utils.ts";
import { normalizeClaudeModelOptions } from "../../provider/Layers/ClaudeProvider.ts";
Expand Down Expand Up @@ -70,7 +72,11 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
outputSchemaJson,
modelSelection,
}: {
operation: "generateCommitMessage" | "generatePrContent" | "generateBranchName";
operation:
| "generateCommitMessage"
| "generatePrContent"
| "generateBranchName"
| "generateThreadTitle";
cwd: string;
prompt: string;
outputSchemaJson: S;
Expand Down Expand Up @@ -299,10 +305,39 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
};
});

const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn(
"ClaudeTextGeneration.generateThreadTitle",
)(function* (input) {
const { prompt, outputSchema } = buildThreadTitlePrompt({
message: input.message,
attachments: input.attachments,
});

if (input.modelSelection.provider !== "claudeAgent") {
return yield* new TextGenerationError({
operation: "generateThreadTitle",
detail: "Invalid model selection.",
});
}

const generated = yield* runClaudeJson({
operation: "generateThreadTitle",
cwd: input.cwd,
prompt,
outputSchemaJson: outputSchema,
modelSelection: input.modelSelection,
});

return {
title: sanitizeThreadTitle(generated.title),
};
});

return {
generateCommitMessage,
generatePrContent,
generateBranchName,
generateThreadTitle,
} satisfies TextGenerationShape;
});

Expand Down
64 changes: 64 additions & 0 deletions apps/server/src/git/Layers/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,70 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => {
),
);

it.effect("generates thread titles and trims them for sidebar use", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
title:
' "Investigate websocket reconnect regressions after worktree restore" \nignored line',
}),
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateThreadTitle({
cwd: process.cwd(),
message: "Please investigate websocket reconnect regressions after a worktree restore.",
modelSelection: DEFAULT_TEST_MODEL_SELECTION,
});

expect(generated.title).toBe("Investigate websocket reconnect regressions aft...");
}),
),
);

it.effect("falls back when thread title normalization becomes whitespace-only", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
title: ' """ """ ',
}),
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateThreadTitle({
cwd: process.cwd(),
message: "Name this thread.",
modelSelection: DEFAULT_TEST_MODEL_SELECTION,
});

expect(generated.title).toBe("New thread");
}),
),
);

it.effect("trims whitespace exposed after quote removal in thread titles", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
title: ` "' hello world '" `,
}),
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateThreadTitle({
cwd: process.cwd(),
message: "Name this thread.",
modelSelection: DEFAULT_TEST_MODEL_SELECTION,
});

expect(generated.title).toBe("hello world");
}),
),
);

it.effect("omits attachment metadata section when no attachments are provided", () =>
withFakeCodexEnv(
{
Expand Down
50 changes: 47 additions & 3 deletions apps/server/src/git/Layers/CodexTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,28 @@ import { ServerConfig } from "../../config.ts";
import { TextGenerationError } from "../Errors.ts";
import {
type BranchNameGenerationInput,
type ThreadTitleGenerationResult,
type TextGenerationShape,
TextGeneration,
} from "../Services/TextGeneration.ts";
import {
buildBranchNamePrompt,
buildCommitMessagePrompt,
buildPrContentPrompt,
buildThreadTitlePrompt,
} from "../Prompts.ts";
import {
normalizeCliError,
sanitizeCommitSubject,
sanitizePrTitle,
sanitizeThreadTitle,
toJsonSchemaObject,
} from "../Utils.ts";
import { normalizeCodexModelOptions } from "../../provider/Layers/CodexProvider.ts";
import { ServerSettingsService } from "../../serverSettings.ts";

const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low";
const CODEX_TIMEOUT_MS = 180_000;

const makeCodexTextGeneration = Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
Expand Down Expand Up @@ -83,7 +85,11 @@ const makeCodexTextGeneration = Effect.gen(function* () {
fileSystem.remove(filePath).pipe(Effect.catch(() => Effect.void));

const materializeImageAttachments = (
_operation: "generateCommitMessage" | "generatePrContent" | "generateBranchName",
_operation:
| "generateCommitMessage"
| "generatePrContent"
| "generateBranchName"
| "generateThreadTitle",
attachments: BranchNameGenerationInput["attachments"],
): Effect.Effect<MaterializedImageAttachments, TextGenerationError> =>
Effect.gen(function* () {
Expand Down Expand Up @@ -124,7 +130,11 @@ const makeCodexTextGeneration = Effect.gen(function* () {
cleanupPaths = [],
modelSelection,
}: {
operation: "generateCommitMessage" | "generatePrContent" | "generateBranchName";
operation:
| "generateCommitMessage"
| "generatePrContent"
| "generateBranchName"
| "generateThreadTitle";
cwd: string;
prompt: string;
outputSchemaJson: S;
Expand Down Expand Up @@ -363,10 +373,44 @@ const makeCodexTextGeneration = Effect.gen(function* () {
};
});

const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn(
"CodexTextGeneration.generateThreadTitle",
)(function* (input) {
const { imagePaths } = yield* materializeImageAttachments(
"generateThreadTitle",
input.attachments,
);
const { prompt, outputSchema } = buildThreadTitlePrompt({
message: input.message,
attachments: input.attachments,
});

if (input.modelSelection.provider !== "codex") {
return yield* new TextGenerationError({
operation: "generateThreadTitle",
detail: "Invalid model selection.",
});
}

const generated = yield* runCodexJson({
operation: "generateThreadTitle",
cwd: input.cwd,
prompt,
outputSchemaJson: outputSchema,
imagePaths,
modelSelection: input.modelSelection,
});

return {
title: sanitizeThreadTitle(generated.title),
} satisfies ThreadTitleGenerationResult;
});

return {
generateCommitMessage,
generatePrContent,
generateBranchName,
generateThreadTitle,
} satisfies TextGenerationShape;
});

Expand Down
25 changes: 24 additions & 1 deletion apps/server/src/git/Layers/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";
import { expect } from "vitest";
import type { GitActionProgressEvent } from "@t3tools/contracts";
import type { GitActionProgressEvent, ModelSelection } from "@t3tools/contracts";

import { GitCommandError, GitHubCliError, TextGenerationError } from "../Errors.ts";
import { type GitManagerShape } from "../Services/GitManager.ts";
Expand Down Expand Up @@ -49,6 +49,7 @@ interface FakeGitTextGeneration {
stagedSummary: string;
stagedPatch: string;
includeBranch?: boolean;
modelSelection: ModelSelection;
}) => Effect.Effect<
{ subject: string; body: string; branch?: string | undefined },
TextGenerationError
Expand All @@ -60,11 +61,18 @@ interface FakeGitTextGeneration {
commitSummary: string;
diffSummary: string;
diffPatch: string;
modelSelection: ModelSelection;
}) => Effect.Effect<{ title: string; body: string }, TextGenerationError>;
generateBranchName: (input: {
cwd: string;
message: string;
modelSelection: ModelSelection;
}) => Effect.Effect<{ branch: string }, TextGenerationError>;
generateThreadTitle: (input: {
cwd: string;
message: string;
modelSelection: ModelSelection;
}) => Effect.Effect<{ title: string }, TextGenerationError>;
}

type FakePullRequest = NonNullable<FakeGhScenario["pullRequest"]>;
Expand Down Expand Up @@ -168,6 +176,10 @@ function createTextGeneration(overrides: Partial<FakeGitTextGeneration> = {}): T
Effect.succeed({
branch: "update-workflow",
}),
generateThreadTitle: () =>
Effect.succeed({
title: "Update workflow",
}),
...overrides,
};

Expand Down Expand Up @@ -205,6 +217,17 @@ function createTextGeneration(overrides: Partial<FakeGitTextGeneration> = {}): T
}),
),
),
generateThreadTitle: (input) =>
implementation.generateThreadTitle(input).pipe(
Effect.mapError(
(cause) =>
new TextGenerationError({
operation: "generateThreadTitle",
detail: "fake text generation failed",
...(cause !== undefined ? { cause } : {}),
}),
),
),
};
}

Expand Down
1 change: 1 addition & 0 deletions apps/server/src/git/Layers/RoutingTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const makeRoutingTextGeneration = Effect.gen(function* () {
route(input.modelSelection.provider).generateCommitMessage(input),
generatePrContent: (input) => route(input.modelSelection.provider).generatePrContent(input),
generateBranchName: (input) => route(input.modelSelection.provider).generateBranchName(input),
generateThreadTitle: (input) => route(input.modelSelection.provider).generateThreadTitle(input),
} satisfies TextGenerationShape;
});

Expand Down
Loading
Loading