| author | |
| committer | |
| log | d9f59be7044a7f6d193e73ff9fa06127009e0c91 |
| tree | 9bd4485d8074d5f33024e4e7adaeebefc6e386ce |
| parent | a4446e263d77bd5c32ce6bb4921d6464000765de |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>3 files changed, 77 insertions(+), 1 deletions(-)
apps/web/src/components/Sidebar.tsx+2| ... | ... | @@ -17,6 +17,7 @@ import { |
| 17 | 17 | resolveThreadPr, |
| 18 | 18 | terminalStatusFromRunningIds, |
| 19 | 19 | ThreadStatusLabel, |
| 20 | ThreadWorktreeIndicator, | |
| 20 | 21 | } from "./ThreadStatusIndicators"; |
| 21 | 22 | import { ProjectFavicon } from "./ProjectFavicon"; |
| 22 | 23 | import { useAtomValue } from "@effect/atom-react"; |
| ... | ... | @@ -743,6 +744,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr |
| 743 | 744 | </TooltipPopup> |
| 744 | 745 | </Tooltip> |
| 745 | 746 | )} |
| 747 | <ThreadWorktreeIndicator thread={thread} /> | |
| 746 | 748 | {terminalStatus && ( |
| 747 | 749 | <Tooltip> |
| 748 | 750 | <TooltipTrigger |
apps/web/src/components/ThreadStatusIndicators.test.tsx created+39| ... | ... | @@ -0,0 +1,39 @@ |
| 1 | import { ThreadId } from "@t3tools/contracts"; | |
| 2 | import { renderToStaticMarkup } from "react-dom/server"; | |
| 3 | import { describe, expect, it } from "vite-plus/test"; | |
| 4 | ||
| 5 | import { ThreadWorktreeIndicator } from "./ThreadStatusIndicators"; | |
| 6 | ||
| 7 | describe("ThreadWorktreeIndicator", () => { | |
| 8 | it("renders the worktree folder and branch in an accessible label", () => { | |
| 9 | const markup = renderToStaticMarkup( | |
| 10 | <ThreadWorktreeIndicator | |
| 11 | thread={{ | |
| 12 | id: ThreadId.make("thread-1"), | |
| 13 | branch: "feature/sidebar-indicator", | |
| 14 | worktreePath: "/tmp/worktrees/sidebar-indicator", | |
| 15 | }} | |
| 16 | />, | |
| 17 | ); | |
| 18 | ||
| 19 | expect(markup).toContain('role="img"'); | |
| 20 | expect(markup).toContain( | |
| 21 | 'aria-label="Worktree: sidebar-indicator (feature/sidebar-indicator)"', | |
| 22 | ); | |
| 23 | expect(markup).toContain('data-testid="thread-worktree-thread-1"'); | |
| 24 | }); | |
| 25 | ||
| 26 | it.each([null, "", " "])("renders nothing for an absent worktree path", (worktreePath) => { | |
| 27 | const markup = renderToStaticMarkup( | |
| 28 | <ThreadWorktreeIndicator | |
| 29 | thread={{ | |
| 30 | id: ThreadId.make("thread-1"), | |
| 31 | branch: "main", | |
| 32 | worktreePath, | |
| 33 | }} | |
| 34 | />, | |
| 35 | ); | |
| 36 | ||
| 37 | expect(markup).toBe(""); | |
| 38 | }); | |
| 39 | }); |
apps/web/src/components/ThreadStatusIndicators.tsx+36-1| ... | ... | @@ -4,7 +4,7 @@ import { |
| 4 | 4 | scopeThreadRef, |
| 5 | 5 | } from "@t3tools/client-runtime/environment"; |
| 6 | 6 | import type { VcsStatusResult } from "@t3tools/contracts"; |
| 7 | import { CloudIcon, GitPullRequestIcon, TerminalIcon } from "lucide-react"; | |
| 7 | import { CloudIcon, FolderGit2Icon, GitPullRequestIcon, TerminalIcon } from "lucide-react"; | |
| 8 | 8 | import { useMemo } from "react"; |
| 9 | 9 | import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments"; |
| 10 | 10 | import { useProject } from "../state/entities"; |
| ... | ... | @@ -15,6 +15,7 @@ import { useUiStateStore } from "../uiStateStore"; |
| 15 | 15 | import { resolveChangeRequestPresentation } from "../sourceControlPresentation"; |
| 16 | 16 | import { resolveThreadStatusPill, type ThreadStatusPill } from "./Sidebar.logic"; |
| 17 | 17 | import type { SidebarThreadSummary } from "../types"; |
| 18 | import { formatWorktreePathForDisplay } from "../worktreeCleanup"; | |
| 18 | 19 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 19 | 20 | |
| 20 | 21 | export interface PrStatusIndicator { |
| ... | ... | @@ -94,6 +95,40 @@ export function terminalStatusFromRunningIds( |
| 94 | 95 | }; |
| 95 | 96 | } |
| 96 | 97 | |
| 98 | export function ThreadWorktreeIndicator({ | |
| 99 | thread, | |
| 100 | }: { | |
| 101 | thread: Pick<SidebarThreadSummary, "id" | "branch" | "worktreePath">; | |
| 102 | }) { | |
| 103 | const worktreePath = thread.worktreePath?.trim(); | |
| 104 | if (!worktreePath) { | |
| 105 | return null; | |
| 106 | } | |
| 107 | ||
| 108 | const displayPath = formatWorktreePathForDisplay(worktreePath); | |
| 109 | const tooltip = thread.branch | |
| 110 | ? `Worktree: ${displayPath} (${thread.branch})` | |
| 111 | : `Worktree: ${displayPath}`; | |
| 112 | ||
| 113 | return ( | |
| 114 | <Tooltip> | |
| 115 | <TooltipTrigger | |
| 116 | render={ | |
| 117 | <span | |
| 118 | role="img" | |
| 119 | aria-label={tooltip} | |
| 120 | data-testid={`thread-worktree-${thread.id}`} | |
| 121 | className="inline-flex items-center justify-center" | |
| 122 | /> | |
| 123 | } | |
| 124 | > | |
| 125 | <FolderGit2Icon className="size-3 text-muted-foreground/40" /> | |
| 126 | </TooltipTrigger> | |
| 127 | <TooltipPopup side="top">{tooltip}</TooltipPopup> | |
| 128 | </Tooltip> | |
| 129 | ); | |
| 130 | } | |
| 131 | ||
| 97 | 132 | export function ThreadStatusLabel({ |
| 98 | 133 | status, |
| 99 | 134 | compact = false, |