import { useNavigate } from "@tanstack/react-router";import { useRef, useState } from "react";import type { ProviderInstanceId, SourceControlWritingStyleMode } from "@t3tools/contracts";import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";import { createModelSelection } from "@t3tools/shared/model";import { resolveSourceControlWriterModelSelection } from "@t3tools/shared/serverSettings"; import { useScopedSettings, useScopedSettingsMixed, useUpdateScopedSettings,} from "./useScopedSettings";import { useScopedModelDisabledReason } from "./useScopedModelAvailability";import { useSettingsScope } from "./SettingsScopeContext";import { applyProviderInstanceSettings, deriveProviderInstanceEntries, sortProviderInstanceEntries,} from "../../providerInstances";import { getCustomModelOptionsByInstance, resolveAppModelSelectionState,} from "../../modelSelection";import { EMPTY_SERVER_PROVIDERS } from "../../state/server";import { ProviderModelPicker } from "../chat/ProviderModelPicker";import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";import { Switch } from "../ui/switch";import { Textarea } from "../ui/textarea";import { toastManager } from "../ui/toast";import { Button } from "../ui/button";import { SETTINGS_PICKER_TRIGGER_CLASSNAME, SettingResetButton, SettingsRow, SettingsSection,} from "./settingsLayout";import { searchableSetting } from "./settingsSearch"; const MODE_OPTIONS: Record<SourceControlWritingStyleMode, { label: string; description: string }> = { repo_conventions: { label: "Repository conventions", description: "In each project, matches recent change descriptions and change request titles.", }, conventional_commits: { label: "Conventional Commits", description: "Use Conventional Commit prefixes and keep change request text concise.", }, custom: { label: "Custom instructions", description: "Use your instructions for change descriptions and change requests in every project.", }, }; export function SourceControlWritingSettingsSection() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); const navigate = useNavigate(); const { environment, connectedEnvironments } = useSettingsScope(); // The representative supplies the provider list; a model choice is checked // against every target before it fans out. const environmentId = environment?.environmentId ?? null; const hasServerTargets = connectedEnvironments.length > 0; const serverProviders = environment?.serverConfig?.providers ?? EMPTY_SERVER_PROVIDERS; const writingStyleMixed = useScopedSettingsMixed(["sourceControlWritingStyle"]); const customInstructionsRef = useRef<HTMLTextAreaElement>(null); const [editingAllInstructions, setEditingAllInstructions] = useState(false); const [allInstructions, setAllInstructions] = useState<string | null>(null); const style = settings.sourceControlWritingStyle; const defaults = DEFAULT_UNIFIED_SETTINGS.sourceControlWritingStyle; const isSourceControlWritingStyleDirty = writingStyleMixed || style.mode !== defaults.mode || style.customInstructions !== defaults.customInstructions; const textGenerationProviders = serverProviders.filter( (provider) => provider.supportsTextGeneration !== false, ); const defaultModelSelection = resolveAppModelSelectionState(settings, textGenerationProviders); const usesDedicatedModel = settings.sourceControlWriterModelSelection !== null; const activeSelection = resolveAppModelSelectionState( { ...settings, textGenerationModelSelection: resolveSourceControlWriterModelSelection( settings, textGenerationProviders, ), }, textGenerationProviders, ); const instanceEntries = sortProviderInstanceEntries( applyProviderInstanceSettings(deriveProviderInstanceEntries(textGenerationProviders), settings), ); const canEnableDedicatedModel = instanceEntries.some( (entry) => entry.instanceId === defaultModelSelection.instanceId && entry.enabled && entry.isAvailable, ); const modelOptionsByInstance = getCustomModelOptionsByInstance( settings, textGenerationProviders, activeSelection.instanceId, activeSelection.model, ); const writerModelDisabledReason = useScopedModelDisabledReason(settings, instanceEntries); return ( <SettingsSection id="source-control-text-generation" title="Text generation"> <SettingsRow serverScoped settingKeys={["sourceControlWritingStyle"]} {...searchableSetting("source-control-writing-style")} description={MODE_OPTIONS[style.mode].description} resetAction={ isSourceControlWritingStyleDirty ? ( <SettingResetButton label="source control writing style" onClick={() => updateSettings({ sourceControlWritingStyle: { mode: defaults.mode, customInstructions: defaults.customInstructions, }, }) } /> ) : null } control={ <Select value={style.mode} onValueChange={(value) => { const customInstructions = customInstructionsRef.current?.value.trim(); updateSettings({ sourceControlWritingStyle: { mode: value as SourceControlWritingStyleMode, ...(customInstructions !== undefined ? { customInstructions } : {}), }, }); }} > <SelectTrigger size="sm" className="w-full sm:w-56" aria-label="Source control writing style" > <SelectValue>{MODE_OPTIONS[style.mode].label}</SelectValue> </SelectTrigger> <SelectPopup align="end" alignItemWithTrigger={false}> {(Object.keys(MODE_OPTIONS) as SourceControlWritingStyleMode[]).map((mode) => ( <SelectItem key={mode} hideIndicator value={mode}> {MODE_OPTIONS[mode].label} </SelectItem> ))} </SelectPopup> </Select> } > {writingStyleMixed ? ( <div className="mt-3 max-w-2xl space-y-2 pb-3.5"> {editingAllInstructions ? ( <> <Textarea value={allInstructions ?? ""} onChange={(event) => setAllInstructions(event.target.value)} rows={4} aria-label="Custom source control instructions for all selected environments" placeholder="Write the instructions each selected environment should use." /> <Button size="sm" variant="outline" disabled={allInstructions === null} onClick={() => { if (allInstructions === null) return; updateSettings({ sourceControlWritingStyle: { mode: "custom", customInstructions: allInstructions.trim(), }, }); setEditingAllInstructions(false); }} > Apply instructions to all </Button> </> ) : ( <Button size="sm" variant="outline" onClick={() => { setAllInstructions(null); setEditingAllInstructions(true); }} > Write custom instructions for all </Button> )} </div> ) : style.mode === "custom" ? ( <div className="mt-3 max-w-2xl pb-3.5"> <Textarea key={style.customInstructions} ref={customInstructionsRef} defaultValue={style.customInstructions} onBlur={(event) => { const customInstructions = event.target.value.trim(); if (customInstructions !== style.customInstructions) { updateSettings({ sourceControlWritingStyle: { customInstructions } }); } }} rows={4} placeholder="Keep titles concise. Use short bullet points in descriptions." aria-label="Custom source control writing instructions" /> </div> ) : null} </SettingsRow> <SettingsRow serverScoped settingKeys={["sourceControlWritingStyle"]} {...searchableSetting("follow-change-request-templates")} description="Use the repository's template for change request descriptions when available." resetAction={ writingStyleMixed || style.followChangeRequestTemplates !== defaults.followChangeRequestTemplates ? ( <SettingResetButton label="change request templates" onClick={() => updateSettings({ sourceControlWritingStyle: { followChangeRequestTemplates: defaults.followChangeRequestTemplates, }, }) } /> ) : null } control={ <Switch checked={style.followChangeRequestTemplates} onCheckedChange={(checked) => updateSettings({ sourceControlWritingStyle: { followChangeRequestTemplates: Boolean(checked), }, }) } aria-label="Follow change request templates" /> } /> <SettingsRow serverScoped settingKeys={["sourceControlWriterModelSelection"]} {...searchableSetting("source-control-writer-model")} description="Model for source control text and branch or bookmark names. Off uses the environment's text generation model." control={ !hasServerTargets ? ( <span className="text-sm text-muted-foreground"> Connect an environment to choose its source control writer model. </span> ) : ( <div className="flex flex-wrap items-center justify-end gap-2"> {usesDedicatedModel && !canEnableDedicatedModel ? ( <span className="text-sm text-muted-foreground"> No text generation providers available. </span> ) : null} {usesDedicatedModel && canEnableDedicatedModel ? ( <ProviderModelPicker activeInstanceId={activeSelection.instanceId} model={activeSelection.model} lockedProvider={null} instanceEntries={instanceEntries} modelOptionsByInstance={modelOptionsByInstance} triggerVariant="outline" triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME} triggerAriaLabel="Source control writer model" {...(environmentId ? { onOpenProviderSetup: (instanceId: ProviderInstanceId) => { void navigate({ to: "/settings/providers", search: { environmentId, instanceId }, }); }, } : {})} getModelDisabledReason={writerModelDisabledReason} onInstanceModelChange={(instanceId, model) => { const reason = writerModelDisabledReason(instanceId, model); if (reason) { toastManager.add({ type: "error", title: "Source control writer model not saved", description: reason, }); return; } updateSettings({ sourceControlWriterModelSelection: createModelSelection(instanceId, model), }); }} /> ) : null} <Switch checked={usesDedicatedModel} disabled={!usesDedicatedModel && !canEnableDedicatedModel} onCheckedChange={(checked) => updateSettings({ sourceControlWriterModelSelection: checked ? createModelSelection( defaultModelSelection.instanceId, defaultModelSelection.model, defaultModelSelection.options, ) : null, }) } aria-label="Use a separate source control writer model" /> </div> ) } /> </SettingsSection> );}
import { useNavigate } from "@tanstack/react-router";
import { useRef, useState } from "react";
import type { ProviderInstanceId, SourceControlWritingStyleMode } from "@t3tools/contracts";
import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";
import { createModelSelection } from "@t3tools/shared/model";
import { resolveSourceControlWriterModelSelection } from "@t3tools/shared/serverSettings";
import {
useScopedSettings,
useScopedSettingsMixed,
useUpdateScopedSettings,
} from "./useScopedSettings";
import { useScopedModelDisabledReason } from "./useScopedModelAvailability";
import { useSettingsScope } from "./SettingsScopeContext";
import {
applyProviderInstanceSettings,
deriveProviderInstanceEntries,
sortProviderInstanceEntries,
} from "../../providerInstances";
import {
getCustomModelOptionsByInstance,
resolveAppModelSelectionState,
} from "../../modelSelection";
import { EMPTY_SERVER_PROVIDERS } from "../../state/server";
import { ProviderModelPicker } from "../chat/ProviderModelPicker";
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
import { Switch } from "../ui/switch";
import { Textarea } from "../ui/textarea";
import { toastManager } from "../ui/toast";
import { Button } from "../ui/button";
import {
SETTINGS_PICKER_TRIGGER_CLASSNAME,
SettingResetButton,
SettingsRow,
SettingsSection,
} from "./settingsLayout";
import { searchableSetting } from "./settingsSearch";
const MODE_OPTIONS: Record<SourceControlWritingStyleMode, { label: string; description: string }> =
{
repo_conventions: {
label: "Repository conventions",
description: "In each project, matches recent change descriptions and change request titles.",
},
conventional_commits: {
label: "Conventional Commits",
description: "Use Conventional Commit prefixes and keep change request text concise.",
},
custom: {
label: "Custom instructions",
description:
"Use your instructions for change descriptions and change requests in every project.",
},
};
export function SourceControlWritingSettingsSection() {
const settings = useScopedSettings();
const updateSettings = useUpdateScopedSettings();
const navigate = useNavigate();
const { environment, connectedEnvironments } = useSettingsScope();
// The representative supplies the provider list; a model choice is checked
// against every target before it fans out.
const environmentId = environment?.environmentId ?? null;
const hasServerTargets = connectedEnvironments.length > 0;
const serverProviders = environment?.serverConfig?.providers ?? EMPTY_SERVER_PROVIDERS;
const writingStyleMixed = useScopedSettingsMixed(["sourceControlWritingStyle"]);
const customInstructionsRef = useRef<HTMLTextAreaElement>(null);
const [editingAllInstructions, setEditingAllInstructions] = useState(false);
const [allInstructions, setAllInstructions] = useState<string | null>(null);
const style = settings.sourceControlWritingStyle;
const defaults = DEFAULT_UNIFIED_SETTINGS.sourceControlWritingStyle;
const isSourceControlWritingStyleDirty =
writingStyleMixed ||
style.mode !== defaults.mode ||
style.customInstructions !== defaults.customInstructions;
const textGenerationProviders = serverProviders.filter(
(provider) => provider.supportsTextGeneration !== false,
);
const defaultModelSelection = resolveAppModelSelectionState(settings, textGenerationProviders);
const usesDedicatedModel = settings.sourceControlWriterModelSelection !== null;
const activeSelection = resolveAppModelSelectionState(
{
...settings,
textGenerationModelSelection: resolveSourceControlWriterModelSelection(
settings,
textGenerationProviders,
),
},
textGenerationProviders,
);
const instanceEntries = sortProviderInstanceEntries(
applyProviderInstanceSettings(deriveProviderInstanceEntries(textGenerationProviders), settings),
);
const canEnableDedicatedModel = instanceEntries.some(
(entry) =>
entry.instanceId === defaultModelSelection.instanceId && entry.enabled && entry.isAvailable,
);
const modelOptionsByInstance = getCustomModelOptionsByInstance(
settings,
textGenerationProviders,
activeSelection.instanceId,
activeSelection.model,
);
const writerModelDisabledReason = useScopedModelDisabledReason(settings, instanceEntries);
return (
<SettingsSection id="source-control-text-generation" title="Text generation">
<SettingsRow
serverScoped
settingKeys={["sourceControlWritingStyle"]}
{...searchableSetting("source-control-writing-style")}
description={MODE_OPTIONS[style.mode].description}
resetAction={
isSourceControlWritingStyleDirty ? (
<SettingResetButton
label="source control writing style"
onClick={() =>
updateSettings({
sourceControlWritingStyle: {
mode: defaults.mode,
customInstructions: defaults.customInstructions,
},
})
}
/>
) : null
}
control={
<Select
value={style.mode}
onValueChange={(value) => {
const customInstructions = customInstructionsRef.current?.value.trim();
updateSettings({
sourceControlWritingStyle: {
mode: value as SourceControlWritingStyleMode,
...(customInstructions !== undefined ? { customInstructions } : {}),
},
});
}}
>
<SelectTrigger
size="sm"
className="w-full sm:w-56"
aria-label="Source control writing style"
>
<SelectValue>{MODE_OPTIONS[style.mode].label}</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
{(Object.keys(MODE_OPTIONS) as SourceControlWritingStyleMode[]).map((mode) => (
<SelectItem key={mode} hideIndicator value={mode}>
{MODE_OPTIONS[mode].label}
</SelectItem>
))}
</SelectPopup>
</Select>
}
>
{writingStyleMixed ? (
<div className="mt-3 max-w-2xl space-y-2 pb-3.5">
{editingAllInstructions ? (
<>
<Textarea
value={allInstructions ?? ""}
onChange={(event) => setAllInstructions(event.target.value)}
rows={4}
aria-label="Custom source control instructions for all selected environments"
placeholder="Write the instructions each selected environment should use."
/>
<Button
size="sm"
variant="outline"
disabled={allInstructions === null}
onClick={() => {
if (allInstructions === null) return;
updateSettings({
sourceControlWritingStyle: {
mode: "custom",
customInstructions: allInstructions.trim(),
},
});
setEditingAllInstructions(false);
}}
>
Apply instructions to all
</Button>
</>
) : (
<Button
size="sm"
variant="outline"
onClick={() => {
setAllInstructions(null);
setEditingAllInstructions(true);
}}
>
Write custom instructions for all
</Button>
)}
</div>
) : style.mode === "custom" ? (
<div className="mt-3 max-w-2xl pb-3.5">
<Textarea
key={style.customInstructions}
ref={customInstructionsRef}
defaultValue={style.customInstructions}
onBlur={(event) => {
const customInstructions = event.target.value.trim();
if (customInstructions !== style.customInstructions) {
updateSettings({ sourceControlWritingStyle: { customInstructions } });
}
}}
rows={4}
placeholder="Keep titles concise. Use short bullet points in descriptions."
aria-label="Custom source control writing instructions"
/>
</div>
) : null}
</SettingsRow>
<SettingsRow
serverScoped
settingKeys={["sourceControlWritingStyle"]}
{...searchableSetting("follow-change-request-templates")}
description="Use the repository's template for change request descriptions when available."
resetAction={
writingStyleMixed ||
style.followChangeRequestTemplates !== defaults.followChangeRequestTemplates ? (
<SettingResetButton
label="change request templates"
onClick={() =>
updateSettings({
sourceControlWritingStyle: {
followChangeRequestTemplates: defaults.followChangeRequestTemplates,
},
})
}
/>
) : null
}
control={
<Switch
checked={style.followChangeRequestTemplates}
onCheckedChange={(checked) =>
updateSettings({
sourceControlWritingStyle: {
followChangeRequestTemplates: Boolean(checked),
},
})
}
aria-label="Follow change request templates"
/>
}
/>
<SettingsRow
serverScoped
settingKeys={["sourceControlWriterModelSelection"]}
{...searchableSetting("source-control-writer-model")}
description="Model for source control text and branch or bookmark names. Off uses the environment's text generation model."
control={
!hasServerTargets ? (
<span className="text-sm text-muted-foreground">
Connect an environment to choose its source control writer model.
</span>
) : (
<div className="flex flex-wrap items-center justify-end gap-2">
{usesDedicatedModel && !canEnableDedicatedModel ? (
<span className="text-sm text-muted-foreground">
No text generation providers available.
</span>
) : null}
{usesDedicatedModel && canEnableDedicatedModel ? (
<ProviderModelPicker
activeInstanceId={activeSelection.instanceId}
model={activeSelection.model}
lockedProvider={null}
instanceEntries={instanceEntries}
modelOptionsByInstance={modelOptionsByInstance}
triggerVariant="outline"
triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
triggerAriaLabel="Source control writer model"
{...(environmentId
? {
onOpenProviderSetup: (instanceId: ProviderInstanceId) => {
void navigate({
to: "/settings/providers",
search: { environmentId, instanceId },
});
},
}
: {})}
getModelDisabledReason={writerModelDisabledReason}
onInstanceModelChange={(instanceId, model) => {
const reason = writerModelDisabledReason(instanceId, model);
if (reason) {
toastManager.add({
type: "error",
title: "Source control writer model not saved",
description: reason,
});
return;
}
updateSettings({
sourceControlWriterModelSelection: createModelSelection(instanceId, model),
});
}}
/>
) : null}
<Switch
checked={usesDedicatedModel}
disabled={!usesDedicatedModel && !canEnableDedicatedModel}
onCheckedChange={(checked) =>
updateSettings({
sourceControlWriterModelSelection: checked
? createModelSelection(
defaultModelSelection.instanceId,
defaultModelSelection.model,
defaultModelSelection.options,
)
: null,
})
}
aria-label="Use a separate source control writer model"
/>
</div>
)
}
/>
</SettingsSection>
);
}