authorgravatar for 10465470+TheIcarusWings@users.noreply.github.comIcarus Wings <10465470+TheIcarusWings@users.noreply.github.com> 2026-06-19 19:23:47+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 11:23:47-07:00
logd9f59be7044a7f6d193e73ff9fa06127009e0c91
tree9bd4485d8074d5f33024e4e7adaeebefc6e386ce
parenta4446e263d77bd5c32ce6bb4921d6464000765de
signature Signed by PGP key B5690EEEBB952194

feat(sidebar): worktree indicator on session rows (#3057)

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 {
1717 resolveThreadPr,
1818 terminalStatusFromRunningIds,
1919 ThreadStatusLabel,
20 ThreadWorktreeIndicator,
2021} from "./ThreadStatusIndicators";
2122import { ProjectFavicon } from "./ProjectFavicon";
2223import { useAtomValue } from "@effect/atom-react";
......@@ -743,6 +744,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
743744 </TooltipPopup>
744745 </Tooltip>
745746 )}
747 <ThreadWorktreeIndicator thread={thread} />
746748 {terminalStatus && (
747749 <Tooltip>
748750 <TooltipTrigger
apps/web/src/components/ThreadStatusIndicators.test.tsx created+39
......@@ -0,0 +1,39 @@
1import { ThreadId } from "@t3tools/contracts";
2import { renderToStaticMarkup } from "react-dom/server";
3import { describe, expect, it } from "vite-plus/test";
4
5import { ThreadWorktreeIndicator } from "./ThreadStatusIndicators";
6
7describe("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 {
44 scopeThreadRef,
55} from "@t3tools/client-runtime/environment";
66import type { VcsStatusResult } from "@t3tools/contracts";
7import { CloudIcon, GitPullRequestIcon, TerminalIcon } from "lucide-react";
7import { CloudIcon, FolderGit2Icon, GitPullRequestIcon, TerminalIcon } from "lucide-react";
88import { useMemo } from "react";
99import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
1010import { useProject } from "../state/entities";
......@@ -15,6 +15,7 @@ import { useUiStateStore } from "../uiStateStore";
1515import { resolveChangeRequestPresentation } from "../sourceControlPresentation";
1616import { resolveThreadStatusPill, type ThreadStatusPill } from "./Sidebar.logic";
1717import type { SidebarThreadSummary } from "../types";
18import { formatWorktreePathForDisplay } from "../worktreeCleanup";
1819import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
1920
2021export interface PrStatusIndicator {
......@@ -94,6 +95,40 @@ export function terminalStatusFromRunningIds(
9495 };
9596}
9697
98export 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
97132export function ThreadStatusLabel({
98133 status,
99134 compact = false,