From 4de1c61ec6f396c3d009d9df8e9da026b8584690 Mon Sep 17 00:00:00 2001 From: Tsavo Knott Date: Fri, 30 Oct 2026 20:36:02 -0400 Subject: [PATCH 1/5] fix(usage): sign new accounts in only to dirs Paradev creates Add account built the new profile's home from the profile name, which started as "work" and fell back to "work" when empty, and ran the CLI login against whatever was there. `codex login` logs out and revokes the login already in its home before the browser step, so a name that matched an existing home ended that login, even when the sign-in was cancelled. The command now creates the home with plain mkdir and writes a .superset-created marker (the agent id) before the login runs. It reuses a dir only when that dir has the marker and still holds no login, so Try again keeps working; anything else exits before the CLI starts. The check runs in the user's shell, so it also covers the command shown for remote hosts. The API-key variants get the same guard. The dialog has no default name, keeps Sign in disabled until a name is entered, and says when the name's home is already a discovered login instead of offering to sign in to it. While a profile is being added, a login that was already at that path when the dialog opened is never taken as the new sign-in. Before, a token refresh in that home could read as a finished sign-in, and the dialog then provisioned that home. Co-Authored-By: Claude Opus 5.5 --- .../AddAccountDialog/AddAccountDialog.tsx | 47 ++-- .../utils/profileName/index.ts | 5 + .../utils/profileName/profileName.test.ts | 53 ++++ .../utils/profileName/profileName.ts | 30 +++ .../addAccountCommand.test.ts | 230 ++++++++++++++++-- .../addAccountCommand/addAccountCommand.ts | 43 +++- .../findCompletedLogin.test.ts | 18 ++ .../findCompletedLogin/findCompletedLogin.ts | 5 + apps/docs/content/docs/usage.mdx | 2 +- packages/i18n/locales/cs/messages.po | 3 + packages/i18n/locales/de/messages.po | 3 + packages/i18n/locales/en/messages.po | 3 + packages/i18n/locales/es/messages.po | 3 + packages/i18n/locales/fr/messages.po | 3 + packages/i18n/locales/id/messages.po | 3 + packages/i18n/locales/it/messages.po | 3 + packages/i18n/locales/ja/messages.po | 3 + packages/i18n/locales/ko/messages.po | 3 + packages/i18n/locales/nl/messages.po | 3 + packages/i18n/locales/pl/messages.po | 3 + packages/i18n/locales/pt-BR/messages.po | 3 + packages/i18n/locales/ru/messages.po | 3 + packages/i18n/locales/tr/messages.po | 3 + packages/i18n/locales/vi/messages.po | 3 + packages/i18n/locales/zh-CN/messages.po | 3 + packages/i18n/locales/zh-TW/messages.po | 3 + 26 files changed, 436 insertions(+), 48 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/utils/profileName/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/utils/profileName/profileName.test.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/utils/profileName/profileName.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/AddAccountDialog.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/AddAccountDialog.tsx index 76a9dfe0c5a..b9dd40f21a7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/AddAccountDialog.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/AddAccountDialog.tsx @@ -34,6 +34,11 @@ import { } from "../../utils/findCompletedLogin"; import { switchSignInCommand } from "../../utils/switchSignInCommand"; import type { ManagedAgent } from "../../utils/visibleQuotaAgents"; +import { + isProfileHomeTaken, + profileHome, + slugifyProfileName, +} from "./utils/profileName"; type Agent = ManagedAgent; @@ -52,15 +57,6 @@ export interface SwitchSignInTarget { label: string; } -function slugify(name: string): string { - return ( - name - .toLowerCase() - .replace(/[^a-z0-9-]+/g, "-") - .replace(/^-+|-+$/g, "") || "work" - ); -} - interface AddAccountDialogProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -104,22 +100,30 @@ export function AddAccountDialog({ const agent = switchTarget?.agent ?? addAgent; const billingId = useId(); const nameId = useId(); - const [name, setName] = useState("work"); + const nameErrorId = useId(); + const [name, setName] = useState(""); const [credentialKind, setCredentialKind] = useState("subscription"); const [copied, setCopied] = useState(false); const [found, setFound] = useState(null); const baselineRef = useRef(null); - const slug = slugify(name); + const slug = slugifyProfileName(name); + const profileDir = `~/.${agent}-${slug}`; const loginsQuery = useHostUsageLogins(hostUrl, open && !found); const setDefault = useSetDefaultUsageAccount(hostUrl); + const homeTaken = + !switchTarget && + !!loginsQuery.data && + isProfileHomeTaken(loginsQuery.data, agent, slug); + const canSignIn = !!switchTarget || (slug !== "" && !homeTaken); // The first poll result after opening is the baseline; anything beyond it // is the sign-in we are waiting for. useEffect(() => { if (!open) { baselineRef.current = null; + setName(""); setFound(null); setCopied(false); setTerminalAttempt(0); @@ -149,7 +153,7 @@ export function AddAccountDialog({ const selection = switchTarget ? switchTarget.selection - : `${logins.homeDir}/.${agent}-${slug}`; + : profileHome(logins.homeDir, agent, slug); const fresh = findCompletedLogin({ agent, credentialKind: switchTarget?.credentialKind ?? credentialKind, @@ -157,6 +161,7 @@ export function AddAccountDialog({ baseline: baselineRef.current, current: logins, commandSucceeded, + newProfile: !switchTarget, }); if (fresh) { setFound(fresh); @@ -357,8 +362,20 @@ export function AddAccountDialog({ onChange={(event) => setName(event.target.value)} className="h-9 min-w-0 flex-1 text-sm" placeholder="work" + aria-invalid={homeTaken || undefined} + aria-describedby={homeTaken ? nameErrorId : undefined} /> + {homeTaken && ( +

+ + {profileDir} already exists. Choose another name. + +

