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 {...@@ -17,6 +17,7 @@ import {
17 resolveThreadPr,17 resolveThreadPr,
18 terminalStatusFromRunningIds,18 terminalStatusFromRunningIds,
19 ThreadStatusLabel,19 ThreadStatusLabel,
20 ThreadWorktreeIndicator,
20} from "./ThreadStatusIndicators";21} from "./ThreadStatusIndicators";
21import { ProjectFavicon } from "./ProjectFavicon";22import { ProjectFavicon } from "./ProjectFavicon";
22import { useAtomValue } from "@effect/atom-react";23import { useAtomValue } from "@effect/atom-react";
...@@ -743,6 +744,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr...@@ -743,6 +744,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
743 </TooltipPopup>744 </TooltipPopup>
744 </Tooltip>745 </Tooltip>
745 )}746 )}
747 <ThreadWorktreeIndicator thread={thread} />
746 {terminalStatus && (748 {terminalStatus && (
747 <Tooltip>749 <Tooltip>
748 <TooltipTrigger750 <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 {...@@ -4,7 +4,7 @@ import {
4 scopeThreadRef,4 scopeThreadRef,
5} from "@t3tools/client-runtime/environment";5} from "@t3tools/client-runtime/environment";
6import type { VcsStatusResult } from "@t3tools/contracts";6import type { VcsStatusResult } from "@t3tools/contracts";
7import { CloudIcon, GitPullRequestIcon, TerminalIcon } from "lucide-react";7import { CloudIcon, FolderGit2Icon, GitPullRequestIcon, TerminalIcon } from "lucide-react";
8import { useMemo } from "react";8import { useMemo } from "react";
9import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";9import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
10import { useProject } from "../state/entities";10import { useProject } from "../state/entities";
...@@ -15,6 +15,7 @@ import { useUiStateStore } from "../uiStateStore";...@@ -15,6 +15,7 @@ import { useUiStateStore } from "../uiStateStore";
15import { resolveChangeRequestPresentation } from "../sourceControlPresentation";15import { resolveChangeRequestPresentation } from "../sourceControlPresentation";
16import { resolveThreadStatusPill, type ThreadStatusPill } from "./Sidebar.logic";16import { resolveThreadStatusPill, type ThreadStatusPill } from "./Sidebar.logic";
17import type { SidebarThreadSummary } from "../types";17import type { SidebarThreadSummary } from "../types";
18import { formatWorktreePathForDisplay } from "../worktreeCleanup";
18import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";19import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
1920
20export interface PrStatusIndicator {21export interface PrStatusIndicator {
...@@ -94,6 +95,40 @@ export function terminalStatusFromRunningIds(...@@ -94,6 +95,40 @@ export function terminalStatusFromRunningIds(
94 };95 };
95}96}
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
97export function ThreadStatusLabel({132export function ThreadStatusLabel({
98 status,133 status,
99 compact = false,134 compact = false,