feat(web,ui): per-provider git forge API base URL and detection URLs

Configure a default API base URL per git provider (github/gitlab/gitea)
in settings.json gitProviders, with GitHub Enterprise support (Octokit
baseUrl + device-flow web origin derived from the API base), and replace
the client-side custom-domains list with server-persisted detection URL
chips (SSH/HTTPS forms normalized to hosts). The configured API base host
auto-counts as a detection host. Settings round-trip through the existing
/api/config/settings sanitizer; the UI store hydrates from server settings
with a one-time localStorage migration.
This commit is contained in:
2026-08-17 09:55:11 +00:00
parent a87b3fd228
commit 0fc857959e
55 changed files with 1685 additions and 272 deletions
@@ -11,7 +11,8 @@ import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { Icon } from "@/components/icon/Icon";
import { SettingsSection, SettingsGroupTitle } from '@/components/sections/shared/SettingsSection';
import { CustomDomainsInput } from '@/components/sections/shared/CustomDomainsInput';
import { ProviderApiBaseUrlInput } from '@/components/sections/shared/ProviderApiBaseUrlInput';
import { ProviderDetectUrlsInput } from '@/components/sections/shared/ProviderDetectUrlsInput';
type GitHubUser = {
login: string;
@@ -446,7 +447,10 @@ export const GitHubSettings: React.FC = () => {
</div>
)}
<CustomDomainsInput provider="github" />
<div className="flex flex-col gap-4 pt-4">
<ProviderApiBaseUrlInput provider="github" />
<ProviderDetectUrlsInput provider="github" />
</div>
</SettingsSection>
@@ -11,7 +11,9 @@ import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { Icon } from '@/components/icon/Icon';
import { SettingsSection } from '@/components/sections/shared/SettingsSection';
import { CustomDomainsInput } from '@/components/sections/shared/CustomDomainsInput';
import { ProviderApiBaseUrlInput } from '@/components/sections/shared/ProviderApiBaseUrlInput';
import { ProviderDetectUrlsInput } from '@/components/sections/shared/ProviderDetectUrlsInput';
import { useGitProviderDomainsStore } from '@/stores/useGitProviderDomainsStore';
const getBaseUrlHost = (baseUrl?: string | null): string => {
if (!baseUrl) return '';
@@ -34,7 +36,11 @@ export const GitLabSettings: React.FC = () => {
const [isBusy, setIsBusy] = React.useState(false);
const [accessToken, setAccessToken] = React.useState('');
const [baseUrl, setBaseUrl] = React.useState('');
// Prefill the connect form with the server-side default API base URL when the
// user has not typed one yet; the per-account base URL still wins on connect.
const [baseUrl, setBaseUrl] = React.useState(
useGitProviderDomainsStore((state) => state.apiBaseUrls.gitlab),
);
React.useEffect(() => {
(async () => {
@@ -290,7 +296,10 @@ export const GitLabSettings: React.FC = () => {
)}
</div>
<CustomDomainsInput provider="gitlab" />
<div className="flex flex-col gap-4 pt-4">
<ProviderApiBaseUrlInput provider="gitlab" />
<ProviderDetectUrlsInput provider="gitlab" />
</div>
</SettingsSection>
);
};
@@ -11,7 +11,9 @@ import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { Icon } from '@/components/icon/Icon';
import { SettingsSection } from '@/components/sections/shared/SettingsSection';
import { CustomDomainsInput } from '@/components/sections/shared/CustomDomainsInput';
import { ProviderApiBaseUrlInput } from '@/components/sections/shared/ProviderApiBaseUrlInput';
import { ProviderDetectUrlsInput } from '@/components/sections/shared/ProviderDetectUrlsInput';
import { useGitProviderDomainsStore } from '@/stores/useGitProviderDomainsStore';
const getBaseUrlHost = (baseUrl?: string | null): string => {
if (!baseUrl) return '';
@@ -34,7 +36,11 @@ export const GiteaSettings: React.FC = () => {
const [isBusy, setIsBusy] = React.useState(false);
const [accessToken, setAccessToken] = React.useState('');
const [baseUrl, setBaseUrl] = React.useState('');
// Prefill the connect form with the server-side default API base URL when the
// user has not typed one yet; the per-account base URL still wins on connect.
const [baseUrl, setBaseUrl] = React.useState(
useGitProviderDomainsStore((state) => state.apiBaseUrls.gitea),
);
React.useEffect(() => {
(async () => {
@@ -300,7 +306,10 @@ export const GiteaSettings: React.FC = () => {
)}
</div>
<CustomDomainsInput provider="gitea" />
<div className="flex flex-col gap-4 pt-4">
<ProviderApiBaseUrlInput provider="gitea" />
<ProviderDetectUrlsInput provider="gitea" />
</div>
</SettingsSection>
);
};
@@ -1,50 +0,0 @@
import React from 'react';
import { Input } from '@/components/ui/input';
import { useI18n } from '@/lib/i18n';
import { useGitProviderDomainsStore, type GitProviderName } from '@/stores/useGitProviderDomainsStore';
/**
* Comma-separated custom-domain input for a git provider. Commits (normalizes,
* dedupes, persists) on blur or Enter; the field reflects the persisted,
* normalized list joined by ', '.
*/
export const CustomDomainsInput: React.FC<{ provider: GitProviderName }> = ({ provider }) => {
const { t } = useI18n();
const domains = useGitProviderDomainsStore((state) => state.domains[provider]);
const setDomains = useGitProviderDomainsStore((state) => state.setDomains);
const [value, setValue] = React.useState(domains.join(', '));
React.useEffect(() => {
setValue(domains.join(', '));
}, [domains]);
const commit = React.useCallback(() => {
setDomains(provider, value.split(',').map((entry) => entry.trim()).filter(Boolean));
}, [provider, setDomains, value]);
return (
<div className="flex min-w-0 flex-col gap-1">
<label htmlFor={`${provider}-custom-domains`} className="typography-settings-field-label text-foreground">
{t(`settings.${provider}.page.customDomains.label`)}
</label>
<Input
id={`${provider}-custom-domains`}
type="text"
value={value}
onChange={(event) => setValue(event.target.value)}
onBlur={commit}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
commit();
}
}}
placeholder={t(`settings.${provider}.page.customDomains.placeholder`)}
className="h-9 max-w-[24rem]"
/>
<span className="typography-micro text-muted-foreground">
{t(`settings.${provider}.page.customDomains.description`)}
</span>
</div>
);
};
@@ -0,0 +1,98 @@
import React from 'react';
import { Input } from '@/components/ui/input';
import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { reportSettingsSaveState } from '@/lib/persistence';
import { SettingsStackedField } from '@/components/sections/shared/SettingsSection';
import {
useGitProviderDomainsStore,
type GitProviderName,
} from '@/stores/useGitProviderDomainsStore';
/**
* Persist the full `gitProviders` settings object. The server replaces the
* whole `gitProviders` key on PUT, so every provider must be sent together —
* sending a single provider alone would wipe the others.
*/
// eslint-disable-next-line react-refresh/only-export-components
export const saveGitProvidersConfig = async (): Promise<void> => {
const { domains, apiBaseUrls } = useGitProviderDomainsStore.getState();
const payload = {
gitProviders: {
github: { apiBaseUrl: apiBaseUrls.github, detectUrls: domains.github },
gitlab: { apiBaseUrl: apiBaseUrls.gitlab, detectUrls: domains.gitlab },
gitea: { apiBaseUrl: apiBaseUrls.gitea, detectUrls: domains.gitea },
},
};
reportSettingsSaveState('saving');
try {
const response = await runtimeFetch('/api/config/settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(response.statusText);
}
reportSettingsSaveState('saved');
} catch (error) {
console.warn('Failed to persist git provider settings:', error);
reportSettingsSaveState('error');
}
};
/**
* Server-side default API base URL for a git provider's API calls. A per-account
* base URL still wins once an account is connected. Commits (updates the store
* cache optimistically and persists the full gitProviders object) on blur or
* Enter; the settings round-trip re-hydrates the store afterwards.
*/
export const ProviderApiBaseUrlInput: React.FC<{ provider: GitProviderName }> = ({ provider }) => {
const { t } = useI18n();
const storedValue = useGitProviderDomainsStore((state) => state.apiBaseUrls[provider]);
const setApiBaseUrl = useGitProviderDomainsStore((state) => state.setApiBaseUrl);
const [draft, setDraft] = React.useState(storedValue);
React.useEffect(() => {
setDraft(storedValue);
}, [storedValue]);
const commit = React.useCallback(() => {
const next = draft.trim();
if (next !== storedValue) {
setApiBaseUrl(provider, next);
void saveGitProvidersConfig();
} else {
// Blur with no real change: drop incidental whitespace from the draft.
setDraft(storedValue);
}
}, [draft, provider, setApiBaseUrl, storedValue]);
return (
<SettingsStackedField
label={t(`settings.${provider}.page.apiBaseUrl.label`)}
description={t(`settings.${provider}.page.apiBaseUrl.description`)}
descriptionPlacement="after"
settingsItem={`git.${provider}-api-base-url`}
>
<Input
type="text"
value={draft}
onChange={(event) => setDraft(event.target.value)}
onBlur={commit}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
commit();
}
}}
placeholder={t(`settings.${provider}.page.apiBaseUrl.placeholder`)}
aria-label={t(`settings.${provider}.page.apiBaseUrl.label`)}
className="h-9"
/>
</SettingsStackedField>
);
};
@@ -0,0 +1,118 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/icon/Icon';
import { Input } from '@/components/ui/input';
import { useI18n } from '@/lib/i18n';
import { SettingsStackedField } from '@/components/sections/shared/SettingsSection';
import {
normalizeProviderDomain,
useGitProviderDomainsStore,
type GitProviderName,
} from '@/stores/useGitProviderDomainsStore';
import { saveGitProvidersConfig } from './ProviderApiBaseUrlInput';
/**
* Detection URLs for a git provider: an SSH or HTTPS URL typed into the field
* becomes a chip (displayed as its bare hostname) on Enter or comma, and the X
* on a chip removes it. The hosts feed provider autodetection of repo remotes.
* Each change updates the store cache optimistically and persists the full
* gitProviders object; the settings round-trip re-hydrates the store.
*/
export const ProviderDetectUrlsInput: React.FC<{ provider: GitProviderName }> = ({ provider }) => {
const { t } = useI18n();
const domains = useGitProviderDomainsStore((state) => state.domains[provider]);
const setDomains = useGitProviderDomainsStore((state) => state.setDomains);
const [draft, setDraft] = React.useState('');
const [invalid, setInvalid] = React.useState(false);
const commitDraft = React.useCallback(() => {
const raw = draft.trim();
if (!raw) {
setDraft('');
setInvalid(false);
return;
}
const next = [...domains];
let added = false;
for (const part of raw.split(',')) {
const host = normalizeProviderDomain(part);
if (host && !next.includes(host)) {
next.push(host);
added = true;
}
}
if (added) {
setDomains(provider, next);
setDraft('');
setInvalid(false);
void saveGitProvidersConfig();
} else {
// Unparseable input: keep the draft so the user can fix it.
setInvalid(true);
}
}, [draft, domains, provider, setDomains]);
const removeChip = React.useCallback((host: string) => {
setDomains(provider, domains.filter((entry) => entry !== host));
void saveGitProvidersConfig();
}, [domains, provider, setDomains]);
return (
<SettingsStackedField
label={t(`settings.${provider}.page.detectUrls.label`)}
description={t(`settings.${provider}.page.detectUrls.description`)}
descriptionPlacement="after"
settingsItem={`git.${provider}-detect-urls`}
>
<div className="flex min-w-0 flex-1 flex-col gap-2">
{domains.length > 0 ? (
<div className="flex flex-wrap items-center gap-1.5">
{domains.map((host) => (
<span
key={host}
className="inline-flex h-6 items-center gap-0.5 rounded-md border border-border/60 bg-[var(--surface-elevated)] pl-2 pr-1"
>
<span className="typography-micro font-mono text-foreground">{host}</span>
<Button
type="button"
variant="ghost"
size="xs"
aria-label={t('settings.gitProviders.detectUrls.remove', { host })}
title={t('settings.gitProviders.detectUrls.remove', { host })}
onClick={() => removeChip(host)}
className="h-4 w-4 p-0 text-muted-foreground hover:text-[var(--status-error)]"
>
<Icon name="close" className="size-3" />
</Button>
</span>
))}
</div>
) : null}
<Input
type="text"
value={draft}
onChange={(event) => {
setDraft(event.target.value);
setInvalid(false);
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ',') {
event.preventDefault();
commitDraft();
}
}}
onBlur={commitDraft}
placeholder={t(`settings.${provider}.page.detectUrls.placeholder`)}
aria-label={t('settings.gitProviders.detectUrls.add')}
aria-invalid={invalid || undefined}
className="h-9"
/>
{invalid ? (
<p className="typography-micro text-[var(--status-error)]">
{t('settings.gitProviders.detectUrls.invalid')}
</p>
) : null}
</div>
</SettingsStackedField>
);
};
+7 -11
View File
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { resolveGitProvider, useGitProvider } from '@/lib/gitProvider';
import { resolveGitProvider, useGitProvider, buildGitProviderHosts } from '@/lib/gitProvider';
import type { GitProviderHosts } from '@/lib/gitProvider';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
@@ -10,20 +10,16 @@ import { useGitLabAuthStore } from '@/stores/useGitLabAuthStore';
import { useGitProviderDomainsStore } from '@/stores/useGitProviderDomainsStore';
/**
* Provider-host sets derived from the connected accounts and the
* user-configured custom domains, mirroring the `hosts` memo inside
* `useGitProvider` so the imperative resolver classifies directories the same
* way the hook does.
* Provider-host sets derived from the connected accounts, the configured api
* base urls and the user-configured custom domains, mirroring the `hosts` memo
* inside `useGitProvider` so the imperative resolver classifies directories the
* same way the hook does.
*/
const buildProviderHosts = (): GitProviderHosts => {
const gitlabAccounts = useGitLabAuthStore.getState().status?.accounts;
const giteaAccounts = useGiteaAuthStore.getState().status?.accounts;
const domains = useGitProviderDomainsStore.getState().domains;
return {
github: domains.github,
gitlab: [...(gitlabAccounts ?? []).map((account) => account.baseUrl), ...domains.gitlab],
gitea: [...(giteaAccounts ?? []).map((account) => account.baseUrl), ...domains.gitea],
};
const { domains, apiBaseUrls } = useGitProviderDomainsStore.getState();
return buildGitProviderHosts({ domains, apiBaseUrls, gitlabAccounts, giteaAccounts });
};
/**
+8
View File
@@ -217,6 +217,14 @@ export type DesktopSettings = {
sttModel?: string;
sttLocalModel?: string;
sttLanguage?: string;
// Per-provider git forge configuration (server-side settings.json): the API
// base URL for provider API calls and the bare hosts that auto-detect the
// provider. Server-authoritative; the client stores only a localStorage cache.
gitProviders?: {
github?: { apiBaseUrl?: string; detectUrls?: string[] };
gitlab?: { apiBaseUrl?: string; detectUrls?: string[] };
gitea?: { apiBaseUrl?: string; detectUrls?: string[] };
};
// Global draft welcome starters (pinned commands/skills), persisted to settings.json
draftStarters?: DraftStarterRef[];
draftStartersVisible?: boolean;
+71 -1
View File
@@ -1,8 +1,13 @@
import { describe, expect, test } from 'bun:test';
import { detectGitProvider, type GitProviderHosts } from './gitProvider';
import { buildGitProviderHosts, detectGitProvider, type GitProviderHosts } from './gitProvider';
const EMPTY_HOSTS: GitProviderHosts = { github: [], gitlab: [], gitea: [] };
const emptyInput = {
domains: { github: [], gitlab: [], gitea: [] },
apiBaseUrls: { github: '', gitlab: '', gitea: '' },
};
describe('detectGitProvider', () => {
test('returns null with no remotes', () => {
expect(detectGitProvider([], EMPTY_HOSTS)).toBeNull();
@@ -135,3 +140,68 @@ describe('detectGitProvider', () => {
expect(detectGitProvider(['https://GITHUB.com/owner/repo.git'], EMPTY_HOSTS)).toBe('github');
});
});
describe('buildGitProviderHosts', () => {
test('adds no hosts when nothing is configured', () => {
expect(buildGitProviderHosts(emptyInput)).toEqual(EMPTY_HOSTS);
});
test('auto-adds the github api base host so the provider is detected', () => {
const hosts = buildGitProviderHosts({
...emptyInput,
apiBaseUrls: { github: 'https://github.example.com/api/v3', gitlab: '', gitea: '' },
});
expect(hosts.github).toEqual(['github.example.com']);
expect(detectGitProvider(['git@github.example.com:owner/repo.git'], hosts)).toBe('github');
});
test('a github api base of api.github.com maps to github.com and changes nothing', () => {
const hosts = buildGitProviderHosts({
...emptyInput,
apiBaseUrls: { github: 'https://api.github.com', gitlab: '', gitea: '' },
});
// github.com is already a built-in detection host; no behavior change.
expect(hosts.github).toEqual(['github.com']);
expect(detectGitProvider(['git@github.com:owner/repo.git'], hosts)).toBe('github');
// The api host itself is not a web/remote host.
expect(detectGitProvider(['git@api.github.com:owner/repo.git'], hosts)).toBe('other');
});
test('auto-adds the gitlab api base host so the provider is detected', () => {
const hosts = buildGitProviderHosts({
...emptyInput,
apiBaseUrls: { github: '', gitlab: 'https://gitlab.example.com', gitea: '' },
});
expect(hosts.gitlab).toEqual(['gitlab.example.com']);
expect(detectGitProvider(['git@gitlab.example.com:group/project.git'], hosts)).toBe('gitlab');
});
test('auto-adds the gitea api base host so the provider is detected', () => {
const hosts = buildGitProviderHosts({
...emptyInput,
apiBaseUrls: { github: '', gitlab: '', gitea: 'https://gitea.example.com' },
});
expect(hosts.gitea).toEqual(['gitea.example.com']);
expect(detectGitProvider(['git@gitea.example.com:owner/repo.git'], hosts)).toBe('gitea');
});
test('combines account base urls, api base host and custom domains, normalized and deduped', () => {
const hosts = buildGitProviderHosts({
domains: { github: [], gitlab: ['https://gitlab.example.com', 'gitlab.internal'], gitea: ['codeberg.org'] },
apiBaseUrls: { github: '', gitlab: 'https://gitlab.example.com/', gitea: '' },
gitlabAccounts: [{ baseUrl: 'https://gitlab.example.com' }, { baseUrl: 'ssh://git@gl.other.example.com/x' }],
giteaAccounts: [{ baseUrl: 'https://gitea.example.com' }],
});
expect(hosts.gitlab).toEqual(['gitlab.example.com', 'gl.other.example.com', 'gitlab.internal']);
expect(hosts.gitea).toEqual(['gitea.example.com', 'codeberg.org']);
expect(hosts.github).toEqual([]);
});
test('dedupes the api base host against configured domains', () => {
const hosts = buildGitProviderHosts({
domains: { github: ['github.example.com'], gitlab: [], gitea: [] },
apiBaseUrls: { github: 'https://github.example.com/api/v3', gitlab: '', gitea: '' },
});
expect(hosts.github).toEqual(['github.example.com']);
});
});
+63 -10
View File
@@ -3,15 +3,20 @@ import { parseGitHost } from '@/lib/gitHost';
import { getRemotes } from '@/lib/gitApi';
import { useGitLabAuthStore } from '@/stores/useGitLabAuthStore';
import { useGiteaAuthStore } from '@/stores/useGiteaAuthStore';
import { useGitProviderDomainsStore, normalizeProviderDomain } from '@/stores/useGitProviderDomainsStore';
import {
useGitProviderDomainsStore,
normalizeProviderDomain,
type GitProviderApiBaseUrls,
type GitProviderDomains,
} from '@/stores/useGitProviderDomainsStore';
export type GitProvider = 'github' | 'gitlab' | 'gitea' | 'other';
/**
* Per-provider hostname sets used for detection: custom user-configured
* domains (from the domains store) plus account-derived base-URL hostnames.
* Built-in defaults (github.com, gitlab.com) are applied inside the detection
* logic and never need to be present here.
* domains (from the domains store), account-derived base-URL hostnames, and the
* configured api base host. Built-in defaults (github.com, gitlab.com) are
* applied inside the detection logic and never need to be present here.
*/
export type GitProviderHosts = {
github: string[];
@@ -32,6 +37,57 @@ const normalizeHostList = (hosts: string[] | undefined): string[] => {
return result;
};
const GITHUB_API_HOST = 'api.github.com';
/**
* Hostname of a configured api base URL, or null when unset/unparseable.
* GitHub Enterprise remotes point at the web host, not the api subdomain, so an
* `api.github.com` api base maps back to `github.com` (the built-in web host).
*/
const apiBaseHost = (apiBaseUrl: string | undefined): string | null => {
const host = normalizeProviderDomain(apiBaseUrl ?? '');
if (!host) return null;
return host === GITHUB_API_HOST ? 'github.com' : host;
};
/**
* Build the per-provider detection host sets. Each provider gets its configured
* api base host (auto-added; github's `api.github.com` maps to `github.com`),
* plus the account-derived base-URL hosts (gitlab/gitea) and the custom domains
* from the domains store. All entries are normalized and deduped.
*/
export const buildGitProviderHosts = (input: {
domains: GitProviderDomains;
apiBaseUrls: GitProviderApiBaseUrls;
gitlabAccounts?: Array<{ baseUrl?: string }>;
giteaAccounts?: Array<{ baseUrl?: string }>;
}): GitProviderHosts => {
const githubApiHost = apiBaseHost(input.apiBaseUrls.github);
const gitlabApiHost = apiBaseHost(input.apiBaseUrls.gitlab);
const giteaApiHost = apiBaseHost(input.apiBaseUrls.gitea);
return {
github: normalizeHostList([
...(githubApiHost ? [githubApiHost] : []),
...input.domains.github,
]),
gitlab: normalizeHostList([
...(input.gitlabAccounts ?? [])
.map((account) => account.baseUrl)
.filter((url): url is string => Boolean(url)),
...(gitlabApiHost ? [gitlabApiHost] : []),
...input.domains.gitlab,
]),
gitea: normalizeHostList([
...(input.giteaAccounts ?? [])
.map((account) => account.baseUrl)
.filter((url): url is string => Boolean(url)),
...(giteaApiHost ? [giteaApiHost] : []),
...input.domains.gitea,
]),
};
};
/**
* Classify a repository by the hosts of its remotes. Returns null when there
* are no remotes to inspect. Built-in defaults apply always: `github.com` is
@@ -104,13 +160,10 @@ export const useGitProvider = (directory: string | null | undefined): GitProvide
const gitlabAccounts = useGitLabAuthStore((state) => state.status?.accounts);
const giteaAccounts = useGiteaAuthStore((state) => state.status?.accounts);
const domains = useGitProviderDomainsStore((state) => state.domains);
const apiBaseUrls = useGitProviderDomainsStore((state) => state.apiBaseUrls);
const hosts = useMemo<GitProviderHosts>(
() => ({
github: domains.github,
gitlab: [...(gitlabAccounts ?? []).map((account) => account.baseUrl), ...domains.gitlab],
gitea: [...(giteaAccounts ?? []).map((account) => account.baseUrl), ...domains.gitea],
}),
[domains, gitlabAccounts, giteaAccounts],
() => buildGitProviderHosts({ domains, apiBaseUrls, gitlabAccounts, giteaAccounts }),
[domains, apiBaseUrls, gitlabAccounts, giteaAccounts],
);
const [provider, setProvider] = useState<GitProvider | null>(null);
@@ -1635,9 +1635,12 @@ export const settingsDict = {
'settings.github.page.toast.ghCliEnabled': 'gh CLI Fallback aktiviert',
'settings.github.page.toast.ghCliDisabled': 'gh CLI Fallback deaktiviert',
'settings.github.page.toast.ghCliUpdateFailed': 'Fehler beim Aktualisieren der gh CLI Einstellung',
'settings.github.page.customDomains.label': 'Benutzerdefinierte Domains',
'settings.github.page.customDomains.description': 'Repositories auf diesen Domains werden als GitHub behandelt.',
'settings.github.page.customDomains.placeholder': 'github.example.com, git.mycompany.com',
'settings.github.page.apiBaseUrl.label': 'API-Basis-URL',
'settings.github.page.apiBaseUrl.placeholder': 'https://github.example.com/api/v3',
'settings.github.page.apiBaseUrl.description': 'Standard-Basis-URL für API-Aufrufe. Für GitHub Enterprise verwende die Adresse deines Servers, z. B. https://github.example.com/api/v3.',
'settings.github.page.detectUrls.label': 'Erkennungs-URLs',
'settings.github.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.github.page.detectUrls.description': 'Repositories, die per SSH oder HTTPS von diesen Hosts geklont wurden, werden als GitHub erkannt.',
'settings.gitlab.page.title': 'GitLab Personal Access Token',
'settings.gitlab.page.description': 'Fügen Sie ein GitLab Personal Access Token ein, um eine Verbindung herzustellen. Legen Sie die Basis-URL fest, wenn Sie eine selbst gehostete GitLab-Instanz verwenden.',
'settings.gitlab.page.tooltip.connectAccount': 'Verbinden Sie ein GitLab-Konto für Issue- und Merge-Request-Workflows in der App.',
@@ -1645,9 +1648,12 @@ export const settingsDict = {
'settings.gitlab.page.accessToken.placeholder': 'Fügen Sie Ihr GitLab Personal Access Token ein',
'settings.gitlab.page.baseUrl.label': 'Basis-URL (optional)',
'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com',
'settings.gitlab.page.customDomains.label': 'Benutzerdefinierte Domains',
'settings.gitlab.page.customDomains.description': 'Repositories auf diesen Domains werden als GitLab behandelt.',
'settings.gitlab.page.customDomains.placeholder': 'gitlab.example.com, git.company.com',
'settings.gitlab.page.apiBaseUrl.label': 'API-Basis-URL',
'settings.gitlab.page.apiBaseUrl.placeholder': 'https://gitlab.example.com',
'settings.gitlab.page.apiBaseUrl.description': 'Standard-Basis-URL für API-Aufrufe. Für selbst gehostete Instanzen verwende die Adresse deines Servers, z. B. https://gitlab.example.com.',
'settings.gitlab.page.detectUrls.label': 'Erkennungs-URLs',
'settings.gitlab.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitlab.page.detectUrls.description': 'Repositories, die per SSH oder HTTPS von diesen Hosts geklont wurden, werden als GitLab erkannt.',
'settings.gitlab.page.actions.connect': 'GitLab verbinden',
'settings.gitlab.page.actions.disconnect': 'Trennen',
'settings.gitlab.page.actions.switch': 'Wechseln zu',
@@ -1687,9 +1693,15 @@ export const settingsDict = {
'settings.gitea.page.toast.disconnectFailed': 'Trennen von Gitea fehlgeschlagen',
'settings.gitea.page.toast.accountSwitched': 'Gitea-Konto gewechselt',
'settings.gitea.page.toast.accountSwitchFailed': 'Wechsel des Gitea-Kontos fehlgeschlagen',
'settings.gitea.page.customDomains.label': 'Benutzerdefinierte Domains',
'settings.gitea.page.customDomains.description': 'Repositories auf diesen Domains werden als Gitea oder Forgejo behandelt.',
'settings.gitea.page.customDomains.placeholder': 'gitea.example.com, git.company.com',
'settings.gitea.page.apiBaseUrl.label': 'API-Basis-URL',
'settings.gitea.page.apiBaseUrl.placeholder': 'https://gitea.example.com',
'settings.gitea.page.apiBaseUrl.description': 'Standard-Basis-URL für API-Aufrufe. Für selbst gehostete Instanzen verwende die Adresse deines Servers, z. B. https://gitea.example.com.',
'settings.gitea.page.detectUrls.label': 'Erkennungs-URLs',
'settings.gitea.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitea.page.detectUrls.description': 'Repositories, die per SSH oder HTTPS von diesen Hosts geklont wurden, werden als Gitea oder Forgejo erkannt.',
'settings.gitProviders.detectUrls.add': 'Erkennungs-URL hinzufügen',
'settings.gitProviders.detectUrls.remove': '{host} entfernen',
'settings.gitProviders.detectUrls.invalid': 'Gib eine gültige SSH- oder HTTPS-URL bzw. einen gültigen Hostnamen ein.',
'settings.notifications.page.delivery.title': 'Benachrichtigungsübermittlung',
'settings.notifications.page.delivery.enableAria': 'Benachrichtigungen aktivieren',
'settings.notifications.page.delivery.enableLabel': 'Benachrichtigungen aktivieren',
@@ -1701,9 +1701,12 @@ export const settingsDict = {
'settings.github.page.toast.ghCliEnabled': 'gh CLI fallback enabled',
'settings.github.page.toast.ghCliDisabled': 'gh CLI fallback disabled',
'settings.github.page.toast.ghCliUpdateFailed': 'Failed to update gh CLI setting',
'settings.github.page.customDomains.label': 'Custom domains',
'settings.github.page.customDomains.description': 'Repos hosted on these domains are treated as GitHub.',
'settings.github.page.customDomains.placeholder': 'github.example.com, git.mycompany.com',
'settings.github.page.apiBaseUrl.label': 'API base URL',
'settings.github.page.apiBaseUrl.placeholder': 'https://github.example.com/api/v3',
'settings.github.page.apiBaseUrl.description': 'Default base URL for API calls. For GitHub Enterprise, use your server address, e.g. https://github.example.com/api/v3.',
'settings.github.page.detectUrls.label': 'Detection URLs',
'settings.github.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.github.page.detectUrls.description': 'Repos cloned over SSH or HTTPS from these hosts are recognized as GitHub.',
'settings.gitlab.page.title': 'GitLab Personal Access Token',
'settings.gitlab.page.description': 'Paste a GitLab personal access token to connect. Set the base URL when using a self-hosted GitLab instance.',
'settings.gitlab.page.tooltip.connectAccount': 'Connect a GitLab account for in-app issue and merge request workflows.',
@@ -1711,9 +1714,12 @@ export const settingsDict = {
'settings.gitlab.page.accessToken.placeholder': 'Paste your GitLab personal access token',
'settings.gitlab.page.baseUrl.label': 'Base URL (optional)',
'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com',
'settings.gitlab.page.customDomains.label': 'Custom domains',
'settings.gitlab.page.customDomains.description': 'Repos hosted on these domains are treated as GitLab.',
'settings.gitlab.page.customDomains.placeholder': 'gitlab.example.com, git.company.com',
'settings.gitlab.page.apiBaseUrl.label': 'API base URL',
'settings.gitlab.page.apiBaseUrl.placeholder': 'https://gitlab.example.com',
'settings.gitlab.page.apiBaseUrl.description': 'Default base URL for API calls. For self-hosted instances, use your server address, e.g. https://gitlab.example.com.',
'settings.gitlab.page.detectUrls.label': 'Detection URLs',
'settings.gitlab.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitlab.page.detectUrls.description': 'Repos cloned over SSH or HTTPS from these hosts are recognized as GitLab.',
'settings.gitlab.page.actions.connect': 'Connect GitLab',
'settings.gitlab.page.actions.disconnect': 'Disconnect',
'settings.gitlab.page.actions.switch': 'Switch to',
@@ -1753,9 +1759,15 @@ export const settingsDict = {
'settings.gitea.page.toast.disconnectFailed': 'Failed to disconnect Gitea',
'settings.gitea.page.toast.accountSwitched': 'Gitea account switched',
'settings.gitea.page.toast.accountSwitchFailed': 'Failed to switch Gitea account',
'settings.gitea.page.customDomains.label': 'Custom domains',
'settings.gitea.page.customDomains.description': 'Repos hosted on these domains are treated as Gitea or Forgejo.',
'settings.gitea.page.customDomains.placeholder': 'gitea.example.com, git.company.com',
'settings.gitea.page.apiBaseUrl.label': 'API base URL',
'settings.gitea.page.apiBaseUrl.placeholder': 'https://gitea.example.com',
'settings.gitea.page.apiBaseUrl.description': 'Default base URL for API calls. For self-hosted instances, use your server address, e.g. https://gitea.example.com.',
'settings.gitea.page.detectUrls.label': 'Detection URLs',
'settings.gitea.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitea.page.detectUrls.description': 'Repos cloned over SSH or HTTPS from these hosts are recognized as Gitea or Forgejo.',
'settings.gitProviders.detectUrls.add': 'Add a detection URL',
'settings.gitProviders.detectUrls.remove': 'Remove {host}',
'settings.gitProviders.detectUrls.invalid': 'Enter a valid SSH or HTTPS URL or hostname.',
'settings.notifications.page.delivery.title': 'Notification Delivery',
'settings.notifications.page.delivery.enableAria': 'Enable notifications',
'settings.notifications.page.delivery.enableLabel': 'Enable Notifications',
@@ -1678,9 +1678,12 @@ export const settingsDict = {
"settings.github.page.toast.ghCliEnabled": "Respaldo de gh CLI activado",
"settings.github.page.toast.ghCliDisabled": "Respaldo de gh CLI desactivado",
"settings.github.page.toast.ghCliUpdateFailed": "No se pudo actualizar la configuración de gh CLI",
"settings.github.page.customDomains.label": "Dominios personalizados",
"settings.github.page.customDomains.description": "Los repositorios alojados en estos dominios se tratarán como GitHub.",
"settings.github.page.customDomains.placeholder": "github.example.com, git.mycompany.com",
"settings.github.page.apiBaseUrl.label": "URL base de la API",
"settings.github.page.apiBaseUrl.placeholder": "https://github.example.com/api/v3",
"settings.github.page.apiBaseUrl.description": "URL base por defecto para las llamadas a la API. Para GitHub Enterprise, usa la dirección de tu servidor, p. ej. https://github.example.com/api/v3.",
"settings.github.page.detectUrls.label": "URL de detección",
"settings.github.page.detectUrls.placeholder": "ssh://git@git.example.com:2222, https://git.example.com",
"settings.github.page.detectUrls.description": "Los repositorios clonados por SSH o HTTPS desde estos hosts se reconocen como GitHub.",
"settings.gitlab.page.title": "Token de acceso personal de GitLab",
"settings.gitlab.page.description": "Pega un token de acceso personal de GitLab para conectarte. Establece la URL base cuando uses una instancia de GitLab autoalojada.",
"settings.gitlab.page.tooltip.connectAccount": "Conecta una cuenta de GitLab para los flujos de trabajo de issues y merge requests en la aplicación.",
@@ -1688,9 +1691,12 @@ export const settingsDict = {
"settings.gitlab.page.accessToken.placeholder": "Pega tu token de acceso personal de GitLab",
"settings.gitlab.page.baseUrl.label": "URL base (opcional)",
"settings.gitlab.page.baseUrl.placeholder": "https://gitlab.com",
"settings.gitlab.page.customDomains.label": "Dominios personalizados",
"settings.gitlab.page.customDomains.description": "Los repositorios alojados en estos dominios se tratarán como GitLab.",
"settings.gitlab.page.customDomains.placeholder": "gitlab.example.com, git.company.com",
"settings.gitlab.page.apiBaseUrl.label": "URL base de la API",
"settings.gitlab.page.apiBaseUrl.placeholder": "https://gitlab.example.com",
"settings.gitlab.page.apiBaseUrl.description": "URL base por defecto para las llamadas a la API. Para instancias autoalojadas, usa la dirección de tu servidor, p. ej. https://gitlab.example.com.",
"settings.gitlab.page.detectUrls.label": "URL de detección",
"settings.gitlab.page.detectUrls.placeholder": "ssh://git@git.example.com:2222, https://git.example.com",
"settings.gitlab.page.detectUrls.description": "Los repositorios clonados por SSH o HTTPS desde estos hosts se reconocen como GitLab.",
"settings.gitlab.page.actions.connect": "Conectar GitLab",
"settings.gitlab.page.actions.disconnect": "Desconectar",
"settings.gitlab.page.actions.switch": "Cambiar a",
@@ -1730,9 +1736,15 @@ export const settingsDict = {
"settings.gitea.page.toast.disconnectFailed": "No se pudo desconectar Gitea",
"settings.gitea.page.toast.accountSwitched": "Cuenta de Gitea cambiada",
"settings.gitea.page.toast.accountSwitchFailed": "No se pudo cambiar la cuenta de Gitea",
"settings.gitea.page.customDomains.label": "Dominios personalizados",
"settings.gitea.page.customDomains.description": "Los repositorios alojados en estos dominios se tratarán como Gitea o Forgejo.",
"settings.gitea.page.customDomains.placeholder": "gitea.example.com, git.company.com",
"settings.gitea.page.apiBaseUrl.label": "URL base de la API",
"settings.gitea.page.apiBaseUrl.placeholder": "https://gitea.example.com",
"settings.gitea.page.apiBaseUrl.description": "URL base por defecto para las llamadas a la API. Para instancias autoalojadas, usa la dirección de tu servidor, p. ej. https://gitea.example.com.",
"settings.gitea.page.detectUrls.label": "URL de detección",
"settings.gitea.page.detectUrls.placeholder": "ssh://git@git.example.com:2222, https://git.example.com",
"settings.gitea.page.detectUrls.description": "Los repositorios clonados por SSH o HTTPS desde estos hosts se reconocen como Gitea o Forgejo.",
"settings.gitProviders.detectUrls.add": "Añadir una URL de detección",
"settings.gitProviders.detectUrls.remove": "Quitar {host}",
"settings.gitProviders.detectUrls.invalid": "Introduce una URL SSH o HTTPS o un nombre de host válido.",
"settings.notifications.page.delivery.title": "Entrega de notificaciones",
"settings.notifications.page.delivery.enableAria": "Habilitar notificaciones",
"settings.notifications.page.delivery.enableLabel": "Habilitar notificaciones",
@@ -1596,9 +1596,12 @@ export const settingsDict = {
'settings.github.page.toast.ghCliEnabled': 'Solution de secours gh CLI activée',
'settings.github.page.toast.ghCliDisabled': 'Solution de secours gh CLI désactivée',
'settings.github.page.toast.ghCliUpdateFailed': 'Échec de la mise à jour du paramètre gh CLI',
'settings.github.page.customDomains.label': 'Domaines personnalisés',
'settings.github.page.customDomains.description': 'Les dépôts hébergés sur ces domaines sont traités comme GitHub.',
'settings.github.page.customDomains.placeholder': 'github.example.com, git.mycompany.com',
'settings.github.page.apiBaseUrl.label': 'URL de base de lAPI',
'settings.github.page.apiBaseUrl.placeholder': 'https://github.example.com/api/v3',
'settings.github.page.apiBaseUrl.description': 'URL de base par défaut pour les appels API. Pour GitHub Enterprise, utilisez ladresse de votre serveur, ex. https://github.example.com/api/v3.',
'settings.github.page.detectUrls.label': 'URL de détection',
'settings.github.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.github.page.detectUrls.description': 'Les dépôts clonés en SSH ou HTTPS depuis ces hôtes sont reconnus comme GitHub.',
'settings.gitlab.page.title': 'Jeton d\'accès personnel GitLab',
'settings.gitlab.page.description': 'Collez un jeton d\'accès personnel GitLab pour vous connecter. Définissez l\'URL de base si vous utilisez une instance GitLab auto-hébergée.',
'settings.gitlab.page.tooltip.connectAccount': 'Connectez un compte GitLab pour les workflows d\'issues et de merge requests dans l\'application.',
@@ -1606,9 +1609,12 @@ export const settingsDict = {
'settings.gitlab.page.accessToken.placeholder': 'Collez votre jeton d\'accès personnel GitLab',
'settings.gitlab.page.baseUrl.label': 'URL de base (facultatif)',
'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com',
'settings.gitlab.page.customDomains.label': 'Domaines personnalisés',
'settings.gitlab.page.customDomains.description': 'Les dépôts hébergés sur ces domaines sont traités comme GitLab.',
'settings.gitlab.page.customDomains.placeholder': 'gitlab.example.com, git.company.com',
'settings.gitlab.page.apiBaseUrl.label': 'URL de base de lAPI',
'settings.gitlab.page.apiBaseUrl.placeholder': 'https://gitlab.example.com',
'settings.gitlab.page.apiBaseUrl.description': 'URL de base par défaut pour les appels API. Pour les instances auto-hébergées, utilisez ladresse de votre serveur, ex. https://gitlab.example.com.',
'settings.gitlab.page.detectUrls.label': 'URL de détection',
'settings.gitlab.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitlab.page.detectUrls.description': 'Les dépôts clonés en SSH ou HTTPS depuis ces hôtes sont reconnus comme GitLab.',
'settings.gitlab.page.actions.connect': 'Connecter GitLab',
'settings.gitlab.page.actions.disconnect': 'Déconnecter',
'settings.gitlab.page.actions.switch': 'Passer à',
@@ -1648,9 +1654,15 @@ export const settingsDict = {
'settings.gitea.page.toast.disconnectFailed': 'Échec de la déconnexion du Gitea',
'settings.gitea.page.toast.accountSwitched': 'Le compte Gitea a changé',
'settings.gitea.page.toast.accountSwitchFailed': 'Échec du changement de compte Gitea',
'settings.gitea.page.customDomains.label': 'Domaines personnalisés',
'settings.gitea.page.customDomains.description': 'Les dépôts hébergés sur ces domaines sont traités comme Gitea ou Forgejo.',
'settings.gitea.page.customDomains.placeholder': 'gitea.example.com, git.company.com',
'settings.gitea.page.apiBaseUrl.label': 'URL de base de lAPI',
'settings.gitea.page.apiBaseUrl.placeholder': 'https://gitea.example.com',
'settings.gitea.page.apiBaseUrl.description': 'URL de base par défaut pour les appels API. Pour les instances auto-hébergées, utilisez ladresse de votre serveur, ex. https://gitea.example.com.',
'settings.gitea.page.detectUrls.label': 'URL de détection',
'settings.gitea.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitea.page.detectUrls.description': 'Les dépôts clonés en SSH ou HTTPS depuis ces hôtes sont reconnus comme Gitea ou Forgejo.',
'settings.gitProviders.detectUrls.add': 'Ajouter une URL de détection',
'settings.gitProviders.detectUrls.remove': 'Retirer {host}',
'settings.gitProviders.detectUrls.invalid': 'Saisissez une URL SSH ou HTTPS ou un nom dhôte valide.',
'settings.notifications.page.delivery.title': 'Envoi des notifications',
'settings.notifications.page.delivery.enableAria': 'Activer les notifications',
'settings.notifications.page.delivery.enableLabel': 'Activer les notifications',
@@ -1711,9 +1711,12 @@ export const settingsDict = {
'settings.github.page.toast.ghCliEnabled': 'gh CLI フォールバックを有効化しました',
'settings.github.page.toast.ghCliDisabled': 'gh CLI フォールバックを無効化しました',
'settings.github.page.toast.ghCliUpdateFailed': 'gh CLI 設定の更新に失敗しました',
'settings.github.page.customDomains.label': 'カスタムドメイン',
'settings.github.page.customDomains.description': 'これらのドメインでホストされているリポジトリは GitHub として扱われます。',
'settings.github.page.customDomains.placeholder': 'github.example.com, git.mycompany.com',
'settings.github.page.apiBaseUrl.label': 'API ベース URL',
'settings.github.page.apiBaseUrl.placeholder': 'https://github.example.com/api/v3',
'settings.github.page.apiBaseUrl.description': 'API 呼び出しに使用する既定のベース URL です。GitHub Enterprise の場合はサーバーアドレスを指定します。例: https://github.example.com/api/v3。',
'settings.github.page.detectUrls.label': '検出 URL',
'settings.github.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.github.page.detectUrls.description': 'これらのホストから SSH または HTTPS でクローンしたリポジトリは GitHub として認識されます。',
'settings.gitlab.page.title': 'GitLab パーソナルアクセストークン',
'settings.gitlab.page.description': 'GitLab パーソナルアクセストークンを貼り付けて接続します。セルフホストの GitLab インスタンスを使用する場合はベース URL を設定してください。',
'settings.gitlab.page.tooltip.connectAccount': 'アプリ内の Issue とマージリクエストのワークフロー用に GitLab アカウントを接続します。',
@@ -1721,9 +1724,12 @@ export const settingsDict = {
'settings.gitlab.page.accessToken.placeholder': 'GitLab パーソナルアクセストークンを貼り付け',
'settings.gitlab.page.baseUrl.label': 'ベース URL(任意)',
'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com',
'settings.gitlab.page.customDomains.label': 'カスタムドメイン',
'settings.gitlab.page.customDomains.description': 'これらのドメインでホストされているリポジトリは GitLab として扱われます。',
'settings.gitlab.page.customDomains.placeholder': 'gitlab.example.com, git.company.com',
'settings.gitlab.page.apiBaseUrl.label': 'API ベース URL',
'settings.gitlab.page.apiBaseUrl.placeholder': 'https://gitlab.example.com',
'settings.gitlab.page.apiBaseUrl.description': 'API 呼び出しに使用する既定のベース URL です。セルフホストインスタンスの場合はサーバーアドレスを指定します。例: https://gitlab.example.com',
'settings.gitlab.page.detectUrls.label': '検出 URL',
'settings.gitlab.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitlab.page.detectUrls.description': 'これらのホストから SSH または HTTPS でクローンしたリポジトリは GitLab として認識されます。',
'settings.gitlab.page.actions.connect': 'GitLab に接続',
'settings.gitlab.page.actions.disconnect': '切断',
'settings.gitlab.page.actions.switch': '切り替え',
@@ -1763,9 +1769,15 @@ export const settingsDict = {
'settings.gitea.page.toast.disconnectFailed': 'Gitea の切断に失敗しました',
'settings.gitea.page.toast.accountSwitched': 'Gitea アカウントを切り替えました',
'settings.gitea.page.toast.accountSwitchFailed': 'Gitea アカウントの切り替えに失敗しました',
'settings.gitea.page.customDomains.label': 'カスタムドメイン',
'settings.gitea.page.customDomains.description': 'これらのドメインでホストされているリポジトリは Gitea または Forgejo として扱われます。',
'settings.gitea.page.customDomains.placeholder': 'gitea.example.com, git.company.com',
'settings.gitea.page.apiBaseUrl.label': 'API ベース URL',
'settings.gitea.page.apiBaseUrl.placeholder': 'https://gitea.example.com',
'settings.gitea.page.apiBaseUrl.description': 'API 呼び出しに使用する既定のベース URL です。セルフホストインスタンスの場合はサーバーアドレスを指定します。例: https://gitea.example.com。',
'settings.gitea.page.detectUrls.label': '検出 URL',
'settings.gitea.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitea.page.detectUrls.description': 'これらのホストから SSH または HTTPS でクローンしたリポジトリは Gitea または Forgejo として認識されます。',
'settings.gitProviders.detectUrls.add': '検出 URL を追加',
'settings.gitProviders.detectUrls.remove': '{host} を削除',
'settings.gitProviders.detectUrls.invalid': '有効な SSH または HTTPS の URL またはホスト名を入力してください。',
'settings.notifications.page.delivery.title': '通知配信',
'settings.notifications.page.delivery.enableAria': '通知を有効化',
'settings.notifications.page.delivery.enableLabel': '通知を有効化',
@@ -1678,9 +1678,12 @@ export const settingsDict = {
'settings.github.page.toast.ghCliEnabled': 'gh CLI 대체 활성화됨',
'settings.github.page.toast.ghCliDisabled': 'gh CLI 대체 비활성화됨',
'settings.github.page.toast.ghCliUpdateFailed': 'gh CLI 설정을 업데이트하지 못했습니다',
'settings.github.page.customDomains.label': '사용자 지정 도메인',
'settings.github.page.customDomains.description': '이 도메인에서 호스팅되는 저장소는 GitHub로 취급됩니다.',
'settings.github.page.customDomains.placeholder': 'github.example.com, git.mycompany.com',
'settings.github.page.apiBaseUrl.label': 'API 기본 URL',
'settings.github.page.apiBaseUrl.placeholder': 'https://github.example.com/api/v3',
'settings.github.page.apiBaseUrl.description': 'API 호출에 사용되는 기본 URL입니다. GitHub Enterprise를 사용하는 경우 서버 주소를 입력하세요(예: https://github.example.com/api/v3).',
'settings.github.page.detectUrls.label': '감지 URL',
'settings.github.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.github.page.detectUrls.description': '이 호스트에서 SSH 또는 HTTPS로 복제한 저장소는 GitHub로 인식됩니다.',
'settings.gitlab.page.title': 'GitLab 개인 액세스 토큰',
'settings.gitlab.page.description': '연결하려면 GitLab 개인 액세스 토큰을 붙여넣으세요. 자체 호스팅 GitLab 인스턴스를 사용하는 경우 기본 URL을 설정하세요.',
'settings.gitlab.page.tooltip.connectAccount': '앱 내 이슈 및 병합 요청 워크플로에 GitLab 계정을 연결합니다.',
@@ -1688,9 +1691,12 @@ export const settingsDict = {
'settings.gitlab.page.accessToken.placeholder': 'GitLab 개인 액세스 토큰을 붙여넣으세요',
'settings.gitlab.page.baseUrl.label': '기본 URL(선택 사항)',
'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com',
'settings.gitlab.page.customDomains.label': '사용자 지정 도메인',
'settings.gitlab.page.customDomains.description': '이 도메인에서 호스팅되는 저장소는 GitLab로 취급됩니다.',
'settings.gitlab.page.customDomains.placeholder': 'gitlab.example.com, git.company.com',
'settings.gitlab.page.apiBaseUrl.label': 'API 기본 URL',
'settings.gitlab.page.apiBaseUrl.placeholder': 'https://gitlab.example.com',
'settings.gitlab.page.apiBaseUrl.description': 'API 호출에 사용되는 기본 URL입니다. 자체 호스팅 인스턴스의 경우 서버 주소를 입력하세요(예: https://gitlab.example.com).',
'settings.gitlab.page.detectUrls.label': '감지 URL',
'settings.gitlab.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitlab.page.detectUrls.description': '이 호스트에서 SSH 또는 HTTPS로 복제한 저장소는 GitLab으로 인식됩니다.',
'settings.gitlab.page.actions.connect': 'GitLab 연결',
'settings.gitlab.page.actions.disconnect': '연결 해제',
'settings.gitlab.page.actions.switch': '전환',
@@ -1730,9 +1736,15 @@ export const settingsDict = {
'settings.gitea.page.toast.disconnectFailed': 'Gitea 연결을 해제하지 못했습니다',
'settings.gitea.page.toast.accountSwitched': 'Gitea 계정이 전환되었습니다',
'settings.gitea.page.toast.accountSwitchFailed': 'Gitea 계정을 전환하지 못했습니다',
'settings.gitea.page.customDomains.label': '사용자 지정 도메인',
'settings.gitea.page.customDomains.description': '이 도메인에서 호스팅되는 저장소는 Gitea 또는 Forgejo로 취급됩니다.',
'settings.gitea.page.customDomains.placeholder': 'gitea.example.com, git.company.com',
'settings.gitea.page.apiBaseUrl.label': 'API 기본 URL',
'settings.gitea.page.apiBaseUrl.placeholder': 'https://gitea.example.com',
'settings.gitea.page.apiBaseUrl.description': 'API 호출에 사용되는 기본 URL입니다. 자체 호스팅 인스턴스의 경우 서버 주소를 입력하세요(예: https://gitea.example.com).',
'settings.gitea.page.detectUrls.label': '감지 URL',
'settings.gitea.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitea.page.detectUrls.description': '이 호스트에서 SSH 또는 HTTPS로 복제한 저장소는 Gitea 또는 Forgejo로 인식됩니다.',
'settings.gitProviders.detectUrls.add': '감지 URL 추가',
'settings.gitProviders.detectUrls.remove': '{host} 제거',
'settings.gitProviders.detectUrls.invalid': '유효한 SSH 또는 HTTPS URL이나 호스트 이름을 입력하세요.',
'settings.notifications.page.delivery.title': '알림',
'settings.notifications.page.delivery.enableAria': '알림 활성화',
'settings.notifications.page.delivery.enableLabel': '알림 활성화',
@@ -318,9 +318,12 @@ export const settingsDict = {
'settings.github.page.toast.ghCliEnabled': 'Rezerwa gh CLI włączona',
'settings.github.page.toast.ghCliDisabled': 'Rezerwa gh CLI wyłączona',
'settings.github.page.toast.ghCliUpdateFailed': 'Nie udało się zaktualizować ustawienia gh CLI',
'settings.github.page.customDomains.label': 'Niestandardowe domeny',
'settings.github.page.customDomains.description': 'Repozytoria hostowane na tych domenach będą traktowane jako GitHub.',
'settings.github.page.customDomains.placeholder': 'github.example.com, git.mycompany.com',
'settings.github.page.apiBaseUrl.label': 'Adres URL API',
'settings.github.page.apiBaseUrl.placeholder': 'https://github.example.com/api/v3',
'settings.github.page.apiBaseUrl.description': 'Domyślny adres URL dla wywołań API. W przypadku GitHub Enterprise podaj adres swojego serwera, np. https://github.example.com/api/v3.',
'settings.github.page.detectUrls.label': 'Adresy URL wykrywania',
'settings.github.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.github.page.detectUrls.description': 'Repozytoria klonowane przez SSH lub HTTPS z tych hostów są rozpoznawane jako GitHub.',
'settings.github.page.oauth.title': 'Token OAuth GitHub',
'settings.github.page.ghCli.title': 'Token CLI GitHub',
'settings.github.page.ghCli.activeDescription': 'Uwierzytelniono przez gh CLI',
@@ -336,9 +339,12 @@ export const settingsDict = {
'settings.gitlab.page.accessToken.placeholder': 'Wklej swój osobisty token dostępu GitLab',
'settings.gitlab.page.baseUrl.label': 'Podstawowy URL (opcjonalnie)',
'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com',
'settings.gitlab.page.customDomains.label': 'Niestandardowe domeny',
'settings.gitlab.page.customDomains.description': 'Repozytoria hostowane na tych domenach będą traktowane jako GitLab.',
'settings.gitlab.page.customDomains.placeholder': 'gitlab.example.com, git.company.com',
'settings.gitlab.page.apiBaseUrl.label': 'Adres URL API',
'settings.gitlab.page.apiBaseUrl.placeholder': 'https://gitlab.example.com',
'settings.gitlab.page.apiBaseUrl.description': 'Domyślny adres URL dla wywołań API. W przypadku instancji hostowanych samodzielnie podaj adres swojego serwera, np. https://gitlab.example.com.',
'settings.gitlab.page.detectUrls.label': 'Adresy URL wykrywania',
'settings.gitlab.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitlab.page.detectUrls.description': 'Repozytoria klonowane przez SSH lub HTTPS z tych hostów są rozpoznawane jako GitLab.',
'settings.gitlab.page.actions.connect': 'Połącz GitLab',
'settings.gitlab.page.actions.disconnect': 'Odłącz',
'settings.gitlab.page.actions.switch': 'Przełącz na',
@@ -378,9 +384,15 @@ export const settingsDict = {
'settings.gitea.page.toast.disconnectFailed': 'Nie udało się odłączyć Gitea',
'settings.gitea.page.toast.accountSwitched': 'Konto Gitea zostało przełączone',
'settings.gitea.page.toast.accountSwitchFailed': 'Nie udało się przełączyć konta Gitea',
'settings.gitea.page.customDomains.label': 'Niestandardowe domeny',
'settings.gitea.page.customDomains.description': 'Repozytoria hostowane na tych domenach będą traktowane jako Gitea lub Forgejo.',
'settings.gitea.page.customDomains.placeholder': 'gitea.example.com, git.company.com',
'settings.gitea.page.apiBaseUrl.label': 'Adres URL API',
'settings.gitea.page.apiBaseUrl.placeholder': 'https://gitea.example.com',
'settings.gitea.page.apiBaseUrl.description': 'Domyślny adres URL dla wywołań API. W przypadku instancji hostowanych samodzielnie podaj adres swojego serwera, np. https://gitea.example.com.',
'settings.gitea.page.detectUrls.label': 'Adresy URL wykrywania',
'settings.gitea.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitea.page.detectUrls.description': 'Repozytoria klonowane przez SSH lub HTTPS z tych hostów są rozpoznawane jako Gitea lub Forgejo.',
'settings.gitProviders.detectUrls.add': 'Dodaj adres URL wykrywania',
'settings.gitProviders.detectUrls.remove': 'Usuń {host}',
'settings.gitProviders.detectUrls.invalid': 'Podaj prawidłowy adres URL SSH lub HTTPS albo nazwę hosta.',
'settings.magicPrompts.page.actions.resetAllOverrides': 'Zresetuj wszystkie nadpisania',
'settings.magicPrompts.page.actions.resetToDefault': 'Zresetuj do domyślnych',
'settings.magicPrompts.page.actions.resetting': 'Resetowanie...',
@@ -1678,9 +1678,12 @@ export const settingsDict = {
"settings.github.page.toast.ghCliEnabled": "Alternativa gh CLI ativada",
"settings.github.page.toast.ghCliDisabled": "Alternativa gh CLI desativada",
"settings.github.page.toast.ghCliUpdateFailed": "Falha ao atualizar configuração do gh CLI",
"settings.github.page.customDomains.label": "Domínios personalizados",
"settings.github.page.customDomains.description": "Repositórios hospedados nesses domínios serão tratados como GitHub.",
"settings.github.page.customDomains.placeholder": "github.example.com, git.mycompany.com",
"settings.github.page.apiBaseUrl.label": "URL base da API",
"settings.github.page.apiBaseUrl.placeholder": "https://github.example.com/api/v3",
"settings.github.page.apiBaseUrl.description": "URL base padrão para chamadas de API. Para GitHub Enterprise, use o endereço do seu servidor, ex.: https://github.example.com/api/v3.",
"settings.github.page.detectUrls.label": "URLs de detecção",
"settings.github.page.detectUrls.placeholder": "ssh://git@git.example.com:2222, https://git.example.com",
"settings.github.page.detectUrls.description": "Repositórios clonados via SSH ou HTTPS desses hosts são reconhecidos como GitHub.",
"settings.gitlab.page.title": "Token de acesso pessoal do GitLab",
"settings.gitlab.page.description": "Cole um token de acesso pessoal do GitLab para conectar. Defina a URL base ao usar uma instância GitLab auto-hospedada.",
"settings.gitlab.page.tooltip.connectAccount": "Conecte uma conta do GitLab para fluxos de trabalho de issues e merge requests no aplicativo.",
@@ -1688,9 +1691,12 @@ export const settingsDict = {
"settings.gitlab.page.accessToken.placeholder": "Cole seu token de acesso pessoal do GitLab",
"settings.gitlab.page.baseUrl.label": "URL base (opcional)",
"settings.gitlab.page.baseUrl.placeholder": "https://gitlab.com",
"settings.gitlab.page.customDomains.label": "Domínios personalizados",
"settings.gitlab.page.customDomains.description": "Repositórios hospedados nesses domínios serão tratados como GitLab.",
"settings.gitlab.page.customDomains.placeholder": "gitlab.example.com, git.company.com",
"settings.gitlab.page.apiBaseUrl.label": "URL base da API",
"settings.gitlab.page.apiBaseUrl.placeholder": "https://gitlab.example.com",
"settings.gitlab.page.apiBaseUrl.description": "URL base padrão para chamadas de API. Para instâncias auto-hospedadas, use o endereço do seu servidor, ex.: https://gitlab.example.com.",
"settings.gitlab.page.detectUrls.label": "URLs de detecção",
"settings.gitlab.page.detectUrls.placeholder": "ssh://git@git.example.com:2222, https://git.example.com",
"settings.gitlab.page.detectUrls.description": "Repositórios clonados via SSH ou HTTPS desses hosts são reconhecidos como GitLab.",
"settings.gitlab.page.actions.connect": "Conectar GitLab",
"settings.gitlab.page.actions.disconnect": "Desconectar",
"settings.gitlab.page.actions.switch": "Alternar para",
@@ -1730,9 +1736,15 @@ export const settingsDict = {
"settings.gitea.page.toast.disconnectFailed": "Falha ao desconectar o Gitea",
"settings.gitea.page.toast.accountSwitched": "Conta do Gitea alterada",
"settings.gitea.page.toast.accountSwitchFailed": "Falha ao alternar a conta do Gitea",
"settings.gitea.page.customDomains.label": "Domínios personalizados",
"settings.gitea.page.customDomains.description": "Repositórios hospedados nesses domínios serão tratados como Gitea ou Forgejo.",
"settings.gitea.page.customDomains.placeholder": "gitea.example.com, git.company.com",
"settings.gitea.page.apiBaseUrl.label": "URL base da API",
"settings.gitea.page.apiBaseUrl.placeholder": "https://gitea.example.com",
"settings.gitea.page.apiBaseUrl.description": "URL base padrão para chamadas de API. Para instâncias auto-hospedadas, use o endereço do seu servidor, ex.: https://gitea.example.com.",
"settings.gitea.page.detectUrls.label": "URLs de detecção",
"settings.gitea.page.detectUrls.placeholder": "ssh://git@git.example.com:2222, https://git.example.com",
"settings.gitea.page.detectUrls.description": "Repositórios clonados via SSH ou HTTPS desses hosts são reconhecidos como Gitea ou Forgejo.",
"settings.gitProviders.detectUrls.add": "Adicionar uma URL de detecção",
"settings.gitProviders.detectUrls.remove": "Remover {host}",
"settings.gitProviders.detectUrls.invalid": "Digite uma URL SSH ou HTTPS ou um nome de host válido.",
"settings.notifications.page.delivery.title": "Entrega de notificações",
"settings.notifications.page.delivery.enableAria": "Ativar notificações",
"settings.notifications.page.delivery.enableLabel": "Ativar notificações",
@@ -1678,9 +1678,12 @@ export const settingsDict = {
"settings.github.page.toast.ghCliEnabled": "Резервний варіант gh CLI увімкнено",
"settings.github.page.toast.ghCliDisabled": "Резервний варіант gh CLI вимкнено",
"settings.github.page.toast.ghCliUpdateFailed": "Не вдалося оновити налаштування gh CLI",
"settings.github.page.customDomains.label": "Власні домени",
"settings.github.page.customDomains.description": "Репозиторії, розміщені на цих доменах, вважатимуться GitHub.",
"settings.github.page.customDomains.placeholder": "github.example.com, git.mycompany.com",
"settings.github.page.apiBaseUrl.label": "Базова URL-адреса API",
"settings.github.page.apiBaseUrl.placeholder": "https://github.example.com/api/v3",
"settings.github.page.apiBaseUrl.description": "Типова базова URL-адреса для викликів API. Для GitHub Enterprise вкажіть адресу свого сервера, напр. https://github.example.com/api/v3.",
"settings.github.page.detectUrls.label": "URL-адреси виявлення",
"settings.github.page.detectUrls.placeholder": "ssh://git@git.example.com:2222, https://git.example.com",
"settings.github.page.detectUrls.description": "Репозиторії, клоновані через SSH або HTTPS із цих хостів, розпізнаються як GitHub.",
"settings.gitlab.page.title": "Персональний токен доступу GitLab",
"settings.gitlab.page.description": "Вставте персональний токен доступу GitLab, щоб підключитися. Вкажіть базову URL-адресу, якщо використовуєте самостійно розміщену інстанцію GitLab.",
"settings.gitlab.page.tooltip.connectAccount": "Підключіть обліковий запис GitLab для роботи з issues та merge requests в застосунку.",
@@ -1688,9 +1691,12 @@ export const settingsDict = {
"settings.gitlab.page.accessToken.placeholder": "Вставте ваш персональний токен доступу GitLab",
"settings.gitlab.page.baseUrl.label": "Базова URL-адреса (необов'язково)",
"settings.gitlab.page.baseUrl.placeholder": "https://gitlab.com",
"settings.gitlab.page.customDomains.label": "Власні домени",
"settings.gitlab.page.customDomains.description": "Репозиторії, розміщені на цих доменах, вважатимуться GitLab.",
"settings.gitlab.page.customDomains.placeholder": "gitlab.example.com, git.company.com",
"settings.gitlab.page.apiBaseUrl.label": "Базова URL-адреса API",
"settings.gitlab.page.apiBaseUrl.placeholder": "https://gitlab.example.com",
"settings.gitlab.page.apiBaseUrl.description": "Типова базова URL-адреса для викликів API. Для саморозміщених екземплярів вкажіть адресу свого сервера, напр. https://gitlab.example.com.",
"settings.gitlab.page.detectUrls.label": "URL-адреси виявлення",
"settings.gitlab.page.detectUrls.placeholder": "ssh://git@git.example.com:2222, https://git.example.com",
"settings.gitlab.page.detectUrls.description": "Репозиторії, клоновані через SSH або HTTPS із цих хостів, розпізнаються як GitLab.",
"settings.gitlab.page.actions.connect": "Підключити GitLab",
"settings.gitlab.page.actions.disconnect": "Відключити",
"settings.gitlab.page.actions.switch": "Перемкнути на",
@@ -1730,9 +1736,15 @@ export const settingsDict = {
"settings.gitea.page.toast.disconnectFailed": "Не вдалося відключити Gitea",
"settings.gitea.page.toast.accountSwitched": "Обліковий запис Gitea перемкнено",
"settings.gitea.page.toast.accountSwitchFailed": "Не вдалося перемкнути обліковий запис Gitea",
"settings.gitea.page.customDomains.label": "Власні домени",
"settings.gitea.page.customDomains.description": "Репозиторії, розміщені на цих доменах, вважатимуться Gitea або Forgejo.",
"settings.gitea.page.customDomains.placeholder": "gitea.example.com, git.company.com",
"settings.gitea.page.apiBaseUrl.label": "Базова URL-адреса API",
"settings.gitea.page.apiBaseUrl.placeholder": "https://gitea.example.com",
"settings.gitea.page.apiBaseUrl.description": "Типова базова URL-адреса для викликів API. Для саморозміщених екземплярів вкажіть адресу свого сервера, напр. https://gitea.example.com.",
"settings.gitea.page.detectUrls.label": "URL-адреси виявлення",
"settings.gitea.page.detectUrls.placeholder": "ssh://git@git.example.com:2222, https://git.example.com",
"settings.gitea.page.detectUrls.description": "Репозиторії, клоновані через SSH або HTTPS із цих хостів, розпізнаються як Gitea або Forgejo.",
"settings.gitProviders.detectUrls.add": "Додати URL-адресу виявлення",
"settings.gitProviders.detectUrls.remove": "Видалити {host}",
"settings.gitProviders.detectUrls.invalid": "Введіть коректну URL-адресу SSH або HTTPS чи ім'я хоста.",
"settings.notifications.page.delivery.title": "Доставка сповіщень",
"settings.notifications.page.delivery.enableAria": "Увімкнути сповіщення",
"settings.notifications.page.delivery.enableLabel": "Увімкнути сповіщення",
@@ -1678,9 +1678,12 @@ export const settingsDict = {
'settings.github.page.toast.ghCliEnabled': 'gh CLI 备用已启用',
'settings.github.page.toast.ghCliDisabled': 'gh CLI 备用已禁用',
'settings.github.page.toast.ghCliUpdateFailed': '更新 gh CLI 设置失败',
'settings.github.page.customDomains.label': '自定义域名',
'settings.github.page.customDomains.description': '托管在这些域名上的仓库将被视为 GitHub',
'settings.github.page.customDomains.placeholder': 'github.example.com, git.mycompany.com',
'settings.github.page.apiBaseUrl.label': 'API 基础 URL',
'settings.github.page.apiBaseUrl.placeholder': 'https://github.example.com/api/v3',
'settings.github.page.apiBaseUrl.description': 'API 调用的默认基础 URL。GitHub Enterprise 用户可填写服务器地址,例如 https://github.example.com/api/v3。',
'settings.github.page.detectUrls.label': '检测 URL',
'settings.github.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.github.page.detectUrls.description': '通过这些主机以 SSH 或 HTTPS 方式克隆的仓库将被识别为 GitHub。',
'settings.gitlab.page.title': 'GitLab 个人访问令牌',
'settings.gitlab.page.description': '粘贴 GitLab 个人访问令牌以连接。使用自托管 GitLab 实例时,请设置基础 URL。',
'settings.gitlab.page.tooltip.connectAccount': '连接 GitLab 账户,以便在应用内使用 Issue 和合并请求工作流。',
@@ -1688,9 +1691,12 @@ export const settingsDict = {
'settings.gitlab.page.accessToken.placeholder': '粘贴你的 GitLab 个人访问令牌',
'settings.gitlab.page.baseUrl.label': '基础 URL(可选)',
'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com',
'settings.gitlab.page.customDomains.label': '自定义域名',
'settings.gitlab.page.customDomains.description': '托管在这些域名上的仓库将被视为 GitLab',
'settings.gitlab.page.customDomains.placeholder': 'gitlab.example.com, git.company.com',
'settings.gitlab.page.apiBaseUrl.label': 'API 基础 URL',
'settings.gitlab.page.apiBaseUrl.placeholder': 'https://gitlab.example.com',
'settings.gitlab.page.apiBaseUrl.description': 'API 调用的默认基础 URL。自托管实例请填写服务器地址,例如 https://gitlab.example.com',
'settings.gitlab.page.detectUrls.label': '检测 URL',
'settings.gitlab.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitlab.page.detectUrls.description': '通过这些主机以 SSH 或 HTTPS 方式克隆的仓库将被识别为 GitLab。',
'settings.gitlab.page.actions.connect': '连接 GitLab',
'settings.gitlab.page.actions.disconnect': '断开连接',
'settings.gitlab.page.actions.switch': '切换到',
@@ -1730,9 +1736,15 @@ export const settingsDict = {
'settings.gitea.page.toast.disconnectFailed': '断开 Gitea 失败',
'settings.gitea.page.toast.accountSwitched': 'Gitea 账户已切换',
'settings.gitea.page.toast.accountSwitchFailed': '切换 Gitea 账户失败',
'settings.gitea.page.customDomains.label': '自定义域名',
'settings.gitea.page.customDomains.description': '托管在这些域名上的仓库将被视为 Gitea 或 Forgejo。',
'settings.gitea.page.customDomains.placeholder': 'gitea.example.com, git.company.com',
'settings.gitea.page.apiBaseUrl.label': 'API 基础 URL',
'settings.gitea.page.apiBaseUrl.placeholder': 'https://gitea.example.com',
'settings.gitea.page.apiBaseUrl.description': 'API 调用的默认基础 URL。自托管实例请填写服务器地址,例如 https://gitea.example.com。',
'settings.gitea.page.detectUrls.label': '检测 URL',
'settings.gitea.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitea.page.detectUrls.description': '通过这些主机以 SSH 或 HTTPS 方式克隆的仓库将被识别为 Gitea 或 Forgejo。',
'settings.gitProviders.detectUrls.add': '添加检测 URL',
'settings.gitProviders.detectUrls.remove': '移除 {host}',
'settings.gitProviders.detectUrls.invalid': '请输入有效的 SSH 或 HTTPS URL 或主机名。',
'settings.notifications.page.delivery.title': '通知投递',
'settings.notifications.page.delivery.enableAria': '启用通知',
'settings.notifications.page.delivery.enableLabel': '启用通知',
@@ -1585,9 +1585,12 @@ export const settingsDict = {
'settings.github.page.toast.ghCliEnabled': 'gh CLI 備用已啟用',
'settings.github.page.toast.ghCliDisabled': 'gh CLI 備用已停用',
'settings.github.page.toast.ghCliUpdateFailed': '更新 gh CLI 設定失敗',
'settings.github.page.customDomains.label': '自訂網域',
'settings.github.page.customDomains.description': '託管在這些網域上的存放庫將視為 GitHub',
'settings.github.page.customDomains.placeholder': 'github.example.com, git.mycompany.com',
'settings.github.page.apiBaseUrl.label': 'API 基礎 URL',
'settings.github.page.apiBaseUrl.placeholder': 'https://github.example.com/api/v3',
'settings.github.page.apiBaseUrl.description': 'API 呼叫的預設基礎 URL。GitHub Enterprise 使用者可填寫伺服器位址,例如 https://github.example.com/api/v3。',
'settings.github.page.detectUrls.label': '偵測 URL',
'settings.github.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.github.page.detectUrls.description': '透過這些主機以 SSH 或 HTTPS 方式複製的存放庫將識別為 GitHub。',
'settings.gitlab.page.title': 'GitLab 個人存取權杖',
'settings.gitlab.page.description': '貼上 GitLab 個人存取權杖以連線。使用自架 GitLab 執行個體時,請設定基礎 URL。',
'settings.gitlab.page.tooltip.connectAccount': '連線 GitLab 帳號,以在應用程式內使用 Issue 與合併請求工作流程。',
@@ -1595,9 +1598,12 @@ export const settingsDict = {
'settings.gitlab.page.accessToken.placeholder': '貼上你的 GitLab 個人存取權杖',
'settings.gitlab.page.baseUrl.label': '基礎 URL(選用)',
'settings.gitlab.page.baseUrl.placeholder': 'https://gitlab.com',
'settings.gitlab.page.customDomains.label': '自訂網域',
'settings.gitlab.page.customDomains.description': '託管在這些網域上的存放庫將視為 GitLab',
'settings.gitlab.page.customDomains.placeholder': 'gitlab.example.com, git.company.com',
'settings.gitlab.page.apiBaseUrl.label': 'API 基礎 URL',
'settings.gitlab.page.apiBaseUrl.placeholder': 'https://gitlab.example.com',
'settings.gitlab.page.apiBaseUrl.description': 'API 呼叫的預設基礎 URL。自架執行個體請填寫伺服器位址,例如 https://gitlab.example.com',
'settings.gitlab.page.detectUrls.label': '偵測 URL',
'settings.gitlab.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitlab.page.detectUrls.description': '透過這些主機以 SSH 或 HTTPS 方式複製的存放庫將識別為 GitLab。',
'settings.gitlab.page.actions.connect': '連線 GitLab',
'settings.gitlab.page.actions.disconnect': '中斷連線',
'settings.gitlab.page.actions.switch': '切換到',
@@ -1637,9 +1643,15 @@ export const settingsDict = {
'settings.gitea.page.toast.disconnectFailed': '中斷 Gitea 失敗',
'settings.gitea.page.toast.accountSwitched': 'Gitea 帳號已切換',
'settings.gitea.page.toast.accountSwitchFailed': '切換 Gitea 帳號失敗',
'settings.gitea.page.customDomains.label': '自訂網域',
'settings.gitea.page.customDomains.description': '託管在這些網域上的存放庫將視為 Gitea 或 Forgejo。',
'settings.gitea.page.customDomains.placeholder': 'gitea.example.com, git.company.com',
'settings.gitea.page.apiBaseUrl.label': 'API 基礎 URL',
'settings.gitea.page.apiBaseUrl.placeholder': 'https://gitea.example.com',
'settings.gitea.page.apiBaseUrl.description': 'API 呼叫的預設基礎 URL。自架執行個體請填寫伺服器位址,例如 https://gitea.example.com。',
'settings.gitea.page.detectUrls.label': '偵測 URL',
'settings.gitea.page.detectUrls.placeholder': 'ssh://git@git.example.com:2222, https://git.example.com',
'settings.gitea.page.detectUrls.description': '透過這些主機以 SSH 或 HTTPS 方式複製的存放庫將識別為 Gitea 或 Forgejo。',
'settings.gitProviders.detectUrls.add': '新增偵測 URL',
'settings.gitProviders.detectUrls.remove': '移除 {host}',
'settings.gitProviders.detectUrls.invalid': '請輸入有效的 SSH 或 HTTPS URL 或主機名稱。',
'settings.notifications.page.delivery.title': '通知傳遞',
'settings.notifications.page.delivery.enableAria': '啟用通知',
'settings.notifications.page.delivery.enableLabel': '啟用通知',
+12 -1
View File
@@ -30,7 +30,10 @@ const sessionWith = (linked: unknown): Session =>
({ metadata: { openchamber: { linked_issues: linked } } } as unknown as Session);
const resetStores = () => {
useGitProviderDomainsStore.setState({ domains: { github: [], gitlab: [], gitea: [] } });
useGitProviderDomainsStore.setState({
domains: { github: [], gitlab: [], gitea: [] },
apiBaseUrls: { github: '', gitlab: '', gitea: '' },
});
useGiteaAuthStore.setState({ status: null });
useGitLabAuthStore.setState({ status: null });
};
@@ -274,6 +277,14 @@ describe('deriveLinkedIssueProvider', () => {
expect(deriveLinkedIssueProvider('https://git.example.com/owner/repo/pulls/5')).toBe('gitea');
});
test('derives a self-hosted github host from the configured api base url', () => {
useGitProviderDomainsStore.setState({
domains: { github: [], gitlab: [], gitea: [] },
apiBaseUrls: { github: 'https://github.example.com/api/v3', gitlab: '', gitea: '' },
});
expect(deriveLinkedIssueProvider('https://github.example.com/owner/repo/issues/1')).toBe('github');
});
test('derives a self-hosted gitlab host from the domains store', () => {
useGitProviderDomainsStore.setState({
domains: { github: [], gitlab: ['gitlab.example.com'], gitea: [] },
+18 -21
View File
@@ -1,9 +1,10 @@
import type { Session } from '@opencode-ai/sdk/v2';
import type { ForgeProviderKind } from '@/lib/forge/types';
import { parseGitHost } from '@/lib/gitHost';
import { buildGitProviderHosts } from '@/lib/gitProvider';
import { useGiteaAuthStore } from '@/stores/useGiteaAuthStore';
import { useGitLabAuthStore } from '@/stores/useGitLabAuthStore';
import { normalizeProviderDomain, useGitProviderDomainsStore } from '@/stores/useGitProviderDomainsStore';
import { useGitProviderDomainsStore } from '@/stores/useGitProviderDomainsStore';
import { getSessionMetadata, type SessionMetadataRecord } from './sessionReviewMetadata';
/**
@@ -119,10 +120,11 @@ const getIssueUrlHost = (url: string): string | null => {
/**
* Which forge a link url belongs to. Well-known hosts resolve without any
* state; self-hosted hosts resolve through the connected auth accounts' base
* urls and the user-configured domains, in precedence order github -> gitlab ->
* gitea. Returns null when nothing is known never a guess, so github-branded
* UI is not offered for an unknown host. github.com is matched first so it can
* never be mistaken for a gitea host.
* urls, the configured api base urls and the user-configured domains, in
* precedence order github -> gitlab -> gitea. Returns null when nothing is
* known never a guess, so github-branded UI is not offered for an unknown
* host. github.com is matched first so it can never be mistaken for a gitea
* host.
*/
export const deriveLinkedIssueProvider = (url: string): ForgeProviderKind | null => {
const host = getIssueUrlHost(url);
@@ -132,23 +134,18 @@ export const deriveLinkedIssueProvider = (url: string): ForgeProviderKind | null
if (host === 'gitlab.com') return 'gitlab';
if (host === 'gitea.com') return 'gitea';
const giteaAccountHosts = (useGiteaAuthStore.getState().status?.accounts ?? [])
.map((account) => normalizeProviderDomain(account.baseUrl))
.filter((candidate): candidate is string => candidate !== null);
if (giteaAccountHosts.includes(host)) return 'gitea';
const { domains, apiBaseUrls } = useGitProviderDomainsStore.getState();
const gitlabAccounts = useGitLabAuthStore.getState().status?.accounts;
const giteaAccounts = useGiteaAuthStore.getState().status?.accounts;
// github.com is covered by the built-in match above; the configured GitHub
// api base host (GitHub Enterprise) is auto-added here via
// `buildGitProviderHosts`, so a GitHub link on a self-hosted instance
// resolves without any separate chip or account entry.
const hosts = buildGitProviderHosts({ domains, apiBaseUrls, gitlabAccounts, giteaAccounts });
const gitlabAccountHosts = (useGitLabAuthStore.getState().status?.accounts ?? [])
.map((account) => normalizeProviderDomain(account.baseUrl))
.filter((candidate): candidate is string => candidate !== null);
if (gitlabAccountHosts.includes(host)) return 'gitlab';
// GitHub accounts carry no base URL (they are github.com-only, which the
// built-in match above already handles), so there is no host to consult.
const { domains } = useGitProviderDomainsStore.getState();
if (domains.github.includes(host)) return 'github';
if (domains.gitlab.includes(host)) return 'gitlab';
if (domains.gitea.includes(host)) return 'gitea';
if (hosts.github.includes(host)) return 'github';
if (hosts.gitlab.includes(host)) return 'gitlab';
if (hosts.gitea.includes(host)) return 'gitea';
return null;
};
@@ -11,7 +11,10 @@ import {
} from './linkedSessionMatches';
const resetStores = () => {
useGitProviderDomainsStore.setState({ domains: { github: [], gitlab: [], gitea: [] } });
useGitProviderDomainsStore.setState({
domains: { github: [], gitlab: [], gitea: [] },
apiBaseUrls: { github: '', gitlab: '', gitea: '' },
});
useGiteaAuthStore.setState({ status: null });
useGitLabAuthStore.setState({ status: null });
};
+53
View File
@@ -3,6 +3,11 @@ import { sanitizeWorkStatusHiddenSections } from '@/components/chat/work-status/
import { createProjectIdFromPath } from '@/lib/projectId';
import { useUIStore } from '@/stores/useUIStore';
import { isMonoFontOption, isUiFontOption } from '@/lib/fontOptions';
import {
useGitProviderDomainsStore,
normalizeApiBaseUrl,
normalizeDomainList,
} from '@/stores/useGitProviderDomainsStore';
import {
DEFAULT_FOLLOW_UP_BEHAVIOR,
isFollowUpBehavior,
@@ -323,6 +328,39 @@ const sanitizeStringArray = (value: unknown): string[] | undefined => {
return Array.from(new Set(value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)));
};
const GIT_PROVIDER_NAMES = ['github', 'gitlab', 'gitea'] as const;
const sanitizeGitProviders = (value: unknown): DesktopSettings['gitProviders'] | undefined => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined;
}
const source = value as Record<string, unknown>;
const result: NonNullable<DesktopSettings['gitProviders']> = {};
let hasAny = false;
for (const provider of GIT_PROVIDER_NAMES) {
const entry = source[provider];
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
continue;
}
const record = entry as Record<string, unknown>;
const providerConfig: { apiBaseUrl?: string; detectUrls?: string[] } = {};
const apiBaseUrl = normalizeApiBaseUrl(record.apiBaseUrl);
if (apiBaseUrl) {
providerConfig.apiBaseUrl = apiBaseUrl;
}
// detectUrls are bare hosts (scheme/port/path stripped) via parseGitHost.
const detectUrls = normalizeDomainList(record.detectUrls);
if (detectUrls.length > 0) {
providerConfig.detectUrls = detectUrls;
}
if (Object.keys(providerConfig).length > 0) {
result[provider] = providerConfig;
hasAny = true;
}
}
return hasAny ? result : undefined;
};
const sanitizeRecentEfforts = (value: unknown): Record<string, string[]> | undefined => {
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
const result: Record<string, string[]> = {};
@@ -1617,6 +1655,11 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
result.sttLanguage = candidate.sttLanguage.trim();
}
const gitProviders = sanitizeGitProviders(candidate.gitProviders);
if (gitProviders) {
result.gitProviders = gitProviders;
}
return result;
};
@@ -1794,6 +1837,16 @@ export const syncDesktopSettings = async (): Promise<void> => {
} catch (error) {
console.warn('applyDesktopUiPreferences failed:', error);
}
try {
// Server `gitProviders` settings are authoritative for provider api base
// urls and detect hosts; hydrate the domains store (localStorage stays a
// cache for the one-time migration of pre-feature custom domains).
if (settings.gitProviders !== undefined) {
useGitProviderDomainsStore.getState().hydrateFromServer(settings.gitProviders);
}
} catch (error) {
console.warn('applyGitProviderSettings failed:', error);
}
const migrationPatch: Partial<DesktopSettings> = {};
if (shouldPersistCraftGoalMigration) {
if (authoritativeSettings.draftStarters) {
+42
View File
@@ -501,6 +501,48 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
titleKey: 'settings.gitlab.page.actions.connect',
keywords: ['gitlab', 'account', 'pat', 'personal access token', 'issues', 'merge requests'],
},
{
id: 'git.github-api-base-url',
page: 'git',
titleKey: 'settings.github.page.apiBaseUrl.label',
descriptionKey: 'settings.github.page.apiBaseUrl.description',
keywords: ['github', 'api', 'base url', 'enterprise', 'self-hosted', 'server'],
},
{
id: 'git.github-detect-urls',
page: 'git',
titleKey: 'settings.github.page.detectUrls.label',
descriptionKey: 'settings.github.page.detectUrls.description',
keywords: ['github', 'detect', 'remote', 'host', 'domain', 'ssh', 'url', 'self-hosted'],
},
{
id: 'git.gitlab-api-base-url',
page: 'git',
titleKey: 'settings.gitlab.page.apiBaseUrl.label',
descriptionKey: 'settings.gitlab.page.apiBaseUrl.description',
keywords: ['gitlab', 'api', 'base url', 'self-hosted', 'server', 'instance'],
},
{
id: 'git.gitlab-detect-urls',
page: 'git',
titleKey: 'settings.gitlab.page.detectUrls.label',
descriptionKey: 'settings.gitlab.page.detectUrls.description',
keywords: ['gitlab', 'detect', 'remote', 'host', 'domain', 'ssh', 'url', 'self-hosted'],
},
{
id: 'git.gitea-api-base-url',
page: 'git',
titleKey: 'settings.gitea.page.apiBaseUrl.label',
descriptionKey: 'settings.gitea.page.apiBaseUrl.description',
keywords: ['gitea', 'forgejo', 'api', 'base url', 'self-hosted', 'server', 'instance'],
},
{
id: 'git.gitea-detect-urls',
page: 'git',
titleKey: 'settings.gitea.page.detectUrls.label',
descriptionKey: 'settings.gitea.page.detectUrls.description',
keywords: ['gitea', 'forgejo', 'detect', 'remote', 'host', 'domain', 'ssh', 'url', 'self-hosted'],
},
{
id: 'git.identities',
page: 'git',
+19
View File
@@ -12,6 +12,7 @@ import { useSessionUIStore } from "@/sync/session-ui-store";
import { useSelectionStore } from "@/sync/selection-store";
import { getRegisteredRuntimeAPIs } from "@/contexts/runtimeAPIRegistry";
import { updateDesktopSettings } from "@/lib/persistence";
import { useGitProviderDomainsStore } from "@/stores/useGitProviderDomainsStore";
import { useDirectoryStore } from "@/stores/useDirectoryStore";
import { useProjectsStore } from "@/stores/useProjectsStore";
import { resolveProjectForSessionDirectory } from "@/lib/projectResolution";
@@ -65,6 +66,8 @@ interface OpenChamberDefaults {
sttModel?: string;
sttLocalModel?: string;
sttLanguage?: string;
/** Raw `gitProviders` section of server settings (per-provider apiBaseUrl/detectUrls). */
gitProviders?: unknown;
}
const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
@@ -103,6 +106,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
const sttModel = typeof data?.sttModel === 'string' ? data.sttModel.trim() : undefined;
const sttLocalModel = typeof data?.sttLocalModel === 'string' ? data.sttLocalModel.trim() : undefined;
const sttLanguage = typeof data?.sttLanguage === 'string' ? data.sttLanguage.trim() : undefined;
const gitProviders = data?.gitProviders;
return finish('runtime-settings', {
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
@@ -118,6 +122,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
sttModel,
sttLocalModel,
sttLanguage,
gitProviders,
});
}
} catch {
@@ -149,6 +154,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
const sttModel = typeof data?.sttModel === 'string' ? data.sttModel.trim() : undefined;
const sttLocalModel = typeof data?.sttLocalModel === 'string' ? data.sttLocalModel.trim() : undefined;
const sttLanguage = typeof data?.sttLanguage === 'string' ? data.sttLanguage.trim() : undefined;
const gitProviders = data?.gitProviders;
return finish('settings-route', {
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
@@ -164,6 +170,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
sttModel,
sttLocalModel,
sttLanguage,
gitProviders,
});
} catch (error) {
markStartupTrace('config.defaults:error', { error: error instanceof Error ? error.message : String(error) });
@@ -2008,6 +2015,18 @@ export const useConfigStore = create<ConfigStore>()(
const safeAgents = Array.isArray(agents) ? agents : [];
// Seed the git provider domains store from the server
// `gitProviders` settings (authoritative); the runtime
// settings path or the fetch path above provided them.
// Failure here must not break the agent load.
if (openChamberDefaults.gitProviders !== undefined) {
try {
useGitProviderDomainsStore.getState().hydrateFromServer(openChamberDefaults.gitProviders);
} catch {
// Ignore — non-authoritative state stays as-is.
}
}
const providerLoad = _inFlightProviders.get(directoryKey);
if (providerLoad) {
markStartupTrace('loadAgents:awaitProviders', { directoryKey, source });
@@ -4,6 +4,7 @@ import { useGitProviderDomainsStore } from './useGitProviderDomainsStore';
const resetDomains = () => {
useGitProviderDomainsStore.setState({
domains: { github: [], gitlab: [], gitea: [] },
apiBaseUrls: { github: '', gitlab: '', gitea: '' },
});
};
@@ -36,4 +37,101 @@ describe('useGitProviderDomainsStore', () => {
]);
expect(useGitProviderDomainsStore.getState().domains.gitlab).toEqual(['gitlab.example.com']);
});
test('setApiBaseUrl defaults to an empty string', () => {
resetDomains();
const store = useGitProviderDomainsStore.getState();
store.setApiBaseUrl('github', '');
store.setApiBaseUrl('gitlab', ' ');
expect(useGitProviderDomainsStore.getState().apiBaseUrls).toEqual({
github: '',
gitlab: '',
gitea: '',
});
});
test('setApiBaseUrl strips trailing slashes but keeps scheme and path', () => {
resetDomains();
const store = useGitProviderDomainsStore.getState();
store.setApiBaseUrl('github', ' https://github.example.com/api/v3/ ');
store.setApiBaseUrl('gitlab', 'https://gitlab.example.com');
store.setApiBaseUrl('gitea', 'gitea.example.com/path//');
expect(useGitProviderDomainsStore.getState().apiBaseUrls).toEqual({
github: 'https://github.example.com/api/v3',
gitlab: 'https://gitlab.example.com',
gitea: 'gitea.example.com/path',
});
});
test('hydrateFromServer sets apiBaseUrls and domains from the server config', () => {
resetDomains();
useGitProviderDomainsStore.getState().hydrateFromServer({
github: { apiBaseUrl: 'https://github.example.com/api/v3/', detectUrls: ['github.example.com', 'ssh://git@gh.example.com/x'] },
gitlab: { detectUrls: [] },
});
const { domains, apiBaseUrls } = useGitProviderDomainsStore.getState();
expect(apiBaseUrls).toEqual({
github: 'https://github.example.com/api/v3',
gitlab: '',
gitea: '',
});
expect(domains.github).toEqual(['github.example.com', 'gh.example.com']);
expect(domains.gitlab).toEqual([]);
expect(domains.gitea).toEqual([]);
});
test('hydrateFromServer ignores malformed config', () => {
resetDomains();
useGitProviderDomainsStore.getState().hydrateFromServer({
github: 'not an object',
gitlab: { apiBaseUrl: 42, detectUrls: 'nope' },
});
const { domains, apiBaseUrls } = useGitProviderDomainsStore.getState();
expect(apiBaseUrls).toEqual({ github: '', gitlab: '', gitea: '' });
expect(domains).toEqual({ github: [], gitlab: [], gitea: [] });
});
test('hydrateFromServer with no config keeps cached domains and clears nothing', () => {
resetDomains();
useGitProviderDomainsStore.getState().setDomains('github', ['github.example.com']);
useGitProviderDomainsStore.getState().setApiBaseUrl('github', 'https://github.example.com/api/v3');
useGitProviderDomainsStore.getState().hydrateFromServer(undefined);
const { domains, apiBaseUrls } = useGitProviderDomainsStore.getState();
expect(domains.github).toEqual(['github.example.com']);
expect(apiBaseUrls.github).toBe('');
});
test('server detect urls win over cached domains', () => {
resetDomains();
useGitProviderDomainsStore.getState().setDomains('gitea', ['codeberg.org']);
useGitProviderDomainsStore.getState().hydrateFromServer({
gitea: { detectUrls: ['gitea.example.com'] },
});
expect(useGitProviderDomainsStore.getState().domains.gitea).toEqual(['gitea.example.com']);
});
test('migration keeps cached domains when the server lacks detect urls', () => {
resetDomains();
useGitProviderDomainsStore.getState().setDomains('gitea', ['codeberg.org']);
useGitProviderDomainsStore.getState().hydrateFromServer({
gitea: { apiBaseUrl: 'https://gitea.example.com/api/v1' },
});
const { domains, apiBaseUrls } = useGitProviderDomainsStore.getState();
// Server provided an api base but no detect urls: cached domains survive.
expect(domains.gitea).toEqual(['codeberg.org']);
expect(apiBaseUrls.gitea).toBe('https://gitea.example.com/api/v1');
});
test('migration applies per provider: configured providers win, cached ones survive', () => {
resetDomains();
useGitProviderDomainsStore.getState().setDomains('github', ['gh.example.com']);
useGitProviderDomainsStore.getState().setDomains('gitlab', ['gitlab.example.com']);
useGitProviderDomainsStore.getState().hydrateFromServer({
github: { detectUrls: ['github.example.com'] },
});
const { domains } = useGitProviderDomainsStore.getState();
expect(domains.github).toEqual(['github.example.com']);
expect(domains.gitlab).toEqual(['gitlab.example.com']);
expect(domains.gitea).toEqual([]);
});
});
@@ -15,9 +15,27 @@ export type GitProviderDomains = {
gitea: string[];
};
/**
* Per-provider configured API base URLs, as entered in server settings
* (`gitProviders.<provider>.apiBaseUrl`). Normalized: empty string when unset,
* otherwise trimmed with any trailing slashes stripped but scheme and path kept
* (e.g. `https://github.example.com/api/v3`).
*/
export type GitProviderApiBaseUrls = {
github: string;
gitlab: string;
gitea: string;
};
const DOMAINS_STORAGE_KEY = 'openchamber.git-provider-domains';
const EMPTY_DOMAINS: GitProviderDomains = { github: [], gitlab: [], gitea: [] };
const EMPTY_API_BASE_URLS: GitProviderApiBaseUrls = { github: '', gitlab: '', gitea: '' };
const isRecord = (value: unknown): value is Record<string, unknown> =>
Boolean(value && typeof value === 'object' && !Array.isArray(value));
const GIT_PROVIDERS = ['github', 'gitlab', 'gitea'] as const;
/**
* Normalize a raw user-supplied domain into a bare hostname. Accepts plain
@@ -27,7 +45,11 @@ const EMPTY_DOMAINS: GitProviderDomains = { github: [], gitlab: [], gitea: [] };
*/
export const normalizeProviderDomain = (raw: string): string | null => parseGitHost(raw);
const normalizeDomainList = (entries: unknown): string[] => {
/**
* Normalize arbitrary input into a list of bare, deduped hostnames. Non-array
* input yields `[]`; each entry runs through `normalizeProviderDomain`.
*/
export const normalizeDomainList = (entries: unknown): string[] => {
if (!Array.isArray(entries)) {
return [];
}
@@ -43,15 +65,55 @@ const normalizeDomainList = (entries: unknown): string[] => {
return result;
};
/**
* Normalize a configured API base URL: empty (or non-string) input collapses to
* `''`; otherwise trim and strip trailing slashes while keeping scheme + path.
*/
export const normalizeApiBaseUrl = (raw: unknown): string => {
const value = typeof raw === 'string' ? raw.trim() : '';
if (!value) {
return '';
}
return value.replace(/\/+$/, '');
};
/**
* Normalize a server `gitProviders` config into per-provider `{ apiBaseUrl,
* detectUrls }` pairs. Unknown or malformed entries are dropped; provider keys
* outside the known set are ignored.
*/
const normalizeGitProvidersConfig = (config: unknown): {
apiBaseUrls: GitProviderApiBaseUrls;
domains: GitProviderDomains;
} => {
const apiBaseUrls: GitProviderApiBaseUrls = { ...EMPTY_API_BASE_URLS };
const domains: GitProviderDomains = { github: [], gitlab: [], gitea: [] };
if (!isRecord(config)) {
return { apiBaseUrls, domains };
}
for (const provider of GIT_PROVIDERS) {
const entry = config[provider];
if (!isRecord(entry)) continue;
apiBaseUrls[provider] = normalizeApiBaseUrl(entry.apiBaseUrl);
domains[provider] = normalizeDomainList(entry.detectUrls);
}
return { apiBaseUrls, domains };
};
type GitProviderDomainsStore = {
domains: GitProviderDomains;
apiBaseUrls: GitProviderApiBaseUrls;
setDomains: (provider: GitProviderName, domains: string[]) => void;
setApiBaseUrl: (provider: GitProviderName, url: string) => void;
/** Apply the server's `gitProviders` settings, keeping the server authoritative. */
hydrateFromServer: (gitProvidersConfig?: unknown) => void;
};
export const useGitProviderDomainsStore = create<GitProviderDomainsStore>()(
persist(
(set, get) => ({
domains: EMPTY_DOMAINS,
apiBaseUrls: EMPTY_API_BASE_URLS,
setDomains: (provider, domains) => {
set({
domains: {
@@ -60,21 +122,53 @@ export const useGitProviderDomainsStore = create<GitProviderDomainsStore>()(
},
} as Partial<GitProviderDomainsStore>);
},
setApiBaseUrl: (provider, url) => {
set({
apiBaseUrls: {
...get().apiBaseUrls,
[provider]: normalizeApiBaseUrl(url),
},
} as Partial<GitProviderDomainsStore>);
},
hydrateFromServer: (gitProvidersConfig) => {
const { apiBaseUrls, domains } = normalizeGitProvidersConfig(gitProvidersConfig);
const currentDomains = get().domains;
// One-time migration: when the server has no detect urls for a provider,
// keep whatever was previously persisted locally (the localStorage cache,
// hydrated into `domains`) so existing users' custom domains are not lost
// on upgrade. Whenever the server carries detect urls it wins outright,
// and the persist middleware keeps mirroring state back to localStorage
// as a cache — the server stays authoritative on later hydrates.
for (const provider of GIT_PROVIDERS) {
if (domains[provider].length === 0 && currentDomains[provider].length > 0) {
domains[provider] = currentDomains[provider];
}
}
set({ domains, apiBaseUrls } as Partial<GitProviderDomainsStore>);
},
}),
{
name: DOMAINS_STORAGE_KEY,
storage: createDeferredSafeJSONStorage(),
partialize: (state) => ({ domains: state.domains }),
partialize: (state) => ({ domains: state.domains, apiBaseUrls: state.apiBaseUrls }),
merge: (persistedState, currentState) => {
const persisted = (persistedState as { domains?: Partial<GitProviderDomains> } | null)?.domains;
const persisted = (persistedState as {
domains?: Partial<GitProviderDomains>;
apiBaseUrls?: Partial<GitProviderApiBaseUrls>;
} | null);
return {
...currentState,
// Missing or malformed entries collapse to empty arrays so the full
// three-provider shape is always produced after hydration.
// Missing or malformed entries collapse to the canonical shape so the
// full three-provider shape is always produced after hydration.
domains: {
github: normalizeDomainList(persisted?.github),
gitlab: normalizeDomainList(persisted?.gitlab),
gitea: normalizeDomainList(persisted?.gitea),
github: normalizeDomainList(persisted?.domains?.github),
gitlab: normalizeDomainList(persisted?.domains?.gitlab),
gitea: normalizeDomainList(persisted?.domains?.gitea),
},
apiBaseUrls: {
github: normalizeApiBaseUrl(persisted?.apiBaseUrls?.github),
gitlab: normalizeApiBaseUrl(persisted?.apiBaseUrls?.gitlab),
gitea: normalizeApiBaseUrl(persisted?.apiBaseUrls?.gitea),
},
};
},