332 lines · 12.9 KB · TypeScript
RawDownload
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>  );}