+ )}
Billing @@ -397,7 +414,7 @@ export function AddAccountDialog({
)} - {!canRunLocally && commandDetails} + {!canRunLocally && canSignIn && commandDetails} {canRunLocally && open && terminalAttempt > 0 && (
@@ -432,7 +449,7 @@ export function AddAccountDialog({
)} {terminalAttempt === 0 && - (!canRunLocally || loginsQuery.isError) && + ((!canRunLocally && canSignIn) || loginsQuery.isError) && loginStatus}
@@ -471,7 +488,7 @@ export function AddAccountDialog({ {canRunLocally && !terminalRunning && (
- {homeTaken && ( + {(homeTaken || nameUnusable) && (

- - {profileDir} already exists. Choose another name. - + {homeTaken ? ( + + {profileDir} already exists. Choose another name. + + ) : ( + Use letters A–Z, digits, or hyphens. + )}

)}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/utils/profileName/profileName.test.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/utils/profileName/profileName.test.ts index 17d8b5c21f1..1553615fc27 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/utils/profileName/profileName.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AddAccountDialog/utils/profileName/profileName.test.ts @@ -10,6 +10,7 @@ const logins: UsageLogins = { email: "person@example.com", credentialKind: "subscription", fingerprint: "token-one", + createdByParadev: true, }, ], codex: [ @@ -17,11 +18,13 @@ const logins: UsageLogins = { home: "/home/test/.codex", fingerprint: null, credentialKind: "subscription", + createdByParadev: false, }, { home: "/home/test/.codex-personal", fingerprint: "token-two", credentialKind: "subscription", + createdByParadev: false, }, ], claudeDefaultEmail: null, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/utils/findCompletedLogin/findCompletedLogin.test.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/utils/findCompletedLogin/findCompletedLogin.test.ts index c0ce80af0e4..b3e0376e062 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/utils/findCompletedLogin/findCompletedLogin.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/utils/findCompletedLogin/findCompletedLogin.test.ts @@ -15,6 +15,7 @@ const profile = { email: "person@example.com", credentialKind: "subscription" as const, fingerprint: "token-one", + createdByParadev: true, }; const check = ( current: UsageLogins, @@ -73,6 +74,26 @@ describe("findCompletedLogin", () => { true, ), ).toBeNull()); + it("does not take a login in a dir the add command did not create as a new profile", () => + expect( + check( + { ...empty, claude: [{ ...profile, createdByParadev: false }] }, + empty, + true, + true, + ), + ).toBeNull()); + it("accepts a new profile from a host that does not report who created the dir", () => { + const { createdByParadev: _, ...fromOlderHost } = profile; + expect( + check( + { ...empty, claude: [fromOlderHost as typeof profile] }, + empty, + true, + true, + )?.selection, + ).toBe(selection); + }); it("still requires credentials after CLI success", () => expect( check( @@ -150,6 +171,7 @@ describe("findCompletedLogin", () => { home: "/home/test/.codex-work", credentialKind: "subscription", fingerprint: null, + createdByParadev: true, }, ], }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/utils/findCompletedLogin/findCompletedLogin.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/utils/findCompletedLogin/findCompletedLogin.ts index 5e74687e888..da46bd98b4d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/utils/findCompletedLogin/findCompletedLogin.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/utils/findCompletedLogin/findCompletedLogin.ts @@ -24,7 +24,8 @@ export function findCompletedLogin({ current: UsageLogins; commandSucceeded: boolean; /** Adding a profile: a login already at `selection` in the baseline is - * not the new sign-in. */ + * not the new sign-in, and neither is one in a dir that the add command + * did not create. */ newProfile: boolean; }): FoundLogin | null { const read = (logins: UsageLogins) => { @@ -34,6 +35,7 @@ export function findCompletedLogin({ fingerprint: logins.claudeDefaultFingerprint, label: logins.claudeDefaultEmail ?? "Claude Code", credentialKind: "subscription", + createdByParadev: false, }; const entry = logins.claude.find( (login) => login.configDir === selection, @@ -51,6 +53,8 @@ export function findCompletedLogin({ const after = read(current); if (!after?.fingerprint || after.credentialKind !== credentialKind) return null; + // Hosts older than createdByParadev leave it out. + if (newProfile && after.createdByParadev === false) return null; if (!commandSucceeded && after.fingerprint === before?.fingerprint) return null; return { selection, label: after.label, credentialKind }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/hooks/useHostUsageLogins/useHostUsageLogins.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/hooks/useHostUsageLogins/useHostUsageLogins.ts index 6bb4f5c9771..9b6100f3a37 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/hooks/useHostUsageLogins/useHostUsageLogins.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/hooks/useHostUsageLogins/useHostUsageLogins.ts @@ -13,10 +13,17 @@ const LOGIN_POLL_INTERVAL_MS = 3_000; * the new profile lands on disk. The poll keeps running while the window is * unfocused: the user is off in a terminal (or a browser tab) finishing the * sign-in for exactly as long as this query is enabled. + * + * Each `opening` of the dialog polls its own query, so its first result is + * never one cached by an earlier opening. */ -export function useHostUsageLogins(hostUrl: string | null, enabled: boolean) { +export function useHostUsageLogins( + hostUrl: string | null, + enabled: boolean, + opening = 0, +) { return useQuery({ - queryKey: ["host-usage-logins", hostUrl], + queryKey: ["host-usage-logins", hostUrl, opening], enabled: !!hostUrl && enabled, queryFn: () => { if (!hostUrl) return null; diff --git a/packages/host-service/src/trpc/router/usage/fixtures/logins-created-marker.checks.ts b/packages/host-service/src/trpc/router/usage/fixtures/logins-created-marker.checks.ts new file mode 100644 index 00000000000..94a37f783c7 --- /dev/null +++ b/packages/host-service/src/trpc/router/usage/fixtures/logins-created-marker.checks.ts @@ -0,0 +1,54 @@ +import { expect, it } from "bun:test"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { homedir, tmpdir } from "node:os"; +import { join } from "node:path"; +import type { HostServiceContext } from "../../../../types"; +import { PROFILE_CREATED_MARKER } from "../profiles"; +import { usageRouter } from "../usage"; + +// Run by usage.test.ts with HOME set to a temporary dir. Add account takes a +// new profile's sign-in only from a dir its own command created, so logins +// must say which dirs those are. +it("reports which profile dirs Paradev created", async () => { + const home = homedir(); + expect(home.startsWith(tmpdir())).toBe(true); + const profileDir = (name: string, files: Record) => { + const dir = join(home, name); + mkdirSync(dir); + for (const [file, content] of Object.entries(files)) { + writeFileSync(join(dir, file), content); + } + return dir; + }; + const codexAuth = '{"tokens":{"access_token":"token"}}'; + const claudeState = '{"oauthAccount":{"emailAddress":"me@example.com"}}'; + const codexMade = profileDir(".codex-made", { + "auth.json": codexAuth, + [PROFILE_CREATED_MARKER]: "codex", + }); + const codexOwn = profileDir(".codex-own", { "auth.json": codexAuth }); + const claudeMade = profileDir(".claude-made", { + ".claude.json": claudeState, + [PROFILE_CREATED_MARKER]: "claude", + }); + const claudeOwn = profileDir(".claude-own", { ".claude.json": claudeState }); + + const logins = await usageRouter + .createCaller({ isAuthenticated: true } as HostServiceContext) + .logins(); + + expect( + Object.fromEntries( + logins.codex.map((login) => [login.home, login.createdByParadev]), + ), + ).toEqual({ + [join(home, ".codex")]: false, + [codexMade]: true, + [codexOwn]: false, + }); + expect( + Object.fromEntries( + logins.claude.map((login) => [login.configDir, login.createdByParadev]), + ), + ).toEqual({ [claudeMade]: true, [claudeOwn]: false }); +}); diff --git a/packages/host-service/src/trpc/router/usage/usage.test.ts b/packages/host-service/src/trpc/router/usage/usage.test.ts new file mode 100644 index 00000000000..4b4685191f4 --- /dev/null +++ b/packages/host-service/src/trpc/router/usage/usage.test.ts @@ -0,0 +1,39 @@ +import { afterEach, expect, it } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const homes: string[] = []; +afterEach(() => { + for (const home of homes.splice(0)) { + rmSync(home, { recursive: true, force: true }); + } +}); + +// Bun's os.homedir() does not follow a HOME set at runtime, so the logins +// check runs in a child process whose HOME is a temporary dir. +it("usage.logins reports which profile dirs Paradev created", () => { + const home = mkdtempSync(join(tmpdir(), "superset-usage-logins-")); + homes.push(home); + const env: Record = { + ...process.env, + HOME: home, + }; + delete env.CODEX_HOME; + delete env.SUPERSET_DEFAULT_CODEX_HOME; + + const result = Bun.spawnSync({ + cmd: [ + process.execPath, + "test", + join(import.meta.dir, "fixtures/logins-created-marker.checks.ts"), + ], + cwd: process.cwd(), + env, + }); + + expect( + result.exitCode, + result.stdout.toString() + result.stderr.toString(), + ).toBe(0); +}); diff --git a/packages/host-service/src/trpc/router/usage/usage.ts b/packages/host-service/src/trpc/router/usage/usage.ts index 6fd9312fd2b..d1b2964f1bb 100644 --- a/packages/host-service/src/trpc/router/usage/usage.ts +++ b/packages/host-service/src/trpc/router/usage/usage.ts @@ -34,6 +34,7 @@ import { removeClaudeProfile, removeCodexHome } from "./profile-remove"; import { discoverClaudeProfiles, discoverCodexHomes, + hasProfileCreatedMarker, readCodexProfileKind, } from "./profiles"; import { validateSessionAccount } from "./session-account/session-account"; @@ -138,7 +139,8 @@ export const usageRouter = router({ * while an add-account or switch-sign-in flow is pending in a terminal. * The default-slot fields let the UI notice a `/login` that re-signed the * system-default login by its credential fingerprint, including re-login - * to the same account. + * to the same account. createdByParadev lets Add account take a new + * profile's sign-in only from a dir its own command created. */ logins: queryProcedure.query(async () => { const [profiles, codexHomes, claudeDefaultEmail, claudeDefaultFingerprint] = @@ -168,7 +170,12 @@ export const usageRouter = router({ // No readable auth.json — fingerprint stays null. } } - return { home, fingerprint, credentialKind }; + return { + home, + fingerprint, + credentialKind, + createdByParadev: await hasProfileCreatedMarker(home, "codex"), + }; }), ); return { @@ -183,6 +190,10 @@ export const usageRouter = router({ profile.credentialKind === "api_key" ? profile.loginFingerprint : await readClaudeLoginFingerprint(profile.configDir), + createdByParadev: await hasProfileCreatedMarker( + profile.configDir, + "claude", + ), })), ), codex, diff --git a/packages/i18n/locales/cs/messages.po b/packages/i18n/locales/cs/messages.po index ae4bcc2c43e..70807b87d11 100644 --- a/packages/i18n/locales/cs/messages.po +++ b/packages/i18n/locales/cs/messages.po @@ -20467,6 +20467,9 @@ msgstr "Použít pro nové agenty" msgid "Use global default" msgstr "Použít globální výchozí" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Použijte písmena A–Z, číslice nebo pomlčky." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Použijte malá písmena, číslice, pomlčky a podtržítka" diff --git a/packages/i18n/locales/de/messages.po b/packages/i18n/locales/de/messages.po index da803a98175..edb381d0d43 100644 --- a/packages/i18n/locales/de/messages.po +++ b/packages/i18n/locales/de/messages.po @@ -20467,6 +20467,9 @@ msgstr "Für neue Agenten verwenden" msgid "Use global default" msgstr "Globalen Standard verwenden" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Verwende Buchstaben A–Z, Ziffern oder Bindestriche." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Verwende Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche" diff --git a/packages/i18n/locales/en/messages.po b/packages/i18n/locales/en/messages.po index bdc00ead952..96e608dcb13 100644 --- a/packages/i18n/locales/en/messages.po +++ b/packages/i18n/locales/en/messages.po @@ -20467,6 +20467,9 @@ msgstr "Use for new agents" msgid "Use global default" msgstr "Use global default" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Use letters A–Z, digits, or hyphens." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Use lowercase letters, numbers, dashes and underscores" diff --git a/packages/i18n/locales/es/messages.po b/packages/i18n/locales/es/messages.po index 04ab4d748a9..c7889017940 100644 --- a/packages/i18n/locales/es/messages.po +++ b/packages/i18n/locales/es/messages.po @@ -20467,6 +20467,9 @@ msgstr "Usar para los agentes nuevos" msgid "Use global default" msgstr "Usar el valor global predeterminado" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Usa letras de la A a la Z, números o guiones." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Usa minúsculas, números, guiones y guiones bajos" diff --git a/packages/i18n/locales/fr/messages.po b/packages/i18n/locales/fr/messages.po index 192ee05e573..ee3facbb250 100644 --- a/packages/i18n/locales/fr/messages.po +++ b/packages/i18n/locales/fr/messages.po @@ -20467,6 +20467,9 @@ msgstr "Utiliser pour les nouveaux agents" msgid "Use global default" msgstr "Utiliser la valeur globale par défaut" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Utilisez des lettres de A à Z, des chiffres ou des traits d'union." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Utilisez des minuscules, des chiffres, des tirets et des tirets bas" diff --git a/packages/i18n/locales/id/messages.po b/packages/i18n/locales/id/messages.po index 1d6848d6618..d9342cf924e 100644 --- a/packages/i18n/locales/id/messages.po +++ b/packages/i18n/locales/id/messages.po @@ -20467,6 +20467,9 @@ msgstr "Pakai untuk agen baru" msgid "Use global default" msgstr "Pakai bawaan global" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Gunakan huruf A–Z, angka, atau tanda hubung." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Gunakan huruf kecil, angka, tanda hubung, dan garis bawah" diff --git a/packages/i18n/locales/it/messages.po b/packages/i18n/locales/it/messages.po index 06c9829a3fa..accb6ed027a 100644 --- a/packages/i18n/locales/it/messages.po +++ b/packages/i18n/locales/it/messages.po @@ -20467,6 +20467,9 @@ msgstr "Usa per i nuovi agenti" msgid "Use global default" msgstr "Usa il valore predefinito globale" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Usa lettere dalla A alla Z, cifre o trattini." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Usa lettere minuscole, numeri, trattini e trattini bassi" diff --git a/packages/i18n/locales/ja/messages.po b/packages/i18n/locales/ja/messages.po index f8b9f7ff688..1ac5adca346 100644 --- a/packages/i18n/locales/ja/messages.po +++ b/packages/i18n/locales/ja/messages.po @@ -20467,6 +20467,9 @@ msgstr "新規エージェントに使用" msgid "Use global default" msgstr "グローバルのデフォルトを使用" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "A–Z の英字、数字、ハイフンを使用してください。" + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "小文字、数字、ハイフン、アンダースコアを使用してください" diff --git a/packages/i18n/locales/ko/messages.po b/packages/i18n/locales/ko/messages.po index d48825cedc7..6a4fd830949 100644 --- a/packages/i18n/locales/ko/messages.po +++ b/packages/i18n/locales/ko/messages.po @@ -20467,6 +20467,9 @@ msgstr "새 에이전트에 사용" msgid "Use global default" msgstr "전역 기본값 사용" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "A–Z 영문자, 숫자, 하이픈을 사용해 주세요." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "소문자, 숫자, 하이픈, 밑줄을 사용하세요" diff --git a/packages/i18n/locales/nl/messages.po b/packages/i18n/locales/nl/messages.po index 5c1711834d1..47c0d4fd650 100644 --- a/packages/i18n/locales/nl/messages.po +++ b/packages/i18n/locales/nl/messages.po @@ -20467,6 +20467,9 @@ msgstr "Gebruiken voor nieuwe agents" msgid "Use global default" msgstr "Globale standaard gebruiken" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Gebruik letters A–Z, cijfers of koppeltekens." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Gebruik kleine letters, cijfers, streepjes en underscores" diff --git a/packages/i18n/locales/pl/messages.po b/packages/i18n/locales/pl/messages.po index f38529568e0..d34f481bbdd 100644 --- a/packages/i18n/locales/pl/messages.po +++ b/packages/i18n/locales/pl/messages.po @@ -20467,6 +20467,9 @@ msgstr "Używaj dla nowych agentów" msgid "Use global default" msgstr "Użyj domyślnego globalnego" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Użyj liter A–Z, cyfr lub łączników." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Używaj małych liter, cyfr, myślników i podkreśleń" diff --git a/packages/i18n/locales/pt-BR/messages.po b/packages/i18n/locales/pt-BR/messages.po index 561cdf027ac..56484bdb557 100644 --- a/packages/i18n/locales/pt-BR/messages.po +++ b/packages/i18n/locales/pt-BR/messages.po @@ -20467,6 +20467,9 @@ msgstr "Usar para novos agentes" msgid "Use global default" msgstr "Usar o padrão global" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Use letras de A a Z, números ou hifens." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Use letras minúsculas, números, hífens e sublinhados" diff --git a/packages/i18n/locales/ru/messages.po b/packages/i18n/locales/ru/messages.po index 8cec86f804c..84e4c95db50 100644 --- a/packages/i18n/locales/ru/messages.po +++ b/packages/i18n/locales/ru/messages.po @@ -20467,6 +20467,9 @@ msgstr "Использовать для новых агентов" msgid "Use global default" msgstr "Использовать общий по умолчанию" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Используйте латинские буквы A–Z, цифры или дефисы." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Используйте строчные буквы, цифры, дефисы и подчёркивания" diff --git a/packages/i18n/locales/tr/messages.po b/packages/i18n/locales/tr/messages.po index 16d7baed201..292460a588f 100644 --- a/packages/i18n/locales/tr/messages.po +++ b/packages/i18n/locales/tr/messages.po @@ -20467,6 +20467,9 @@ msgstr "Yeni ajanlar için kullan" msgid "Use global default" msgstr "Genel varsayılanı kullan" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Lütfen A–Z harfleri, rakam veya kısa çizgi kullanın." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Küçük harf, rakam, tire ve alt çizgi kullanın" diff --git a/packages/i18n/locales/vi/messages.po b/packages/i18n/locales/vi/messages.po index 0edc3a798e5..70bcbed95d9 100644 --- a/packages/i18n/locales/vi/messages.po +++ b/packages/i18n/locales/vi/messages.po @@ -20467,6 +20467,9 @@ msgstr "Dùng cho agent mới" msgid "Use global default" msgstr "Dùng mặc định toàn cục" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "Hãy dùng chữ cái A–Z, chữ số hoặc dấu gạch ngang." + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "Dùng chữ thường, số, dấu gạch ngang và gạch dưới" diff --git a/packages/i18n/locales/zh-CN/messages.po b/packages/i18n/locales/zh-CN/messages.po index 057a657fcbb..741ee9c008c 100644 --- a/packages/i18n/locales/zh-CN/messages.po +++ b/packages/i18n/locales/zh-CN/messages.po @@ -20467,6 +20467,9 @@ msgstr "用于新智能体" msgid "Use global default" msgstr "使用全局默认值" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "请使用字母 A–Z、数字或连字符。" + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "请使用小写字母、数字、连字符和下划线" diff --git a/packages/i18n/locales/zh-TW/messages.po b/packages/i18n/locales/zh-TW/messages.po index e7cd0d83b67..e9031cb8df8 100644 --- a/packages/i18n/locales/zh-TW/messages.po +++ b/packages/i18n/locales/zh-TW/messages.po @@ -20467,6 +20467,9 @@ msgstr "用於新代理程式" msgid "Use global default" msgstr "使用全域性預設值" +msgid "Use letters A–Z, digits, or hyphens." +msgstr "請使用字母 A–Z、數字或連字號。" + msgid "Use lowercase letters, numbers, dashes and underscores" msgstr "請使用小寫字母、數字、連字號和底線" From 5362d222b9e790982d6fc7959f4cbe3a38a965e0 Mon Sep 17 00:00:00 2001 From: Tsavo Knott Date: Sat, 31 Oct 2026 02:43:12 -0400 Subject: [PATCH 5/5] fix(usage): offer Remove only for profiles Paradev created Remove refuses a dir without the created marker, which includes every profile added before the marker existed, but the card menu still offered it and the confirmation still promised to delete the profile. usage.quota now reports createdByParadev for each account. For any other profile the menu lists Remove as unavailable, with the reason. A host older than the field reports nothing, so its profiles show the same; that host's Remove does not check the marker, so it is no longer reached from the menu. The docs say which profiles Remove leaves in place, and that on macOS a Claude Code profile deleted by hand leaves its Keychain items behind. Co-Authored-By: Claude Opus 5.5 --- .../usage/components/UsageView/UsageView.tsx | 17 ++-- .../AccountMenuItems.test.tsx | 78 +++++++++++++++++++ .../AccountMenuItems/AccountMenuItems.tsx | 43 ++++++++++ .../components/AccountMenuItems/index.ts | 1 + apps/docs/content/docs/usage.mdx | 2 +- .../src/trpc/router/usage/profiles.test.ts | 27 +++++++ .../src/trpc/router/usage/profiles.ts | 12 +++ .../src/trpc/router/usage/types.ts | 3 + .../src/trpc/router/usage/usage.ts | 27 ++++--- packages/i18n/locales/cs/messages.po | 3 + packages/i18n/locales/de/messages.po | 3 + packages/i18n/locales/en/messages.po | 3 + packages/i18n/locales/es/messages.po | 3 + packages/i18n/locales/fr/messages.po | 3 + packages/i18n/locales/id/messages.po | 3 + packages/i18n/locales/it/messages.po | 3 + packages/i18n/locales/ja/messages.po | 3 + packages/i18n/locales/ko/messages.po | 3 + packages/i18n/locales/nl/messages.po | 3 + packages/i18n/locales/pl/messages.po | 3 + packages/i18n/locales/pt-BR/messages.po | 3 + packages/i18n/locales/ru/messages.po | 3 + packages/i18n/locales/tr/messages.po | 3 + packages/i18n/locales/vi/messages.po | 3 + packages/i18n/locales/zh-CN/messages.po | 3 + packages/i18n/locales/zh-TW/messages.po | 3 + 26 files changed, 239 insertions(+), 22 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/AccountMenuItems.test.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/AccountMenuItems.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/UsageView.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/UsageView.tsx index 36590fc9655..7e782da39c3 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/UsageView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/UsageView.tsx @@ -6,7 +6,6 @@ import { Button } from "@superset/ui/button"; import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuTrigger, } from "@superset/ui/dropdown-menu"; import { toast } from "@superset/ui/sonner"; @@ -43,6 +42,7 @@ import { useRestartAgentSessions } from "../../hooks/useRestartAgentSessions"; import { useSetDefaultUsageAccount } from "../../hooks/useSetDefaultUsageAccount"; import { LeaderboardCard } from "../LeaderboardCard"; import { UsageHistorySection } from "../UsageHistorySection"; +import { AccountMenuItems } from "./components/AccountMenuItems"; import type { SwitchSignInTarget } from "./components/AddAccountDialog"; import { AddAccountDialog } from "./components/AddAccountDialog"; import { RemoveAccountDialog } from "./components/RemoveAccountDialog"; @@ -243,16 +243,11 @@ function AccountCard({ - {onSwitchSignIn && ( - - Switch sign-in… - - )} - {onRemove && ( - - Remove… - - )} + )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/AccountMenuItems.test.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/AccountMenuItems.test.tsx new file mode 100644 index 00000000000..ee4b438fdf5 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/AccountMenuItems.test.tsx @@ -0,0 +1,78 @@ +import { afterEach, describe, expect, mock, test } from "bun:test"; + +( + globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } +).IS_REACT_ACT_ENVIRONMENT = true; + +const { act, cleanup, fireEvent, render, within } = await import( + "@testing-library/react" +); +const { DropdownMenu, DropdownMenuContent } = await import( + "@superset/ui/dropdown-menu" +); +const { AccountMenuItems } = await import("./AccountMenuItems"); + +afterEach(cleanup); + +async function menu({ + createdByParadev, + removable = true, +}: { + createdByParadev: boolean; + removable?: boolean; +}) { + const onRemove = mock(() => {}); + let view!: ReturnType; + await act(async () => { + view = render( + + + {}} + onRemove={removable ? onRemove : null} + createdByParadev={createdByParadev} + /> + + , + ); + }); + const ui = within(view.baseElement as HTMLElement); + const removeItem = () => + ui.queryByText("Remove…")?.closest("[role=menuitem]") ?? null; + return { ui, onRemove, removeItem }; +} + +describe("a profile Paradev created", () => { + test("can be removed", async () => { + const { onRemove, removeItem } = await menu({ createdByParadev: true }); + const item = removeItem(); + expect(item?.getAttribute("data-disabled")).toBeNull(); + await act(async () => { + fireEvent.click(item as Element); + }); + expect(onRemove).toHaveBeenCalledTimes(1); + }); +}); + +// A profile from an earlier build, a dir the user made, or a host too old +// to say: Remove would delete nothing, so the menu says why up front. +describe("a profile Paradev did not create", () => { + test("lists Remove as unavailable, with the reason", async () => { + const { ui, removeItem } = await menu({ createdByParadev: false }); + expect(removeItem()?.getAttribute("data-disabled")).not.toBeNull(); + expect( + ui.getByText("Paradev only removes profiles it created."), + ).toBeDefined(); + }); +}); + +describe("the system-default login", () => { + test("has no Remove", async () => { + const { ui, removeItem } = await menu({ + createdByParadev: false, + removable: false, + }); + expect(removeItem()).toBeNull(); + expect(ui.getByText("Switch sign-in…")).toBeDefined(); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/AccountMenuItems.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/AccountMenuItems.tsx new file mode 100644 index 00000000000..c72bf8b77a0 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/AccountMenuItems.tsx @@ -0,0 +1,43 @@ +import { Trans } from "@lingui/react/macro"; +import { DropdownMenuItem } from "@superset/ui/dropdown-menu"; + +interface AccountMenuItemsProps { + onSwitchSignIn: (() => void) | null; + /** Null for the system-default login, which is never removable. */ + onRemove: (() => void) | null; + /** Whether the host reports that Paradev created the profile dir. Remove + * deletes no other dir. */ + createdByParadev: boolean; +} + +/** The items of an account card's "…" menu. */ +export function AccountMenuItems({ + onSwitchSignIn, + onRemove, + createdByParadev, +}: AccountMenuItemsProps) { + return ( + <> + {onSwitchSignIn && ( + + Switch sign-in… + + )} + {onRemove && + (createdByParadev ? ( + + Remove… + + ) : ( + + + Remove… + + + Paradev only removes profiles it created. + + + ))} + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/index.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/index.ts new file mode 100644 index 00000000000..2b67cc98624 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/usage/components/UsageView/components/AccountMenuItems/index.ts @@ -0,0 +1 @@ +export { AccountMenuItems } from "./AccountMenuItems"; diff --git a/apps/docs/content/docs/usage.mdx b/apps/docs/content/docs/usage.mdx index 57acb735297..d598a61e520 100644 --- a/apps/docs/content/docs/usage.mdx +++ b/apps/docs/content/docs/usage.mdx @@ -24,7 +24,7 @@ Run different agents on different subscriptions from one machine: - **Add account** creates a second profile. Pick a profile name and billing type, then complete Claude Code or Codex sign-in in the terminal inside the dialog. Click a login URL to open your system browser. Paradev detects a successful sign-in automatically. For a remote host, copy the shown command and run it in a terminal on that host. Each profile gets a new config dir: Paradev won't sign in to a dir it didn't create, so a login you or another tool keep there stays untouched. - **Make default** picks which account new agent launches use. When a provider has several accounts, the cards read as a radio group with the default checked. Switching reaches terminals you already have open too: a running agent keeps its account until you relaunch it. - Your setup comes along: skills, plugins, MCP servers, and settings are shared across accounts, and for Claude Code there's one conversation history: `--resume` shows the same sessions whichever account is active. -- Logins, credentials, and account identity stay per account. The **…** menu on a card offers **Switch sign-in…** (re-login that profile to a different account) and, for secondary profiles, **Remove…**, which deletes that profile and its credentials, never your shared setup or history. Remove only deletes profiles Paradev created; it leaves a config dir you made yourself in place. The system-default login can't be removed. +- Logins, credentials, and account identity stay per account. The **…** menu on a card offers **Switch sign-in…** (re-login that profile to a different account) and, for secondary profiles, **Remove…**, which deletes that profile and its credentials, never your shared setup or history. Remove only deletes profiles Paradev created. For a config dir you made yourself, a profile an earlier Paradev version added, or a profile on a remote host that runs an older version, **Remove…** is unavailable: delete the folder yourself if you no longer need it. On macOS, a Claude Code profile's sign-in also stays in the Keychain, as `Claude Code-credentials-…` items you can delete in Keychain Access. The system-default login can't be removed. - If a sign-in expires, the card shows the exact login command to run, with click-to-copy. Under the hood, a profile is just a config dir the CLI is pointed at (`CLAUDE_CONFIG_DIR` / `CODEX_HOME`). Paradev's agent wrappers re-resolve the current default every time the agent starts in a Paradev terminal, so switching works for agents you launch by hand too, and a value you export yourself always wins. diff --git a/packages/host-service/src/trpc/router/usage/profiles.test.ts b/packages/host-service/src/trpc/router/usage/profiles.test.ts index c10eba510f2..cbddb3cecf4 100644 --- a/packages/host-service/src/trpc/router/usage/profiles.test.ts +++ b/packages/host-service/src/trpc/router/usage/profiles.test.ts @@ -12,6 +12,8 @@ import { API_BILLING_MARKER, claudeKeychainAccounts, discoverClaudeProfiles, + isCreatedProfile, + PROFILE_CREATED_MARKER, readCodexProfileKind, } from "./profiles"; @@ -158,3 +160,28 @@ describe("discoverClaudeProfiles", () => { }); }); }); + +// Remove deletes only such a profile, and Add takes a new sign-in only from +// one, so every other dir must read as not created. +describe("isCreatedProfile", () => { + it("holds for a dir whose marker names the agent", async () => { + const dir = tempProfile(); + writeFileSync(join(dir, PROFILE_CREATED_MARKER), "codex"); + expect(await isCreatedProfile("codex", dir)).toBe(true); + }); + + it("does not hold for an unmarked dir or another agent's marker", async () => { + const dir = tempProfile(); + writeFileSync(join(dir, API_BILLING_MARKER), "codex"); + expect(await isCreatedProfile("codex", dir)).toBe(false); + writeFileSync(join(dir, PROFILE_CREATED_MARKER), "claude"); + expect(await isCreatedProfile("codex", dir)).toBe(false); + }); + + it("does not hold for the system default or an agent Paradev does not manage", async () => { + const dir = tempProfile(); + writeFileSync(join(dir, PROFILE_CREATED_MARKER), "grok"); + expect(await isCreatedProfile("claude", null)).toBe(false); + expect(await isCreatedProfile("grok", dir)).toBe(false); + }); +}); diff --git a/packages/host-service/src/trpc/router/usage/profiles.ts b/packages/host-service/src/trpc/router/usage/profiles.ts index 5ca2cf86bc3..87f65e48ce4 100644 --- a/packages/host-service/src/trpc/router/usage/profiles.ts +++ b/packages/host-service/src/trpc/router/usage/profiles.ts @@ -178,6 +178,18 @@ export async function hasProfileCreatedMarker( ); } +/** Whether `selection` is a profile dir Paradev created for `agent`. The + * system default (null) and other agents' logins never are. */ +export async function isCreatedProfile( + agent: string, + selection: string | null, +): Promise { + if (selection === null || (agent !== "claude" && agent !== "codex")) { + return false; + } + return hasProfileCreatedMarker(selection, agent); +} + /** * Extra Claude profile dirs beyond the defaults. Default homes are excluded — * callers already cover `~/.claude` and `~/.config/claude`. `candidates` diff --git a/packages/host-service/src/trpc/router/usage/types.ts b/packages/host-service/src/trpc/router/usage/types.ts index 25c4f954772..74960efb320 100644 --- a/packages/host-service/src/trpc/router/usage/types.ts +++ b/packages/host-service/src/trpc/router/usage/types.ts @@ -99,5 +99,8 @@ export interface UsageAccount { selection: string | null; /** Whether newly launched agents use this account (host-wide default). */ isDefault: boolean; + /** Whether Paradev created this profile dir, so Remove can delete it. + * Decorated per query; hosts older than this field leave it out. */ + createdByParadev?: boolean; fetchedAt: Date; } diff --git a/packages/host-service/src/trpc/router/usage/usage.ts b/packages/host-service/src/trpc/router/usage/usage.ts index d1b2964f1bb..343fee67b03 100644 --- a/packages/host-service/src/trpc/router/usage/usage.ts +++ b/packages/host-service/src/trpc/router/usage/usage.ts @@ -35,6 +35,7 @@ import { discoverClaudeProfiles, discoverCodexHomes, hasProfileCreatedMarker, + isCreatedProfile, readCodexProfileKind, } from "./profiles"; import { validateSessionAccount } from "./session-account/session-account"; @@ -118,20 +119,26 @@ export const usageRouter = router({ quota: queryProcedure .meta({ timeoutMs: 15_000 }) .input(z.object({ forceRefresh: z.boolean().optional() }).optional()) - .query(async ({ ctx, input }) => { + .query(async ({ ctx, input }): Promise => { const accounts = await getQuota(input?.forceRefresh ?? false); // isDefault is applied per query, not cached with the quota: changing // the default must reflect immediately without re-hitting providers. const defaults = getDefaultAccountSelections(ctx.db); - return accounts.map((account) => ({ - ...account, - isDefault: - account.agent === "claude" - ? account.selection === defaults.claudeConfigDir - : account.agent === "codex" - ? account.selection === defaults.codexHome - : false, - })); + return Promise.all( + accounts.map(async (account) => ({ + ...account, + isDefault: + account.agent === "claude" + ? account.selection === defaults.claudeConfigDir + : account.agent === "codex" + ? account.selection === defaults.codexHome + : false, + createdByParadev: await isCreatedProfile( + account.agent, + account.selection, + ), + })), + ); }), /** diff --git a/packages/i18n/locales/cs/messages.po b/packages/i18n/locales/cs/messages.po index 70807b87d11..16162b05549 100644 --- a/packages/i18n/locales/cs/messages.po +++ b/packages/i18n/locales/cs/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev na iPadu s pull requestem a seznamem workspaců" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev získá pouze oprávnění, o která tento konektor žádá. Kdykoli je můžete odvolat odpojením." +msgid "Paradev only removes profiles it created." +msgstr "Paradev odebírá jen profily, které sám vytvořil." + msgid "Paradev originals" msgstr "Originály Paradev" diff --git a/packages/i18n/locales/de/messages.po b/packages/i18n/locales/de/messages.po index edb381d0d43..94c5115d549 100644 --- a/packages/i18n/locales/de/messages.po +++ b/packages/i18n/locales/de/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev auf dem iPad mit einem Pull Request und einer Workspace-Liste" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev erhält nur die Berechtigungen, die dieser Connector anfordert. Trennen Sie die Verbindung jederzeit, um sie zu widerrufen." +msgid "Paradev only removes profiles it created." +msgstr "Paradev entfernt nur Profile, die es selbst erstellt hat." + msgid "Paradev originals" msgstr "Paradev-Originale" diff --git a/packages/i18n/locales/en/messages.po b/packages/i18n/locales/en/messages.po index 96e608dcb13..c6d3b26e065 100644 --- a/packages/i18n/locales/en/messages.po +++ b/packages/i18n/locales/en/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev on iPad, showing a pull request and a workspace list" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." +msgid "Paradev only removes profiles it created." +msgstr "Paradev only removes profiles it created." + msgid "Paradev originals" msgstr "Paradev originals" diff --git a/packages/i18n/locales/es/messages.po b/packages/i18n/locales/es/messages.po index c7889017940..81a6993e57f 100644 --- a/packages/i18n/locales/es/messages.po +++ b/packages/i18n/locales/es/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev en iPad, con una pull request y una lista de workspaces" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev solo recibe los permisos que pide este conector. Desconéctalo en cualquier momento para revocarlos." +msgid "Paradev only removes profiles it created." +msgstr "Paradev solo quita los perfiles que creó." + msgid "Paradev originals" msgstr "Originales de Paradev" diff --git a/packages/i18n/locales/fr/messages.po b/packages/i18n/locales/fr/messages.po index ee3facbb250..91a4361ddfc 100644 --- a/packages/i18n/locales/fr/messages.po +++ b/packages/i18n/locales/fr/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev sur iPad, avec une pull request et une liste de workspaces" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev ne reçoit que les autorisations demandées par ce connecteur. Déconnectez-le à tout moment pour les révoquer." +msgid "Paradev only removes profiles it created." +msgstr "Paradev ne retire que les profils qu'il a créés." + msgid "Paradev originals" msgstr "Originaux Paradev" diff --git a/packages/i18n/locales/id/messages.po b/packages/i18n/locales/id/messages.po index d9342cf924e..715215b9967 100644 --- a/packages/i18n/locales/id/messages.po +++ b/packages/i18n/locales/id/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev di iPad, menampilkan pull request dan daftar workspace" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev hanya menerima izin yang diminta konektor ini. Putuskan koneksi kapan saja untuk mencabutnya." +msgid "Paradev only removes profiles it created." +msgstr "Paradev hanya menghapus profil yang dibuatnya sendiri." + msgid "Paradev originals" msgstr "Orisinal Paradev" diff --git a/packages/i18n/locales/it/messages.po b/packages/i18n/locales/it/messages.po index accb6ed027a..30a49896e7d 100644 --- a/packages/i18n/locales/it/messages.po +++ b/packages/i18n/locales/it/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev su iPad, con una pull request e un elenco di workspace" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev riceve solo i permessi richiesti da questo connettore. Scollegalo in qualsiasi momento per revocarli." +msgid "Paradev only removes profiles it created." +msgstr "Paradev rimuove solo i profili che ha creato." + msgid "Paradev originals" msgstr "Originali Paradev" diff --git a/packages/i18n/locales/ja/messages.po b/packages/i18n/locales/ja/messages.po index 1ac5adca346..ffce45e6b66 100644 --- a/packages/i18n/locales/ja/messages.po +++ b/packages/i18n/locales/ja/messages.po @@ -13905,6 +13905,9 @@ msgstr "iPad 上の Paradev。プルリクエストとワークスペース一 msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev はこのコネクタが要求する権限のみを受け取ります。接続を解除すればいつでも取り消せます。" +msgid "Paradev only removes profiles it created." +msgstr "Paradev が削除するのは、Paradev が作成したプロファイルだけです。" + msgid "Paradev originals" msgstr "Paradev独自コンポーネント" diff --git a/packages/i18n/locales/ko/messages.po b/packages/i18n/locales/ko/messages.po index 6a4fd830949..0d9d3e03c65 100644 --- a/packages/i18n/locales/ko/messages.po +++ b/packages/i18n/locales/ko/messages.po @@ -13905,6 +13905,9 @@ msgstr "iPad의 Paradev. 풀 리퀘스트와 워크스페이스 목록이 보입 msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev는 이 커넥터가 요청한 권한만 받습니다. 언제든지 연결을 해제해 취소할 수 있습니다." +msgid "Paradev only removes profiles it created." +msgstr "Paradev는 직접 만든 프로필만 제거합니다." + msgid "Paradev originals" msgstr "Paradev 오리지널" diff --git a/packages/i18n/locales/nl/messages.po b/packages/i18n/locales/nl/messages.po index 47c0d4fd650..308fa4d4144 100644 --- a/packages/i18n/locales/nl/messages.po +++ b/packages/i18n/locales/nl/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev op iPad, met een pull request en een lijst met workspaces" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev ontvangt alleen de rechten die deze connector vraagt. Verbreek de verbinding wanneer je wilt om ze in te trekken." +msgid "Paradev only removes profiles it created." +msgstr "Paradev verwijdert alleen profielen die het zelf heeft gemaakt." + msgid "Paradev originals" msgstr "Paradev-originelen" diff --git a/packages/i18n/locales/pl/messages.po b/packages/i18n/locales/pl/messages.po index d34f481bbdd..e3636747fe5 100644 --- a/packages/i18n/locales/pl/messages.po +++ b/packages/i18n/locales/pl/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev na iPadzie z pull requestem i listą workspace’ów" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev otrzymuje tylko uprawnienia, o które prosi ten konektor. Odłącz go w dowolnej chwili, aby je cofnąć." +msgid "Paradev only removes profiles it created." +msgstr "Paradev usuwa tylko profile, które sam utworzył." + msgid "Paradev originals" msgstr "Oryginały Paradev" diff --git a/packages/i18n/locales/pt-BR/messages.po b/packages/i18n/locales/pt-BR/messages.po index 56484bdb557..dd99fad900f 100644 --- a/packages/i18n/locales/pt-BR/messages.po +++ b/packages/i18n/locales/pt-BR/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev no iPad, mostrando um pull request e uma lista de workspaces" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "O Paradev recebe apenas as permissões que este conector solicita. Desconecte a qualquer momento para revogá-las." +msgid "Paradev only removes profiles it created." +msgstr "O Paradev só remove perfis que ele mesmo criou." + msgid "Paradev originals" msgstr "Originais do Paradev" diff --git a/packages/i18n/locales/ru/messages.po b/packages/i18n/locales/ru/messages.po index 84e4c95db50..aa8cbdfc2ca 100644 --- a/packages/i18n/locales/ru/messages.po +++ b/packages/i18n/locales/ru/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev на iPad: пул-реквест и список воркспе msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev получает только те разрешения, которые запрашивает этот коннектор. Отключите его в любой момент, чтобы отозвать их." +msgid "Paradev only removes profiles it created." +msgstr "Paradev удаляет только профили, которые создал сам." + msgid "Paradev originals" msgstr "Оригиналы Paradev" diff --git a/packages/i18n/locales/tr/messages.po b/packages/i18n/locales/tr/messages.po index 292460a588f..cd07eee4b30 100644 --- a/packages/i18n/locales/tr/messages.po +++ b/packages/i18n/locales/tr/messages.po @@ -13905,6 +13905,9 @@ msgstr "iPad'de Paradev; bir pull request ve bir çalışma alanı listesi göst msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev yalnızca bu bağlayıcının istediği izinleri alır. İzinleri geri almak için istediğiniz zaman bağlantıyı kesin." +msgid "Paradev only removes profiles it created." +msgstr "Paradev yalnızca kendi oluşturduğu profilleri kaldırır." + msgid "Paradev originals" msgstr "Paradev özgünleri" diff --git a/packages/i18n/locales/vi/messages.po b/packages/i18n/locales/vi/messages.po index 70bcbed95d9..4949cf786ff 100644 --- a/packages/i18n/locales/vi/messages.po +++ b/packages/i18n/locales/vi/messages.po @@ -13905,6 +13905,9 @@ msgstr "Paradev trên iPad, hiển thị một pull request và danh sách works msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev chỉ nhận các quyền mà trình kết nối này yêu cầu. Ngắt kết nối bất cứ lúc nào để thu hồi." +msgid "Paradev only removes profiles it created." +msgstr "Paradev chỉ gỡ những hồ sơ do chính nó tạo." + msgid "Paradev originals" msgstr "Thành phần riêng của Paradev" diff --git a/packages/i18n/locales/zh-CN/messages.po b/packages/i18n/locales/zh-CN/messages.po index 741ee9c008c..2bbfbaaddf0 100644 --- a/packages/i18n/locales/zh-CN/messages.po +++ b/packages/i18n/locales/zh-CN/messages.po @@ -13905,6 +13905,9 @@ msgstr "iPad 上的 Paradev,显示一个拉取请求和工作区列表" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev 只获得此连接器请求的权限。你可以随时断开连接以撤销。" +msgid "Paradev only removes profiles it created." +msgstr "Paradev 只会移除它创建的配置文件。" + msgid "Paradev originals" msgstr "Paradev原创组件" diff --git a/packages/i18n/locales/zh-TW/messages.po b/packages/i18n/locales/zh-TW/messages.po index e9031cb8df8..946acc10f98 100644 --- a/packages/i18n/locales/zh-TW/messages.po +++ b/packages/i18n/locales/zh-TW/messages.po @@ -13905,6 +13905,9 @@ msgstr "iPad 上的 Paradev,顯示一個拉取請求和工作區清單" msgid "Paradev only receives the permissions this connector asks for. Disconnect at any time to revoke them." msgstr "Paradev 只取得此連接器請求的權限。你可以隨時中斷連線以撤銷。" +msgid "Paradev only removes profiles it created." +msgstr "Paradev 只會移除它建立的配置檔。" + msgid "Paradev originals" msgstr "Paradev原創元件"