authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-12 22:52:04-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-12 22:57:56-08:00
log4f5eb3be4d973fdd89194f26caf685137496dbfb
tree38a4c77bb5a18f7bdd0d35af2bedb3ed1fbaaca6
parentb6523a15dc7f032b2804c07f2422daf89f8bcd88

Show worktree name in thread delete confirmation

- Format orphaned worktree paths to display only the final segment - Apply formatting in Sidebar delete prompt for cleaner UX - Add tests for Unix, Windows, trailing slash, and fallback cases

3 files changed, 44 insertions(+), 3 deletions(-)

apps/web/src/components/Sidebar.tsx+5-2
......@@ -12,7 +12,7 @@ import {
1212 type Thread,
1313} from "../types";
1414import { useNativeApi } from "../hooks/useNativeApi";
15import { getOrphanedWorktreePathForThread } from "../worktreeCleanup";
15import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";
1616
1717const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;
1818function formatRelativeTime(iso: string): string {
......@@ -265,12 +265,15 @@ export default function Sidebar() {
265265 if (!thread) return;
266266 const threadProject = state.projects.find((project) => project.id === thread.projectId);
267267 const orphanedWorktreePath = getOrphanedWorktreePathForThread(state.threads, threadId);
268 const displayWorktreePath = orphanedWorktreePath
269 ? formatWorktreePathForDisplay(orphanedWorktreePath)
270 : null;
268271 const shouldDeleteWorktree =
269272 orphanedWorktreePath !== null &&
270273 (await api.dialogs.confirm(
271274 [
272275 "This thread is the only one linked to this worktree:",
273 orphanedWorktreePath,
276 displayWorktreePath ?? orphanedWorktreePath,
274277 "",
275278 "Delete the worktree too?",
276279 ].join("\n"),
apps/web/src/worktreeCleanup.test.ts+27-1
......@@ -1,7 +1,7 @@
11import { describe, expect, it } from "vitest";
22
33import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, type Thread } from "./types";
4import { getOrphanedWorktreePathForThread } from "./worktreeCleanup";
4import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "./worktreeCleanup";
55
66function makeThread(overrides: Partial<Thread> = {}): Thread {
77 return {
......@@ -69,3 +69,29 @@ describe("getOrphanedWorktreePathForThread", () => {
6969 expect(result).toBe("/tmp/repo/worktrees/feature-a");
7070 });
7171});
72
73describe("formatWorktreePathForDisplay", () => {
74 it("shows only the last path segment for unix-like paths", () => {
75 const result = formatWorktreePathForDisplay(
76 "/Users/julius/.t3/worktrees/codething-mvp/codething-4e609bb8",
77 );
78 expect(result).toBe("codething-4e609bb8");
79 });
80
81 it("normalizes windows separators before selecting the final segment", () => {
82 const result = formatWorktreePathForDisplay(
83 "C:\\Users\\julius\\.t3\\worktrees\\codething-mvp\\codething-4e609bb8",
84 );
85 expect(result).toBe("codething-4e609bb8");
86 });
87
88 it("uses the final segment even when outside ~/.t3/worktrees", () => {
89 const result = formatWorktreePathForDisplay("/tmp/custom-worktrees/my-worktree");
90 expect(result).toBe("my-worktree");
91 });
92
93 it("ignores trailing slashes", () => {
94 const result = formatWorktreePathForDisplay("/tmp/custom-worktrees/my-worktree/");
95 expect(result).toBe("my-worktree");
96 });
97});
apps/web/src/worktreeCleanup.ts+12
......@@ -31,3 +31,15 @@ export function getOrphanedWorktreePathForThread(
3131
3232 return isShared ? null : targetWorktreePath;
3333}
34
35export function formatWorktreePathForDisplay(worktreePath: string): string {
36 const trimmed = worktreePath.trim();
37 if (!trimmed) {
38 return worktreePath;
39 }
40
41 const normalized = trimmed.replace(/\\/g, "/").replace(/\/+$/, "");
42 const parts = normalized.split("/");
43 const lastPart = parts[parts.length - 1]?.trim() ?? "";
44 return lastPart.length > 0 ? lastPart : trimmed;
45}