Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
2 changes: 1 addition & 1 deletion apps/marketing/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface Props {

const {
title = "T3 Code",
description = "T3 Code — The best way to code with AI.",
description = "T3 Code — A great way to code with agents.",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥲

} = Astro.props;
---

Expand Down
2 changes: 1 addition & 1 deletion apps/marketing/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Layout from "../layouts/Layout.astro";
---

<Layout>
<h1 class="tagline">T3 Code is the best way to code with AI.</h1>
<h1 class="tagline">T3 Code is a great way to code with agents.</h1>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥲

also very unrelated to the PR


<a
id="download-btn"
Expand Down
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
10 changes: 1 addition & 9 deletions apps/server/src/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as path from "node:path";
import { execFileSync, spawn } from "node:child_process";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";

Check warning on line 5 in apps/server/src/bootstrap.test.ts

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint(no-unused-vars)

Identifier 'assert' is imported but never used.
import { FileSystem, Schema } from "effect";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
Expand All @@ -10,7 +10,7 @@
import { TestClock } from "effect/testing";
import { vi } from "vitest";

import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap";
import { readBootstrapEnvelope } from "./bootstrap";
import { assertNone, assertSome } from "@effect/vitest/utils";

const openSyncInterceptor = vi.hoisted(() => ({ failPath: null as string | null }));
Expand Down Expand Up @@ -38,14 +38,6 @@
const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });

it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
it.effect("uses platform-specific fd paths", () =>
Effect.sync(() => {
assert.equal(resolveFdPath(3, "linux"), "/proc/self/fd/3");
assert.equal(resolveFdPath(3, "darwin"), "/dev/fd/3");
assert.equal(resolveFdPath(3, "win32"), undefined);
}),
);

it.effect("reads a bootstrap envelope from a provided fd", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
28 changes: 22 additions & 6 deletions apps/server/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ const isUnavailableBootstrapFdError = Predicate.compose(
(_) => _.code === "EBADF" || _.code === "ENOENT",
);

const isUnavailableBootstrapFdPathError = Predicate.compose(
Predicate.hasProperty("code"),
(_) => _.code === "EBADF" || _.code === "ENOENT" || _.code === "ENXIO",
);

const isFdReady = (fd: number) =>
Effect.try({
try: () => NFS.fstatSync(fd),
Expand All @@ -106,6 +111,16 @@ const isFdReady = (fd: number) =>
const makeBootstrapInputStream = (fd: number) =>
Effect.try<Readable, BootstrapError>({
try: () => {
if (process.platform === "win32") {
const stream = new Net.Socket({
fd,
readable: true,
writable: false,
});
stream.setEncoding("utf8");
return stream;
}

const fdPath = resolveFdPath(fd);
if (fdPath === undefined) {
return makeDirectBootstrapStream(fd);
Expand All @@ -126,12 +141,16 @@ const makeBootstrapInputStream = (fd: number) =>
}
return makeDirectBootstrapStream(fd);
}
throw error;
if (!isUnavailableBootstrapFdPathError(error)) {
throw error;
}

return makeDirectBootstrapStream(fd);
}
},
catch: (error) =>
new BootstrapError({
message: "Failed to duplicate bootstrap fd.",
message: "Failed to open bootstrap fd.",
cause: error,
}),
});
Expand Down Expand Up @@ -163,11 +182,8 @@ export function resolveFdPath(
fd: number,
platform: NodeJS.Platform = process.platform,
): string | undefined {
if (platform === "linux") {
return `/proc/self/fd/${fd}`;
}
if (platform === "win32") {
return undefined;
}
return `/dev/fd/${fd}`;
return platform === "linux" ? `/proc/self/fd/${fd}` : `/dev/fd/${fd}`;
}
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
Loading