1import { useRef } from "react";
2import { BranchNamingMode, DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts";
3
4import { Input } from "../ui/input";
5import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
6import { Textarea } from "../ui/textarea";
7import { SettingsRow, SettingResetButton } from "./settingsLayout";
8import { useSettingsScope } from "./SettingsScopeContext";
9import { searchableSetting } from "./settingsSearch";
10import {
11 useScopedSettings,
12 useScopedSettingsMixed,
13 useUpdateScopedSettings,
14} from "./useScopedSettings";
15
16const MODES = {
17 static: "Static prefix",
18 semantic: "Semantic prefix",
19 custom: "Custom instructions",
20} satisfies Record<BranchNamingMode, string>;
21
22export function BranchNamingSettings() {
23 const settings = useScopedSettings();
24 const { targets } = useSettingsScope();
25 const scopeKey = targets.map((target) => `${target.environmentId}:${target.projectId}`).join(",");
26 const prefixEdited = useRef(false);
27 const instructionsEdited = useRef(false);
28 const updateSettings = useUpdateScopedSettings();
29 const modeMixed = useScopedSettingsMixed(["branchNamingMode"]);
30 const prefixMixed = useScopedSettingsMixed(["branchNamePrefix"]);
31 const instructionsMixed = useScopedSettingsMixed(["branchNameInstructions"]);
32
33 return (
34 <>
35 <SettingsRow
36 serverScoped
37 settingKeys={["branchNamingMode"]}
38 {...searchableSetting("worktree-branch-naming")}
39 description="Choose how new worktree branches are named from your first message."
40 status={
41 !modeMixed && settings.branchNamingMode === "semantic"
42 ? "The model chooses a prefix that describes the work, such as feat/add-search, fix/login-timeout, or refactor/auth."
43 : undefined
44 }
45 resetAction={
46 settings.branchNamingMode !== DEFAULT_SERVER_SETTINGS.branchNamingMode || modeMixed ? (
47 <SettingResetButton
48 label="branch naming"
49 onClick={() =>
50 updateSettings({ branchNamingMode: DEFAULT_SERVER_SETTINGS.branchNamingMode })
51 }
52 />
53 ) : null
54 }
55 control={
56 <Select
57 value={modeMixed ? null : settings.branchNamingMode}
58 onValueChange={(value) => {
59 if (BranchNamingMode.literals.includes(value as BranchNamingMode)) {
60 updateSettings({ branchNamingMode: value as BranchNamingMode });
61 }
62 }}
63 >
64 <SelectTrigger size="sm" aria-label="Worktree branch naming">
65 <SelectValue>
66 {(value: BranchNamingMode | null) => (value === null ? "Mixed" : MODES[value])}
67 </SelectValue>
68 </SelectTrigger>
69 <SelectPopup align="end" alignItemWithTrigger={false}>
70 {BranchNamingMode.literals.map((mode) => (
71 <SelectItem key={mode} value={mode}>
72 {MODES[mode]}
73 </SelectItem>
74 ))}
75 </SelectPopup>
76 </Select>
77 }
78 />
79 {!modeMixed && settings.branchNamingMode === "static" ? (
80 <SettingsRow
81 serverScoped
82 settingKeys={["branchNamePrefix"]}
83 title="Branch prefix"
84 description="For example, t3 or t3/ produces t3/add-search. Leave empty for no prefix."
85 resetAction={
86 prefixMixed ||
87 settings.branchNamePrefix !== DEFAULT_SERVER_SETTINGS.branchNamePrefix ? (
88 <SettingResetButton
89 label="branch prefix"
90 onClick={() =>
91 updateSettings({ branchNamePrefix: DEFAULT_SERVER_SETTINGS.branchNamePrefix })
92 }
93 />
94 ) : null
95 }
96 control={
97 <Input
98 key={`${scopeKey}:${prefixMixed}:${settings.branchNamePrefix}`}
99 aria-label="Branch prefix"
100 autoCapitalize="none"
101 spellCheck={false}
102 onChange={() => {
103 prefixEdited.current = true;
104 }}
105 placeholder={prefixMixed ? "Mixed" : "No prefix"}
106 defaultValue={prefixMixed ? "" : settings.branchNamePrefix}
107 onBlur={(event) => {
108 const value = event.target.value.trim();
109 if (prefixEdited.current && (prefixMixed || value !== settings.branchNamePrefix))
110 updateSettings({ branchNamePrefix: value });
111 prefixEdited.current = false;
112 }}
113 />
114 }
115 />
116 ) : null}
117 {!modeMixed && settings.branchNamingMode === "custom" ? (
118 <SettingsRow
119 serverScoped
120 settingKeys={["branchNameInstructions"]}
121 title="Branch naming instructions"
122 description="Appended to the naming prompt. The model returns the complete branch name; no prefix or suffix is added."
123 resetAction={
124 instructionsMixed || settings.branchNameInstructions !== "" ? (
125 <SettingResetButton
126 label="branch naming instructions"
127 onClick={() => updateSettings({ branchNameInstructions: "" })}
128 />
129 ) : null
130 }
131 >
132 <div className="mt-3 max-w-2xl pb-3.5">
133 <Textarea
134 key={`${scopeKey}:${instructionsMixed}:${settings.branchNameInstructions}`}
135 aria-label="Branch naming instructions"
136 onChange={() => {
137 instructionsEdited.current = true;
138 }}
139 rows={4}
140 defaultValue={instructionsMixed ? "" : settings.branchNameInstructions}
141 placeholder={
142 instructionsMixed
143 ? "Mixed. Enter instructions to apply to all selected targets."
144 : "Use julius/ followed by the issue ID and a short description."
145 }
146 onBlur={(event) => {
147 const value = event.target.value.trim();
148 if (
149 instructionsEdited.current &&
150 (instructionsMixed || value !== settings.branchNameInstructions)
151 )
152 updateSettings({ branchNameInstructions: value });
153 instructionsEdited.current = false;
154 }}
155 />
156 </div>
157 </SettingsRow>
158 ) : null}
159 </>
160 );
161}