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 {...@@ -12,7 +12,7 @@ import {
12 type Thread,12 type Thread,
13} from "../types";13} from "../types";
14import { useNativeApi } from "../hooks/useNativeApi";14import { useNativeApi } from "../hooks/useNativeApi";
15import { getOrphanedWorktreePathForThread } from "../worktreeCleanup";15import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";
1616
17const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;17const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;
18function formatRelativeTime(iso: string): string {18function formatRelativeTime(iso: string): string {
...@@ -265,12 +265,15 @@ export default function Sidebar() {...@@ -265,12 +265,15 @@ export default function Sidebar() {
265 if (!thread) return;265 if (!thread) return;
266 const threadProject = state.projects.find((project) => project.id === thread.projectId);266 const threadProject = state.projects.find((project) => project.id === thread.projectId);
267 const orphanedWorktreePath = getOrphanedWorktreePathForThread(state.threads, threadId);267 const orphanedWorktreePath = getOrphanedWorktreePathForThread(state.threads, threadId);
268 const displayWorktreePath = orphanedWorktreePath
269 ? formatWorktreePathForDisplay(orphanedWorktreePath)
270 : null;
268 const shouldDeleteWorktree =271 const shouldDeleteWorktree =
269 orphanedWorktreePath !== null &&272 orphanedWorktreePath !== null &&
270 (await api.dialogs.confirm(273 (await api.dialogs.confirm(
271 [274 [
272 "This thread is the only one linked to this worktree:",275 "This thread is the only one linked to this worktree:",
273 orphanedWorktreePath,276 displayWorktreePath ?? orphanedWorktreePath,
274 "",277 "",
275 "Delete the worktree too?",278 "Delete the worktree too?",
276 ].join("\n"),279 ].join("\n"),
apps/web/src/worktreeCleanup.test.ts+27-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, type Thread } from "./types";3import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, type Thread } from "./types";
4import { getOrphanedWorktreePathForThread } from "./worktreeCleanup";4import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "./worktreeCleanup";
55
6function makeThread(overrides: Partial<Thread> = {}): Thread {6function makeThread(overrides: Partial<Thread> = {}): Thread {
7 return {7 return {
...@@ -69,3 +69,29 @@ describe("getOrphanedWorktreePathForThread", () => {...@@ -69,3 +69,29 @@ describe("getOrphanedWorktreePathForThread", () => {
69 expect(result).toBe("/tmp/repo/worktrees/feature-a");69 expect(result).toBe("/tmp/repo/worktrees/feature-a");
70 });70 });
71});71});
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(...@@ -31,3 +31,15 @@ export function getOrphanedWorktreePathForThread(
3131
32 return isShared ? null : targetWorktreePath;32 return isShared ? null : targetWorktreePath;
33}33}
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}