Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -21,6 +21,7 @@ import {
AndroidSettingsEnvironmentFilter,
SettingsEnvironmentFilterHeader,
} from "./components/SettingsEnvironmentFilterHeader";
import { BranchNamingSettings } from "./components/BranchNamingSettings";
import { SettingsChoiceRow } from "./components/SettingsChoiceRow";
import { SettingsSection } from "./components/SettingsSection";
import { SettingsSwitchRow } from "./components/SettingsSwitchRow";
Expand All @@ -45,7 +46,13 @@ const PAGE_TITLES: Record<SettingsPage, string> = {

const PAGE_PROJECT_KEYS: Record<SettingsPage, readonly ProjectScopedServerSettingKey[]> = {
"new-threads": ["defaultThreadEnvMode", "worktreeSubmodules", "defaultRuntimeMode"],
"source-control": ["defaultAutoPull", "newWorktreesStartFromOrigin"],
"source-control": [
"defaultAutoPull",
"newWorktreesStartFromOrigin",
"branchNamingMode",
"branchNamePrefix",
"branchNameInstructions",
],
"agent-behavior": ["responseStreamingMode", "enableAgentBrowserAccess"],
maintenance: ["continueThreadsAfterServerUpdate"],
};
Expand Down Expand Up @@ -314,6 +321,16 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {

{props.page === "source-control" ? (
<>
<BranchNamingSettings
key={targets
.map((target) => `${target.environment.environmentId}:${target.projectId}`)
.join(",")}
mode={uniform("branchNamingMode")}
prefix={uniform("branchNamePrefix")}
instructions={uniform("branchNameInstructions")}
disabled={disabledFor("branchNamingMode")}
onChange={write}
/>
<SettingsSection title="Default branch">
<SettingsSwitchRow
icon="arrow.down.circle"
Expand Down
114 changes: 114 additions & 0 deletions apps/mobile/src/features/settings/components/BranchNamingSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { BranchNamingMode, type ServerSettingsPatch } from "@t3tools/contracts";
import { useRef } from "react";
import { View } from "react-native";

import { AppText as Text, AppTextInput } from "../../../components/AppText";
import { SettingsChoiceRow } from "./SettingsChoiceRow";
import { SettingsSection } from "./SettingsSection";

const MODES = {
static: { label: "Static prefix", description: "Add your prefix to the generated branch name." },
semantic: {
label: "Semantic prefix",
description: "Let the model choose feat/, fix/, refactor/, or another prefix.",
},
custom: {
label: "Custom instructions",
description: "Generate the complete name with no added prefix or suffix.",
},
} satisfies Record<BranchNamingMode, { label: string; description: string }>;

export function BranchNamingSettings(props: {
mode: BranchNamingMode | null;
prefix: string | null;
instructions: string | null;
disabled: boolean;
onChange: (patch: ServerSettingsPatch) => void;
}) {
const prefixEdited = useRef(false);
const instructionsEdited = useRef(false);
return (
<SettingsSection
title="Worktree branch naming"
trailing={
props.mode === null ? <Text className="text-xs text-foreground-muted">Mixed</Text> : null
}
>
{BranchNamingMode.literals.map((mode, index) => (
<SettingsChoiceRow
key={mode}
label={MODES[mode].label}
description={MODES[mode].description}
selected={props.mode === mode}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
separated={index > 0}
disabled={props.disabled}
onPress={() => props.onChange({ branchNamingMode: mode })}
/>
))}
{props.mode === "static" ? (
<View className="gap-2 px-4 py-3">
<Text className="text-sm text-foreground-muted">
Use t3code or t3code/ for t3code/add-search. Leave empty for no prefix.
</Text>
<AppTextInput
key={props.prefix}
accessibilityLabel="Branch prefix"
onChangeText={() => {
prefixEdited.current = true;
}}
defaultValue={props.prefix ?? ""}
placeholder={props.prefix === null ? "Mixed" : "No prefix"}
editable={!props.disabled}
autoCapitalize="none"
autoCorrect={false}
className="min-h-10 rounded-xl px-3 py-2 text-base text-foreground"
onEndEditing={(event) => {
const value = event.nativeEvent.text.trim();
if (
!props.disabled &&
prefixEdited.current &&
(props.prefix === null || value !== props.prefix)
)
props.onChange({ branchNamePrefix: value });
prefixEdited.current = false;
}}
/>
</View>
) : null}
{props.mode === "custom" ? (
<View className="gap-2 px-4 py-3">
<Text className="text-sm text-foreground-muted">
Append instructions to the naming prompt.
</Text>
<AppTextInput
key={props.instructions}
accessibilityLabel="Branch naming instructions"
onChangeText={() => {
instructionsEdited.current = true;
}}
defaultValue={props.instructions ?? ""}
placeholder={
props.instructions === null
? "Mixed. Enter instructions for all selected targets."
: "Use julius/ followed by the issue ID and a short description."
}
editable={!props.disabled}
multiline
autoCapitalize="sentences"
className="min-h-24 rounded-xl px-3 py-2 text-base text-foreground"
onEndEditing={(event) => {
const value = event.nativeEvent.text.trim();
if (
!props.disabled &&
instructionsEdited.current &&
(props.instructions === null || value !== props.instructions)
)
props.onChange({ branchNameInstructions: value });
instructionsEdited.current = false;
}}
/>
</View>
) : null}
</SettingsSection>
);
}
1 change: 1 addition & 0 deletions apps/server/src/git/GitWorkflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export class GitWorkflowService extends Context.Service<
input: VcsSwitchRefInput,
) => Effect.Effect<VcsSwitchRefResult, GitCommandError>;
readonly renameBranch: (input: {
readonly exactName?: boolean;
readonly cwd: string;
readonly oldBranch: string;
readonly newBranch: string;
Expand Down
21 changes: 18 additions & 3 deletions apps/server/src/orchestration-v2/ThreadLaunchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,23 @@ const make = Effect.gen(function* () {
);
return yield* textGeneration
.generateBranchName({
naming: {
mode: settings.branchNamingMode,
prefix: settings.branchNamePrefix,
instructions: settings.branchNameInstructions,
},
cwd,
message: message.text,
attachments: message.attachments,
...(message.context ? { context: message.context } : {}),
modelSelection,
})
.pipe(Effect.map((result) => result.branch));
.pipe(
Effect.map((result) => ({
branch: result.branch,
exactName: settings.branchNamingMode === "custom",
})),
);
});

// The server owns worktree naming: without an explicit branch, provision
Expand Down Expand Up @@ -369,8 +379,13 @@ const make = Effect.gen(function* () {
const oldBranch = branch;
const worktreeCwd = worktreePath;
yield* generateBranchNameFor(worktreeCwd, initialMessage).pipe(
Effect.flatMap((newBranch) =>
git.renameBranch({ cwd: worktreeCwd, oldBranch, newBranch }),
Effect.flatMap(({ branch: newBranch, exactName }) =>
git.renameBranch({
cwd: worktreeCwd,
oldBranch,
newBranch,
...(exactName ? { exactName: true } : {}),
}),
),
Effect.flatMap((renamed) =>
threads.dispatch({
Expand Down
10 changes: 7 additions & 3 deletions apps/server/src/textGeneration/AntigravityTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
type ProviderSetupError,
TextGenerationError,
} from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { formatGeneratedBranchName, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { extractJsonObject } from "@t3tools/shared/schemaJson";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -381,10 +381,14 @@ export const makeAntigravityTextGeneration = Effect.fn("makeAntigravityTextGener
Effect.fn("AntigravityTextGeneration.generateBranchName")(function* (input) {
const generated = yield* runAntigravityJson({
operation: "generateBranchName",
...buildBranchNamePrompt({ message: input.message, attachments: input.attachments }),
...buildBranchNamePrompt({
message: input.message,
attachments: input.attachments,
naming: input.naming,
}),
modelSelection: input.modelSelection,
});
return { branch: sanitizeBranchFragment(generated.branch) };
return { branch: formatGeneratedBranchName(generated.branch, input.naming) };
});

const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] =
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/textGeneration/ClaudeTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as Stream from "effect/Stream";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { type ClaudeSettings, type ModelSelection } from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { formatGeneratedBranchName, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { resolveSpawnCommand } from "@t3tools/shared/shell";

import { TextGenerationError } from "@t3tools/contracts";
Expand Down Expand Up @@ -372,6 +372,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
const { prompt, outputSchema } = buildBranchNamePrompt({
message: input.message,
attachments: input.attachments,
naming: input.naming,
});

const generated = yield* runClaudeJson({
Expand All @@ -383,7 +384,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
});

return {
branch: sanitizeBranchFragment(generated.branch),
branch: formatGeneratedBranchName(generated.branch, input.naming),
};
});

Expand Down
44 changes: 44 additions & 0 deletions apps/server/src/textGeneration/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,50 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
),
);

for (const example of [
{
mode: "static",
output: "Add Search",
expected: "team/add-search",
instruction: "without a prefix or namespace",
},
{
mode: "semantic",
output: "feat/add-search",
expected: "feat/add-search",
instruction: "semantic prefix",
},
{
mode: "custom",
output: "Julius/ABC-123.v2",
expected: "Julius/ABC-123.v2",
instruction: "Preserve the issue ID and capitalization.",
},
] as const) {
it.effect(`generates a branch using ${example.mode} naming`, () =>
withFakeCodexEnv(
{
output: JSON.stringify({ branch: example.output }),
stdinMustContain: example.instruction,
},
(textGeneration) =>
Effect.gen(function* () {
const generated = yield* textGeneration.generateBranchName({
cwd: process.cwd(),
message: "Add search",
modelSelection: DEFAULT_TEST_MODEL_SELECTION,
naming: {
mode: example.mode,
prefix: "team/",
instructions: "Preserve the issue ID and capitalization.",
},
});
expect(generated.branch).toBe(example.expected);
}),
),
);
}

it.effect("generates branch names even when the ambient scope is already closed", () =>
withFakeCodexEnv(
{
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/textGeneration/CodexTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
type ServerProviderModel,
TextGenerationError,
} from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { formatGeneratedBranchName, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { resolveSpawnCommand } from "@t3tools/shared/shell";

import { resolveAttachmentPath } from "../attachmentStore.ts";
Expand Down Expand Up @@ -391,6 +391,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
const { prompt, outputSchema } = buildBranchNamePrompt({
message: input.message,
attachments: input.attachments,
naming: input.naming,
});

const generated = yield* runCodexJson({
Expand All @@ -403,7 +404,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
});

return {
branch: sanitizeBranchFragment(generated.branch),
branch: formatGeneratedBranchName(generated.branch, input.naming),
};
});

Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/textGeneration/CursorTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type ModelSelection,
type ProviderSetupError,
} from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { formatGeneratedBranchName, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { extractJsonObject } from "@t3tools/shared/schemaJson";

import { TextGenerationError } from "@t3tools/contracts";
Expand Down Expand Up @@ -260,6 +260,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu
const { prompt, outputSchema } = buildBranchNamePrompt({
message: input.message,
attachments: input.attachments,
naming: input.naming,
});

const generated = yield* runCursorJson({
Expand All @@ -270,7 +271,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu
});

return {
branch: sanitizeBranchFragment(generated.branch),
branch: formatGeneratedBranchName(generated.branch, input.naming),
};
});

Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/textGeneration/GrokTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ChildProcessSpawner } from "effect/unstable/process";
import type * as EffectAcpErrors from "effect-acp/errors";

import { type GrokSettings, type ModelSelection } from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { formatGeneratedBranchName, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
import { extractJsonObject } from "@t3tools/shared/schemaJson";

Expand Down Expand Up @@ -224,6 +224,7 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi
const { prompt, outputSchema } = buildBranchNamePrompt({
message: input.message,
attachments: input.attachments,
naming: input.naming,
});

const generated = yield* runGrokJson({
Expand All @@ -235,7 +236,7 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi
});

return {
branch: sanitizeBranchFragment(generated.branch),
branch: formatGeneratedBranchName(generated.branch, input.naming),
};
});

Expand Down
Loading
Loading