- Detect and route jj repositories alongside git - Thread VCS kind through status and UI labels - Add jj driver contract coverage and shared freshness helper
Showing the changes to apps/web/src (12 of 30 files). Show all
Unified · Split
| @@ -23,6 +23,7 @@ import { cn } from "../lib/utils"; · 22 unmodified lines | |||
| 23 | import { parsePullRequestReference } from "../pullRequestReference"; | 23 | import { parsePullRequestReference } from "../pullRequestReference"; |
| 24 | import { useStore } from "../store"; | 24 | import { useStore } from "../store"; |
| 25 | import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors"; | 25 | import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors"; |
| 26 | import { resolveVcsTerms } from "../vcsPresentation"; | ||
| 26 | import { | 27 | import { |
| 27 | deriveLocalBranchNameFromRemoteRef, | 28 | deriveLocalBranchNameFromRemoteRef, |
| 28 | resolveBranchSelectionTarget, | 29 | resolveBranchSelectionTarget, |
| @@ -66,10 +67,11 @@ function getBranchTriggerLabel(input: { · 37 unmodified lines | |||
| 66 | activeWorktreePath: string | null; | 67 | activeWorktreePath: string | null; |
| 67 | effectiveEnvMode: "local" | "worktree"; | 68 | effectiveEnvMode: "local" | "worktree"; |
| 68 | resolvedActiveBranch: string | null; | 69 | resolvedActiveBranch: string | null; |
| 70 | refNoun: string; | ||
| 69 | }): string { | 71 | }): string { |
| 70 | const { activeWorktreePath, effectiveEnvMode, resolvedActiveBranch } = input; | 72 | const { activeWorktreePath, effectiveEnvMode, resolvedActiveBranch, refNoun } = input; |
| 71 | if (!resolvedActiveBranch) { | 73 | if (!resolvedActiveBranch) { |
| 72 | return "Select ref"; | 74 | return `Select ${refNoun}`; |
| 73 | } | 75 | } |
| 74 | if (effectiveEnvMode === "worktree" && !activeWorktreePath) { | 76 | if (effectiveEnvMode === "worktree" && !activeWorktreePath) { |
| 75 | return `From ${resolvedActiveBranch}`; | 77 | return `From ${resolvedActiveBranch}`; |
| @@ -201,6 +203,7 @@ export function BranchToolbarBranchSelector({ · 125 unmodified lines | |||
| 201 | const deferredBranchQuery = useDeferredValue(branchQuery); | 203 | const deferredBranchQuery = useDeferredValue(branchQuery); |
| 202 | 204 | ||
| 203 | const branchStatusQuery = useGitStatus({ environmentId, cwd: branchCwd }); | 205 | const branchStatusQuery = useGitStatus({ environmentId, cwd: branchCwd }); |
| 206 | const vcsTerms = resolveVcsTerms(branchStatusQuery.data?.kind); | ||
| 204 | const trimmedBranchQuery = branchQuery.trim(); | 207 | const trimmedBranchQuery = branchQuery.trim(); |
| 205 | const deferredTrimmedBranchQuery = deferredBranchQuery.trim(); | 208 | const deferredTrimmedBranchQuery = deferredBranchQuery.trim(); |
| 206 | 209 | ||
| @@ -289,11 +292,11 @@ export function BranchToolbarBranchSelector({ · 82 unmodified lines | |||
| 289 | const shouldVirtualizeBranchList = filteredBranchPickerItems.length > 40; | 292 | const shouldVirtualizeBranchList = filteredBranchPickerItems.length > 40; |
| 290 | const totalBranchCount = branchesSearchData?.pages[0]?.totalCount ?? 0; | 293 | const totalBranchCount = branchesSearchData?.pages[0]?.totalCount ?? 0; |
| 291 | const branchStatusText = isBranchesSearchPending | 294 | const branchStatusText = isBranchesSearchPending |
| 292 | ? "Loading refs..." | 295 | ? `Loading ${vcsTerms.refNounPlural}...` |
| 293 | : isFetchingNextPage | 296 | : isFetchingNextPage |
| 294 | ? "Loading more refs..." | 297 | ? `Loading more ${vcsTerms.refNounPlural}...` |
| 295 | : hasNextPage | 298 | : hasNextPage |
| 296 | ? `Showing ${refs.length} of ${totalBranchCount} refs` | 299 | ? `Showing ${refs.length} of ${totalBranchCount} ${vcsTerms.refNounPlural}` |
| 297 | : null; | 300 | : null; |
| 298 | 301 | ||
| 299 | // --------------------------------------------------------------------------- | 302 | // --------------------------------------------------------------------------- |
| @@ -357,7 +360,7 @@ export function BranchToolbarBranchSelector({ · 57 unmodified lines | |||
| 357 | toastManager.add( | 360 | toastManager.add( |
| 358 | stackedThreadToast({ | 361 | stackedThreadToast({ |
| 359 | type: "error", | 362 | type: "error", |
| 360 | title: "Failed to switch ref.", | 363 | title: `Failed to switch ${vcsTerms.refNoun}.`, |
| 361 | description: toBranchActionErrorMessage(error), | 364 | description: toBranchActionErrorMessage(error), |
| 362 | }), | 365 | }), |
| 363 | ); | 366 | ); |
| @@ -389,7 +392,7 @@ export function BranchToolbarBranchSelector({ · 25 unmodified lines | |||
| 389 | toastManager.add( | 392 | toastManager.add( |
| 390 | stackedThreadToast({ | 393 | stackedThreadToast({ |
| 391 | type: "error", | 394 | type: "error", |
| 392 | title: "Failed to create and switch ref.", | 395 | title: `Failed to create and switch ${vcsTerms.refNoun}.`, |
| 393 | description: toBranchActionErrorMessage(error), | 396 | description: toBranchActionErrorMessage(error), |
| 394 | }), | 397 | }), |
| 395 | ); | 398 | ); |
| @@ -488,6 +491,7 @@ export function BranchToolbarBranchSelector({ · 92 unmodified lines | |||
| 488 | activeWorktreePath, | 491 | activeWorktreePath, |
| 489 | effectiveEnvMode, | 492 | effectiveEnvMode, |
| 490 | resolvedActiveBranch, | 493 | resolvedActiveBranch, |
| 494 | refNoun: vcsTerms.refNoun, | ||
| 491 | }); | 495 | }); |
| 492 | 496 | ||
| 493 | function renderPickerItem(itemValue: string, index: number) { | 497 | function renderPickerItem(itemValue: string, index: number) { |
| @@ -524,7 +528,9 @@ export function BranchToolbarBranchSelector({ · 30 unmodified lines | |||
| 524 | value={itemValue} | 528 | value={itemValue} |
| 525 | onClick={() => createRef(trimmedBranchQuery)} | 529 | onClick={() => createRef(trimmedBranchQuery)} |
| 526 | > | 530 | > |
| 527 | <span className="truncate">Create new ref "{trimmedBranchQuery}"</span> | 531 | <span className="truncate"> |
| 532 | Create new {vcsTerms.refNoun} "{trimmedBranchQuery}" | ||
| 533 | </span> | ||
| 528 | </ComboboxItem> | 534 | </ComboboxItem> |
| 529 | ); | 535 | ); |
| 530 | } | 536 | } |
| @@ -1458,7 +1458,8 @@ export default function ChatView(props: ChatViewProps) { · 1457 unmodified lines | |||
| 1458 | ? terminalLaunchContext | 1458 | ? terminalLaunchContext |
| 1459 | : (storeServerTerminalLaunchContext ?? null); | 1459 | : (storeServerTerminalLaunchContext ?? null); |
| 1460 | // Default true while loading to avoid toolbar flicker. | 1460 | // Default true while loading to avoid toolbar flicker. |
| 1461 | const isGitRepo = gitStatusQuery.data?.isRepo ?? true; | 1461 | const vcsKind = gitStatusQuery.data?.kind ?? null; |
| 1462 | const isVcsRepo = gitStatusQuery.data?.isRepo ?? true; | ||
| 1462 | const terminalShortcutLabelOptions = useMemo( | 1463 | const terminalShortcutLabelOptions = useMemo( |
| 1463 | () => ({ | 1464 | () => ({ |
| 1464 | context: { | 1465 | context: { |
| @@ -2136,7 +2137,7 @@ export default function ChatView(props: ChatViewProps) { · 671 unmodified lines | |||
| 2136 | : (activeThread?.branch ?? null); | 2137 | : (activeThread?.branch ?? null); |
| 2137 | const sendEnvMode = resolveSendEnvMode({ | 2138 | const sendEnvMode = resolveSendEnvMode({ |
| 2138 | requestedEnvMode: envMode, | 2139 | requestedEnvMode: envMode, |
| 2139 | isGitRepo, | 2140 | isGitRepo: isVcsRepo, |
| 2140 | }); | 2141 | }); |
| 2141 | 2142 | ||
| 2142 | useEffect(() => { | 2143 | useEffect(() => { |
| @@ -3297,7 +3298,8 @@ export default function ChatView(props: ChatViewProps) { · 1154 unmodified lines | |||
| 3297 | {...(routeKind === "draft" && draftId ? { draftId } : {})} | 3298 | {...(routeKind === "draft" && draftId ? { draftId } : {})} |
| 3298 | activeThreadTitle={activeThread.title} | 3299 | activeThreadTitle={activeThread.title} |
| 3299 | activeProjectName={activeProject?.name} | 3300 | activeProjectName={activeProject?.name} |
| 3300 | isGitRepo={isGitRepo} | 3301 | isVcsRepo={isVcsRepo} |
| 3302 | vcsKind={vcsKind} | ||
| 3301 | openInCwd={gitCwd} | 3303 | openInCwd={gitCwd} |
| 3302 | activeProjectScripts={activeProject?.scripts} | 3304 | activeProjectScripts={activeProject?.scripts} |
| 3303 | preferredScriptId={ | 3305 | preferredScriptId={ |
| @@ -3377,7 +3379,7 @@ export default function ChatView(props: ChatViewProps) { · 73 unmodified lines | |||
| 3377 | <div | 3379 | <div |
| 3378 | className={cn( | 3380 | className={cn( |
| 3379 | "pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] pt-1.5 sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)] sm:pt-2", | 3381 | "pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] pt-1.5 sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)] sm:pt-2", |
| 3380 | isGitRepo | 3382 | isVcsRepo |
| 3381 | ? "pb-[calc(env(safe-area-inset-bottom)+0.25rem)]" | 3383 | ? "pb-[calc(env(safe-area-inset-bottom)+0.25rem)]" |
| 3382 | : "pb-[calc(env(safe-area-inset-bottom)+0.75rem)] sm:pb-[calc(env(safe-area-inset-bottom)+1rem)]", | 3384 | : "pb-[calc(env(safe-area-inset-bottom)+0.75rem)] sm:pb-[calc(env(safe-area-inset-bottom)+1rem)]", |
| 3383 | )} | 3385 | )} |
| @@ -3450,7 +3452,7 @@ export default function ChatView(props: ChatViewProps) { · 66 unmodified lines | |||
| 3450 | setThreadError={setThreadError} | 3452 | setThreadError={setThreadError} |
| 3451 | onExpandImage={onExpandTimelineImage} | 3453 | onExpandImage={onExpandTimelineImage} |
| 3452 | /> | 3454 | /> |
| 3453 | {isGitRepo && ( | 3455 | {isVcsRepo && ( |
| 3454 | <BranchToolbar | 3456 | <BranchToolbar |
| 3455 | environmentId={activeThread.environmentId} | 3457 | environmentId={activeThread.environmentId} |
| 3456 | threadId={activeThread.id} | 3458 | threadId={activeThread.id} |
| @@ -35,6 +35,7 @@ import { createThreadSelectorByRef } from "../storeSelectors"; · 34 unmodified lines | |||
| 35 | import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes"; | 35 | import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes"; |
| 36 | import { useSettings } from "../hooks/useSettings"; | 36 | import { useSettings } from "../hooks/useSettings"; |
| 37 | import { formatShortTimestamp } from "../timestampFormat"; | 37 | import { formatShortTimestamp } from "../timestampFormat"; |
| 38 | import { resolveVcsTerms } from "../vcsPresentation"; | ||
| 38 | import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; | 39 | import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; |
| 39 | import { ToggleGroup, Toggle } from "./ui/toggle-group"; | 40 | import { ToggleGroup, Toggle } from "./ui/toggle-group"; |
| 40 | 41 | ||
| @@ -201,7 +202,8 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { · 160 unmodified lines | |||
| 201 | environmentId: activeThread?.environmentId ?? null, | 202 | environmentId: activeThread?.environmentId ?? null, |
| 202 | cwd: activeCwd ?? null, | 203 | cwd: activeCwd ?? null, |
| 203 | }); | 204 | }); |
| 204 | const isGitRepo = gitStatusQuery.data?.isRepo ?? true; | 205 | const isVcsRepo = gitStatusQuery.data?.isRepo ?? true; |
| 206 | const vcsTerms = resolveVcsTerms(gitStatusQuery.data?.kind); | ||
| 205 | const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = | 207 | const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = |
| 206 | useTurnDiffSummaries(activeThread); | 208 | useTurnDiffSummaries(activeThread); |
| 207 | const orderedTurnDiffSummaries = useMemo( | 209 | const orderedTurnDiffSummaries = useMemo( |
| @@ -278,7 +280,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { · 70 unmodified lines | |||
| 278 | fromTurnCount: activeCheckpointRange?.fromTurnCount ?? null, | 280 | fromTurnCount: activeCheckpointRange?.fromTurnCount ?? null, |
| 279 | toTurnCount: activeCheckpointRange?.toTurnCount ?? null, | 281 | toTurnCount: activeCheckpointRange?.toTurnCount ?? null, |
| 280 | cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : conversationCacheScope, | 282 | cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : conversationCacheScope, |
| 281 | enabled: isGitRepo, | 283 | enabled: isVcsRepo, |
| 282 | }), | 284 | }), |
| 283 | ); | 285 | ); |
| 284 | const selectedTurnCheckpointDiff = selectedTurn | 286 | const selectedTurnCheckpointDiff = selectedTurn |
| @@ -561,9 +563,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { · 276 unmodified lines | |||
| 561 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> | 563 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> |
| 562 | Select a thread to inspect turn diffs. | 564 | Select a thread to inspect turn diffs. |
| 563 | </div> | 565 | </div> |
| 564 | ) : !isGitRepo ? ( | 566 | ) : !isVcsRepo ? ( |
| 565 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> | 567 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> |
| 566 | Turn diffs are unavailable because this project is not a git repository. | 568 | Turn diffs are unavailable because this project is not a {vcsTerms.repositoryNoun}. |
| 567 | </div> | 569 | </div> |
| 568 | ) : orderedTurnDiffSummaries.length === 0 ? ( | 570 | ) : orderedTurnDiffSummaries.length === 0 ? ( |
| 569 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> | 571 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> |
| @@ -113,6 +113,7 @@ vi.mock("~/lib/gitStatusState", () => ({ · 112 unmodified lines | |||
| 113 | resetGitStatusStateForTests: () => undefined, | 113 | resetGitStatusStateForTests: () => undefined, |
| 114 | useGitStatus: vi.fn(() => ({ | 114 | useGitStatus: vi.fn(() => ({ |
| 115 | data: { | 115 | data: { |
| 116 | kind: "git", | ||
| 116 | isRepo: true, | 117 | isRepo: true, |
| 117 | sourceControlProvider: { | 118 | sourceControlProvider: { |
| 118 | kind: "github", | 119 | kind: "github", |
| @@ -13,6 +13,7 @@ import { · 12 unmodified lines | |||
| 13 | 13 | ||
| 14 | function status(overrides: Partial<VcsStatusResult> = {}): VcsStatusResult { | 14 | function status(overrides: Partial<VcsStatusResult> = {}): VcsStatusResult { |
| 15 | return { | 15 | return { |
| 16 | kind: "git", | ||
| 16 | isRepo: true, | 17 | isRepo: true, |
| 17 | hasPrimaryRemote: true, | 18 | hasPrimaryRemote: true, |
| 18 | isDefaultRef: false, | 19 | isDefaultRef: false, |
| @@ -54,6 +54,7 @@ import { readEnvironmentApi } from "~/environmentApi"; · 53 unmodified lines | |||
| 54 | import { readLocalApi } from "~/localApi"; | 54 | import { readLocalApi } from "~/localApi"; |
| 55 | import { useStore } from "~/store"; | 55 | import { useStore } from "~/store"; |
| 56 | import { createThreadSelectorByRef } from "~/storeSelectors"; | 56 | import { createThreadSelectorByRef } from "~/storeSelectors"; |
| 57 | import { resolveVcsActionPresentation, resolveVcsTerms } from "~/vcsPresentation"; | ||
| 57 | 58 | ||
| 58 | interface GitActionsControlProps { | 59 | interface GitActionsControlProps { |
| 59 | gitCwd: string | null; | 60 | gitCwd: string | null; |
| @@ -324,8 +325,12 @@ export default function GitActionsControl({ · 264 unmodified lines | |||
| 324 | }); | 325 | }); |
| 325 | // Default to true while loading so we don't flash init controls. | 326 | // Default to true while loading so we don't flash init controls. |
| 326 | const isRepo = gitStatus?.isRepo ?? true; | 327 | const isRepo = gitStatus?.isRepo ?? true; |
| 328 | const vcsKind = gitStatus?.kind ?? null; | ||
| 329 | const vcsTerms = resolveVcsTerms(vcsKind); | ||
| 330 | const vcsActionPresentation = resolveVcsActionPresentation(vcsKind); | ||
| 331 | const supportsGitWorkflowActions = !isRepo || vcsActionPresentation.supportsGitWorkflowActions; | ||
| 327 | const hasPrimaryRemote = gitStatus?.hasPrimaryRemote ?? false; | 332 | const hasPrimaryRemote = gitStatus?.hasPrimaryRemote ?? false; |
| 328 | const gitStatusForActions = gitStatus; | 333 | const gitStatusForActions = supportsGitWorkflowActions ? gitStatus : null; |
| 329 | 334 | ||
| 330 | const allFiles = gitStatusForActions?.workingTree.files ?? []; | 335 | const allFiles = gitStatusForActions?.workingTree.files ?? []; |
| 331 | const selectedFiles = allFiles.filter((f) => !excludedFiles.has(f.path)); | 336 | const selectedFiles = allFiles.filter((f) => !excludedFiles.has(f.path)); |
| @@ -874,6 +879,25 @@ export default function GitActionsControl({ · 542 unmodified lines | |||
| 874 | > | 879 | > |
| 875 | {initMutation.isPending ? "Initializing..." : "Initialize Git"} | 880 | {initMutation.isPending ? "Initializing..." : "Initialize Git"} |
| 876 | </Button> | 881 | </Button> |
| 882 | ) : !supportsGitWorkflowActions ? ( | ||
| 883 | <Popover> | ||
| 884 | <PopoverTrigger | ||
| 885 | openOnHover | ||
| 886 | render={ | ||
| 887 | <Button | ||
| 888 | aria-disabled="true" | ||
| 889 | className="cursor-not-allowed opacity-70" | ||
| 890 | size="xs" | ||
| 891 | variant="outline" | ||
| 892 | /> | ||
| 893 | } | ||
| 894 | > | ||
| 895 | {vcsTerms.systemName} {vcsTerms.repositoryNoun} | ||
| 896 | </PopoverTrigger> | ||
| 897 | <PopoverPopup tooltipStyle side="bottom" align="end"> | ||
| 898 | {vcsActionPresentation.unsupportedGitWorkflowDescription} | ||
| 899 | </PopoverPopup> | ||
| 900 | </Popover> | ||
| 877 | ) : ( | 901 | ) : ( |
| 878 | <Group aria-label="Git actions" className="shrink-0"> | 902 | <Group aria-label="Git actions" className="shrink-0"> |
| 879 | {quickActionDisabledReason ? ( | 903 | {quickActionDisabledReason ? ( |
| @@ -17,6 +17,8 @@ import { Toggle } from "../ui/toggle"; · 16 unmodified lines | |||
| 17 | import { SidebarTrigger } from "../ui/sidebar"; | 17 | import { SidebarTrigger } from "../ui/sidebar"; |
| 18 | import { OpenInPicker } from "./OpenInPicker"; | 18 | import { OpenInPicker } from "./OpenInPicker"; |
| 19 | import { usePrimaryEnvironmentId } from "../../environments/primary"; | 19 | import { usePrimaryEnvironmentId } from "../../environments/primary"; |
| 20 | import { resolveVcsTerms } from "../../vcsPresentation"; | ||
| 21 | import type { VcsDriverKind } from "@t3tools/contracts"; | ||
| 20 | 22 | ||
| 21 | interface ChatHeaderProps { | 23 | interface ChatHeaderProps { |
| 22 | activeThreadEnvironmentId: EnvironmentId; | 24 | activeThreadEnvironmentId: EnvironmentId; |
| @@ -24,7 +26,8 @@ interface ChatHeaderProps { · 1 unmodified line | |||
| 24 | draftId?: DraftId; | 26 | draftId?: DraftId; |
| 25 | activeThreadTitle: string; | 27 | activeThreadTitle: string; |
| 26 | activeProjectName: string | undefined; | 28 | activeProjectName: string | undefined; |
| 27 | isGitRepo: boolean; | 29 | isVcsRepo: boolean; |
| 30 | vcsKind: VcsDriverKind | null; | ||
| 28 | openInCwd: string | null; | 31 | openInCwd: string | null; |
| 29 | activeProjectScripts: ProjectScript[] | undefined; | 32 | activeProjectScripts: ProjectScript[] | undefined; |
| 30 | preferredScriptId: string | null; | 33 | preferredScriptId: string | null; |
| @@ -50,7 +53,8 @@ export const ChatHeader = memo(function ChatHeader({ · 19 unmodified lines | |||
| 50 | draftId, | 53 | draftId, |
| 51 | activeThreadTitle, | 54 | activeThreadTitle, |
| 52 | activeProjectName, | 55 | activeProjectName, |
| 53 | isGitRepo, | 56 | isVcsRepo, |
| 57 | vcsKind, | ||
| 54 | openInCwd, | 58 | openInCwd, |
| 55 | activeProjectScripts, | 59 | activeProjectScripts, |
| 56 | preferredScriptId, | 60 | preferredScriptId, |
| @@ -72,6 +76,7 @@ export const ChatHeader = memo(function ChatHeader({ · 15 unmodified lines | |||
| 72 | const primaryEnvironmentId = usePrimaryEnvironmentId(); | 76 | const primaryEnvironmentId = usePrimaryEnvironmentId(); |
| 73 | const isRemoteEnvironment = | 77 | const isRemoteEnvironment = |
| 74 | primaryEnvironmentId !== null && activeThreadEnvironmentId !== primaryEnvironmentId; | 78 | primaryEnvironmentId !== null && activeThreadEnvironmentId !== primaryEnvironmentId; |
| 79 | const vcsTerms = resolveVcsTerms(vcsKind); | ||
| 75 | 80 | ||
| 76 | return ( | 81 | return ( |
| 77 | <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2"> | 82 | <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2"> |
| @@ -88,9 +93,9 @@ export const ChatHeader = memo(function ChatHeader({ · 10 unmodified lines | |||
| 88 | <span className="min-w-0 truncate">{activeProjectName}</span> | 93 | <span className="min-w-0 truncate">{activeProjectName}</span> |
| 89 | </Badge> | 94 | </Badge> |
| 90 | )} | 95 | )} |
| 91 | {activeProjectName && !isGitRepo && ( | 96 | {activeProjectName && !isVcsRepo && ( |
| 92 | <Badge variant="outline" className="shrink-0 text-[10px] text-amber-700"> | 97 | <Badge variant="outline" className="shrink-0 text-[10px] text-amber-700"> |
| 93 | No Git | 98 | No repository |
| 94 | </Badge> | 99 | </Badge> |
| 95 | )} | 100 | )} |
| 96 | </div> | 101 | </div> |
| @@ -154,15 +159,15 @@ export const ChatHeader = memo(function ChatHeader({ · 57 unmodified lines | |||
| 154 | aria-label="Toggle diff panel" | 159 | aria-label="Toggle diff panel" |
| 155 | variant="outline" | 160 | variant="outline" |
| 156 | size="xs" | 161 | size="xs" |
| 157 | disabled={!isGitRepo && !diffOpen} | 162 | disabled={!isVcsRepo && !diffOpen} |
| 158 | > | 163 | > |
| 159 | <DiffIcon className="size-3" /> | 164 | <DiffIcon className="size-3" /> |
| 160 | </Toggle> | 165 | </Toggle> |
| 161 | } | 166 | } |
| 162 | /> | 167 | /> |
| 163 | <TooltipPopup side="bottom"> | 168 | <TooltipPopup side="bottom"> |
| 164 | {!isGitRepo && !diffOpen | 169 | {!isVcsRepo && !diffOpen |
| 165 | ? "Diff panel is unavailable because this project is not a git repository." | 170 | ? `Diff panel is unavailable because this project is not a supported ${vcsTerms.repositoryNoun}.` |
| 166 | : diffToggleShortcutLabel | 171 | : diffToggleShortcutLabel |
| 167 | ? `Toggle diff panel (${diffToggleShortcutLabel})` | 172 | ? `Toggle diff panel (${diffToggleShortcutLabel})` |
| 168 | : "Toggle diff panel"} | 173 | : "Toggle diff panel"} |
| @@ -40,6 +40,7 @@ const TARGET = { environmentId: ENVIRONMENT_ID, cwd: "/repo" } as const; · 39 unmodified lines | |||
| 40 | const FRESH_TARGET = { environmentId: ENVIRONMENT_ID, cwd: "/fresh" } as const; | 40 | const FRESH_TARGET = { environmentId: ENVIRONMENT_ID, cwd: "/fresh" } as const; |
| 41 | 41 | ||
| 42 | const BASE_STATUS: VcsStatusResult = { | 42 | const BASE_STATUS: VcsStatusResult = { |
| 43 | kind: "git", | ||
| 43 | isRepo: true, | 44 | isRepo: true, |
| 44 | hasPrimaryRemote: true, | 45 | hasPrimaryRemote: true, |
| 45 | isDefaultRef: false, | 46 | isDefaultRef: false, |
| @@ -64,7 +64,7 @@ function normalizeCheckpointErrorMessage(error: unknown): string { · 63 unmodified lines | |||
| 64 | 64 | ||
| 65 | const lower = message.toLowerCase(); | 65 | const lower = message.toLowerCase(); |
| 66 | if (lower.includes("not a git repository")) { | 66 | if (lower.includes("not a git repository")) { |
| 67 | return "Turn diffs are unavailable because this project is not a git repository."; | 67 | return "Turn diffs are unavailable because this project is not a repository."; |
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | if ( | 70 | if ( |
| @@ -262,6 +262,7 @@ const baseServerConfig: ServerConfig = { · 261 unmodified lines | |||
| 262 | }; | 262 | }; |
| 263 | 263 | ||
| 264 | const baseGitStatus: VcsStatusResult = { | 264 | const baseGitStatus: VcsStatusResult = { |
| 265 | kind: "git", | ||
| 265 | isRepo: true, | 266 | isRepo: true, |
| 266 | hasPrimaryRemote: true, | 267 | hasPrimaryRemote: true, |
| 267 | isDefaultRef: false, | 268 | isDefaultRef: false, |
| @@ -19,6 +19,7 @@ import { createWsRpcClient } from "./wsRpcClient"; · 18 unmodified lines | |||
| 19 | import { type WsTransport } from "./wsTransport"; | 19 | import { type WsTransport } from "./wsTransport"; |
| 20 | 20 | ||
| 21 | const baseLocalStatus: VcsStatusLocalResult = { | 21 | const baseLocalStatus: VcsStatusLocalResult = { |
| 22 | kind: "git", | ||
| 22 | isRepo: true, | 23 | isRepo: true, |
| 23 | hasPrimaryRemote: true, | 24 | hasPrimaryRemote: true, |
| 24 | isDefaultRef: false, | 25 | isDefaultRef: false, |
| @@ -0,0 +1,67 @@ | |||
| 1 | import type { VcsDriverKind } from "@t3tools/contracts"; | ||
| 2 | |||
| 3 | export interface VcsTerms { | ||
| 4 | readonly systemName: string; | ||
| 5 | readonly repositoryNoun: string; | ||
| 6 | readonly refNoun: string; | ||
| 7 | readonly refNounPlural: string; | ||
| 8 | readonly currentRefFallback: string; | ||
| 9 | } | ||
| 10 | |||
| 11 | export interface VcsActionPresentation { | ||
| 12 | readonly supportsGitWorkflowActions: boolean; | ||
| 13 | readonly unsupportedGitWorkflowDescription: string; | ||
| 14 | } | ||
| 15 | |||
| 16 | const gitLikeTerms: VcsTerms = { | ||
| 17 | systemName: "Git", | ||
| 18 | repositoryNoun: "repository", | ||
| 19 | refNoun: "branch", | ||
| 20 | refNounPlural: "branches", | ||
| 21 | currentRefFallback: "checkout", | ||
| 22 | }; | ||
| 23 | |||
| 24 | const unknownTerms: VcsTerms = { | ||
| 25 | systemName: "VCS", | ||
| 26 | repositoryNoun: "repository", | ||
| 27 | refNoun: "ref", | ||
| 28 | refNounPlural: "refs", | ||
| 29 | currentRefFallback: "checkout", | ||
| 30 | }; | ||
| 31 | |||
| 32 | export function resolveVcsTerms(kind: VcsDriverKind | null | undefined): VcsTerms { | ||
| 33 | if (kind === "jj") { | ||
| 34 | return { | ||
| 35 | systemName: "JJ", | ||
| 36 | repositoryNoun: "workspace", | ||
| 37 | refNoun: "bookmark", | ||
| 38 | refNounPlural: "bookmarks", | ||
| 39 | currentRefFallback: "working copy", | ||
| 40 | }; | ||
| 41 | } | ||
| 42 | |||
| 43 | if (kind === "sapling") { | ||
| 44 | return { | ||
| 45 | ...gitLikeTerms, | ||
| 46 | systemName: "Sapling", | ||
| 47 | }; | ||
| 48 | } | ||
| 49 | |||
| 50 | if (kind === "git") { | ||
| 51 | return gitLikeTerms; | ||
| 52 | } | ||
| 53 | |||
| 54 | return unknownTerms; | ||
| 55 | } | ||
| 56 | |||
| 57 | export function resolveVcsActionPresentation( | ||
| 58 | kind: VcsDriverKind | null | undefined, | ||
| 59 | ): VcsActionPresentation { | ||
| 60 | const terms = resolveVcsTerms(kind); | ||
| 61 | const supportsGitWorkflowActions = kind === undefined || kind === null || kind === "git"; | ||
| 62 | |||
| 63 | return { | ||
| 64 | supportsGitWorkflowActions, | ||
| 65 | unsupportedGitWorkflowDescription: `Git commit, push, and PR actions are not available for this ${terms.systemName} ${terms.repositoryNoun}.`, | ||
| 66 | }; | ||
| 67 | } | ||