From 9352439a7a44b285db64c6b256581717ff16886d Mon Sep 17 00:00:00 2001 From: Mason Nightlinger Date: Mon, 2 Nov 2026 11:11:05 -0600 Subject: [PATCH 01/22] feat(web): option to always say "pull request" instead of host terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitLab projects already show "merge request"/"MR", but each screen read the host's terms straight from its definition, so there was no way to opt out and several thread-scoped strings were still hard-coded to "pull request". Every client read now goes through `changeRequestTermsFor` in client-runtime, which honors a new client setting, `providerSpecificTerminology` (default on). It's exposed as "Use version control specific language" under Settings → Source control. Stacked git actions forward the choice to the server, so progress labels and completion toasts match. The right panel tab labels, command palette entries, composer PR picker, linked-PRs empty state, and copy/link toasts now use the active project's terms. Co-Authored-By: Claude Opus 5.5 --- .../src/components/ComposerContextSheet.tsx | 12 ++-- .../pull-requests/PullRequestControls.tsx | 6 +- .../pull-requests/PullRequestEditScreen.tsx | 4 +- .../pull-requests/PullRequestFiles.tsx | 6 +- .../pull-requests/PullRequestListRow.tsx | 4 +- .../PullRequestMergeControls.tsx | 4 +- .../pull-requests/PullRequestReviewSheet.tsx | 8 ++- .../PullRequestReviewersScreen.tsx | 6 +- .../pull-requests/PullRequestRouteScreen.tsx | 8 +-- .../pull-requests/PullRequestStatusRows.tsx | 5 +- .../guide/PullRequestGuideScreen.tsx | 4 +- .../pull-requests/pullRequestAgentTasks.ts | 4 +- .../features/pull-requests/pullRequestLink.ts | 4 +- .../pull-requests/usePullRequestAction.ts | 4 +- .../usePullRequestListActions.ts | 11 ++-- .../pull-requests/usePullRequestThreads.ts | 7 ++- .../threads/LinkedPullRequestList.tsx | 7 ++- .../features/threads/ThreadGitControls.tsx | 10 +--- .../features/threads/git/GitConfirmSheet.tsx | 6 +- .../features/threads/git/GitOverviewSheet.tsx | 6 +- .../threads/git/gitSheetComponents.tsx | 6 +- .../threads/new-task-context-presentation.ts | 8 ++- .../threads/useLinkedPullRequestActions.ts | 10 ++-- .../src/state/thread-pr-presentation.ts | 10 ++-- apps/mobile/src/state/threadOutboxDelivery.ts | 12 ++-- apps/server/src/git/GitManager.test.ts | 55 +++++++++++++++++++ apps/server/src/git/GitManager.ts | 36 +++++++----- apps/web/src/components/ChatMarkdown.tsx | 12 ++-- apps/web/src/components/ChatView.tsx | 9 ++- apps/web/src/components/CommandPalette.tsx | 28 ++++++++-- .../components/PullRequestContextDetails.tsx | 7 ++- apps/web/src/components/RightPanelTabs.tsx | 45 +++++++++------ .../src/components/ThreadStatusIndicators.tsx | 9 ++- apps/web/src/components/chat/ChatComposer.tsx | 7 ++- .../components/chat/ComposerCommandMenu.tsx | 18 ++++-- .../components/chat/ThreadDetailsPrRow.tsx | 5 +- .../components/chat/useComposerMenuItems.ts | 14 +++-- .../pullRequest/PullRequestCodeTab.tsx | 4 +- .../pullRequest/PullRequestCommentForm.tsx | 4 +- .../pullRequest/PullRequestComposer.tsx | 3 +- .../pullRequest/PullRequestDetailCrumbs.tsx | 7 ++- .../pullRequest/PullRequestDetailPanel.tsx | 5 +- .../pullRequest/PullRequestGhosts.tsx | 4 +- .../pullRequest/PullRequestSummaryTab.tsx | 3 +- .../pullRequest/PullRequestThreadLinks.tsx | 8 ++- .../pullRequest/PullRequestTimelineTab.tsx | 4 +- .../pullRequest/PullRequestTitleEditor.tsx | 4 +- .../pullRequest/PullRequestsPage.tsx | 11 ++-- .../pullRequest/ThreadPullRequestsPanel.tsx | 18 ++++-- .../pullRequest/pullRequestDetail.logic.ts | 8 +-- .../pullRequest/usePullRequestActions.ts | 12 ++-- .../pullRequest/useThreadLinkedPullRequest.ts | 9 ++- .../settings/SourceControlSettings.tsx | 35 ++++++++++++ .../src/components/settings/settingsSearch.ts | 8 +++ apps/web/src/hooks/useSettings.ts | 2 + apps/web/src/lib/composerContextRecords.ts | 3 +- apps/web/src/sourceControlPresentation.tsx | 7 ++- docs/user/source-control.md | 4 +- .../src/pullRequestLinkInput.ts | 8 ++- .../src/sourceControlClients.test.ts | 34 +++++++++++- .../src/sourceControlClients.ts | 22 ++++++++ .../client-runtime/src/state/gitActions.ts | 4 +- .../client-runtime/src/state/vcsAction.ts | 2 + .../client-runtime/src/threadReference.ts | 23 ++++---- .../src/work-log/presentation.ts | 8 ++- packages/contracts/src/git.ts | 2 + packages/contracts/src/settings.test.ts | 9 +++ packages/contracts/src/settings.ts | 5 ++ .../src/shared/definition.ts | 15 +++++ 69 files changed, 505 insertions(+), 207 deletions(-) diff --git a/apps/mobile/src/components/ComposerContextSheet.tsx b/apps/mobile/src/components/ComposerContextSheet.tsx index 8121bfdece763..2f9ef02a07dba 100644 --- a/apps/mobile/src/components/ComposerContextSheet.tsx +++ b/apps/mobile/src/components/ComposerContextSheet.tsx @@ -11,7 +11,10 @@ import type { ScopedThreadRef, } from "@t3tools/contracts"; import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { videoMimeType } from "@t3tools/shared/video"; import { Platform, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -183,10 +186,9 @@ export function ComposerContextSheet(props: { record?.kind === "review-comment" && "pullRequest" in record ? record.pullRequest?.url : undefined; - const changeRequestTerm = ( - (pullRequestUrl ? sourceControlClients.findByChangeRequestUrl(pullRequestUrl) : undefined) ?? - sourceControlClients.get(undefined) - ).changeRequest.singular; + const changeRequestTerm = changeRequestTermsFor( + pullRequestUrl ? sourceControlClients.findByChangeRequestUrl(pullRequestUrl) : undefined, + ).singular; const terminal = record?.kind === "terminal" && !("payload" in record) ? record : null; return ( diff --git a/apps/mobile/src/features/pull-requests/PullRequestControls.tsx b/apps/mobile/src/features/pull-requests/PullRequestControls.tsx index e816e53c2923c..7312d51189a14 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestControls.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestControls.tsx @@ -1,4 +1,4 @@ -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; import Animated from "react-native-reanimated"; import { PR_ROW_LAYOUT } from "./pullRequestAnimations"; import { useNavigation } from "@react-navigation/native"; @@ -123,9 +123,7 @@ export function PullRequestActionToolbar(props: { const pendingReview = canReview && draft.comments.length > 0; const showAgentControls = agent.canAsk || !permissions.connected; const canOpenThread = agent.threads.length > 0 || showAgentControls; - const { shortLabel } = sourceControlClients.get( - (props.pr ?? props.params.initialSummary)?.provider, - ).changeRequest; + const { shortLabel } = changeRequestTermsFor((props.pr ?? props.params.initialSummary)?.provider); const threadLabel = agent.threads.length ? "Continue thread" : `Work on ${shortLabel}`; const primaryLabel = pendingReview ? `Submit review · ${draft.comments.length}` : threadLabel; const chooseThread = agent.threads.length > 1; diff --git a/apps/mobile/src/features/pull-requests/PullRequestEditScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestEditScreen.tsx index 8546302b7cf30..daea489bfcbeb 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestEditScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestEditScreen.tsx @@ -1,7 +1,7 @@ import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { capitalizeTerm, - sourceControlClients, + changeRequestTermsFor, } from "@t3tools/client-runtime/source-control-clients"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { useAtomValue } from "@effect/atom-react"; @@ -40,7 +40,7 @@ export function PullRequestEditScreen({ route }: StaticScreenProps label.name).join(", ")}` : ""}, ${item.additions} additions, ${item.deletions} deletions`} + accessibilityLabel={`${capitalizeTerm(changeRequestTermsFor(item.provider).singular)} ${item.number}, ${item.title}, ${status.label}${conflict ? `, ${conflict.label.toLowerCase()}` : ""}, by ${item.author?.login ?? "unknown author"}${review ? `, ${review.accessibleLabel}` : ""}${item.checksState ? `, checks ${item.checksState}` : ""}${item.stack ? `, stack ${item.stack.position} of ${item.stack.size}` : ""}${item.labels.length ? `, ${item.labels.map((label) => label.name).join(", ")}` : ""}, ${item.additions} additions, ${item.deletions} deletions`} accessibilityHint="Double tap to open. Touch and hold for quick actions." onPress={() => props.onSelect(item)} disabled={props.busy} diff --git a/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx b/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx index 8ba68d2a2bfa9..bf174cc0d3ef6 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx @@ -1,7 +1,7 @@ import * as Dialog from "../../components/ui/Dialog"; import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; import type { PullRequestDetail, PullRequestMergeMethod } from "@t3tools/contracts"; import { AsyncResult } from "effect/reactivity"; import { useRef, useState } from "react"; @@ -32,7 +32,7 @@ export function PullRequestMergeControls(props: { readonly stack: PullRequestStackState; }) { const { pr, stack } = props; - const terminology = sourceControlClients.get(pr.provider).changeRequest; + const terminology = changeRequestTermsFor(pr.provider); const target = pullRequestTarget(props.params); const permissions = usePullRequestPermissions(props.params, pr); const preferences = useAtomValue(mobilePreferencesAtom); diff --git a/apps/mobile/src/features/pull-requests/PullRequestReviewSheet.tsx b/apps/mobile/src/features/pull-requests/PullRequestReviewSheet.tsx index bb923689d4266..2ced101d3a5ee 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestReviewSheet.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestReviewSheet.tsx @@ -1,7 +1,10 @@ import { AsyncResult } from "effect/reactivity"; import { useAtomRefresh } from "@effect/atom-react"; import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { settlePromise, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import type { PullRequestReviewVerdict } from "@t3tools/contracts"; import { useRef, useState } from "react"; @@ -242,8 +245,7 @@ export function PullRequestReviewSheet({ {reviewing && !canSubmitComments ? ( You can no longer submit line comments on this{" "} - {sourceControlClients.get(result.data?.provider).changeRequest.singular}. Your drafts are - kept here. + {changeRequestTermsFor(result.data?.provider).singular}. Your drafts are kept here. ) : null} {submission.error || result.error || store.error ? ( diff --git a/apps/mobile/src/features/pull-requests/PullRequestReviewersScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestReviewersScreen.tsx index 829a62d4b4b7f..65bdae2ac7b85 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestReviewersScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestReviewersScreen.tsx @@ -5,7 +5,7 @@ import { usePullRequestConnected } from "./usePullRequestConnected"; import { useAtomValue } from "@effect/atom-react"; import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; import type { PullRequestReviewerCandidate } from "@t3tools/contracts"; import { useRef, useState } from "react"; import { @@ -53,9 +53,7 @@ export function PullRequestReviewersScreen({ route }: StaticScreenProps diff --git a/apps/mobile/src/features/pull-requests/guide/PullRequestGuideScreen.tsx b/apps/mobile/src/features/pull-requests/guide/PullRequestGuideScreen.tsx index 3cae97487f9fb..aae501c59d482 100644 --- a/apps/mobile/src/features/pull-requests/guide/PullRequestGuideScreen.tsx +++ b/apps/mobile/src/features/pull-requests/guide/PullRequestGuideScreen.tsx @@ -1,4 +1,4 @@ -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; import { Toolbar } from "../../../components/ui/Toolbar"; import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; import type { FileDiffMetadata } from "@pierre/diffs"; @@ -110,7 +110,7 @@ export function PullRequestGuideStopScreen({ route }: StaticScreenProps { const { environmentId } = entry; - const terminology = sourceControlClients.get(entry.provider).changeRequest; + const terminology = changeRequestTermsFor(entry.provider); if (pending.current) return; pending.current = true; setBusyKey(scopedPullRequestListKey(entry)); @@ -138,10 +138,7 @@ export function usePullRequestListActions( (value) => value === action, ); if (!supported) return; - const label = pullRequestActionLabel( - supported, - sourceControlClients.get(entry.provider).changeRequest, - ); + const label = pullRequestActionLabel(supported, changeRequestTermsFor(entry.provider)); Dialog.alert(`${label}?`, `#${entry.number} · ${entry.title}`, [ { text: "Cancel", style: "cancel" }, { @@ -152,7 +149,7 @@ export function usePullRequestListActions( ]); }; const actions = (entry: EnvironmentPullRequestListEntry): MenuProps["actions"] => { - const terminology = sourceControlClients.get(entry.provider).changeRequest; + const terminology = changeRequestTermsFor(entry.provider); return [ { id: "open", diff --git a/apps/mobile/src/features/pull-requests/usePullRequestThreads.ts b/apps/mobile/src/features/pull-requests/usePullRequestThreads.ts index e19dc333f2e3a..76fd42b2cd731 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestThreads.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestThreads.ts @@ -18,7 +18,10 @@ import { type PullRequestLinkedThreadsResult, } from "@t3tools/contracts"; import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { normalizeThreadPullRequestKey } from "@t3tools/client-runtime/thread-pull-requests"; import { useRef, useState } from "react"; import { Platform } from "react-native"; @@ -103,7 +106,7 @@ export function usePullRequestThreads( const [busy, setBusy] = useState(false); const ask = async (thread?: LinkedThread, task: PullRequestAgentTask = "ask") => { if (!pr || !canAsk || pending.current) return; - const terminology = sourceControlClients.get(pr.provider).changeRequest; + const terminology = changeRequestTermsFor(pr.provider); pending.current = true; setBusy(true); const result = await settlePromise(async () => { diff --git a/apps/mobile/src/features/threads/LinkedPullRequestList.tsx b/apps/mobile/src/features/threads/LinkedPullRequestList.tsx index fe4fddd3e2c37..0060c84d6902f 100644 --- a/apps/mobile/src/features/threads/LinkedPullRequestList.tsx +++ b/apps/mobile/src/features/threads/LinkedPullRequestList.tsx @@ -1,6 +1,7 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { capitalizeTerm, + changeRequestTermsFor, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { @@ -80,9 +81,9 @@ export const LinkedPullRequestRow = memo(function LinkedPullRequestRow(props: { const watching = open && link.watch !== undefined; const additions = snapshot?.additions ?? 0; const deletions = snapshot?.deletions ?? 0; - const { shortLabel, singular } = ( - sourceControlClients.findByChangeRequestUrl(link.url) ?? sourceControlClients.get(undefined) - ).changeRequest; + const { shortLabel, singular } = changeRequestTermsFor( + sourceControlClients.findByChangeRequestUrl(link.url), + ); const metaLineHeight = useScaledTextRole("label").lineHeight * useWindowDimensions().fontScale; const row = ( { - const { shortLabel, singular } = sourceControlClients.get( - gitStatus?.sourceControlProvider?.kind, - ).changeRequest; + const { shortLabel, singular } = changeRequestTermsFor(gitStatus?.sourceControlProvider?.kind); const prUrl = gitStatus?.pr?.state === "open" ? gitStatus.pr.url : null; if (!prUrl) { Dialog.alert(`No open ${shortLabel}`, `This branch does not have an open ${singular}.`); diff --git a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx index ad76eb41540e0..1bb6704d657fa 100644 --- a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx @@ -1,5 +1,5 @@ import { resolveDefaultBranchActionDialogCopy } from "@t3tools/client-runtime/state/vcs"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; import { resolveAutoFeatureBranchName } from "@t3tools/shared/git"; import * as Arr from "effect/Array"; import * as Result from "effect/Result"; @@ -36,9 +36,7 @@ export function GitConfirmSheet(props: GitConfirmSheetProps) { const gitActions = useSelectedThreadGitActions(); const { canWriteSourceControl, canChangeThreadBranch } = gitActions; const { selectedThreadProject } = useThreadSelection(); - const terminology = sourceControlClients.get( - selectedThreadProject?.repositoryIdentity?.provider, - ).changeRequest; + const terminology = changeRequestTermsFor(selectedThreadProject?.repositoryIdentity?.provider); const params = props.route.params; diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index ac60fb66f6aac..c0bbc897bbd63 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -8,7 +8,7 @@ import { requiresDefaultBranchConfirmation, } from "@t3tools/client-runtime/state/vcs"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; import { CommonActions, StackActions, @@ -134,9 +134,9 @@ function GitOverviewSheetContent(props: GitOverviewSheetProps) { }), })); - const { shortLabel, singular } = sourceControlClients.get( + const { shortLabel, singular } = changeRequestTermsFor( gitStatus.data?.sourceControlProvider?.kind, - ).changeRequest; + ); const openExistingPr = async () => { const prUrl = gitStatus.data?.pr?.state === "open" ? gitStatus.data.pr.url : null; diff --git a/apps/mobile/src/features/threads/git/gitSheetComponents.tsx b/apps/mobile/src/features/threads/git/gitSheetComponents.tsx index 4c192ff5bb976..800ff6e8a6b5b 100644 --- a/apps/mobile/src/features/threads/git/gitSheetComponents.tsx +++ b/apps/mobile/src/features/threads/git/gitSheetComponents.tsx @@ -1,4 +1,4 @@ -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; import { SymbolView } from "../../../components/ui/AppSymbol"; import type { ComponentProps } from "react"; import { View } from "react-native"; @@ -59,9 +59,7 @@ export function statusSummary( parts.push(`${gitStatus.behindCount} behind`); } if (gitStatus.pr?.state === "open") { - const { shortLabel } = sourceControlClients.get( - gitStatus.sourceControlProvider?.kind, - ).changeRequest; + const { shortLabel } = changeRequestTermsFor(gitStatus.sourceControlProvider?.kind); parts.push(`${shortLabel} #${gitStatus.pr.number} open`); } diff --git a/apps/mobile/src/features/threads/new-task-context-presentation.ts b/apps/mobile/src/features/threads/new-task-context-presentation.ts index 44efe4cf04843..5e6be81c51487 100644 --- a/apps/mobile/src/features/threads/new-task-context-presentation.ts +++ b/apps/mobile/src/features/threads/new-task-context-presentation.ts @@ -1,5 +1,8 @@ import { sanitizeNewRefName } from "@t3tools/shared/git"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { resolveSendEnvMode } from "@t3tools/shared/projectSettings"; import type { ComposerDraftWorkspaceSelection } from "../../state/use-composer-drafts"; @@ -80,7 +83,8 @@ export function resolveNewTaskBranchLabel(input: { }): string { if (input.workspaceMode === "worktree" && input.pullRequestReference) { const parsed = sourceControlClients.parseChangeRequestUrl(input.pullRequestReference); - if (parsed) return `From ${parsed.definition.changeRequest.shortLabel} #${parsed.link.number}`; + if (parsed) + return `From ${changeRequestTermsFor(parsed.definition).shortLabel} #${parsed.link.number}`; } if (!input.branchName) { return "Choose branch"; diff --git a/apps/mobile/src/features/threads/useLinkedPullRequestActions.ts b/apps/mobile/src/features/threads/useLinkedPullRequestActions.ts index 33ac7013f19d2..06b60ea973d65 100644 --- a/apps/mobile/src/features/threads/useLinkedPullRequestActions.ts +++ b/apps/mobile/src/features/threads/useLinkedPullRequestActions.ts @@ -14,6 +14,7 @@ import { import { planThreadPullRequestMutation } from "@t3tools/client-runtime/thread-pull-request-compatibility"; import { capitalizeTerm, + changeRequestTermsFor, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { AuthOrchestrationOperateScope, type ThreadPullRequestLink } from "@t3tools/contracts"; @@ -35,7 +36,7 @@ export function useLinkedPullRequestActions(input: { readonly runtime: EnvironmentRuntimeState | null; }) { const { thread, project, runtime } = input; - const terminology = sourceControlClients.get(project?.repositoryIdentity?.provider).changeRequest; + const terminology = changeRequestTermsFor(project?.repositoryIdentity?.provider); const capabilities = runtime?.serverConfig?.environment.capabilities; const canOperate = useEnvironmentScope( thread?.environmentId ?? null, @@ -147,10 +148,9 @@ export function useLinkedPullRequestActions(input: { else if (action === "browser") void tryOpenExternalUrl(link.url, "pull-request").then((opened) => { if (!opened) { - const { shortLabel, singular } = ( - sourceControlClients.findByChangeRequestUrl(link.url) ?? - sourceControlClients.get(undefined) - ).changeRequest; + const { shortLabel, singular } = changeRequestTermsFor( + sourceControlClients.findByChangeRequestUrl(link.url), + ); Dialog.alert(`Unable to open ${shortLabel}`, `The ${singular} could not be opened.`); } }); diff --git a/apps/mobile/src/state/thread-pr-presentation.ts b/apps/mobile/src/state/thread-pr-presentation.ts index 2c151676f5c6c..b4ba88036ec59 100644 --- a/apps/mobile/src/state/thread-pr-presentation.ts +++ b/apps/mobile/src/state/thread-pr-presentation.ts @@ -1,8 +1,9 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import type { ThreadPullRequestLink, VcsStatusResult } from "@t3tools/contracts"; import { - sourceControlClients, type ChangeRequestTerminology, + changeRequestTermsFor, + sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { @@ -36,7 +37,7 @@ export function presentThreadPr( pr: ThreadPr, provider: VcsStatusResult["sourceControlProvider"] | null | undefined, ): ThreadPrPresentation { - const { changeRequest } = sourceControlClients.get(provider?.kind); + const changeRequest = changeRequestTermsFor(provider?.kind); const isDraft = pr.state === "open" && pr.isDraft === true; return { kind: "pull-request", @@ -59,8 +60,9 @@ export function presentThreadLinkedPullRequests( const link = resolveThreadCurrentPullRequestLink(links); const badge = resolveThreadPullRequestBadge(links); if (link === null || badge === null) return null; - const { changeRequest } = - sourceControlClients.findByChangeRequestUrl(link.url) ?? sourceControlClients.get(undefined); + const changeRequest = changeRequestTermsFor( + sourceControlClients.findByChangeRequestUrl(link.url), + ); const snapshot = link.snapshot; const linkedCount = badge.kind === "pull-request" && badge.others > 0 ? badge.others + 1 : null; const isMultiple = badge.kind === "stack" || linkedCount !== null; diff --git a/apps/mobile/src/state/threadOutboxDelivery.ts b/apps/mobile/src/state/threadOutboxDelivery.ts index 89d7b68b9201b..7f74358857577 100644 --- a/apps/mobile/src/state/threadOutboxDelivery.ts +++ b/apps/mobile/src/state/threadOutboxDelivery.ts @@ -1,4 +1,7 @@ -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import * as Dialog from "../components/ui/Dialog"; import { type EnvironmentProject, @@ -1026,10 +1029,9 @@ export function createThreadOutboxDelivery(): { return restoreQueuedMessage(persistedMessage, failure.message); if (!AsyncResult.isSuccess(checkout)) return false; if (!checkout.value.worktreePath || !checkout.value.isOnPullRequestHead) { - const { shortLabel } = ( - sourceControlClients.findByChangeRequestUrl(creation.pullRequestReference) ?? - sourceControlClients.get(undefined) - ).changeRequest; + const { shortLabel } = changeRequestTermsFor( + sourceControlClients.findByChangeRequestUrl(creation.pullRequestReference), + ); return restoreQueuedMessage( persistedMessage, `The ${shortLabel} worktree has local changes and could not update to the ${shortLabel} head. Resolve them before trying again.`, diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 350cb9e8340aa..80ffea29f90e1 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -571,6 +571,7 @@ function runStackedAction( commitMessage?: string; featureBranch?: boolean; filePaths?: readonly string[]; + providerTerminology?: boolean; }, options?: Parameters[1], ) { @@ -5521,4 +5522,58 @@ it.layer(layerGitManagerTest)("GitManager", (it) => { ]); }), ); + + it.effect("says pull request for a GitLab branch when provider terminology is off", () => + Effect.gen(function* () { + const repoDir = yield* makeTempDir("t3code-git-manager-"); + yield* initRepo(repoDir); + yield* runGit(repoDir, ["checkout", "-b", "feature/generic-terms"]); + const remoteDir = yield* createBareRemote(); + yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]); + NodeFS.writeFileSync(NodePath.join(repoDir, "terms.txt"), "terms\n"); + yield* runGit(repoDir, ["add", "terms.txt"]); + yield* runGit(repoDir, ["commit", "-m", "Generic terms"]); + yield* runGit(repoDir, ["push", "-u", "origin", "feature/generic-terms"]); + const { manager } = yield* makeManager({ + providerScenario: { + kind: "gitlab", + prListSequence: [ + [], + [ + { + number: 12, + title: "Generic terms", + url: "https://gitlab.com/pingdotgg/codething-mvp/-/merge_requests/12", + baseRefName: "main", + headRefName: "feature/generic-terms", + state: "open", + isCrossRepository: false, + }, + ], + ], + }, + }); + const labels: string[] = []; + + const result = yield* runStackedAction( + manager, + { cwd: repoDir, action: "create_pr", providerTerminology: false }, + { + progressReporter: { + publish: (event) => + Effect.sync(() => { + if (event.kind === "phase_started") labels.push(event.label); + }), + }, + }, + ); + + expect(labels).toEqual([ + "Preparing PR...", + "Generating PR content...", + "Creating pull request...", + ]); + expect(result.toast.title).toBe("Created PR #12"); + }), + ); }); diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 6715f9fd65ed0..9c703d585ce88 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -56,6 +56,7 @@ import { import { isSshRemoteUrl } from "@t3tools/shared/sourceControl"; import { type ChangeRequestTerminology, + resolveChangeRequestTerms, UNKNOWN_SOURCE_CONTROL_CLIENT, } from "@t3tools/source-control-core/shared/definition"; import { @@ -660,11 +661,13 @@ export const make = Effect.gen(function* () { const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd }); const changeRequestTermsFor = Effect.fn("GitManager.changeRequestTermsFor")(function* ( cwd: string, + providerTerminology: boolean, ) { - return yield* sourceControlProvider(cwd).pipe( - Effect.map((provider) => sourceControlDefinitions.get(provider.kind).changeRequest), - Effect.orElseSucceed(() => UNKNOWN_SOURCE_CONTROL_CLIENT.changeRequest), + const definition = yield* sourceControlProvider(cwd).pipe( + Effect.map((provider) => sourceControlDefinitions.get(provider.kind)), + Effect.orElseSucceed(() => UNKNOWN_SOURCE_CONTROL_CLIENT), ); + return resolveChangeRequestTerms(definition, providerTerminology); }); const serverSettingsService = yield* ServerSettings.ServerSettingsService; const threads = yield* ProjectionStore.ProjectionStoreV2; @@ -1893,8 +1896,9 @@ export const make = Effect.gen(function* () { const buildCompletionToast = Effect.fn("buildCompletionToast")(function* ( cwd: string, result: Pick, + providerTerminology: boolean, ) { - const terms = yield* changeRequestTermsFor(cwd); + const terms = yield* changeRequestTermsFor(cwd, providerTerminology); const summary = summarizeGitActionResult(result, terms); let latestOpenPr: PullRequestInfo | null = null; let currentBranchIsDefault = false; @@ -2237,9 +2241,13 @@ export const make = Effect.gen(function* () { fallbackBranch: string | null, threadId: ThreadId | undefined, emit: GitActionProgressEmitter, + providerTerminology: boolean, ) { const provider = yield* sourceControlProvider(cwd); - const terms = sourceControlDefinitions.get(provider.kind).changeRequest; + const terms = resolveChangeRequestTerms( + sourceControlDefinitions.get(provider.kind), + providerTerminology, + ); const details = yield* gitCore.statusDetails(cwd); const branch = details.branch ?? fallbackBranch; if (!branch) { @@ -3145,7 +3153,10 @@ export const make = Effect.gen(function* () { const currentBranch = branchStep.name ?? initialStatus.branch; const commitAction = isCommitAction(input.action) ? input.action : null; - const changeRequestTerms = wantsPr ? yield* changeRequestTermsFor(input.cwd) : null; + const providerTerminology = input.providerTerminology ?? true; + const changeRequestTerms = wantsPr + ? yield* changeRequestTermsFor(input.cwd, providerTerminology) + : null; const commit = commitAction ? yield* Ref.set(currentPhase, Option.some("commit")).pipe( @@ -3194,18 +3205,17 @@ export const make = Effect.gen(function* () { currentBranch, input.threadId, progress.emit, + providerTerminology, ), ), ) : { status: "skipped_not_requested" as const }; - const toast = yield* buildCompletionToast(input.cwd, { - action: input.action, - branch: branchStep, - commit, - push, - pr, - }); + const toast = yield* buildCompletionToast( + input.cwd, + { action: input.action, branch: branchStep, commit, push, pr }, + providerTerminology, + ); const result = { action: input.action, diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index dd37be3b4b6a8..b6ff172719c13 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -174,7 +174,10 @@ import { WORKSPACE_BASENAME_LOOKUP_LIMIT, } from "@t3tools/client-runtime/workspace-basename-lookup"; import { resolvePullRequestPreviewTarget } from "@t3tools/client-runtime/change-request-links"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { useOpenChangeRequestLink } from "~/lib/openPullRequestLink"; import { useOpenLink } from "../browser/useOpenLink"; import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; @@ -1258,10 +1261,9 @@ const CHAT_MARKDOWN_COMPONENTS = { toastManager.add( stackedThreadToast({ type: "error", - title: - operation === "link-pull-request-to-thread" - ? "Unable to link pull request" - : "Unable to unlink pull request", + title: `Unable to ${ + operation === "link-pull-request-to-thread" ? "link" : "unlink" + } ${changeRequestTermsFor(sourceControlClients.findByChangeRequestUrl(href)).singular}`, description: cause instanceof Error ? cause.message : "The request failed.", }), ); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 6fd54db6bd52a..77ea5c7308fa8 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1,6 +1,9 @@ import { ChildThreadApprovals } from "./chat/ChildThreadApprovals"; import { workspaceAreaProps } from "../lib/workspaceFocus"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { resolveNewThreadModelSelection } from "@t3tools/shared/usageLimits"; import { queuedRunEditBlockedReason } from "@t3tools/client-runtime/queued-run-edit"; import { ScrollToEndButton, WorkspaceFileDropOverlay } from "./chat/TimelineOverlays"; @@ -3690,6 +3693,9 @@ export default function ChatView(props: ChatViewProps) { pullRequestRepository={ supportsPullRequests ? activeProjectRepository : null } + pullRequestTerms={changeRequestTermsFor( + activeProject?.repositoryIdentity?.provider, + )} threadPullRequests={(activeThreadShell ?? activeThread)?.pullRequests} onComposerOverlayHeightChange={publishComposerOverlayHeightStable} permissionsShownInStrip={showComposerContextStrip && !isServerThread} @@ -3996,6 +4002,7 @@ export default function ChatView(props: ChatViewProps) { deviceAvailable={activeThreadRef !== null} sideChatAvailable={sideChat.available} sideChatDisabledReason={sideChatDisabledReason} + changeRequestTerms={changeRequestTermsFor(activeProject?.repositoryIdentity?.provider)} > {rightPanelContent} diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 068808707b4f5..a004468b214f7 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -25,7 +25,10 @@ import { type AddProjectRemoteSource, type AddProjectRemoteSourceReadiness, } from "@t3tools/client-runtime/operations/projects"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { connectionStatusText } from "@t3tools/client-runtime/connection"; import { resolveThreadReferenceCopyTarget } from "@t3tools/client-runtime/thread-reference"; import { @@ -1362,7 +1365,13 @@ function OpenCommandPaletteDialog(props: { ? ["copy", "pull request", "link", "reference"] : ["copy", "thread", "thread id", "reference"], title: - activeThreadReferenceCopyTarget.kind === "pull-request" ? "Copy PR link" : "Copy thread", + activeThreadReferenceCopyTarget.kind === "pull-request" + ? `Copy ${ + changeRequestTermsFor( + sourceControlClients.findByChangeRequestUrl(activeThreadReferenceCopyTarget.value), + ).shortLabel + } link` + : "Copy thread", description: activeThreadReferenceCopyTarget.description, icon: , shortcutCommand: "thread.copyReference", @@ -1375,11 +1384,18 @@ function OpenCommandPaletteDialog(props: { threadPullRequestLinkMode(activeThreadServerConfig?.environment.capabilities) !== "unsupported" ) { const threadRef = scopeThreadRef(activeThread.environmentId, activeThread.id); + const { plural, singular } = changeRequestTermsFor( + projects.find( + (project) => + project.environmentId === activeThread.environmentId && + project.id === activeThread.projectId, + )?.repositoryIdentity?.provider, + ); actionItems.push({ kind: "action", value: "action:link-pull-request", - searchTerms: ["link", "pull request", "pr", "attach", "stack"], - title: "Link pull request to thread", + searchTerms: ["link", "pull request", "pr", "merge request", "mr", "attach", "stack"], + title: `Link ${singular} to thread`, icon: , run: async () => { openLinkPullRequestDialog(threadRef); @@ -1388,8 +1404,8 @@ function OpenCommandPaletteDialog(props: { actionItems.push({ kind: "action", value: "action:open-thread-pull-requests", - searchTerms: ["pull requests", "linked", "stack", "prs"], - title: "Show linked pull requests", + searchTerms: ["pull requests", "merge requests", "linked", "stack", "prs", "mrs"], + title: `Show linked ${plural}`, disabled: visibleThreadPullRequests(activeThread.pullRequests).length === 0, icon: , run: async () => { diff --git a/apps/web/src/components/PullRequestContextDetails.tsx b/apps/web/src/components/PullRequestContextDetails.tsx index 1037455724397..8bcf49796e718 100644 --- a/apps/web/src/components/PullRequestContextDetails.tsx +++ b/apps/web/src/components/PullRequestContextDetails.tsx @@ -1,6 +1,7 @@ import type { PullRequestContextMetadata } from "@t3tools/contracts"; import { capitalizeTerm, + changeRequestTermsFor, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { ArrowRightIcon } from "lucide-react"; @@ -12,9 +13,9 @@ import { resolvePullRequestState } from "./pullRequest/pullRequestPresentation"; export function PullRequestContextDetails({ metadata }: { metadata: PullRequestContextMetadata }) { const state = resolvePullRequestState(metadata); // A context chip carries the change request's URL alone, so the URL names its host. - const { singular } = ( - sourceControlClients.findByChangeRequestUrl(metadata.url) ?? sourceControlClients.get(undefined) - ).changeRequest; + const { singular } = changeRequestTermsFor( + sourceControlClients.findByChangeRequestUrl(metadata.url), + ); return (
diff --git a/apps/web/src/components/RightPanelTabs.tsx b/apps/web/src/components/RightPanelTabs.tsx index 596bdee96ad1f..698531fead96f 100644 --- a/apps/web/src/components/RightPanelTabs.tsx +++ b/apps/web/src/components/RightPanelTabs.tsx @@ -1,4 +1,9 @@ -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + capitalizeTerm, + type ChangeRequestTerminology, + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { threadRuntimeIsActive, @@ -153,6 +158,8 @@ export interface RightPanelTabsProps { sideChatAvailable?: boolean; /** Overrides the default reason while side chats are available but cannot start. */ sideChatDisabledReason?: string | null; + /** The thread's host terms; absent reads as the default host's. */ + changeRequestTerms?: ChangeRequestTerminology | undefined; pullRequestStatusSeeds?: Readonly>; children: ReactNode; } @@ -176,8 +183,6 @@ const SURFACE_DISABLED_REASONS = { terminal: "Terminal surfaces are only available from a project thread.", files: "Files are only available when a project is open.", diff: "Diff is only available for server threads in Git repositories.", - pullRequest: "This thread's branch has no pull request yet.", - pullRequests: "No linked pull requests are available for this thread.", device: "Devices are only available from a thread.", sideChat: "Side chats start after the first message.", } as const; @@ -201,8 +206,6 @@ const SURFACE_UNAVAILABLE_HINTS = { terminal: "Available when a project is open.", files: "Available when a project is open.", diff: "Available for Git repositories.", - pullRequest: "No pull request on this branch yet.", - pullRequests: "No linked pull requests available.", device: "Available from a thread.", sideChat: "Available after the first message.", } as const; @@ -333,7 +336,9 @@ function RightPanelEmptyState(props: { deviceAvailable: boolean; sideChatAvailable: boolean; sideChatDisabledReason: string | null; + changeRequestTerms: ChangeRequestTerminology; }) { + const { singular, plural } = props.changeRequestTerms; const [highlight, setHighlight] = useState(-1); const actions = [ @@ -370,19 +375,19 @@ function RightPanelEmptyState(props: { onClick: props.onAddDiff, }, { - label: "Pull request", + label: capitalizeTerm(singular), icon: PullRequestGlyph.pullRequest, shortcut: "P", available: props.pullRequestAvailable, - disabledReason: SURFACE_UNAVAILABLE_HINTS.pullRequest, + disabledReason: `No ${singular} on this branch yet.`, onClick: props.onAddPullRequest, }, { - label: "Linked pull requests", + label: `Linked ${plural}`, icon: PullRequestGlyph.link, shortcut: "L", available: props.pullRequestsAvailable, - disabledReason: SURFACE_UNAVAILABLE_HINTS.pullRequests, + disabledReason: `No linked ${plural} available.`, onClick: props.onAddPullRequests, }, { @@ -571,6 +576,7 @@ function surfaceTitle( surface: RightPanelSurface, sessions: Readonly>, terminalLabelsById: ReadonlyMap, + changeRequestTerms: ChangeRequestTerminology, ): string { switch (surface.kind) { case "diff": @@ -589,7 +595,7 @@ function surfaceTitle( case "pull-request": return `#${surface.number}`; case "pull-requests": - return "Pull requests"; + return capitalizeTerm(changeRequestTerms.plural); case "device": return surface.title ?? surface.target?.name ?? "Device"; case "side-chat": @@ -1023,6 +1029,7 @@ function RightPanelTab( } export function RightPanelTabs(props: RightPanelTabsProps) { + const changeRequestTerms = props.changeRequestTerms ?? changeRequestTermsFor(undefined); const focusedSurface = props.surfaces.find((surface) => surface.id === props.activeSurfaceId); const ownsDesktopTitleBar = isElectron && props.mode === "inline"; const browserProfiles = useBrowserDefaults().profiles; @@ -1124,19 +1131,19 @@ export function RightPanelTabs(props: RightPanelTabsProps) { onClick: props.onAddDiff, }, { - label: "Pull request", + label: capitalizeTerm(changeRequestTerms.singular), icon: PullRequestGlyph.pullRequest, shortcut: "P", available: props.pullRequestAvailable, - disabledReason: SURFACE_DISABLED_REASONS.pullRequest, + disabledReason: `This thread's branch has no ${changeRequestTerms.singular} yet.`, onClick: props.onAddPullRequest, }, { - label: "Linked pull requests", + label: `Linked ${changeRequestTerms.plural}`, icon: PullRequestGlyph.link, shortcut: "L", available: props.pullRequestsAvailable, - disabledReason: SURFACE_DISABLED_REASONS.pullRequests, + disabledReason: `No linked ${changeRequestTerms.plural} are available for this thread.`, onClick: props.onAddPullRequests, }, { @@ -1321,7 +1328,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) { workspaceFocusKey={`panel:${props.environmentId}:${props.activeSurfaceId ?? "empty"}`} workspaceLabel={ focusedSurface - ? `Workspace panel: ${surfaceTitle(focusedSurface, props.previewSessions, props.terminalLabelsById)}` + ? `Workspace panel: ${surfaceTitle(focusedSurface, props.previewSessions, props.terminalLabelsById, changeRequestTerms)}` : "Workspace panel" } mode={props.mode} @@ -1365,7 +1372,12 @@ export function RightPanelTabs(props: RightPanelTabsProps) { dragDisabled={!props.onMoveSurface || renamingDevice === surface.id} active={surface.id === props.activeSurfaceId} pending={props.pendingSurfaceIds.has(surface.id)} - title={surfaceTitle(surface, props.previewSessions, props.terminalLabelsById)} + title={surfaceTitle( + surface, + props.previewSessions, + props.terminalLabelsById, + changeRequestTerms, + )} audio={tabAudioState( previewTabId ? (props.desktopByTabId[previewTabId] ?? null) : null, )} @@ -1543,6 +1555,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) { deviceAvailable={props.deviceAvailable} sideChatAvailable={props.sideChatAvailable === true} sideChatDisabledReason={props.sideChatDisabledReason ?? null} + changeRequestTerms={changeRequestTerms} /> ) : ( props.children diff --git a/apps/web/src/components/ThreadStatusIndicators.tsx b/apps/web/src/components/ThreadStatusIndicators.tsx index ea49b01665b64..5b4cd2147e570 100644 --- a/apps/web/src/components/ThreadStatusIndicators.tsx +++ b/apps/web/src/components/ThreadStatusIndicators.tsx @@ -38,7 +38,10 @@ import { linkedPullRequestDetailAtom, useSharedPullRequestSummary } from "../sta import { useThreadRunningTerminalIds } from "../state/terminalSessions"; import { vcsEnvironment } from "../state/vcs"; import { useUiStateStore } from "../uiStateStore"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { pullRequestMatchesSearchQuery, resolveThreadLastVisitedAt, @@ -123,7 +126,7 @@ function linkedPullRequestReference(fallback: ThreadLinkedPullRequest) { function changeRequestTerminologyForUrl(url: string | undefined) { const host = url === undefined ? null : sourceControlClients.findByChangeRequestUrl(url); - return (host ?? sourceControlClients.get(undefined)).changeRequest; + return changeRequestTermsFor(host); } export function linkedPullRequestSnapshotStatus( @@ -416,7 +419,7 @@ export function prStatusIndicator( provider: VcsStatusResult["sourceControlProvider"] | null | undefined, ): PrStatusIndicator | null { if (!pr) return null; - const { shortLabel } = sourceControlClients.get(provider?.kind).changeRequest; + const { shortLabel } = changeRequestTermsFor(provider?.kind); const state = resolvePullRequestState({ state: pr.state, isDraft: pr.isDraft === true }); const tooltipLead = `${shortLabel} #${pr.number} - ${state.label}`; diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 9ad9158cd4da3..6c1f617405913 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1,3 +1,4 @@ +import type { ChangeRequestTerminology } from "@t3tools/client-runtime/source-control-clients"; import { useComposerTypingGuard } from "./useComposerTypingGuard"; import { insertAttachmentReferencesForDraft } from "./composerAttachmentReferences"; import { DESKTOP_PASTE_AS_TEXT_EVENT } from "../../lib/desktopPasteAsText"; @@ -442,6 +443,7 @@ export interface ChatComposerProps { gitCwd: string | null; pullRequestProjectId: ProjectId | null; pullRequestRepository: string | null; + pullRequestTerms: ChangeRequestTerminology; threadPullRequests: ReadonlyArray | undefined; onComposerOverlayHeightChange: (height: number) => void; /** The new-thread strip names the permissions, so the controls row hides its chip. */ @@ -567,6 +569,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) gitCwd, pullRequestProjectId, pullRequestRepository, + pullRequestTerms, threadPullRequests, onComposerOverlayHeightChange, promptRef, @@ -1151,6 +1154,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) activeThreadId, pullRequestProjectId, pullRequestRepository, + pullRequestTerms, threadPullRequests, planModeUiEnabled, sideChatAvailable: props.sideChatAvailable === true, @@ -3337,7 +3341,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) {composerSuggestionsVisible && composerMenuItems.length === 0 ? isComposerMenuLoading ? composerTriggerKind === "pull-request" - ? "Finding pull request..." + ? `Finding ${pullRequestTerms.singular}...` : composerTriggerKind === "skill" || composerTriggerKind === "slash-command" ? "Searching workspace skills..." : "Searching workspace files..." @@ -3352,6 +3356,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) resolvedTheme={resolvedTheme} isLoading={isComposerMenuLoading} triggerKind={composerTriggerKind} + pullRequestTerms={pullRequestTerms} emptyStateText={composerMenuEmptyState} activeItemId={activeComposerMenuItem?.id ?? null} onHighlightedItemChange={onComposerMenuItemHighlightedStable} diff --git a/apps/web/src/components/chat/ComposerCommandMenu.tsx b/apps/web/src/components/chat/ComposerCommandMenu.tsx index 0b9dda460d8ef..fde495d1882c5 100644 --- a/apps/web/src/components/chat/ComposerCommandMenu.tsx +++ b/apps/web/src/components/chat/ComposerCommandMenu.tsx @@ -1,3 +1,7 @@ +import { + capitalizeTerm, + type ChangeRequestTerminology, +} from "@t3tools/client-runtime/source-control-clients"; import { formatProviderSkillDisplayName } from "@t3tools/shared/inlineSkills"; import type { ComposerPathSearchTarget } from "@t3tools/client-runtime/state/threads"; import { @@ -96,6 +100,7 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { resolvedTheme: "light" | "dark"; isLoading: boolean; triggerKind: ComposerTriggerKind | null; + pullRequestTerms: ChangeRequestTerminology; emptyStateText?: string; activeItemId: string | null; onHighlightedItemChange: (itemId: string | null) => void; @@ -130,7 +135,13 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { {props.items.length > 0 ? ( @@ -155,7 +166,7 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { ? props.triggerKind === "skill" || props.triggerKind === "slash-command" ? "Searching workspace skills..." : props.triggerKind === "pull-request" - ? "Finding pull request..." + ? `Finding ${props.pullRequestTerms.singular}...` : "Searching workspace files..." : (props.emptyStateText ?? (props.triggerKind === "skill" @@ -258,9 +269,8 @@ export function composerSuggestionOptionId(listId: string, itemId: string): stri return `${listId}-${encodeURIComponent(JSON.stringify(itemId))}`; } -const LISTBOX_LABEL_BY_TRIGGER: Record = { +const LISTBOX_LABEL_BY_TRIGGER: Record, string> = { path: "Files and folders", - "pull-request": "Pull requests", "slash-command": "Commands", skill: "Skills", }; diff --git a/apps/web/src/components/chat/ThreadDetailsPrRow.tsx b/apps/web/src/components/chat/ThreadDetailsPrRow.tsx index 8c3f814427991..3ff560178b685 100644 --- a/apps/web/src/components/chat/ThreadDetailsPrRow.tsx +++ b/apps/web/src/components/chat/ThreadDetailsPrRow.tsx @@ -1,6 +1,7 @@ import { resolvePullRequestDetailChecks } from "@t3tools/client-runtime/pull-request-checks"; import { capitalizeTerm, + changeRequestTermsFor, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { PullRequestChangeBreakdownRows } from "../pullRequest/PullRequestChangeBreakdownStat"; @@ -138,9 +139,9 @@ export function ThreadDetailsPrRow({ const checksQuery = useEnvironmentQueryRefresh(checksAtom); const checksAreNewer = useEnvironmentQueryIsNewer(checksAtom, detailAtom); const detail = resolvePullRequestDetailChecks(detailData, checksData, checksAreNewer); - const terminology = sourceControlClients.get( + const terminology = changeRequestTermsFor( detail?.provider ?? project?.repositoryIdentity?.provider, - ).changeRequest; + ); const open = reference !== null && (detail?.state ?? pr?.state) === "open"; const refreshKey = `${environmentId}:${project?.id}:${reference?.host}:${reference?.repository}:${number}`; useLiveRefresh( diff --git a/apps/web/src/components/chat/useComposerMenuItems.ts b/apps/web/src/components/chat/useComposerMenuItems.ts index 5f778ac6cac1e..811834fa4e5fa 100644 --- a/apps/web/src/components/chat/useComposerMenuItems.ts +++ b/apps/web/src/components/chat/useComposerMenuItems.ts @@ -1,3 +1,7 @@ +import { + capitalizeTerm, + type ChangeRequestTerminology, +} from "@t3tools/client-runtime/source-control-clients"; import { matchComposerEnvironmentItems, matchComposerThreadItems, @@ -73,6 +77,7 @@ export function useComposerMenuItems({ showSkillsInSlashMenu, workspaceSkillsScanning, questionAttachmentTarget, + pullRequestTerms, }: { readonly composerTrigger: ComposerTrigger | null; readonly environmentId: EnvironmentId; @@ -81,6 +86,7 @@ export function useComposerMenuItems({ readonly activeThreadId: ThreadId | null; readonly pullRequestProjectId: ProjectId | null; readonly pullRequestRepository: string | null; + readonly pullRequestTerms: ChangeRequestTerminology; readonly threadPullRequests: ReadonlyArray | undefined; readonly planModeUiEnabled: boolean; readonly sideChatAvailable: boolean; @@ -390,17 +396,17 @@ export function useComposerMenuItems({ } if (composerTriggerKind === "pull-request") { if (pullRequestProjectId === null || pullRequestRepository === null) { - return "Pull requests are not available for this project."; + return `${capitalizeTerm(pullRequestTerms.plural)} are not available for this project.`; } if ( pullRequestLookup.error !== null || pullRequestLookup.data?.errors.some((error) => error.projectId === pullRequestProjectId) ) { - return "Pull requests could not be read for this project."; + return `${capitalizeTerm(pullRequestTerms.plural)} could not be read for this project.`; } return composerTrigger?.query - ? `No pull request matches ${composerTrigger.query}.` - : "No pull requests found in this repository."; + ? `No ${pullRequestTerms.singular} matches ${composerTrigger.query}.` + : `No ${pullRequestTerms.plural} found in this repository.`; } return composerTriggerKind === "path" ? "No matching files or folders." diff --git a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx index 8246fad432b43..1734b3f4ea441 100644 --- a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx @@ -1,6 +1,6 @@ import { capitalizeTerm, - sourceControlClients, + changeRequestTermsFor, } from "@t3tools/client-runtime/source-control-clients"; import type { CodeViewItem, DiffLineAnnotation, SelectedLineRange } from "@pierre/diffs"; import type { CodeViewAnnotationArgs, CodeViewDiffItem } from "@pierre/diffs/react"; @@ -1375,7 +1375,7 @@ function PullRequestCodeTab({
); - const term = sourceControlClients.get(detail.provider).changeRequest.singular; + const term = changeRequestTermsFor(detail.provider).singular; if (diffQuery.isPending && loadedSlices.length === 0) { return withToolbar(); } diff --git a/apps/web/src/components/pullRequest/PullRequestCommentForm.tsx b/apps/web/src/components/pullRequest/PullRequestCommentForm.tsx index 8a8ded649a39a..b41db3e5f205e 100644 --- a/apps/web/src/components/pullRequest/PullRequestCommentForm.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCommentForm.tsx @@ -1,4 +1,4 @@ -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; import { useAtomCommand } from "~/state/use-atom-command"; import type { EnvironmentId, PullRequestDetailView, PullRequestRef } from "@t3tools/contracts"; import { SendIcon } from "lucide-react"; @@ -92,7 +92,7 @@ export function PullRequestCommentForm({ value={body} rows={3} placeholder="Leave a comment" - aria-label={`Comment on this ${sourceControlClients.get(detail.provider).changeRequest.singular}`} + aria-label={`Comment on this ${changeRequestTermsFor(detail.provider).singular}`} onChange={(event) => setBody(event.target.value)} onKeyDown={(event) => { if (event.nativeEvent.isComposing || event.keyCode === 229) return; diff --git a/apps/web/src/components/pullRequest/PullRequestComposer.tsx b/apps/web/src/components/pullRequest/PullRequestComposer.tsx index 5c747df739c59..fa830e0b1f191 100644 --- a/apps/web/src/components/pullRequest/PullRequestComposer.tsx +++ b/apps/web/src/components/pullRequest/PullRequestComposer.tsx @@ -1,5 +1,6 @@ import { capitalizeTerm, + changeRequestTermsFor, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import type { EnvironmentId, PullRequestDetailView, PullRequestRef } from "@t3tools/contracts"; @@ -39,7 +40,7 @@ export function PullRequestComposer({ onReviewSubmitted: () => void; openReviewRequest?: number | undefined; }) { - const term = sourceControlClients.get(detail.provider).changeRequest.singular; + const term = changeRequestTermsFor(detail.provider).singular; const [open, setOpen] = useState(false); const [reviewPending, setReviewPending] = useState(false); const [requestedMode, setRequestedMode] = useState<"comment" | "review">("comment"); diff --git a/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx b/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx index 593f9bd9587a3..7a7d6cdc5698c 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx @@ -1,4 +1,7 @@ -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { ArrowLeftIcon, ExternalLinkIcon } from "lucide-react"; import type { MouseEvent as ReactMouseEvent } from "react"; @@ -78,7 +81,7 @@ function NumberLink({ "inline-flex shrink-0 cursor-pointer items-center gap-0.5 font-medium underline-offset-2 hover:underline", toneClassName, )} - aria-label={`Open ${sourceControlClients.get(detail.provider).changeRequest.singular} #${detail.number} on host`} + aria-label={`Open ${changeRequestTermsFor(detail.provider).singular} #${detail.number} on host`} > #{detail.number} diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index 4c8e1e2acbcd6..2ad114a5de0ec 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -69,6 +69,7 @@ import { PullRequestStackMenu } from "./PullRequestStackMenu"; import { PullRequestChangeBreakdownStat } from "./PullRequestChangeBreakdownStat"; import { capitalizeTerm, + changeRequestTermsFor, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { PullRequestThreadLinks } from "./PullRequestThreadLinks"; @@ -469,9 +470,9 @@ export function PullRequestDetailPanel({ const [handoff, setHandoff] = useState(null); const turnRefresh = usePullRequestTurnRefresh(environmentId); const handoffSummary = detail ?? sharedSummary; - const terminology = sourceControlClients.get( + const terminology = changeRequestTermsFor( handoffSummary?.provider ?? listEntry?.provider ?? repositoryIdentity?.provider, - ).changeRequest; + ); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const { copyToClipboard: copyReference } = useCopyToClipboard({ target: `${terminology.singular} reference`, diff --git a/apps/web/src/components/pullRequest/PullRequestGhosts.tsx b/apps/web/src/components/pullRequest/PullRequestGhosts.tsx index c0f7b1959d9e0..66e1f660a1410 100644 --- a/apps/web/src/components/pullRequest/PullRequestGhosts.tsx +++ b/apps/web/src/components/pullRequest/PullRequestGhosts.tsx @@ -12,8 +12,8 @@ import { import type { ReactNode } from "react"; import { capitalizeTerm, - sourceControlClients, type ChangeRequestTerminology, + changeRequestTermsFor, } from "@t3tools/client-runtime/source-control-clients"; import { readLocalApi } from "~/localApi"; @@ -108,7 +108,7 @@ export function PullRequestDetailGhost({ isDraft: summary.isDraft ?? entry?.isDraft, } : entry; - const terminology = sourceControlClients.get(seed?.provider).changeRequest; + const terminology = changeRequestTermsFor(seed?.provider); const statePresentation = seed ? resolvePullRequestState({ state: seed.state, diff --git a/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx b/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx index f8084aa550ad3..e3b82fc8ecdbf 100644 --- a/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx +++ b/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx @@ -1,5 +1,6 @@ import { capitalizeTerm, + changeRequestTermsFor, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { useAtomCommand } from "~/state/use-atom-command"; @@ -407,7 +408,7 @@ export function PullRequestSummaryTab({ onRefresh: () => void; onRefreshChecks?: () => void; }) { - const terminology = sourceControlClients.get(detail.provider).changeRequest; + const terminology = changeRequestTermsFor(detail.provider); const [shown, setShown] = useState({ url: detail.url, count: COMMENT_PAGE }); const shownConversations = shown.url === detail.url ? shown.count : COMMENT_PAGE; const [commentFilter, setCommentFilter] = useLocalStorage( diff --git a/apps/web/src/components/pullRequest/PullRequestThreadLinks.tsx b/apps/web/src/components/pullRequest/PullRequestThreadLinks.tsx index a5b9ce5a3c24c..969359e248a53 100644 --- a/apps/web/src/components/pullRequest/PullRequestThreadLinks.tsx +++ b/apps/web/src/components/pullRequest/PullRequestThreadLinks.tsx @@ -9,7 +9,10 @@ import { useState } from "react"; import { threadPullRequestLinkMode } from "@t3tools/client-runtime/thread-pull-request-compatibility"; import { usePullRequestLinking } from "~/hooks/usePullRequestLinking"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { normalizeThreadPullRequestKey } from "@t3tools/client-runtime/thread-pull-requests"; import { useAnyEnvironmentHiding, @@ -62,8 +65,7 @@ function EnabledPullRequestThreadLinks({ }: PullRequestThreadLinksProps) { const parsedUrl = sourceControlClients.parseChangeRequestUrl(url); const parsed = parsedUrl?.link ?? null; - const term = (parsedUrl?.definition ?? sourceControlClients.get(undefined)).changeRequest - .singular; + const term = changeRequestTermsFor(parsedUrl?.definition).singular; const thread = useThreadShell(threadRef?.environmentId === environmentId ? threadRef : null); const currentThreadRef = thread === null ? null : threadRef; const linking = usePullRequestLinking(environmentId); diff --git a/apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx b/apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx index 1f41f2381148f..14facec190de4 100644 --- a/apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx +++ b/apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx @@ -1,6 +1,6 @@ import { capitalizeTerm, - sourceControlClients, + changeRequestTermsFor, } from "@t3tools/client-runtime/source-control-clients"; import { useAtomCommand } from "~/state/use-atom-command"; import type { @@ -646,7 +646,7 @@ export function PullRequestTimelineTab({ onRefresh: () => void; }) { const events = buildPullRequestTimeline(detail); - const term = sourceControlClients.get(detail.provider).changeRequest.singular; + const term = changeRequestTermsFor(detail.provider).singular; const newestCommitAt = newestPullRequestCommitAt(detail.commits); const reactions: ReactionSurface = { canReact: detail.capabilities.reactions === true, diff --git a/apps/web/src/components/pullRequest/PullRequestTitleEditor.tsx b/apps/web/src/components/pullRequest/PullRequestTitleEditor.tsx index ce6e8a801074d..1c2fb1a3d9e89 100644 --- a/apps/web/src/components/pullRequest/PullRequestTitleEditor.tsx +++ b/apps/web/src/components/pullRequest/PullRequestTitleEditor.tsx @@ -3,7 +3,7 @@ import type { EnvironmentId, PullRequestDetail, PullRequestRef } from "@t3tools/ import { useState } from "react"; import { capitalizeTerm, - sourceControlClients, + changeRequestTermsFor, } from "@t3tools/client-runtime/source-control-clients"; import { pullRequestEnvironment } from "~/state/pullRequests"; @@ -95,7 +95,7 @@ export function PullRequestTitleEditor({ size="sm" disabled={titleSaving} value={titleDraft} - aria-label={`${capitalizeTerm(sourceControlClients.get(detail.provider).changeRequest.singular)} title`} + aria-label={`${capitalizeTerm(changeRequestTermsFor(detail.provider).singular)} title`} onChange={(event) => setTitleScope({ pullRequestKey, text: event.target.value })} onKeyDown={(event) => { if (event.nativeEvent.isComposing || event.keyCode === 229) return; diff --git a/apps/web/src/components/pullRequest/PullRequestsPage.tsx b/apps/web/src/components/pullRequest/PullRequestsPage.tsx index 1b3d1bc74c186..65f2c52209401 100644 --- a/apps/web/src/components/pullRequest/PullRequestsPage.tsx +++ b/apps/web/src/components/pullRequest/PullRequestsPage.tsx @@ -1,4 +1,7 @@ -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { applyPullRequestDraftFilterChange } from "@t3tools/client-runtime/pull-request-query"; import { Spinner } from "~/components/ui/spinner"; import type { PullRequestSpeedActionResult } from "~/components/pullRequest/PullRequestSpeedActions"; @@ -1393,9 +1396,9 @@ export function PullRequestsPage() { event.stopPropagation(); if (event.repeat) return; const url = openPanelPullRequestUrl; - const { shortLabel, singular } = ( - sourceControlClients.findByChangeRequestUrl(url) ?? sourceControlClients.get(undefined) - ).changeRequest; + const { shortLabel, singular } = changeRequestTermsFor( + sourceControlClients.findByChangeRequestUrl(url), + ); void writeTextToClipboard(url, `${singular} link`).then( (didCopy) => { if (didCopy) diff --git a/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx b/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx index 4b1ccae1597a0..628c2691c97a5 100644 --- a/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx +++ b/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx @@ -1,5 +1,7 @@ import type { ProjectId, ScopedThreadRef, ThreadPullRequestLink } from "@t3tools/contracts"; import { + capitalizeTerm, + changeRequestTermsFor, sourceControlClients, UNKNOWN_SOURCE_CONTROL_CLIENT, } from "@t3tools/client-runtime/source-control-clients"; @@ -246,7 +248,7 @@ function LinkRowImpl({ {stack.kind === "native" ? `${sourceControlClients.hostLabelForChangeRequestUrl(link.url)} stack of ${stack.size}: merging a layer lands the ones below it.` - : `${stack.size} ${linkHost.changeRequest.plural} chained by base branch.`} + : `${stack.size} ${changeRequestTermsFor(linkHost).plural} chained by base branch.`} ) : null} @@ -281,7 +283,7 @@ function LinkRowImpl({ ) : null} project.id === thread?.projectId)?.repositoryIdentity + ?.provider, + ); return (
-

No linked pull requests

+

No linked {plural}

- Pull requests the agent opens from this thread land here. Link one yourself from a URL or - a number. + {capitalizeTerm(plural)} the agent opens from this thread land here. Link one yourself + from a URL or a number.

); diff --git a/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts b/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts index 2a7dc7151b92d..81b6a486bd5f3 100644 --- a/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts +++ b/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts @@ -1,7 +1,8 @@ import { capitalizeTerm, - sourceControlClients, type ChangeRequestTerminology, + changeRequestTermsFor, + sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import * as Schema from "effect/Schema"; import { @@ -663,7 +664,7 @@ export function buildPullRequestTimeline( > & Partial>, ): ReadonlyArray { - const term = sourceControlClients.get(detail.provider).changeRequest.singular; + const term = changeRequestTermsFor(detail.provider).singular; return [ { id: "created", @@ -773,8 +774,7 @@ function boundedField(value: string): string { // Prompt builders are handed a change request's URL, not its detail; a URL no host claims reads // as the default host, as an unreported repository does. function terminologyForUrl(url: string): ChangeRequestTerminology { - return (sourceControlClients.findByChangeRequestUrl(url) ?? sourceControlClients.get(undefined)) - .changeRequest; + return changeRequestTermsFor(sourceControlClients.findByChangeRequestUrl(url)); } function reviewThreadContext( diff --git a/apps/web/src/components/pullRequest/usePullRequestActions.ts b/apps/web/src/components/pullRequest/usePullRequestActions.ts index 82ce86ab8f429..13cfe2b492551 100644 --- a/apps/web/src/components/pullRequest/usePullRequestActions.ts +++ b/apps/web/src/components/pullRequest/usePullRequestActions.ts @@ -15,8 +15,9 @@ import type { import { useRef, useState } from "react"; import { capitalizeTerm, - sourceControlClients, type ChangeRequestTerminology, + changeRequestTermsFor, + sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; import { useClientSettings, useEnvironmentSettings } from "~/hooks/useSettings"; @@ -215,7 +216,7 @@ export function usePullRequestCloseBatch(onClosed: (entry: EnvironmentPullReques setClosingKeys(new Set(pending.current)); if (result._tag === "Failure") { failures.push( - `#${entry.number}: ${readableFailure(squashAtomCommandFailure(result), actionFailureHints(sourceControlClients.get(entry.provider).changeRequest.singular).close)}`, + `#${entry.number}: ${readableFailure(squashAtomCommandFailure(result), actionFailureHints(changeRequestTermsFor(entry.provider).singular).close)}`, ); return; } @@ -263,10 +264,9 @@ export function usePullRequestHandoffs({ // Serialize all handoffs because they check out the same pull request. const [handoff, setHandoff] = useState(null); // Prompt links hand over a URL alone, so the URL is what names the host. - const terminology = ( - (detail && sourceControlClients.findByChangeRequestUrl(detail.url)) || - sourceControlClients.get(undefined) - ).changeRequest; + const terminology = changeRequestTermsFor( + detail && sourceControlClients.findByChangeRequestUrl(detail.url), + ); // Questions must not change checkouts or create worktrees. const openThreadWithTask = async ( diff --git a/apps/web/src/components/pullRequest/useThreadLinkedPullRequest.ts b/apps/web/src/components/pullRequest/useThreadLinkedPullRequest.ts index acecaaf6e14de..7da0b2bd58b5b 100644 --- a/apps/web/src/components/pullRequest/useThreadLinkedPullRequest.ts +++ b/apps/web/src/components/pullRequest/useThreadLinkedPullRequest.ts @@ -1,3 +1,7 @@ +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { type EnvironmentId, type ProjectId, @@ -89,7 +93,10 @@ export function useThreadLinkedPullRequest(input: { toastManager.add( stackedThreadToast({ type: "error", - title: "Unable to update the thread pull request", + title: `Unable to update the thread ${ + changeRequestTermsFor(sourceControlClients.findByChangeRequestUrl(replacement.url)) + .singular + }`, description: error instanceof Error ? error.message : "An error occurred.", }), ); diff --git a/apps/web/src/components/settings/SourceControlSettings.tsx b/apps/web/src/components/settings/SourceControlSettings.tsx index b519f7322c7d0..644adcb6318c9 100644 --- a/apps/web/src/components/settings/SourceControlSettings.tsx +++ b/apps/web/src/components/settings/SourceControlSettings.tsx @@ -37,7 +37,9 @@ import { SourceControlTokenSettings } from "./SourceControlTokenSettings"; import { RedactedSensitiveText } from "./RedactedSensitiveText"; import { SourceControlWritingSettingsSection } from "./SourceControlWritingSettings"; import { + SettingResetButton, SettingsPageContainer, + SettingsRow, SettingsSearchTarget, SettingsSection, useSettingsSearchTargetId, @@ -46,6 +48,7 @@ import { searchableSetting, sourceControlHostSettingsSearchId } from "./settings import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; import { sourceControlIcon } from "~/sourceControlPresentation"; +import { useClientSettings, useUpdatePrimarySettings } from "~/hooks/useSettings"; const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = { versionControlSystems: [], @@ -366,6 +369,37 @@ function EmptySourceControlDiscovery({ ); } +function ProviderTerminologySetting() { + const enabled = useClientSettings((settings) => settings.providerSpecificTerminology); + const updateSettings = useUpdatePrimarySettings(); + + return ( + + updateSettings({ providerSpecificTerminology: true })} + /> + ) + } + control={ + + updateSettings({ providerSpecificTerminology: Boolean(checked) }) + } + aria-label="Use version control specific language" + /> + } + /> + + ); +} + export function SourceControlSettingsPanel() { const { scope, environment, connectedEnvironments } = useSettingsScope(); @@ -521,6 +555,7 @@ export function SourceControlSettingsPanel() { /> )} + ); diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index d28e467b0cf70..adcf0e711063f 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -994,6 +994,14 @@ export const SETTINGS_SEARCH_ITEMS = [ scope: "project-defaults", }, ...sourceControlHostSearchItems(sourceControlClients.definitions), + { + id: "provider-specific-terminology", + title: "Use version control specific language", + to: "/settings/source-control", + searchTerms: [ + `terminology wording merge request mr pull request pr ${SOURCE_CONTROL_HOST_LABELS}`, + ], + }, { id: "source-control-writing-style", title: "Source control writing style", diff --git a/apps/web/src/hooks/useSettings.ts b/apps/web/src/hooks/useSettings.ts index 43dfe3e045bb7..c75edbfca462a 100644 --- a/apps/web/src/hooks/useSettings.ts +++ b/apps/web/src/hooks/useSettings.ts @@ -20,6 +20,7 @@ import { type UnifiedSettings, } from "@t3tools/contracts/settings"; import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; +import { setProviderChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import { isAtomCommandInterrupted, runAtomCommand, @@ -94,6 +95,7 @@ function getClientSettingsSnapshot(): ClientSettings { function replaceClientSettingsSnapshot(settings: ClientSettings): void { clientSettingsSnapshot = settings; + setProviderChangeRequestTerms(settings.providerSpecificTerminology); emitClientSettingsChange(); } diff --git a/apps/web/src/lib/composerContextRecords.ts b/apps/web/src/lib/composerContextRecords.ts index c4f123f73dcc4..d6ac5fc24e8a3 100644 --- a/apps/web/src/lib/composerContextRecords.ts +++ b/apps/web/src/lib/composerContextRecords.ts @@ -1,5 +1,6 @@ import { capitalizeTerm, + changeRequestTermsFor, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { @@ -101,7 +102,7 @@ export function pullRequestContextDisplayState( export function pullRequestContextKindLabel(comment: ReviewCommentPresentation): string { const url = comment.pullRequest?.url; const host = url === undefined ? null : sourceControlClients.findByChangeRequestUrl(url); - const term = (host ?? sourceControlClients.get(undefined)).changeRequest.singular; + const term = changeRequestTermsFor(host).singular; const state = pullRequestContextDisplayState(comment); if (state === null) return capitalizeTerm(term); return `${capitalizeTerm(state)} ${term}`; diff --git a/apps/web/src/sourceControlPresentation.tsx b/apps/web/src/sourceControlPresentation.tsx index c122ed0fd38d7..f2267e3089348 100644 --- a/apps/web/src/sourceControlPresentation.tsx +++ b/apps/web/src/sourceControlPresentation.tsx @@ -1,6 +1,9 @@ import type { ElementType } from "react"; import type { SourceControlProviderInfo } from "@t3tools/contracts"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import type { ChangeRequestTerminology, SourceControlDefinition, @@ -42,7 +45,7 @@ function presentDefinition( return { definition, providerName, - terminology: definition.changeRequest, + terminology: changeRequestTermsFor(definition), Icon: sourceControlIcon(definition), }; } diff --git a/docs/user/source-control.md b/docs/user/source-control.md index 9eb2751f9315f..66f28c16b5c0f 100644 --- a/docs/user/source-control.md +++ b/docs/user/source-control.md @@ -169,7 +169,9 @@ uses the project's instructions and recent commit subjects. Open **Pull requests** to review changes and comments, request reviewers, check out a branch, or merge. You can edit review titles and descriptions and your own comments where the host allows it. -GitLab calls these merge requests. +GitLab calls these merge requests, and T3 Code uses that name for GitLab projects. To say +"pull request" everywhere on web and desktop, turn off **Use version control specific language** +in **Settings → Source control**. On mobile, **Pull requests** lists reviews across your connected environments. Open a review to read its description, activity, commits, and changed files. Line comments stay as drafts diff --git a/packages/client-runtime/src/pullRequestLinkInput.ts b/packages/client-runtime/src/pullRequestLinkInput.ts index 472790e5d058a..b3e179ae718fa 100644 --- a/packages/client-runtime/src/pullRequestLinkInput.ts +++ b/packages/client-runtime/src/pullRequestLinkInput.ts @@ -1,7 +1,11 @@ import type { RepositoryIdentity } from "@t3tools/contracts"; import { canonicalizeGitCafePullRequestUrl } from "@t3tools/source-control-definitions/registry"; -import { sourceControlClients, type ChangeRequestTerminology } from "./sourceControlClients.ts"; +import { + type ChangeRequestTerminology, + changeRequestTermsFor, + sourceControlClients, +} from "./sourceControlClients.ts"; const PULL_REQUEST_NUMBER_PATTERN = /^#?(\d+)$/; @@ -47,7 +51,7 @@ export function pullRequestLinkProject(identity: RepositoryIdentity | null | und return { host, repository, - terminology: definition.changeRequest, + terminology: changeRequestTermsFor(definition), webUrl: (number: number) => definition.changeRequestUrl({ host, diff --git a/packages/client-runtime/src/sourceControlClients.test.ts b/packages/client-runtime/src/sourceControlClients.test.ts index 359da39b33b04..933c5dc52a094 100644 --- a/packages/client-runtime/src/sourceControlClients.test.ts +++ b/packages/client-runtime/src/sourceControlClients.test.ts @@ -1,7 +1,12 @@ import { ProjectId, SourceControlProviderKind } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; -import { appSignInTargetForRemote, sourceControlClients } from "./sourceControlClients.ts"; +import { + appSignInTargetForRemote, + changeRequestTermsFor, + setProviderChangeRequestTerms, + sourceControlClients, +} from "./sourceControlClients.ts"; import { normalizeThreadPullRequestKey } from "./threadPullRequests.ts"; const parse = (url: string) => { @@ -386,3 +391,30 @@ describe("appSignInTargetForRemote", () => { expect(appSignInTargetForRemote("https://gitlab.com/acme/web.git")).toBeNull(); }); }); + +describe("changeRequestTermsFor", () => { + it("follows the host unless provider terminology is off", () => { + expect(changeRequestTermsFor("gitlab")).toEqual({ + shortLabel: "MR", + shortLabelWithArticle: "an MR", + singular: "merge request", + plural: "merge requests", + }); + expect(changeRequestTermsFor(sourceControlClients.get("bitbucket")).singular).toBe( + "pull request", + ); + setProviderChangeRequestTerms(false); + try { + for (const kind of ["gitlab", "github", "unknown"]) { + expect(changeRequestTermsFor(kind), kind).toEqual({ + shortLabel: "PR", + shortLabelWithArticle: "a PR", + singular: "pull request", + plural: "pull requests", + }); + } + } finally { + setProviderChangeRequestTerms(true); + } + }); +}); diff --git a/packages/client-runtime/src/sourceControlClients.ts b/packages/client-runtime/src/sourceControlClients.ts index 21cb1530aae30..b22d77d87798a 100644 --- a/packages/client-runtime/src/sourceControlClients.ts +++ b/packages/client-runtime/src/sourceControlClients.ts @@ -1,7 +1,9 @@ import { builtInSourceControlDefinitions } from "@t3tools/source-control-definitions/registry"; import { + type ChangeRequestTerminology, HostCredentialSettings, readSourceControlHostSettings, + resolveChangeRequestTerms, type SourceControlAppSignIn, type SourceControlDefinition, } from "@t3tools/source-control-core/shared/definition"; @@ -25,6 +27,26 @@ export { export const sourceControlClients = builtInSourceControlDefinitions; +// Mirrors the client's `providerSpecificTerminology` setting, so plain functions can read it. +let providerChangeRequestTerms = true; + +export function providerChangeRequestTermsEnabled(): boolean { + return providerChangeRequestTerms; +} + +export function setProviderChangeRequestTerms(enabled: boolean): void { + providerChangeRequestTerms = enabled; +} + +/** The change request terms to show for a host kind or definition, honoring the user's setting. */ +export function changeRequestTermsFor( + host: SourceControlDefinition | string | null | undefined, +): ChangeRequestTerminology { + const definition = + typeof host === "object" && host !== null ? host : sourceControlClients.get(host); + return resolveChangeRequestTerms(definition, providerChangeRequestTerms); +} + /** The saved per-hostname accounts and tokens of a host whose definition has `credentials`. */ export function readHostCredentialSettings( settings: Pick, diff --git a/packages/client-runtime/src/state/gitActions.ts b/packages/client-runtime/src/state/gitActions.ts index fe5a2b77c4e6e..45e00168ac7ab 100644 --- a/packages/client-runtime/src/state/gitActions.ts +++ b/packages/client-runtime/src/state/gitActions.ts @@ -3,7 +3,7 @@ import type { GitStackedAction, VcsStatusResult, } from "@t3tools/contracts"; -import { type ChangeRequestTerminology, sourceControlClients } from "../sourceControlClients.ts"; +import { type ChangeRequestTerminology, changeRequestTermsFor } from "../sourceControlClients.ts"; export type GitActionIconName = "commit" | "push" | "pr"; @@ -46,7 +46,7 @@ export type GitActionRequestInput = Pick< export function changeRequestTerminologyFor( gitStatus: VcsStatusResult | null, ): ChangeRequestTerminology { - return sourceControlClients.get(gitStatus?.sourceControlProvider?.kind).changeRequest; + return changeRequestTermsFor(gitStatus?.sourceControlProvider?.kind); } export function buildMenuItems( diff --git a/packages/client-runtime/src/state/vcsAction.ts b/packages/client-runtime/src/state/vcsAction.ts index 5382e61164472..1835812fcc715 100644 --- a/packages/client-runtime/src/state/vcsAction.ts +++ b/packages/client-runtime/src/state/vcsAction.ts @@ -26,6 +26,7 @@ import { type AtomCommand, type AtomCommandResult, } from "./runtime.ts"; +import { providerChangeRequestTermsEnabled } from "../sourceControlClients.ts"; import { createCommandPermissions } from "./commandPermissions.ts"; import { vcsCommandScheduler } from "./vcsCommandScheduler.ts"; import { invalidateCachedVcsRefs } from "./vcsRefInvalidation.ts"; @@ -474,6 +475,7 @@ export function createVcsActionManager( ...(input.filePaths?.length ? { filePaths: [...input.filePaths] } : {}), ...(input.threadId !== undefined ? { threadId: input.threadId } : {}), ...(input.projectId !== undefined ? { projectId: input.projectId } : {}), + ...(providerChangeRequestTermsEnabled() ? {} : { providerTerminology: false }), }; const clearOwnedState = Effect.sync(() => { const current = registry.get(stateAtom); diff --git a/packages/client-runtime/src/threadReference.ts b/packages/client-runtime/src/threadReference.ts index 94f68202a62cf..6734cbcb78752 100644 --- a/packages/client-runtime/src/threadReference.ts +++ b/packages/client-runtime/src/threadReference.ts @@ -8,6 +8,7 @@ import { threadContextRecord, } from "@t3tools/shared/composerContextReferences"; +import { changeRequestTermsFor, sourceControlClients } from "./sourceControlClients.ts"; import { resolveThreadCurrentPullRequestLink } from "./threadPullRequests.ts"; export interface ThreadReferenceCopyTarget { @@ -53,14 +54,16 @@ export function resolveThreadReferenceCopyTarget(input: { input.openPanelPullRequestUrl ?? resolveThreadCurrentPullRequestLink(input.pullRequests ?? [])?.url ?? input.linkedPullRequestUrl; - return pullRequestUrl - ? { - kind: "pull-request", - value: pullRequestUrl, - description: pullRequestUrl, - clipboardTarget: "pull request link", - successTitle: "PR link copied", - failureTitle: "Failed to copy PR link", - } - : threadChipCopyTarget(input.threadRef, input.title); + if (!pullRequestUrl) return threadChipCopyTarget(input.threadRef, input.title); + const { shortLabel, singular } = changeRequestTermsFor( + sourceControlClients.findByChangeRequestUrl(pullRequestUrl), + ); + return { + kind: "pull-request", + value: pullRequestUrl, + description: pullRequestUrl, + clipboardTarget: `${singular} link`, + successTitle: `${shortLabel} link copied`, + failureTitle: `Failed to copy ${shortLabel} link`, + }; } diff --git a/packages/client-runtime/src/work-log/presentation.ts b/packages/client-runtime/src/work-log/presentation.ts index 29c5f938b5bf0..e40f1a88dac9b 100644 --- a/packages/client-runtime/src/work-log/presentation.ts +++ b/packages/client-runtime/src/work-log/presentation.ts @@ -17,7 +17,10 @@ import { } from "@t3tools/shared/t3McpToolPresentation"; import { classifyMarkdownImageSource } from "@t3tools/client-runtime/markdown-images"; import { resolveMediaSource } from "@t3tools/client-runtime/media-source"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + changeRequestTermsFor, + sourceControlClients, +} from "@t3tools/client-runtime/source-control-clients"; import { isWorkspaceImagePreviewPath } from "@t3tools/shared/mediaPreview"; import { formatTokens } from "@t3tools/shared/usageFormat"; import { classifyToolActivity } from "@t3tools/shared/toolActivity"; @@ -198,8 +201,7 @@ function resolveT3McpToolPresentation( const urlTarget = typeof input?.url === "string" ? sourceControlClients.parseChangeRequestUrl(input.url) : null; const number = urlTarget?.link.number ?? input?.number; - const { shortLabel } = (urlTarget?.definition ?? sourceControlClients.get(undefined)) - .changeRequest; + const { shortLabel } = changeRequestTermsFor(urlTarget?.definition); const target = actionKind !== undefined && actionKind !== "list-prs" && diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 0dc958f59c490..3b8e4d2d4d5bf 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -132,6 +132,8 @@ export const GitRunStackedActionInput = Schema.Struct({ filePaths: Schema.optional(Schema.Array(GitFilePath).check(Schema.isMinLength(1))), threadId: Schema.optional(ThreadId), projectId: Schema.optional(ProjectId), + // False asks for "pull request" wording in progress and toast labels whatever the host calls it. + providerTerminology: Schema.optional(Schema.Boolean), }); export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type; diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 4ff7d2c9618f2..9501f56793f39 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -311,6 +311,15 @@ describe("ClientSettings notifications", () => { }); }); +describe("ClientSettings provider terminology", () => { + it("uses each host's own terms unless turned off", () => { + expect(decodeClientSettings({}).providerSpecificTerminology).toBe(true); + expect( + decodeClientSettings({ providerSpecificTerminology: false }).providerSpecificTerminology, + ).toBe(false); + }); +}); + describe("ClientSettings quit confirmation", () => { it.each([ [true, "hold"], diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index f291cec115f73..b4df35f1a5270 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -463,6 +463,10 @@ export const ClientSettingsSchema = Schema.Struct({ ).pipe(Schema.withDecodingDefault(Effect.succeed({}))), pullRequestLinksOpenInApp: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), + // Off shows "pull request" for every host, including GitLab's merge requests. + providerSpecificTerminology: Schema.Boolean.pipe( + Schema.withDecodingDefault(Effect.succeed(true)), + ), planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), contextWindowMeterEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), @@ -1804,6 +1808,7 @@ export const ClientSettingsPatch = Schema.Struct({ Schema.Record(TrimmedNonEmptyString, PullRequestMergeMethod), ), pullRequestLinksOpenInApp: Schema.optionalKey(Schema.Boolean), + providerSpecificTerminology: Schema.optionalKey(Schema.Boolean), planModeEnabled: Schema.optionalKey(Schema.Boolean), contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean), composerRichTextEnabled: Schema.optionalKey(Schema.Boolean), diff --git a/packages/source-control-core/src/shared/definition.ts b/packages/source-control-core/src/shared/definition.ts index 30e88377590f8..302844c62f429 100644 --- a/packages/source-control-core/src/shared/definition.ts +++ b/packages/source-control-core/src/shared/definition.ts @@ -544,6 +544,21 @@ function defaultRepositorySelector( return identity.owner && identity.name ? `${identity.owner}/${identity.name}` : null; } +const PULL_REQUEST_TERMINOLOGY: ChangeRequestTerminology = { + shortLabel: "PR", + shortLabelWithArticle: "a PR", + singular: "pull request", + plural: "pull requests", +}; + +/** A host's own change request terms, or "pull request" for every host when turned off. */ +export function resolveChangeRequestTerms( + definition: Pick, + useProviderTerms: boolean, +): ChangeRequestTerminology { + return useProviderTerms ? definition.changeRequest : PULL_REQUEST_TERMINOLOGY; +} + export const UNKNOWN_SOURCE_CONTROL_CLIENT: SourceControlDefinition = { kind: SourceControlProviderKind.make("unknown"), label: "source control", From b468170a905aba4079e1e777dcddf4a17a21e218 Mon Sep 17 00:00:00 2001 From: Mason Nightlinger Date: Mon, 2 Nov 2026 12:29:57 -0600 Subject: [PATCH 02/22] feat(web,mobile): name cross-project PR surfaces after the user's hosts The sidebar, Pull requests page, settings section and mobile tabs always said "pull requests", which read oddly for GitLab users once the rest of the UI said "merge requests". These surfaces now use the term every project's host agrees on, and fall back to "pull request" when hosts disagree or the setting is off. Co-Authored-By: Claude Opus 5.5 --- .../SettingsNavigationRouteScreen.tsx | 11 ++++---- .../features/settings/SettingsRouteScreen.tsx | 5 ++-- ...SettingsServerControlsRouteScreen.test.tsx | 3 +++ .../SettingsServerControlsRouteScreen.tsx | 5 +++- .../threads/ComposerCommandPopover.tsx | 24 +++++++++++++----- .../src/features/threads/ThreadComposer.tsx | 2 ++ .../src/features/workspace/WorkspaceDock.tsx | 7 +++++- .../workspace/WorkspaceNativeTabs.ios.tsx | 5 ++-- .../workspace/WorkspaceRootScreen.android.tsx | 9 ++++--- .../workspace/WorkspaceRootScreen.tsx | 16 +++++++----- .../workspace/WorkspaceSearchField.tsx | 5 +++- .../workspace/WorkspaceSectionToolbar.tsx | 5 ++-- .../workspace/WorkspaceToolbar.ios.tsx | 8 ++++-- .../features/workspace/WorkspaceToolbar.tsx | 6 ++++- .../src/features/workspace/navigation-tabs.ts | 11 +++++++- apps/mobile/src/state/entities.ts | 8 +++++- apps/mobile/src/state/projects.ts | 7 ++++++ ...ommandPalette.composition.browser.test.tsx | 2 ++ apps/web/src/components/CommandPalette.tsx | 4 ++- .../pullRequest/PullRequestListEmptyState.tsx | 11 +++++--- .../pullRequest/PullRequestListFilters.tsx | 6 +++-- .../pullRequest/PullRequestsColumn.tsx | 23 +++++++++++------ .../pullRequest/PullRequestsPage.tsx | 25 +++++++++++-------- .../components/settings/SettingsPanels.tsx | 7 ++++-- .../src/components/sidebar/SidebarChrome.tsx | 5 +++- apps/web/src/state/entities.ts | 11 +++++++- apps/web/src/state/projects.ts | 7 ++++++ .../src/sourceControlClients.test.ts | 10 ++++++++ .../src/sourceControlClients.ts | 17 +++++++++++++ 29 files changed, 203 insertions(+), 62 deletions(-) diff --git a/apps/mobile/src/features/settings/SettingsNavigationRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsNavigationRouteScreen.tsx index a4196c07d971f..f030e951be495 100644 --- a/apps/mobile/src/features/settings/SettingsNavigationRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsNavigationRouteScreen.tsx @@ -16,7 +16,7 @@ import { SymbolView } from "../../components/ui/AppSymbol"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; import { NAVIGATION_TAB_ICONS, - WORKSPACE_TAB_LABELS, + useWorkspaceTabLabels, useNavigationTabOrder, useHiddenNavigationTabs, useEnabledNavigationTabs, @@ -28,6 +28,7 @@ import { resolveWorkspaceTabOrder } from "../workspace/workspace-tabs-state"; export function SettingsNavigationRouteScreen() { const insets = useSafeAreaInsets(); const order = useNavigationTabOrder(); + const tabLabels = useWorkspaceTabLabels(); const { layout } = useAdaptiveWorkspaceLayout(); const capacity = Platform.OS === "android" && layout.usesSplitView ? 5 : 4; const hidden = useHiddenNavigationTabs(); @@ -57,7 +58,7 @@ export function SettingsNavigationRouteScreen() { tintColorClassName="accent-icon" /> - {WORKSPACE_TAB_LABELS[tab]} + {tabLabels[tab]} {hidden.includes(tab) ? "Hidden" @@ -68,7 +69,7 @@ export function SettingsNavigationRouteScreen() { save({ @@ -83,7 +84,7 @@ export function SettingsNavigationRouteScreen() { /> save({ @@ -99,7 +100,7 @@ export function SettingsNavigationRouteScreen() { /> save({ diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index 34de55fd44082..0351c8d690d67 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -4,7 +4,7 @@ import { } from "../workspace/navigation-tab-order"; import { NAVIGATION_TAB_ICONS, - WORKSPACE_TAB_LABELS, + useWorkspaceTabLabels, useNavigationTabOrder, useHiddenNavigationTabs, } from "../workspace/navigation-tabs"; @@ -291,10 +291,11 @@ function useOverflowRows(): SettingsListRow[] { const tabs = Platform.OS === "android" && layout.usesSplitView ? [] : overflowNavigationTabs(order, hidden); const selectTab = useSelectWorkspaceTab(); + const tabLabels = useWorkspaceTabLabels(); return tabs.map((tab) => ({ id: tab, icon: NAVIGATION_TAB_ICONS[tab], - label: WORKSPACE_TAB_LABELS[tab], + label: tabLabels[tab], onPress: () => selectTab(tab), })); } diff --git a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx index 698536b9ec1b6..5ea5720201b67 100644 --- a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx +++ b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx @@ -61,6 +61,9 @@ vi.mock("../../state/session", () => ({ })); vi.mock("../../state/server", () => ({ serverEnvironment: { updateSettings: {} } })); vi.mock("../../state/environments", () => ({ useEnvironments: () => ({ environments: [] }) })); +vi.mock("../../state/entities", () => ({ + useProjectsChangeRequestTerms: () => ({ plural: "pull requests" }), +})); vi.mock("../../state/use-atom-command", () => ({ useAtomCommand: () => mocks.updateSettings, })); diff --git a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx index f35c6a29bd846..64642985bca9c 100644 --- a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx @@ -1,4 +1,6 @@ import { useNavigation } from "@react-navigation/native"; +import { useProjectsChangeRequestTerms } from "../../state/entities"; +import { capitalizeTerm } from "@t3tools/client-runtime/source-control-clients"; import { SettingsRow } from "./components/SettingsRow"; import { AuthSettingsWriteScope } from "@t3tools/contracts"; import { readEnvironmentScope, useEnvironmentsWithScope } from "../../state/session"; @@ -177,6 +179,7 @@ export function SettingsEnvironmentMaintenanceRouteScreen() { } function ServerSettingsDetail(props: { readonly page: SettingsPage }) { + const changeRequestTerms = useProjectsChangeRequestTerms(); const insets = useSafeAreaInsets(); const navigation = useNavigation(); const { selectedTargets, projectGroups, selectedProjectKey } = useSettingsEnvironmentFilter(); @@ -413,7 +416,7 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) { canWrite={(environmentId) => writableEnvironments.has(environmentId)} /> )} - + void; + readonly pullRequestTerms: ChangeRequestTerminology; } function PopoverSurface(props: { readonly children: React.ReactNode; readonly style?: ViewStyle }) { @@ -126,10 +131,13 @@ function itemIcon(item: ComposerCommandItem): AppSymbolName | null { } } -function groupLabel(triggerKind: ComposerTriggerKind | null): string | null { +function groupLabel( + triggerKind: ComposerTriggerKind | null, + pullRequestTerms: ChangeRequestTerminology, +): string | null { switch (triggerKind) { case "pull-request": - return "Pull requests"; + return capitalizeTerm(pullRequestTerms.plural); case "slash-command": return "Commands"; case "skill": @@ -141,13 +149,17 @@ function groupLabel(triggerKind: ComposerTriggerKind | null): string | null { } } -function emptyText(triggerKind: ComposerTriggerKind | null, isLoading: boolean): string { +function emptyText( + triggerKind: ComposerTriggerKind | null, + isLoading: boolean, + pullRequestTerms: ChangeRequestTerminology, +): string { if (isLoading) { return triggerKind === "path" ? "Searching files…" : "Loading…"; } switch (triggerKind) { case "pull-request": - return "No matching pull requests."; + return `No matching ${pullRequestTerms.plural}.`; case "path": return "No matching files or folders."; case "skill": @@ -210,7 +222,7 @@ const CommandRow = memo(function CommandRow(props: { export const ComposerCommandPopover = memo(function ComposerCommandPopover( props: ComposerCommandPopoverProps, ) { - const label = groupLabel(props.triggerKind); + const label = groupLabel(props.triggerKind, props.pullRequestTerms); return ( @@ -240,7 +252,7 @@ export const ComposerCommandPopover = memo(function ComposerCommandPopover( ) : ( - {props.error ?? emptyText(props.triggerKind, props.isLoading)} + {props.error ?? emptyText(props.triggerKind, props.isLoading, props.pullRequestTerms)} )} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index ea2928f531374..5bed45f1d4b34 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -65,6 +65,7 @@ import { } from "../../state/use-composer-drafts"; import { appAtomRegistry } from "../../state/atom-registry"; import type { ComposerDocumentAttachment } from "../../lib/composerContext"; +import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; import { useProject, useVisibleThreadShells } from "../../state/entities"; import { useMentionableEnvironments } from "../../state/environments"; import { useLinkedEnvironments } from "../../state/peerLinks"; @@ -839,6 +840,7 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps isLoading={composerMenu.isLoading} error={composerMenu.error} onSelect={composerMenu.onSelect} + pullRequestTerms={changeRequestTermsFor(project?.repositoryIdentity?.provider)} /> ) : null} diff --git a/apps/mobile/src/features/workspace/WorkspaceDock.tsx b/apps/mobile/src/features/workspace/WorkspaceDock.tsx index b4583cbb7d2a4..13bc22e9e7377 100644 --- a/apps/mobile/src/features/workspace/WorkspaceDock.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceDock.tsx @@ -1,5 +1,7 @@ import { Keyboard, Pressable, View } from "react-native"; +import { useProjectsChangeRequestTerms } from "../../state/entities"; +import { capitalizeTerm } from "@t3tools/client-runtime/source-control-clients"; import { SymbolView } from "../../components/ui/AppSymbol"; import { GlassSurface } from "../../components/ui/GlassSurface"; import { useWorkspaceTabs } from "./WorkspaceTabsContext"; @@ -8,6 +10,7 @@ import { PullRequestGlyph } from "../pull-requests/pullRequestGlyphs"; export function WorkspaceDock(props: { readonly onNewTask: () => void }) { const workspace = useWorkspaceTabs()!; + const changeRequestTerms = useProjectsChangeRequestTerms(); return ( void }) { { if (selected) return; diff --git a/apps/mobile/src/features/workspace/WorkspaceNativeTabs.ios.tsx b/apps/mobile/src/features/workspace/WorkspaceNativeTabs.ios.tsx index e93175677fc02..c05b84bcc2520 100644 --- a/apps/mobile/src/features/workspace/WorkspaceNativeTabs.ios.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceNativeTabs.ios.tsx @@ -1,5 +1,5 @@ import { - WORKSPACE_TAB_LABELS, + useWorkspaceTabLabels, NAVIGATION_TAB_ICONS, useEnabledNavigationTabs, } from "./navigation-tabs"; @@ -76,6 +76,7 @@ export function WorkspaceNativeTabs(props: { }) { const workspace = useWorkspaceTabs()!; const order = useEnabledNavigationTabs(); + const tabLabels = useWorkspaceTabLabels(); const primaryTabs = order.slice(0, 4); const overflow = order[4]; const selectTab = useSelectWorkspaceTab(); @@ -125,7 +126,7 @@ export function WorkspaceNativeTabs(props: { key={slot} screenKey={slot} title="" - tabBarItemAccessibilityLabel={settings ? "Settings" : WORKSPACE_TAB_LABELS[tab]} + tabBarItemAccessibilityLabel={settings ? "Settings" : tabLabels[tab]} ios={{ icon: settings ? { type: "sfSymbol", name: "gearshape" } diff --git a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx index a5849398a843d..6743a90022f80 100644 --- a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx @@ -1,4 +1,6 @@ import { useAndroidFloatingTabs } from "../settings/appearance/AppearancePreferencesProvider"; +import { useProjectsChangeRequestTerms } from "../../state/entities"; +import { capitalizeTerm } from "@t3tools/client-runtime/source-control-clients"; import { WorkspaceNoTabs } from "./WorkspaceNoTabs"; import { useEnabledNavigationTabs } from "./navigation-tabs"; import { NAVIGATION_TAB_ORDER } from "./navigation-tab-order"; @@ -27,6 +29,7 @@ export function WorkspaceRootScreen() { const { layout, startNewTask } = useAdaptiveWorkspaceLayout(); const workspace = useWorkspaceTabs()!; const enabled = useEnabledNavigationTabs(); + const changeRequestTerms = useProjectsChangeRequestTerms(); const insets = useSafeAreaInsets(); const { fabClearance, fabSize } = useAndroidControlSizing(); const context = { @@ -76,11 +79,11 @@ export function WorkspaceRootScreen() { ) : layout.usesSplitView ? ( <> - + diff --git a/apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx b/apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx index b9f497cae5608..cefe215f1431d 100644 --- a/apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx @@ -1,5 +1,6 @@ import { WorkspaceNoTabs } from "./WorkspaceNoTabs"; -import { WORKSPACE_TAB_LABELS, useEnabledNavigationTabs } from "./navigation-tabs"; +import { useProjectsChangeRequestTerms } from "../../state/entities"; +import { useEnabledNavigationTabs, useWorkspaceTabLabels } from "./navigation-tabs"; import { WorkspaceExtraTab } from "./WorkspaceExtraTab"; import { SettingsRouteScreen } from "../settings/SettingsRouteScreen"; import { WorkspaceSectionToolbar } from "./WorkspaceSectionToolbar"; @@ -47,19 +48,18 @@ import { import { openSettings } from "../settings/settings-navigation"; import { useSelectWorkspaceTab } from "./useSelectWorkspaceTab"; -const TAB_TITLES = WORKSPACE_TAB_LABELS; - function WorkspaceRootTitle(props: { readonly columnWidth: number; readonly onOpenEnvironments: () => void; }) { const { state, settingsOpen } = useWorkspaceTabs()!; + const tabLabels = useWorkspaceTabLabels(); const maxWidth = Math.max(0, props.columnWidth - (use(HeaderItemsWidthContext) ?? 160)); const enabled = useEnabledNavigationTabs(); const tabTitle = settingsOpen ? "Settings" : enabled.length - ? TAB_TITLES[state.selectedTab] + ? tabLabels[state.selectedTab] : "Navigation"; return ( @@ -97,6 +97,8 @@ function WorkspaceRootContent() { const keyboardHeight = useKeyboardState((keyboard) => keyboard.height); const workspace = useWorkspaceTabs()!; const { state } = workspace; + const tabLabels = useWorkspaceTabLabels(); + const changeRequestTerms = useProjectsChangeRequestTerms(); // Settings is a workspace tab too; its list takes the place of the selected tab's. const settingsOpen = workspace.settingsOpen; const renderHeaderTitle = () => ( @@ -143,7 +145,7 @@ function WorkspaceRootContent() { title: settingsOpen ? "Settings" : enabled.length - ? TAB_TITLES[state.selectedTab] + ? tabLabels[state.selectedTab] : "Navigation", headerShown: true, headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED, @@ -172,7 +174,9 @@ function WorkspaceRootContent() { ? [ { accessibilityLabel: - state.selectedTab === "threads" ? "Search threads" : "Search pull requests", + state.selectedTab === "threads" + ? "Search threads" + : `Search ${changeRequestTerms.plural}`, icon: "magnifyingglass" as const, onPress: () => { dispatchHardwareKeyboardCommand("focusSearch"); diff --git a/apps/mobile/src/features/workspace/WorkspaceSearchField.tsx b/apps/mobile/src/features/workspace/WorkspaceSearchField.tsx index 98fb746e71d19..f4ffe6098204f 100644 --- a/apps/mobile/src/features/workspace/WorkspaceSearchField.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceSearchField.tsx @@ -1,4 +1,5 @@ import type { Ref } from "react"; +import { useProjectsChangeRequestTerms } from "../../state/entities"; import { Keyboard, Pressable, TextInput, View, type TextInputInstance } from "react-native"; import { useResolveClassNames } from "uniwind"; @@ -20,7 +21,9 @@ export function WorkspaceSearchField({ }) { const inputStyle = useResolveClassNames("text-foreground font-sans text-base"); const placeholderStyle = useResolveClassNames("text-foreground-muted"); - const label = props.tab === "pullRequests" ? "Search pull requests" : "Search threads"; + const changeRequestTerms = useProjectsChangeRequestTerms(); + const label = + props.tab === "pullRequests" ? `Search ${changeRequestTerms.plural}` : "Search threads"; return ( - + isSidebarUtilityPage(location.pathname), }); const pullRequestsSupported = usePullRequestsSupported(); + const changeRequestTerms = useProjectsChangeRequestTerms(); const closeMobileSidebar = () => { if (isMobile) { setOpenMobile(false); @@ -274,7 +277,7 @@ const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { {pullRequestsSupported ? ( } - label="Pull requests" + label={capitalizeTerm(changeRequestTerms.plural)} onClick={handlePullRequestsClick} /> ) : null} diff --git a/apps/web/src/state/entities.ts b/apps/web/src/state/entities.ts index 19c0736779897..40b977128dc67 100644 --- a/apps/web/src/state/entities.ts +++ b/apps/web/src/state/entities.ts @@ -28,7 +28,9 @@ import type { import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/thread-checkpoints"; import { Atom } from "effect/reactivity"; import { appAtomRegistry } from "../rpc/atomRegistry"; -import { environmentProjects } from "./projects"; +import { changeRequestTermsForHosts } from "@t3tools/client-runtime/source-control-clients"; +import { useClientSettings } from "../hooks/useSettings"; +import { environmentProjects, projectHostKindsAtom } from "./projects"; import { environmentPresentations } from "./presentation"; import { environmentServerConfigsAtom } from "./server"; import { @@ -96,6 +98,13 @@ export function useProjects(): ReadonlyArray { return useAtomValue(environmentProjects.projectsAtom); } +/** Terms for surfaces spanning every project, such as the sidebar and the pull requests page. */ +export function useProjectsChangeRequestTerms() { + const hostKinds = useAtomValue(projectHostKindsAtom); + const providerTerms = useClientSettings((settings) => settings.providerSpecificTerminology); + return changeRequestTermsForHosts(hostKinds.split(" "), providerTerms); +} + /** Providers are left out; read them with `useEnvironmentProviders()`. */ export function useServerConfigs() { return useAtomValue(environmentPresentations.serverConfigsAtom); diff --git a/apps/web/src/state/projects.ts b/apps/web/src/state/projects.ts index 408a48cae38bd..6bf893b53513e 100644 --- a/apps/web/src/state/projects.ts +++ b/apps/web/src/state/projects.ts @@ -2,6 +2,7 @@ import { createEnvironmentProjectAtoms } from "@t3tools/client-runtime/state/pro import { createProjectEnvironmentAtoms } from "@t3tools/client-runtime/state/projects"; import { createEnvironmentRpcQueryAtomFamily } from "@t3tools/client-runtime/state/runtime"; import { WS_METHODS } from "@t3tools/contracts"; +import { Atom } from "effect/reactivity"; import { environmentCatalog } from "../connection/catalog"; import { connectionAtomRuntime } from "../connection/runtime"; @@ -13,6 +14,12 @@ export const environmentProjects = createEnvironmentProjectAtoms({ snapshotAtom: environmentSnapshotAtom, serverConfigValueAtom: serverEnvironment.configValueAtom, }); +/** The hosts behind every project, as a stable key so readers re-render only when the set changes. */ +export const projectHostKindsAtom = Atom.map(environmentProjects.projectsAtom, (projects) => + [...new Set(projects.flatMap((project) => project.repositoryIdentity?.provider ?? []))] + .toSorted() + .join(" "), +).pipe(Atom.withLabel("web-project-host-kinds")); export const projectEnvironment = createProjectEnvironmentAtoms(connectionAtomRuntime, { projectAtom: environmentProjects.projectAtom, }); diff --git a/packages/client-runtime/src/sourceControlClients.test.ts b/packages/client-runtime/src/sourceControlClients.test.ts index 933c5dc52a094..cdd0e7c1df54a 100644 --- a/packages/client-runtime/src/sourceControlClients.test.ts +++ b/packages/client-runtime/src/sourceControlClients.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from "vite-plus/test"; import { appSignInTargetForRemote, changeRequestTermsFor, + changeRequestTermsForHosts, setProviderChangeRequestTerms, sourceControlClients, } from "./sourceControlClients.ts"; @@ -417,4 +418,13 @@ describe("changeRequestTermsFor", () => { setProviderChangeRequestTerms(true); } }); + + it("names a list after its hosts only when they all agree", () => { + expect(changeRequestTermsForHosts(["gitlab", undefined, "gitlab"]).plural).toBe( + "merge requests", + ); + expect(changeRequestTermsForHosts(["gitlab", "github"]).plural).toBe("pull requests"); + expect(changeRequestTermsForHosts([]).plural).toBe("pull requests"); + expect(changeRequestTermsForHosts(["gitlab"], false).plural).toBe("pull requests"); + }); }); diff --git a/packages/client-runtime/src/sourceControlClients.ts b/packages/client-runtime/src/sourceControlClients.ts index b22d77d87798a..9d0c3e2d9ab16 100644 --- a/packages/client-runtime/src/sourceControlClients.ts +++ b/packages/client-runtime/src/sourceControlClients.ts @@ -6,6 +6,7 @@ import { resolveChangeRequestTerms, type SourceControlAppSignIn, type SourceControlDefinition, + UNKNOWN_SOURCE_CONTROL_CLIENT, } from "@t3tools/source-control-core/shared/definition"; import type { ServerSettings, SourceControlAppAccessInput } from "@t3tools/contracts"; import { parseGitRemoteRepository } from "@t3tools/shared/git"; @@ -47,6 +48,22 @@ export function changeRequestTermsFor( return resolveChangeRequestTerms(definition, providerChangeRequestTerms); } +/** One set of terms for a list spanning hosts: theirs when every host agrees, else "pull request". */ +export function changeRequestTermsForHosts( + hosts: Iterable, + useProviderTerms = providerChangeRequestTerms, +): ChangeRequestTerminology { + const pullRequest = resolveChangeRequestTerms(UNKNOWN_SOURCE_CONTROL_CLIENT, false); + let shared: ChangeRequestTerminology | undefined; + for (const host of hosts) { + if (!host) continue; + const terms = resolveChangeRequestTerms(sourceControlClients.get(host), useProviderTerms); + if (shared !== undefined && shared.plural !== terms.plural) return pullRequest; + shared = terms; + } + return shared ?? pullRequest; +} + /** The saved per-hostname accounts and tokens of a host whose definition has `credentials`. */ export function readHostCredentialSettings( settings: Pick, From 9a33c4dfa4e3d1b833a103f639a49d118e658ef3 Mon Sep 17 00:00:00 2001 From: Mason Nightlinger Date: Mon, 2 Nov 2026 12:56:07 -0600 Subject: [PATCH 03/22] refactor: simplify merge request terminology to a global toggle --- .../src/components/ComposerContextSheet.tsx | 9 +-- .../pull-requests/PullRequestControls.tsx | 4 +- .../pull-requests/PullRequestEditScreen.tsx | 4 +- .../pull-requests/PullRequestFiles.tsx | 4 +- .../pull-requests/PullRequestListRow.tsx | 4 +- .../PullRequestMergeControls.tsx | 4 +- .../pull-requests/PullRequestReviewSheet.tsx | 6 +- .../PullRequestReviewersScreen.tsx | 4 +- .../pull-requests/PullRequestRouteScreen.tsx | 6 +- .../pull-requests/PullRequestStatusRows.tsx | 6 +- .../guide/PullRequestGuideScreen.tsx | 7 +- .../pull-requests/pullRequestActions.test.ts | 9 ++- .../pullRequestAgentTasks.test.ts | 11 +-- .../pull-requests/pullRequestAgentTasks.ts | 4 +- .../pullRequestDiffHighlightFiles.test.ts | 4 +- .../pullRequestDiffPages.test.ts | 15 ++-- .../features/pull-requests/pullRequestLink.ts | 4 +- .../pull-requests/pullRequestStack.test.ts | 13 ++-- .../pull-requests/usePullRequestAction.ts | 4 +- .../pull-requests/usePullRequestDiff.test.tsx | 4 +- .../usePullRequestListActions.ts | 8 +- .../pull-requests/usePullRequestThreads.ts | 4 +- .../review/nativeReviewDiffAdapter.test.ts | 4 +- .../SettingsNavigationRouteScreen.tsx | 11 ++- .../features/settings/SettingsRouteScreen.tsx | 5 +- ...SettingsServerControlsRouteScreen.test.tsx | 3 - .../SettingsServerControlsRouteScreen.tsx | 5 +- .../threads/ComposerCommandPopover.tsx | 24 ++---- .../threads/LinkedPullRequestList.tsx | 7 +- .../src/features/threads/ThreadComposer.tsx | 2 - .../features/threads/ThreadGitControls.tsx | 6 +- .../features/threads/git/GitConfirmSheet.tsx | 6 +- .../features/threads/git/GitOverviewSheet.tsx | 6 +- .../threads/git/gitSheetComponents.tsx | 4 +- .../new-task-context-presentation.test.ts | 2 +- .../threads/new-task-context-presentation.ts | 5 +- .../threads/useLinkedPullRequestActions.ts | 9 +-- .../src/features/workspace/WorkspaceDock.tsx | 7 +- .../workspace/WorkspaceNativeTabs.ios.tsx | 5 +- .../workspace/WorkspaceRootScreen.android.tsx | 9 +-- .../workspace/WorkspaceRootScreen.tsx | 16 ++-- .../workspace/WorkspaceSearchField.tsx | 5 +- .../workspace/WorkspaceSectionToolbar.tsx | 5 +- .../workspace/WorkspaceToolbar.ios.tsx | 8 +- .../features/workspace/WorkspaceToolbar.tsx | 6 +- .../src/features/workspace/navigation-tabs.ts | 11 +-- apps/mobile/src/lib/composerContext.test.ts | 6 +- apps/mobile/src/lib/composerContext.ts | 6 +- apps/mobile/src/state/entities.ts | 8 +- apps/mobile/src/state/projects.ts | 7 -- .../src/state/thread-pr-presentation.ts | 14 +--- apps/mobile/src/state/threadOutboxDelivery.ts | 9 +-- apps/mobile/src/state/use-thread-pr.test.ts | 4 +- apps/mobile/src/state/use-thread-pr.ts | 8 +- apps/server/src/git/GitManager.test.ts | 18 ++--- apps/server/src/git/GitManager.ts | 33 ++------- .../BranchToolbarBranchSelector.tsx | 5 +- apps/web/src/components/ChatMarkdown.tsx | 4 +- apps/web/src/components/ChatView.tsx | 9 +-- ...ommandPalette.composition.browser.test.tsx | 2 - apps/web/src/components/CommandPalette.tsx | 46 +++++------- .../GitActionsControl.logic.test.ts | 12 +-- .../src/components/GitActionsControl.logic.ts | 10 ++- .../components/PullRequestContextDetails.tsx | 12 +-- apps/web/src/components/RightPanelTabs.tsx | 6 +- .../src/components/ThreadStatusIndicators.tsx | 25 ++----- apps/web/src/components/chat/ChatComposer.tsx | 5 +- .../components/chat/ThreadDetailsPrRow.tsx | 6 +- .../components/chat/ThreadDetailsPrRows.tsx | 2 +- .../PullRequestActionsMenu.browser.test.tsx | 4 +- .../pullRequest/PullRequestCodeTab.tsx | 4 +- .../pullRequest/PullRequestCommentForm.tsx | 4 +- .../pullRequest/PullRequestComposer.tsx | 4 +- .../pullRequest/PullRequestDetailCrumbs.tsx | 4 +- .../pullRequest/PullRequestDetailPanel.tsx | 6 +- .../pullRequest/PullRequestGhosts.tsx | 4 +- .../pullRequest/PullRequestListEmptyState.tsx | 4 +- .../pullRequest/PullRequestListFilters.tsx | 4 +- .../PullRequestStackMenu.browser.test.tsx | 4 +- .../pullRequest/PullRequestSummaryTab.tsx | 4 +- .../pullRequest/PullRequestThreadLinks.tsx | 4 +- .../pullRequest/PullRequestTimelineTab.tsx | 4 +- .../pullRequest/PullRequestTitleEditor.tsx | 4 +- .../pullRequest/PullRequestsColumn.tsx | 8 +- .../pullRequest/PullRequestsPage.tsx | 10 +-- .../pullRequest/ThreadPullRequestsPanel.tsx | 11 +-- .../pullRequestDetail.logic.test.ts | 73 ++++++++++++------- .../pullRequest/pullRequestDetail.logic.ts | 23 ++---- .../usePullRequestActions.browser.test.tsx | 4 +- .../pullRequest/usePullRequestActions.ts | 9 +-- .../pullRequest/useThreadLinkedPullRequest.ts | 10 +-- .../components/settings/SettingsPanels.tsx | 4 +- .../settings/SourceControlSettings.tsx | 22 +++--- .../src/components/settings/settingsSearch.ts | 8 +- .../src/components/sidebar/SidebarChrome.tsx | 4 +- .../components/sidebar/SidebarThreadRow.tsx | 2 +- apps/web/src/hooks/useSettings.test.ts | 11 ++- apps/web/src/hooks/useSettings.ts | 14 +++- .../src/lib/composerContextRecords.test.ts | 4 +- apps/web/src/lib/composerContextRecords.ts | 7 +- apps/web/src/sourceControlPresentation.tsx | 4 +- apps/web/src/state/entities.ts | 11 +-- apps/web/src/state/projects.ts | 7 -- docs/user/source-control.md | 5 +- .../src/pullRequestLinkInput.test.ts | 12 ++- .../src/pullRequestLinkInput.ts | 4 +- .../src/sourceControlClients.test.ts | 48 ++++-------- .../src/sourceControlClients.ts | 46 ++++-------- .../src/state/gitActions.test.ts | 20 ++--- .../client-runtime/src/state/gitActions.ts | 17 ++--- .../client-runtime/src/state/vcsAction.ts | 4 +- .../client-runtime/src/threadReference.ts | 6 +- .../src/work-log/presentation.test.ts | 4 +- .../src/work-log/presentation.ts | 4 +- packages/contracts/src/git.ts | 4 +- packages/contracts/src/settings.test.ts | 12 +-- packages/contracts/src/settings.ts | 8 +- .../src/shared/definition.ts | 6 -- .../src/shared/definition.ts | 6 -- .../src/shared/definition.ts | 25 +++---- .../src/shared/definition.ts | 6 -- .../src/shared/definition.ts | 6 -- .../src/shared/definition.ts | 6 -- .../src/shared/definition.ts | 6 -- 124 files changed, 436 insertions(+), 689 deletions(-) diff --git a/apps/mobile/src/components/ComposerContextSheet.tsx b/apps/mobile/src/components/ComposerContextSheet.tsx index 2f9ef02a07dba..d5a86dd10efcd 100644 --- a/apps/mobile/src/components/ComposerContextSheet.tsx +++ b/apps/mobile/src/components/ComposerContextSheet.tsx @@ -11,10 +11,7 @@ import type { ScopedThreadRef, } from "@t3tools/contracts"; import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments"; -import { - changeRequestTermsFor, - sourceControlClients, -} from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import { videoMimeType } from "@t3tools/shared/video"; import { Platform, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -186,9 +183,7 @@ export function ComposerContextSheet(props: { record?.kind === "review-comment" && "pullRequest" in record ? record.pullRequest?.url : undefined; - const changeRequestTerm = changeRequestTermsFor( - pullRequestUrl ? sourceControlClients.findByChangeRequestUrl(pullRequestUrl) : undefined, - ).singular; + const changeRequestTerm = currentChangeRequestTerms().singular; const terminal = record?.kind === "terminal" && !("payload" in record) ? record : null; return ( diff --git a/apps/mobile/src/features/pull-requests/PullRequestControls.tsx b/apps/mobile/src/features/pull-requests/PullRequestControls.tsx index 7312d51189a14..7e2c202a966d9 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestControls.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestControls.tsx @@ -1,4 +1,4 @@ -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import Animated from "react-native-reanimated"; import { PR_ROW_LAYOUT } from "./pullRequestAnimations"; import { useNavigation } from "@react-navigation/native"; @@ -123,7 +123,7 @@ export function PullRequestActionToolbar(props: { const pendingReview = canReview && draft.comments.length > 0; const showAgentControls = agent.canAsk || !permissions.connected; const canOpenThread = agent.threads.length > 0 || showAgentControls; - const { shortLabel } = changeRequestTermsFor((props.pr ?? props.params.initialSummary)?.provider); + const { shortLabel } = currentChangeRequestTerms(); const threadLabel = agent.threads.length ? "Continue thread" : `Work on ${shortLabel}`; const primaryLabel = pendingReview ? `Submit review · ${draft.comments.length}` : threadLabel; const chooseThread = agent.threads.length > 1; diff --git a/apps/mobile/src/features/pull-requests/PullRequestEditScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestEditScreen.tsx index daea489bfcbeb..12fde1644add2 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestEditScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestEditScreen.tsx @@ -1,7 +1,7 @@ import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { capitalizeTerm, - changeRequestTermsFor, + currentChangeRequestTerms, } from "@t3tools/client-runtime/source-control-clients"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { useAtomValue } from "@effect/atom-react"; @@ -40,7 +40,7 @@ export function PullRequestEditScreen({ route }: StaticScreenProps label.name).join(", ")}` : ""}, ${item.additions} additions, ${item.deletions} deletions`} + accessibilityLabel={`${capitalizeTerm(currentChangeRequestTerms().singular)} ${item.number}, ${item.title}, ${status.label}${conflict ? `, ${conflict.label.toLowerCase()}` : ""}, by ${item.author?.login ?? "unknown author"}${review ? `, ${review.accessibleLabel}` : ""}${item.checksState ? `, checks ${item.checksState}` : ""}${item.stack ? `, stack ${item.stack.position} of ${item.stack.size}` : ""}${item.labels.length ? `, ${item.labels.map((label) => label.name).join(", ")}` : ""}, ${item.additions} additions, ${item.deletions} deletions`} accessibilityHint="Double tap to open. Touch and hold for quick actions." onPress={() => props.onSelect(item)} disabled={props.busy} diff --git a/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx b/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx index bf174cc0d3ef6..514caad086389 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx @@ -1,7 +1,7 @@ import * as Dialog from "../../components/ui/Dialog"; import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import type { PullRequestDetail, PullRequestMergeMethod } from "@t3tools/contracts"; import { AsyncResult } from "effect/reactivity"; import { useRef, useState } from "react"; @@ -32,7 +32,7 @@ export function PullRequestMergeControls(props: { readonly stack: PullRequestStackState; }) { const { pr, stack } = props; - const terminology = changeRequestTermsFor(pr.provider); + const terminology = currentChangeRequestTerms(); const target = pullRequestTarget(props.params); const permissions = usePullRequestPermissions(props.params, pr); const preferences = useAtomValue(mobilePreferencesAtom); diff --git a/apps/mobile/src/features/pull-requests/PullRequestReviewSheet.tsx b/apps/mobile/src/features/pull-requests/PullRequestReviewSheet.tsx index 2ced101d3a5ee..be8d3cc42fe97 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestReviewSheet.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestReviewSheet.tsx @@ -2,7 +2,7 @@ import { AsyncResult } from "effect/reactivity"; import { useAtomRefresh } from "@effect/atom-react"; import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { - changeRequestTermsFor, + currentChangeRequestTerms, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { settlePromise, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; @@ -244,8 +244,8 @@ export function PullRequestReviewSheet({ ) : null} {reviewing && !canSubmitComments ? ( - You can no longer submit line comments on this{" "} - {changeRequestTermsFor(result.data?.provider).singular}. Your drafts are kept here. + You can no longer submit line comments on this {currentChangeRequestTerms().singular}. + Your drafts are kept here. ) : null} {submission.error || result.error || store.error ? ( diff --git a/apps/mobile/src/features/pull-requests/PullRequestReviewersScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestReviewersScreen.tsx index 65bdae2ac7b85..e1471f1f6d191 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestReviewersScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestReviewersScreen.tsx @@ -5,7 +5,7 @@ import { usePullRequestConnected } from "./usePullRequestConnected"; import { useAtomValue } from "@effect/atom-react"; import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import type { PullRequestReviewerCandidate } from "@t3tools/contracts"; import { useRef, useState } from "react"; import { @@ -53,7 +53,7 @@ export function PullRequestReviewersScreen({ route }: StaticScreenProps diff --git a/apps/mobile/src/features/pull-requests/guide/PullRequestGuideScreen.tsx b/apps/mobile/src/features/pull-requests/guide/PullRequestGuideScreen.tsx index aae501c59d482..7a8b6fbcbddcb 100644 --- a/apps/mobile/src/features/pull-requests/guide/PullRequestGuideScreen.tsx +++ b/apps/mobile/src/features/pull-requests/guide/PullRequestGuideScreen.tsx @@ -1,4 +1,4 @@ -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import { Toolbar } from "../../../components/ui/Toolbar"; import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; import type { FileDiffMetadata } from "@pierre/diffs"; @@ -108,10 +108,7 @@ export function PullRequestGuideStopScreen({ route }: StaticScreenProps { it("continues a single linked thread, but keeps an explicit new-thread choice separate", () => { @@ -55,9 +58,7 @@ describe("PR agent tasks", () => { expect(pullRequestAgentPrompt("conflicts", GITHUB)).toContain( "Resolve this pull request's conflicts with its base branch. First confirm this thread is on the PR's branch; if it isn't, prepare the PR in its own worktree.", ); - expect( - pullRequestAgentPrompt("conflicts", sourceControlClients.get("gitlab").changeRequest), - ).toContain( + expect(pullRequestAgentPrompt("conflicts", MERGE_REQUEST_TERMINOLOGY)).toContain( "Resolve this merge request's conflicts with its base branch. First confirm this thread is on the MR's branch; if it isn't, prepare the MR in its own worktree.", ); }); diff --git a/apps/mobile/src/features/pull-requests/pullRequestAgentTasks.ts b/apps/mobile/src/features/pull-requests/pullRequestAgentTasks.ts index ceebf86589358..69d22514772d3 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestAgentTasks.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestAgentTasks.ts @@ -1,6 +1,6 @@ import { type ChangeRequestTerminology, - changeRequestTermsFor, + currentChangeRequestTerms, } from "@t3tools/client-runtime/source-control-clients"; import type { PullRequestDetail, PullRequestLinkedThreadsResult } from "@t3tools/contracts"; @@ -34,7 +34,7 @@ export function pullRequestAgentPrompt( } export function pullRequestAgentActions(pr: PullRequestDetail, canWatch: boolean) { - const { shortLabel } = changeRequestTermsFor(pr.provider); + const { shortLabel } = currentChangeRequestTerms(); return [ { task: "ask", diff --git a/apps/mobile/src/features/pull-requests/pullRequestDiffHighlightFiles.test.ts b/apps/mobile/src/features/pull-requests/pullRequestDiffHighlightFiles.test.ts index b48933573f360..a2a5dd5915eac 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestDiffHighlightFiles.test.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestDiffHighlightFiles.test.ts @@ -8,7 +8,7 @@ import { highlightNativeReviewDiffVisibleRows } from "../diffs/nativeReviewDiffH import type { NativeReviewDiffFile } from "../diffs/nativeReviewDiffTypes"; import type { ReviewParsedDiff, ReviewRenderableFile } from "../review/reviewModel"; import { combinePullRequestDiffPages } from "./pullRequestDiffPages"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { PULL_REQUEST_TERMINOLOGY } from "@t3tools/client-runtime/source-control-clients"; import { createPullRequestDiffHighlightCache, createPullRequestDiffHighlightFiles, @@ -345,7 +345,7 @@ function loadPatch(patch: string, truncated = false) { [{ cursor: null, result: { patch, truncated, nextCursor: null } }], new WeakMap(), "complete-source", - sourceControlClients.get("github").changeRequest, + PULL_REQUEST_TERMINOLOGY, ); if (parsed.kind !== "files") throw new Error("Expected parsed files"); const files = parsed.files.map((file) => ({ diff --git a/apps/mobile/src/features/pull-requests/pullRequestDiffPages.test.ts b/apps/mobile/src/features/pull-requests/pullRequestDiffPages.test.ts index ee5f024430747..21248f8a36262 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestDiffPages.test.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestDiffPages.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it, vi } from "vite-plus/test"; import type { PullRequestDiffResult } from "@t3tools/contracts"; import type { ReviewParsedDiff } from "../review/reviewModel"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + MERGE_REQUEST_TERMINOLOGY, + PULL_REQUEST_TERMINOLOGY, +} from "@t3tools/client-runtime/source-control-clients"; import * as reviewModel from "../review/reviewModel"; import { combinePullRequestDiffPages, @@ -9,7 +12,7 @@ import { retainPullRequestDiffPage, } from "./pullRequestDiffPages"; -const GITHUB = sourceControlClients.get("github").changeRequest; +const GITHUB = PULL_REQUEST_TERMINOLOGY; function diff(path: string): PullRequestDiffResult { return { @@ -39,11 +42,7 @@ describe("paged mobile pull request diffs", () => { "first", GITHUB, ); - const otherProvider = getCachedPullRequestDiffPages( - pages, - "first", - sourceControlClients.get("gitlab").changeRequest, - ); + const otherProvider = getCachedPullRequestDiffPages(pages, "first", MERGE_REQUEST_TERMINOLOGY); expect(otherScope).not.toBe(first); expect(otherCursor).not.toBe(first); expect(otherProvider).not.toBe(first); @@ -115,7 +114,7 @@ describe("paged mobile pull request diffs", () => { [{ cursor: null, result: withheldDiff() }], new WeakMap(), "mr", - sourceControlClients.get("gitlab").changeRequest, + MERGE_REQUEST_TERMINOLOGY, ); expect(parsed.kind === "files" ? parsed.notice : null).toBe( "Some file contents are unavailable. Open the merge request on its provider to see the full changes.", diff --git a/apps/mobile/src/features/pull-requests/pullRequestLink.ts b/apps/mobile/src/features/pull-requests/pullRequestLink.ts index fca5636adea41..749aee277440d 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestLink.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestLink.ts @@ -1,4 +1,4 @@ -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import { Platform, Share } from "react-native"; import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; @@ -10,7 +10,7 @@ interface PullRequestLink { export function copyPullRequestLink(pr: PullRequestLink) { copyTextWithHaptic(pr.url, { - target: `${changeRequestTermsFor(pr.provider).singular} link`, + target: `${currentChangeRequestTerms().singular} link`, }); } diff --git a/apps/mobile/src/features/pull-requests/pullRequestStack.test.ts b/apps/mobile/src/features/pull-requests/pullRequestStack.test.ts index 2a2fc9d87cdca..bf24c22bb50cc 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestStack.test.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestStack.test.ts @@ -1,4 +1,7 @@ -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + MERGE_REQUEST_TERMINOLOGY, + PULL_REQUEST_TERMINOLOGY, +} from "@t3tools/client-runtime/source-control-clients"; import { ProjectId, type PullRequestStack, type ThreadPullRequestLink } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import { @@ -8,7 +11,7 @@ import { singlePullRequestMergeAllowed, } from "./pullRequestStack"; -const GITHUB = sourceControlClients.get("github").changeRequest; +const GITHUB = PULL_REQUEST_TERMINOLOGY; const stack: PullRequestStack = { id: "stack", @@ -97,9 +100,9 @@ describe("mobile stack merge scope", () => { }; expect(pullRequestStackMerge(unknown, 2, GITHUB).reason).toContain("latest revisions"); expect(pullRequestStackMerge(stack, 99, GITHUB).reason).toContain("no longer in the stack"); - expect( - pullRequestStackMerge(stack, 99, sourceControlClients.get("gitlab").changeRequest).reason, - ).toBe("This merge request is no longer in the stack. Refresh it."); + expect(pullRequestStackMerge(stack, 99, MERGE_REQUEST_TERMINOLOGY).reason).toBe( + "This merge request is no longer in the stack. Refresh it.", + ); }); it("waits for successful discovery before allowing a single PR merge on modern servers", () => { expect( diff --git a/apps/mobile/src/features/pull-requests/usePullRequestAction.ts b/apps/mobile/src/features/pull-requests/usePullRequestAction.ts index 2ad0661f29499..2b69c57e9cfb0 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestAction.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestAction.ts @@ -2,7 +2,7 @@ import { AsyncResult } from "effect/reactivity"; import * as Dialog from "../../components/ui/Dialog"; import { useAtomRefresh } from "@effect/atom-react"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import type { PullRequestAction, PullRequestDetail, @@ -38,7 +38,7 @@ export function usePullRequestAction(params: PullRequestParams, pr: PullRequestD if (AsyncResult.isFailure(response)) { const cause = squashAtomCommandFailure(response); Dialog.alert( - `Couldn't update this ${changeRequestTermsFor(pr?.provider).singular}`, + `Couldn't update this ${currentChangeRequestTerms().singular}`, cause instanceof Error ? cause.message : "Try again.", ); return; diff --git a/apps/mobile/src/features/pull-requests/usePullRequestDiff.test.tsx b/apps/mobile/src/features/pull-requests/usePullRequestDiff.test.tsx index 3000efb9633bf..003dd8050700b 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestDiff.test.tsx +++ b/apps/mobile/src/features/pull-requests/usePullRequestDiff.test.tsx @@ -1,5 +1,5 @@ import { RegistryContext } from "@effect/atom-react"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { PULL_REQUEST_TERMINOLOGY } from "@t3tools/client-runtime/source-control-clients"; import type { PullRequestDiffResult } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import { Atom, AtomRegistry } from "effect/reactivity"; @@ -53,7 +53,7 @@ function setup() { repository: "repo", number: 1, }, - sourceControlClients.get("github").changeRequest, + PULL_REQUEST_TERMINOLOGY, ); useLayoutEffect(() => { current = result; diff --git a/apps/mobile/src/features/pull-requests/usePullRequestListActions.ts b/apps/mobile/src/features/pull-requests/usePullRequestListActions.ts index d531174fc271f..63d964788b8be 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestListActions.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestListActions.ts @@ -18,7 +18,7 @@ import { openOnProviderLabel, pullRequestActionLabel, } from "./pullRequestActions"; -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import { scopedPullRequestListKey, type EnvironmentPullRequestListEntry } from "./pullRequestList"; import { copyPullRequestLink, sharePullRequestLink } from "./pullRequestLink"; import { pullRequestTarget } from "./usePullRequestDiff"; @@ -58,7 +58,7 @@ export function usePullRequestListActions( action: PullRequestAction | "request-review", ) => { const { environmentId } = entry; - const terminology = changeRequestTermsFor(entry.provider); + const terminology = currentChangeRequestTerms(); if (pending.current) return; pending.current = true; setBusyKey(scopedPullRequestListKey(entry)); @@ -138,7 +138,7 @@ export function usePullRequestListActions( (value) => value === action, ); if (!supported) return; - const label = pullRequestActionLabel(supported, changeRequestTermsFor(entry.provider)); + const label = pullRequestActionLabel(supported, currentChangeRequestTerms()); Dialog.alert(`${label}?`, `#${entry.number} · ${entry.title}`, [ { text: "Cancel", style: "cancel" }, { @@ -149,7 +149,7 @@ export function usePullRequestListActions( ]); }; const actions = (entry: EnvironmentPullRequestListEntry): MenuProps["actions"] => { - const terminology = changeRequestTermsFor(entry.provider); + const terminology = currentChangeRequestTerms(); return [ { id: "open", diff --git a/apps/mobile/src/features/pull-requests/usePullRequestThreads.ts b/apps/mobile/src/features/pull-requests/usePullRequestThreads.ts index 76fd42b2cd731..085262654ffd8 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestThreads.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestThreads.ts @@ -19,7 +19,7 @@ import { } from "@t3tools/contracts"; import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences"; import { - changeRequestTermsFor, + currentChangeRequestTerms, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { normalizeThreadPullRequestKey } from "@t3tools/client-runtime/thread-pull-requests"; @@ -106,7 +106,7 @@ export function usePullRequestThreads( const [busy, setBusy] = useState(false); const ask = async (thread?: LinkedThread, task: PullRequestAgentTask = "ask") => { if (!pr || !canAsk || pending.current) return; - const terminology = changeRequestTermsFor(pr.provider); + const terminology = currentChangeRequestTerms(); pending.current = true; setBusy(true); const result = await settlePromise(async () => { diff --git a/apps/mobile/src/features/review/nativeReviewDiffAdapter.test.ts b/apps/mobile/src/features/review/nativeReviewDiffAdapter.test.ts index 7e620836baccd..78011e9c3fb59 100644 --- a/apps/mobile/src/features/review/nativeReviewDiffAdapter.test.ts +++ b/apps/mobile/src/features/review/nativeReviewDiffAdapter.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vite-plus/test"; import { diffWordsWithSpace } from "diff"; import { AsyncResult } from "effect/reactivity"; -import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { PULL_REQUEST_TERMINOLOGY } from "@t3tools/client-runtime/source-control-clients"; import { EnvironmentId, ProjectId, type PullRequestDiffResult } from "@t3tools/contracts"; import { combinePullRequestDiffPages } from "../pull-requests/pullRequestDiffPages"; @@ -94,7 +94,7 @@ describe("getCachedNativeReviewDiffData", () => { nextCursor: "next", }, }; - const terminology = sourceControlClients.get("github").changeRequest; + const terminology = PULL_REQUEST_TERMINOLOGY; const first = combinePullRequestDiffPages([firstPage], cache, scope, terminology); if (first.kind !== "files") throw new Error("Expected parsed first page"); const before = getCachedNativeReviewDiffData({ parsedDiff: first }); diff --git a/apps/mobile/src/features/settings/SettingsNavigationRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsNavigationRouteScreen.tsx index f030e951be495..a4196c07d971f 100644 --- a/apps/mobile/src/features/settings/SettingsNavigationRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsNavigationRouteScreen.tsx @@ -16,7 +16,7 @@ import { SymbolView } from "../../components/ui/AppSymbol"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; import { NAVIGATION_TAB_ICONS, - useWorkspaceTabLabels, + WORKSPACE_TAB_LABELS, useNavigationTabOrder, useHiddenNavigationTabs, useEnabledNavigationTabs, @@ -28,7 +28,6 @@ import { resolveWorkspaceTabOrder } from "../workspace/workspace-tabs-state"; export function SettingsNavigationRouteScreen() { const insets = useSafeAreaInsets(); const order = useNavigationTabOrder(); - const tabLabels = useWorkspaceTabLabels(); const { layout } = useAdaptiveWorkspaceLayout(); const capacity = Platform.OS === "android" && layout.usesSplitView ? 5 : 4; const hidden = useHiddenNavigationTabs(); @@ -58,7 +57,7 @@ export function SettingsNavigationRouteScreen() { tintColorClassName="accent-icon" /> - {tabLabels[tab]} + {WORKSPACE_TAB_LABELS[tab]} {hidden.includes(tab) ? "Hidden" @@ -69,7 +68,7 @@ export function SettingsNavigationRouteScreen() { save({ @@ -84,7 +83,7 @@ export function SettingsNavigationRouteScreen() { /> save({ @@ -100,7 +99,7 @@ export function SettingsNavigationRouteScreen() { /> save({ diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index 0351c8d690d67..34de55fd44082 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -4,7 +4,7 @@ import { } from "../workspace/navigation-tab-order"; import { NAVIGATION_TAB_ICONS, - useWorkspaceTabLabels, + WORKSPACE_TAB_LABELS, useNavigationTabOrder, useHiddenNavigationTabs, } from "../workspace/navigation-tabs"; @@ -291,11 +291,10 @@ function useOverflowRows(): SettingsListRow[] { const tabs = Platform.OS === "android" && layout.usesSplitView ? [] : overflowNavigationTabs(order, hidden); const selectTab = useSelectWorkspaceTab(); - const tabLabels = useWorkspaceTabLabels(); return tabs.map((tab) => ({ id: tab, icon: NAVIGATION_TAB_ICONS[tab], - label: tabLabels[tab], + label: WORKSPACE_TAB_LABELS[tab], onPress: () => selectTab(tab), })); } diff --git a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx index 5ea5720201b67..698536b9ec1b6 100644 --- a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx +++ b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx @@ -61,9 +61,6 @@ vi.mock("../../state/session", () => ({ })); vi.mock("../../state/server", () => ({ serverEnvironment: { updateSettings: {} } })); vi.mock("../../state/environments", () => ({ useEnvironments: () => ({ environments: [] }) })); -vi.mock("../../state/entities", () => ({ - useProjectsChangeRequestTerms: () => ({ plural: "pull requests" }), -})); vi.mock("../../state/use-atom-command", () => ({ useAtomCommand: () => mocks.updateSettings, })); diff --git a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx index 64642985bca9c..f35c6a29bd846 100644 --- a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx @@ -1,6 +1,4 @@ import { useNavigation } from "@react-navigation/native"; -import { useProjectsChangeRequestTerms } from "../../state/entities"; -import { capitalizeTerm } from "@t3tools/client-runtime/source-control-clients"; import { SettingsRow } from "./components/SettingsRow"; import { AuthSettingsWriteScope } from "@t3tools/contracts"; import { readEnvironmentScope, useEnvironmentsWithScope } from "../../state/session"; @@ -179,7 +177,6 @@ export function SettingsEnvironmentMaintenanceRouteScreen() { } function ServerSettingsDetail(props: { readonly page: SettingsPage }) { - const changeRequestTerms = useProjectsChangeRequestTerms(); const insets = useSafeAreaInsets(); const navigation = useNavigation(); const { selectedTargets, projectGroups, selectedProjectKey } = useSettingsEnvironmentFilter(); @@ -416,7 +413,7 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) { canWrite={(environmentId) => writableEnvironments.has(environmentId)} /> )} - + void; - readonly pullRequestTerms: ChangeRequestTerminology; } function PopoverSurface(props: { readonly children: React.ReactNode; readonly style?: ViewStyle }) { @@ -131,13 +126,10 @@ function itemIcon(item: ComposerCommandItem): AppSymbolName | null { } } -function groupLabel( - triggerKind: ComposerTriggerKind | null, - pullRequestTerms: ChangeRequestTerminology, -): string | null { +function groupLabel(triggerKind: ComposerTriggerKind | null): string | null { switch (triggerKind) { case "pull-request": - return capitalizeTerm(pullRequestTerms.plural); + return "Pull requests"; case "slash-command": return "Commands"; case "skill": @@ -149,17 +141,13 @@ function groupLabel( } } -function emptyText( - triggerKind: ComposerTriggerKind | null, - isLoading: boolean, - pullRequestTerms: ChangeRequestTerminology, -): string { +function emptyText(triggerKind: ComposerTriggerKind | null, isLoading: boolean): string { if (isLoading) { return triggerKind === "path" ? "Searching files…" : "Loading…"; } switch (triggerKind) { case "pull-request": - return `No matching ${pullRequestTerms.plural}.`; + return "No matching pull requests."; case "path": return "No matching files or folders."; case "skill": @@ -222,7 +210,7 @@ const CommandRow = memo(function CommandRow(props: { export const ComposerCommandPopover = memo(function ComposerCommandPopover( props: ComposerCommandPopoverProps, ) { - const label = groupLabel(props.triggerKind, props.pullRequestTerms); + const label = groupLabel(props.triggerKind); return ( @@ -252,7 +240,7 @@ export const ComposerCommandPopover = memo(function ComposerCommandPopover( ) : ( - {props.error ?? emptyText(props.triggerKind, props.isLoading, props.pullRequestTerms)} + {props.error ?? emptyText(props.triggerKind, props.isLoading)} )} diff --git a/apps/mobile/src/features/threads/LinkedPullRequestList.tsx b/apps/mobile/src/features/threads/LinkedPullRequestList.tsx index 0060c84d6902f..42a0ccaf9744a 100644 --- a/apps/mobile/src/features/threads/LinkedPullRequestList.tsx +++ b/apps/mobile/src/features/threads/LinkedPullRequestList.tsx @@ -1,8 +1,7 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { capitalizeTerm, - changeRequestTermsFor, - sourceControlClients, + currentChangeRequestTerms, } from "@t3tools/client-runtime/source-control-clients"; import { pullRequestListLines, @@ -81,9 +80,7 @@ export const LinkedPullRequestRow = memo(function LinkedPullRequestRow(props: { const watching = open && link.watch !== undefined; const additions = snapshot?.additions ?? 0; const deletions = snapshot?.deletions ?? 0; - const { shortLabel, singular } = changeRequestTermsFor( - sourceControlClients.findByChangeRequestUrl(link.url), - ); + const { shortLabel, singular } = currentChangeRequestTerms(); const metaLineHeight = useScaledTextRole("label").lineHeight * useWindowDimensions().fontScale; const row = ( ) : null} diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index b69ae99430899..ba92d7a64ea86 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -17,7 +17,7 @@ import { resolveQuickAction, } from "@t3tools/client-runtime/state/vcs"; import { useNavigation } from "@react-navigation/native"; -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import { useEnvironmentScope } from "../../state/session"; import { PullRequestGlyph } from "../pull-requests/pullRequestGlyphs"; @@ -64,7 +64,7 @@ function compactMenuStatus(gitStatus: VcsStatusResult | null): string { parts.push(`${gitStatus.behindCount} behind`); } if (gitStatus.pr?.state === "open") { - const { shortLabel } = changeRequestTermsFor(gitStatus.sourceControlProvider?.kind); + const { shortLabel } = currentChangeRequestTerms(); parts.push(`${shortLabel} #${gitStatus.pr.number}`); } @@ -167,7 +167,7 @@ function useThreadGitControlModel(props: ThreadGitMenuProps) { })(); const openExistingPr = async () => { - const { shortLabel, singular } = changeRequestTermsFor(gitStatus?.sourceControlProvider?.kind); + const { shortLabel, singular } = currentChangeRequestTerms(); const prUrl = gitStatus?.pr?.state === "open" ? gitStatus.pr.url : null; if (!prUrl) { Dialog.alert(`No open ${shortLabel}`, `This branch does not have an open ${singular}.`); diff --git a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx index 1bb6704d657fa..c10ab91d8b757 100644 --- a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx @@ -1,5 +1,5 @@ import { resolveDefaultBranchActionDialogCopy } from "@t3tools/client-runtime/state/vcs"; -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import { resolveAutoFeatureBranchName } from "@t3tools/shared/git"; import * as Arr from "effect/Array"; import * as Result from "effect/Result"; @@ -15,7 +15,6 @@ import { NativeStackScreenOptions } from "../../../native/StackHeader"; import { AppText as Text } from "../../../components/ui/AppText"; import { useSelectedThreadGitActions } from "../../../state/use-selected-thread-git-actions"; import { useSelectedThreadGitState } from "../../../state/use-selected-thread-git-state"; -import { useThreadSelection } from "../../../state/use-thread-selection"; import { SheetActionButton } from "./gitSheetComponents"; type GitConfirmSheetProps = StaticScreenProps<{ @@ -35,8 +34,7 @@ export function GitConfirmSheet(props: GitConfirmSheetProps) { const gitState = useSelectedThreadGitState(); const gitActions = useSelectedThreadGitActions(); const { canWriteSourceControl, canChangeThreadBranch } = gitActions; - const { selectedThreadProject } = useThreadSelection(); - const terminology = changeRequestTermsFor(selectedThreadProject?.repositoryIdentity?.provider); + const terminology = currentChangeRequestTerms(); const params = props.route.params; diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index c0bbc897bbd63..1fe346742e23f 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -8,7 +8,7 @@ import { requiresDefaultBranchConfirmation, } from "@t3tools/client-runtime/state/vcs"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import { CommonActions, StackActions, @@ -134,9 +134,7 @@ function GitOverviewSheetContent(props: GitOverviewSheetProps) { }), })); - const { shortLabel, singular } = changeRequestTermsFor( - gitStatus.data?.sourceControlProvider?.kind, - ); + const { shortLabel, singular } = currentChangeRequestTerms(); const openExistingPr = async () => { const prUrl = gitStatus.data?.pr?.state === "open" ? gitStatus.data.pr.url : null; diff --git a/apps/mobile/src/features/threads/git/gitSheetComponents.tsx b/apps/mobile/src/features/threads/git/gitSheetComponents.tsx index 800ff6e8a6b5b..bc2edaea32f68 100644 --- a/apps/mobile/src/features/threads/git/gitSheetComponents.tsx +++ b/apps/mobile/src/features/threads/git/gitSheetComponents.tsx @@ -1,4 +1,4 @@ -import { changeRequestTermsFor } from "@t3tools/client-runtime/source-control-clients"; +import { currentChangeRequestTerms } from "@t3tools/client-runtime/source-control-clients"; import { SymbolView } from "../../../components/ui/AppSymbol"; import type { ComponentProps } from "react"; import { View } from "react-native"; @@ -59,7 +59,7 @@ export function statusSummary( parts.push(`${gitStatus.behindCount} behind`); } if (gitStatus.pr?.state === "open") { - const { shortLabel } = changeRequestTermsFor(gitStatus.sourceControlProvider?.kind); + const { shortLabel } = currentChangeRequestTerms(); parts.push(`${shortLabel} #${gitStatus.pr.number} open`); } diff --git a/apps/mobile/src/features/threads/new-task-context-presentation.test.ts b/apps/mobile/src/features/threads/new-task-context-presentation.test.ts index 2bb2659dc6d76..46e44eb0ef24b 100644 --- a/apps/mobile/src/features/threads/new-task-context-presentation.test.ts +++ b/apps/mobile/src/features/threads/new-task-context-presentation.test.ts @@ -51,7 +51,7 @@ describe("resolveNewTaskWorkspaceSelection", () => { describe("resolveNewTaskBranchLabel", () => { it.each([ ["https://github.com/acme/repo/pull/123", "From PR #123"], - ["https://gitlab.com/acme/repo/-/merge_requests/123", "From MR #123"], + ["https://gitlab.com/acme/repo/-/merge_requests/123", "From PR #123"], ])("names the change request a worktree starts from (%s)", (pullRequestReference, label) => { expect( resolveNewTaskBranchLabel({ diff --git a/apps/mobile/src/features/threads/new-task-context-presentation.ts b/apps/mobile/src/features/threads/new-task-context-presentation.ts index 5e6be81c51487..605c10b15a8d7 100644 --- a/apps/mobile/src/features/threads/new-task-context-presentation.ts +++ b/apps/mobile/src/features/threads/new-task-context-presentation.ts @@ -1,6 +1,6 @@ import { sanitizeNewRefName } from "@t3tools/shared/git"; import { - changeRequestTermsFor, + currentChangeRequestTerms, sourceControlClients, } from "@t3tools/client-runtime/source-control-clients"; import { resolveSendEnvMode } from "@t3tools/shared/projectSettings"; @@ -83,8 +83,7 @@ export function resolveNewTaskBranchLabel(input: { }): string { if (input.workspaceMode === "worktree" && input.pullRequestReference) { const parsed = sourceControlClients.parseChangeRequestUrl(input.pullRequestReference); - if (parsed) - return `From ${changeRequestTermsFor(parsed.definition).shortLabel} #${parsed.link.number}`; + if (parsed) return `From ${currentChangeRequestTerms().shortLabel} #${parsed.link.number}`; } if (!input.branchName) { return "Choose branch"; diff --git a/apps/mobile/src/features/threads/useLinkedPullRequestActions.ts b/apps/mobile/src/features/threads/useLinkedPullRequestActions.ts index 06b60ea973d65..430ea4008fe76 100644 --- a/apps/mobile/src/features/threads/useLinkedPullRequestActions.ts +++ b/apps/mobile/src/features/threads/useLinkedPullRequestActions.ts @@ -14,8 +14,7 @@ import { import { planThreadPullRequestMutation } from "@t3tools/client-runtime/thread-pull-request-compatibility"; import { capitalizeTerm, - changeRequestTermsFor, - sourceControlClients, + currentChangeRequestTerms, } from "@t3tools/client-runtime/source-control-clients"; import { AuthOrchestrationOperateScope, type ThreadPullRequestLink } from "@t3tools/contracts"; import * as Clipboard from "expo-clipboard"; @@ -36,7 +35,7 @@ export function useLinkedPullRequestActions(input: { readonly runtime: EnvironmentRuntimeState | null; }) { const { thread, project, runtime } = input; - const terminology = changeRequestTermsFor(project?.repositoryIdentity?.provider); + const terminology = currentChangeRequestTerms(); const capabilities = runtime?.serverConfig?.environment.capabilities; const canOperate = useEnvironmentScope( thread?.environmentId ?? null, @@ -148,9 +147,7 @@ export function useLinkedPullRequestActions(input: { else if (action === "browser") void tryOpenExternalUrl(link.url, "pull-request").then((opened) => { if (!opened) { - const { shortLabel, singular } = changeRequestTermsFor( - sourceControlClients.findByChangeRequestUrl(link.url), - ); + const { shortLabel, singular } = currentChangeRequestTerms(); Dialog.alert(`Unable to open ${shortLabel}`, `The ${singular} could not be opened.`); } }); diff --git a/apps/mobile/src/features/workspace/WorkspaceDock.tsx b/apps/mobile/src/features/workspace/WorkspaceDock.tsx index 13bc22e9e7377..b4583cbb7d2a4 100644 --- a/apps/mobile/src/features/workspace/WorkspaceDock.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceDock.tsx @@ -1,7 +1,5 @@ import { Keyboard, Pressable, View } from "react-native"; -import { useProjectsChangeRequestTerms } from "../../state/entities"; -import { capitalizeTerm } from "@t3tools/client-runtime/source-control-clients"; import { SymbolView } from "../../components/ui/AppSymbol"; import { GlassSurface } from "../../components/ui/GlassSurface"; import { useWorkspaceTabs } from "./WorkspaceTabsContext"; @@ -10,7 +8,6 @@ import { PullRequestGlyph } from "../pull-requests/pullRequestGlyphs"; export function WorkspaceDock(props: { readonly onNewTask: () => void }) { const workspace = useWorkspaceTabs()!; - const changeRequestTerms = useProjectsChangeRequestTerms(); return ( void }) { { if (selected) return; diff --git a/apps/mobile/src/features/workspace/WorkspaceNativeTabs.ios.tsx b/apps/mobile/src/features/workspace/WorkspaceNativeTabs.ios.tsx index c05b84bcc2520..e93175677fc02 100644 --- a/apps/mobile/src/features/workspace/WorkspaceNativeTabs.ios.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceNativeTabs.ios.tsx @@ -1,5 +1,5 @@ import { - useWorkspaceTabLabels, + WORKSPACE_TAB_LABELS, NAVIGATION_TAB_ICONS, useEnabledNavigationTabs, } from "./navigation-tabs"; @@ -76,7 +76,6 @@ export function WorkspaceNativeTabs(props: { }) { const workspace = useWorkspaceTabs()!; const order = useEnabledNavigationTabs(); - const tabLabels = useWorkspaceTabLabels(); const primaryTabs = order.slice(0, 4); const overflow = order[4]; const selectTab = useSelectWorkspaceTab(); @@ -126,7 +125,7 @@ export function WorkspaceNativeTabs(props: { key={slot} screenKey={slot} title="" - tabBarItemAccessibilityLabel={settings ? "Settings" : tabLabels[tab]} + tabBarItemAccessibilityLabel={settings ? "Settings" : WORKSPACE_TAB_LABELS[tab]} ios={{ icon: settings ? { type: "sfSymbol", name: "gearshape" } diff --git a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx index 6743a90022f80..a5849398a843d 100644 --- a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx @@ -1,6 +1,4 @@ import { useAndroidFloatingTabs } from "../settings/appearance/AppearancePreferencesProvider"; -import { useProjectsChangeRequestTerms } from "../../state/entities"; -import { capitalizeTerm } from "@t3tools/client-runtime/source-control-clients"; import { WorkspaceNoTabs } from "./WorkspaceNoTabs"; import { useEnabledNavigationTabs } from "./navigation-tabs"; import { NAVIGATION_TAB_ORDER } from "./navigation-tab-order"; @@ -29,7 +27,6 @@ export function WorkspaceRootScreen() { const { layout, startNewTask } = useAdaptiveWorkspaceLayout(); const workspace = useWorkspaceTabs()!; const enabled = useEnabledNavigationTabs(); - const changeRequestTerms = useProjectsChangeRequestTerms(); const insets = useSafeAreaInsets(); const { fabClearance, fabSize } = useAndroidControlSizing(); const context = { @@ -79,11 +76,11 @@ export function WorkspaceRootScreen() { ) : layout.usesSplitView ? ( <> - + diff --git a/apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx b/apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx index cefe215f1431d..b9f497cae5608 100644 --- a/apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx @@ -1,6 +1,5 @@ import { WorkspaceNoTabs } from "./WorkspaceNoTabs"; -import { useProjectsChangeRequestTerms } from "../../state/entities"; -import { useEnabledNavigationTabs, useWorkspaceTabLabels } from "./navigation-tabs"; +import { WORKSPACE_TAB_LABELS, useEnabledNavigationTabs } from "./navigation-tabs"; import { WorkspaceExtraTab } from "./WorkspaceExtraTab"; import { SettingsRouteScreen } from "../settings/SettingsRouteScreen"; import { WorkspaceSectionToolbar } from "./WorkspaceSectionToolbar"; @@ -48,18 +47,19 @@ import { import { openSettings } from "../settings/settings-navigation"; import { useSelectWorkspaceTab } from "./useSelectWorkspaceTab"; +const TAB_TITLES = WORKSPACE_TAB_LABELS; + function WorkspaceRootTitle(props: { readonly columnWidth: number; readonly onOpenEnvironments: () => void; }) { const { state, settingsOpen } = useWorkspaceTabs()!; - const tabLabels = useWorkspaceTabLabels(); const maxWidth = Math.max(0, props.columnWidth - (use(HeaderItemsWidthContext) ?? 160)); const enabled = useEnabledNavigationTabs(); const tabTitle = settingsOpen ? "Settings" : enabled.length - ? tabLabels[state.selectedTab] + ? TAB_TITLES[state.selectedTab] : "Navigation"; return ( @@ -97,8 +97,6 @@ function WorkspaceRootContent() { const keyboardHeight = useKeyboardState((keyboard) => keyboard.height); const workspace = useWorkspaceTabs()!; const { state } = workspace; - const tabLabels = useWorkspaceTabLabels(); - const changeRequestTerms = useProjectsChangeRequestTerms(); // Settings is a workspace tab too; its list takes the place of the selected tab's. const settingsOpen = workspace.settingsOpen; const renderHeaderTitle = () => ( @@ -145,7 +143,7 @@ function WorkspaceRootContent() { title: settingsOpen ? "Settings" : enabled.length - ? tabLabels[state.selectedTab] + ? TAB_TITLES[state.selectedTab] : "Navigation", headerShown: true, headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED, @@ -174,9 +172,7 @@ function WorkspaceRootContent() { ? [ { accessibilityLabel: - state.selectedTab === "threads" - ? "Search threads" - : `Search ${changeRequestTerms.plural}`, + state.selectedTab === "threads" ? "Search threads" : "Search pull requests", icon: "magnifyingglass" as const, onPress: () => { dispatchHardwareKeyboardCommand("focusSearch"); diff --git a/apps/mobile/src/features/workspace/WorkspaceSearchField.tsx b/apps/mobile/src/features/workspace/WorkspaceSearchField.tsx index f4ffe6098204f..98fb746e71d19 100644 --- a/apps/mobile/src/features/workspace/WorkspaceSearchField.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceSearchField.tsx @@ -1,5 +1,4 @@ import type { Ref } from "react"; -import { useProjectsChangeRequestTerms } from "../../state/entities"; import { Keyboard, Pressable, TextInput, View, type TextInputInstance } from "react-native"; import { useResolveClassNames } from "uniwind"; @@ -21,9 +20,7 @@ export function WorkspaceSearchField({ }) { const inputStyle = useResolveClassNames("text-foreground font-sans text-base"); const placeholderStyle = useResolveClassNames("text-foreground-muted"); - const changeRequestTerms = useProjectsChangeRequestTerms(); - const label = - props.tab === "pullRequests" ? `Search ${changeRequestTerms.plural}` : "Search threads"; + const label = props.tab === "pullRequests" ? "Search pull requests" : "Search threads"; return (