History

Add JJ VCS driver support

- Detect and route jj repositories alongside git - Thread VCS kind through status and UI labels - Add jj driver contract coverage and shared freshness helper

Julius Marminge committed 6 months ago

Commit c31beca1 parent 128b43230 files · +838 −44Browse files

Showing the changes to apps/web/src/components (7 of 30 files). Show all

Unified · Split

7 files
  1. apps/web/src/components/BranchToolbarBranchSelector.tsx +14 −8
  2. apps/web/src/components/ChatView.tsx +7 −5
  3. apps/web/src/components/DiffPanel.tsx +6 −4
  4. apps/web/src/components/GitActionsControl.browser.tsx +1 −0
  5. apps/web/src/components/GitActionsControl.logic.test.ts +1 −0
  6. apps/web/src/components/GitActionsControl.tsx +25 −1
  7. apps/web/src/components/chat/ChatHeader.tsx +12 −7
apps/web/src/components/BranchToolbarBranchSelector.tsx+14−8
@@ -23,6 +23,7 @@ import { cn } from "../lib/utils"; · 22 unmodified lines
23import { parsePullRequestReference } from "../pullRequestReference";23import { parsePullRequestReference } from "../pullRequestReference";
24import { useStore } from "../store";24import { useStore } from "../store";
25import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";25import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";
26import { resolveVcsTerms } from "../vcsPresentation";
26import {27import {
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 = isBranchesSearchPending294 const branchStatusText = isBranchesSearchPending
292 ? "Loading refs..."295 ? `Loading ${vcsTerms.refNounPlural}...`
293 : isFetchingNextPage296 : isFetchingNextPage
294 ? "Loading more refs..."297 ? `Loading more ${vcsTerms.refNounPlural}...`
295 : hasNextPage298 : 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 &quot;{trimmedBranchQuery}&quot;</span>531 <span className="truncate">
532 Create new {vcsTerms.refNoun} &quot;{trimmedBranchQuery}&quot;
533 </span>
528 </ComboboxItem>534 </ComboboxItem>
529 );535 );
530 }536 }
apps/web/src/components/ChatView.tsx+7−5
@@ -1458,7 +1458,8 @@ export default function ChatView(props: ChatViewProps) { · 1457 unmodified lines
1458 ? terminalLaunchContext1458 ? 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 <div3379 <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 isGitRepo3382 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 <BranchToolbar3456 <BranchToolbar
3455 environmentId={activeThread.environmentId}3457 environmentId={activeThread.environmentId}
3456 threadId={activeThread.id}3458 threadId={activeThread.id}
apps/web/src/components/DiffPanel.tsx+6−4
@@ -35,6 +35,7 @@ import { createThreadSelectorByRef } from "../storeSelectors"; · 34 unmodified lines
35import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes";35import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes";
36import { useSettings } from "../hooks/useSettings";36import { useSettings } from "../hooks/useSettings";
37import { formatShortTimestamp } from "../timestampFormat";37import { formatShortTimestamp } from "../timestampFormat";
38import { resolveVcsTerms } from "../vcsPresentation";
38import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";39import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";
39import { ToggleGroup, Toggle } from "./ui/toggle-group";40import { 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 = selectedTurn286 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">
apps/web/src/components/GitActionsControl.browser.tsx+1−0
@@ -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",
apps/web/src/components/GitActionsControl.logic.test.ts+1−0
@@ -13,6 +13,7 @@ import { · 12 unmodified lines
13 13
14function status(overrides: Partial<VcsStatusResult> = {}): VcsStatusResult {14function 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,
apps/web/src/components/GitActionsControl.tsx+25−1
@@ -54,6 +54,7 @@ import { readEnvironmentApi } from "~/environmentApi"; · 53 unmodified lines
54import { readLocalApi } from "~/localApi";54import { readLocalApi } from "~/localApi";
55import { useStore } from "~/store";55import { useStore } from "~/store";
56import { createThreadSelectorByRef } from "~/storeSelectors";56import { createThreadSelectorByRef } from "~/storeSelectors";
57import { resolveVcsActionPresentation, resolveVcsTerms } from "~/vcsPresentation";
57 58
58interface GitActionsControlProps {59interface 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 ? (
apps/web/src/components/chat/ChatHeader.tsx+12−7
@@ -17,6 +17,8 @@ import { Toggle } from "../ui/toggle"; · 16 unmodified lines
17import { SidebarTrigger } from "../ui/sidebar";17import { SidebarTrigger } from "../ui/sidebar";
18import { OpenInPicker } from "./OpenInPicker";18import { OpenInPicker } from "./OpenInPicker";
19import { usePrimaryEnvironmentId } from "../../environments/primary";19import { usePrimaryEnvironmentId } from "../../environments/primary";
20import { resolveVcsTerms } from "../../vcsPresentation";
21import type { VcsDriverKind } from "@t3tools/contracts";
20 22
21interface ChatHeaderProps {23interface 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 Git98 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 && !diffOpen169 {!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 : diffToggleShortcutLabel171 : diffToggleShortcutLabel
167 ? `Toggle diff panel (${diffToggleShortcutLabel})`172 ? `Toggle diff panel (${diffToggleShortcutLabel})`
168 : "Toggle diff panel"}173 : "Toggle diff panel"}