Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 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
77 changes: 64 additions & 13 deletions apps/server/src/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import { vi } from "vitest";
import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap";
import { assertNone, assertSome } from "@effect/vitest/utils";

const openSyncInterceptor = vi.hoisted(() => ({ failPath: null as string | null }));
const bootstrapFsInterceptor = vi.hoisted(() => ({
failOpenPath: null as string | null,
failCreateReadStreamForDuplicatedPath: null as string | null,
duplicatedFdForPathFailure: null as number | null,
}));

vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
Expand All @@ -23,29 +27,41 @@ vi.mock("node:fs", async (importOriginal) => {
const [filePath, flags] = args;
if (
typeof filePath === "string" &&
filePath === openSyncInterceptor.failPath &&
filePath === bootstrapFsInterceptor.failOpenPath &&
flags === "r"
) {
const error = new Error("no such device or address");
Object.assign(error, { code: "ENXIO" });
throw error;
}
return (actual.openSync as (...a: typeof args) => number)(...args);
const fd = (actual.openSync as (...a: typeof args) => number)(...args);
if (
typeof filePath === "string" &&
filePath === bootstrapFsInterceptor.failCreateReadStreamForDuplicatedPath &&
flags === "r"
) {
bootstrapFsInterceptor.duplicatedFdForPathFailure = fd;
}
return fd;
},
createReadStream: (...args: Parameters<typeof actual.createReadStream>) => {
const [, options] = args;
const fd = typeof options === "object" && options && "fd" in options ? options.fd : undefined;
if (typeof fd === "number" && fd === bootstrapFsInterceptor.duplicatedFdForPathFailure) {
const error = new Error("bad file descriptor");
Object.assign(error, { code: "EBADF" });
throw error;
}
return (
actual.createReadStream as (...a: typeof args) => ReturnType<typeof actual.createReadStream>
)(...args);
},
};
});

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 Expand Up @@ -88,14 +104,49 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
// stream's async close and produces an uncaught EBADF.
const fd = NFS.openSync(filePath, "r");

openSyncInterceptor.failPath = `/proc/self/fd/${fd}`;
bootstrapFsInterceptor.failOpenPath = resolveFdPath(fd) ?? null;
try {
const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
assertSome(payload, {
mode: "desktop",
});
} finally {
openSyncInterceptor.failPath = null;
bootstrapFsInterceptor.failOpenPath = null;
}
}),
);

it.effect("closes the duplicated fd before falling back when the duplicated stream fails", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });

yield* fs.writeFileString(
filePath,
`${yield* Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema))({
mode: "desktop",
})}\n`,
);

const fd = NFS.openSync(filePath, "r");
const duplicatedFdPath = resolveFdPath(fd);
assert.notStrictEqual(duplicatedFdPath, undefined);
const closeSyncSpy = vi.spyOn(NFS, "closeSync");
bootstrapFsInterceptor.failCreateReadStreamForDuplicatedPath = duplicatedFdPath ?? null;

try {
const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
assertSome(payload, {
mode: "desktop",
});

const duplicatedFd = bootstrapFsInterceptor.duplicatedFdForPathFailure;
assert.notStrictEqual(duplicatedFd, null);
assert.ok(closeSyncSpy.mock.calls.some(([closedFd]) => closedFd === duplicatedFd));
} finally {
bootstrapFsInterceptor.failCreateReadStreamForDuplicatedPath = null;
bootstrapFsInterceptor.duplicatedFdForPathFailure = null;
closeSyncSpy.mockRestore();
}
}),
);
Expand Down
31 changes: 25 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,19 @@ const makeBootstrapInputStream = (fd: number) =>
}
return makeDirectBootstrapStream(fd);
}
throw error;
if (!isUnavailableBootstrapFdPathError(error)) {
throw error;
}

if (streamFd !== undefined) {
NFS.closeSync(streamFd);
}
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 +185,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
Loading
Loading