Skip to content

Commit 28d03a5

Browse files
committed
Merge branch 'codex/project-sidebar-parity' into main
2 parents 43b5441 + a72ab9c commit 28d03a5

4 files changed

Lines changed: 163 additions & 6 deletions

File tree

src/components/sidebar/SidebarThreadTree.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@
290290
:data-dragging-handle="isDraggingProject(group.projectName)"
291291
@mousedown.left="onProjectHandleMouseDown($event, group.projectName)"
292292
>
293-
<span class="project-title" :title="getProjectDisplayName(group.projectName)">
293+
<span class="project-title" :title="getProjectTooltipTitle(group.projectName)">
294294
{{ getProjectVisibleName(group) }}
295295
</span>
296296
</span>
@@ -1466,6 +1466,10 @@ function isPathLikeProjectName(value: string): boolean {
14661466
return value.includes('/') || value.includes('\\')
14671467
}
14681468
1469+
function getProjectTooltipTitle(projectName: string): string {
1470+
return isPathLikeProjectName(projectName) ? projectName : getProjectDisplayName(projectName)
1471+
}
1472+
14691473
function isDuplicatePathLeafName(value: string): boolean {
14701474
const leafName = getPathLeafName(value)
14711475
if (!leafName) return false

src/composables/useDesktopState.test.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import {
88
import type { UiProjectGroup } from '../types/codex'
99
import type { WorkspaceRootsState } from '../api/codexGateway'
1010

11-
function thread(id: string, cwd: string) {
11+
function thread(id: string, cwd: string, options: { hasWorktree?: boolean } = {}) {
1212
return {
1313
id,
1414
title: id,
1515
projectName: cwd ? cwd.split('/').at(-1) || cwd : 'Projectless',
1616
cwd,
17-
hasWorktree: false,
17+
hasWorktree: options.hasWorktree ?? false,
1818
createdAtIso: '2026-04-28T00:00:00.000Z',
1919
updatedAtIso: '2026-04-28T00:00:00.000Z',
2020
preview: '',
@@ -139,6 +139,79 @@ describe('filterGroupsByWorkspaceRoots', () => {
139139
['local-project', 0],
140140
])
141141
})
142+
143+
it('keeps managed worktree threads under the matching workspace root project', () => {
144+
const groups: UiProjectGroup[] = [
145+
{
146+
projectName: 'codex-web-local',
147+
threads: [
148+
thread('main-chat', '/Users/igor/Git-projects/codex-web-local'),
149+
thread('worktree-chat', '/Users/igor/.codex/worktrees/53e7/codex-web-local', { hasWorktree: true }),
150+
],
151+
},
152+
]
153+
const rootsState: WorkspaceRootsState = {
154+
order: ['/Users/igor/Git-projects/codex-web-local'],
155+
labels: {},
156+
active: ['/Users/igor/Git-projects/codex-web-local'],
157+
projectOrder: ['/Users/igor/Git-projects/codex-web-local'],
158+
}
159+
160+
expect(filterGroupsByWorkspaceRoots(groups, rootsState).map((group) => [group.projectName, group.threads.map((row) => row.id)])).toEqual([
161+
['codex-web-local', ['main-chat', 'worktree-chat']],
162+
])
163+
})
164+
165+
it('keeps unregistered managed worktrees under the main root when another managed worktree root is registered', () => {
166+
const groups: UiProjectGroup[] = [
167+
{
168+
projectName: 'codex-web-local',
169+
threads: [
170+
thread('main-chat', '/Users/igor/Git-projects/codex-web-local'),
171+
thread('registered-worktree-chat', '/Users/igor/.codex/worktrees/a77f/codex-web-local', { hasWorktree: true }),
172+
thread('unregistered-worktree-chat', '/Users/igor/.codex/worktrees/53e7/codex-web-local', { hasWorktree: true }),
173+
],
174+
},
175+
]
176+
const rootsState: WorkspaceRootsState = {
177+
order: [
178+
'/Users/igor/Git-projects/codex-web-local',
179+
'/Users/igor/.codex/worktrees/a77f/codex-web-local',
180+
],
181+
labels: {
182+
'/Users/igor/.codex/worktrees/a77f/codex-web-local': 'codex-web-local2',
183+
},
184+
active: ['/Users/igor/Git-projects/codex-web-local'],
185+
projectOrder: ['/Users/igor/Git-projects/codex-web-local'],
186+
}
187+
188+
expect(filterGroupsByWorkspaceRoots(groups, rootsState).map((group) => [group.projectName, group.threads.map((row) => row.id)])).toEqual([
189+
['/Users/igor/Git-projects/codex-web-local', ['main-chat', 'unregistered-worktree-chat']],
190+
['/Users/igor/.codex/worktrees/a77f/codex-web-local', ['registered-worktree-chat']],
191+
])
192+
})
193+
194+
it('does not group unrelated git worktrees under a same-leaf workspace root project', () => {
195+
const groups: UiProjectGroup[] = [
196+
{
197+
projectName: 'codex-web-local',
198+
threads: [
199+
thread('main-chat', '/Users/igor/Git-projects/codex-web-local'),
200+
thread('other-git-worktree-chat', '/tmp/other/.git/worktrees/codex-web-local', { hasWorktree: true }),
201+
],
202+
},
203+
]
204+
const rootsState: WorkspaceRootsState = {
205+
order: ['/Users/igor/Git-projects/codex-web-local'],
206+
labels: {},
207+
active: ['/Users/igor/Git-projects/codex-web-local'],
208+
projectOrder: ['/Users/igor/Git-projects/codex-web-local'],
209+
}
210+
211+
expect(filterGroupsByWorkspaceRoots(groups, rootsState).map((group) => [group.projectName, group.threads.map((row) => row.id)])).toEqual([
212+
['/Users/igor/Git-projects/codex-web-local', ['main-chat']],
213+
])
214+
})
142215
})
143216

144217
describe('workspace roots project persistence helpers', () => {

src/composables/useDesktopState.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,7 @@ function orderGroupsByWorkspaceProjectOrder(
11171117

11181118
function collectDuplicateProjectLeafNames(groups: UiProjectGroup[], rootsState: WorkspaceRootsState | null): Set<string> {
11191119
const rootByLeafName = new Map<string, Set<string>>()
1120+
const canonicalWorkspaceRootCountsByLeafName = new Map<string, number>()
11201121
const addPath = (value: string): void => {
11211122
const normalizedPath = normalizePathForUi(value).trim()
11221123
if (!normalizedPath) return
@@ -1126,9 +1127,23 @@ function collectDuplicateProjectLeafNames(groups: UiProjectGroup[], rootsState:
11261127
rootByLeafName.set(leafName, existing)
11271128
}
11281129

1129-
for (const rootPath of rootsState?.order ?? []) addPath(rootPath)
1130+
for (const rootPath of rootsState?.order ?? []) {
1131+
const normalizedRootPath = normalizePathForUi(rootPath).trim()
1132+
if (!normalizedRootPath) continue
1133+
const leafName = toProjectName(normalizedRootPath)
1134+
if (!isManagedCodexWorktreePath(normalizedRootPath)) {
1135+
canonicalWorkspaceRootCountsByLeafName.set(leafName, (canonicalWorkspaceRootCountsByLeafName.get(leafName) ?? 0) + 1)
1136+
}
1137+
addPath(rootPath)
1138+
}
11301139
for (const group of groups) {
1131-
for (const thread of group.threads) addPath(thread.cwd)
1140+
for (const thread of group.threads) {
1141+
const normalizedCwd = normalizePathForUi(thread.cwd).trim()
1142+
const leafName = toProjectName(normalizedCwd)
1143+
const isRegisteredRoot = rootsState?.order.some((rootPath) => normalizePathForUi(rootPath).trim() === normalizedCwd) === true
1144+
if (isManagedCodexWorktreePath(normalizedCwd) && !isRegisteredRoot && canonicalWorkspaceRootCountsByLeafName.get(leafName) === 1) continue
1145+
addPath(thread.cwd)
1146+
}
11321147
}
11331148

11341149
const duplicateLeafNames = new Set<string>()
@@ -1138,20 +1153,53 @@ function collectDuplicateProjectLeafNames(groups: UiProjectGroup[], rootsState:
11381153
return duplicateLeafNames
11391154
}
11401155

1156+
function isManagedCodexWorktreePath(value: string): boolean {
1157+
return value.includes('/.codex/worktrees/')
1158+
}
1159+
11411160
function disambiguateProjectGroupsByCwd(
11421161
groups: UiProjectGroup[],
11431162
rootsState: WorkspaceRootsState | null,
11441163
): UiProjectGroup[] {
11451164
const duplicateLeafNames = collectDuplicateProjectLeafNames(groups, rootsState)
11461165
if (duplicateLeafNames.size === 0) return groups
11471166

1167+
const uniqueCanonicalWorkspaceRootLeafNames = new Set<string>()
1168+
const duplicateCanonicalWorkspaceRootLeafNames = new Set<string>()
1169+
const canonicalWorkspaceRootByLeafName = new Map<string, string>()
1170+
const registeredWorkspaceRoots = new Set<string>()
1171+
for (const rootPath of rootsState?.order ?? []) {
1172+
const normalizedRootPath = normalizePathForUi(rootPath).trim()
1173+
if (!normalizedRootPath) continue
1174+
registeredWorkspaceRoots.add(normalizedRootPath)
1175+
if (isManagedCodexWorktreePath(normalizedRootPath)) continue
1176+
const leafName = toProjectName(normalizedRootPath)
1177+
if (uniqueCanonicalWorkspaceRootLeafNames.has(leafName)) {
1178+
uniqueCanonicalWorkspaceRootLeafNames.delete(leafName)
1179+
duplicateCanonicalWorkspaceRootLeafNames.add(leafName)
1180+
canonicalWorkspaceRootByLeafName.delete(leafName)
1181+
} else if (!duplicateCanonicalWorkspaceRootLeafNames.has(leafName)) {
1182+
uniqueCanonicalWorkspaceRootLeafNames.add(leafName)
1183+
canonicalWorkspaceRootByLeafName.set(leafName, normalizedRootPath)
1184+
}
1185+
}
1186+
11481187
const disambiguatedGroups: UiProjectGroup[] = []
11491188
const groupsByProjectName = new Map<string, UiProjectGroup>()
11501189
for (const group of groups) {
11511190
for (const thread of group.threads) {
11521191
const normalizedCwd = normalizePathForUi(thread.cwd).trim()
11531192
const leafName = toProjectName(normalizedCwd)
1154-
const projectName = normalizedCwd && duplicateLeafNames.has(leafName) ? normalizedCwd : group.projectName
1193+
const isRegisteredRoot = registeredWorkspaceRoots.has(normalizedCwd)
1194+
const isCanonicalWorktreeThread = isManagedCodexWorktreePath(normalizedCwd)
1195+
&& !isRegisteredRoot
1196+
&& uniqueCanonicalWorkspaceRootLeafNames.has(leafName)
1197+
let projectName = group.projectName
1198+
if (isCanonicalWorktreeThread && duplicateLeafNames.has(leafName)) {
1199+
projectName = canonicalWorkspaceRootByLeafName.get(leafName) ?? group.projectName
1200+
} else if (normalizedCwd && duplicateLeafNames.has(leafName)) {
1201+
projectName = normalizedCwd
1202+
}
11551203
const nextThread = thread.projectName === projectName ? thread : { ...thread, projectName }
11561204
const existingGroup = groupsByProjectName.get(projectName)
11571205
if (existingGroup) {

tests.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4143,3 +4143,35 @@ Composer runtime options and project menu worktree actions are hidden when the s
41434143

41444144
#### Rollback/Cleanup
41454145
- Remove any disposable plain folder or test chats created for this validation.
4146+
4147+
---
4148+
4149+
### Project worktree threads under canonical project
4150+
4151+
#### Feature/Change Name
4152+
Managed worktree threads remain visible under their matching canonical workspace-root project, and path-like project tooltips expose the full path.
4153+
4154+
#### Prerequisites/Setup
4155+
1. Dev server running (`pnpm run dev`)
4156+
2. Codex global workspace roots include `/Users/igor/Git-projects/codex-web-local`
4157+
3. Thread history contains at least one thread whose cwd is under `/Users/igor/.codex/worktrees/*/codex-web-local`
4158+
4. Light theme and dark theme both available from the appearance switcher
4159+
4160+
#### Steps
4161+
1. In light theme, open the sidebar Projects section.
4162+
2. Scroll to the `codex-web-local` project.
4163+
3. Confirm the project includes the main-root thread and managed worktree threads.
4164+
4. Confirm worktree rows still show the worktree icon.
4165+
5. Confirm unrelated `.git/worktrees` rows with the same leaf folder name are not grouped into this project.
4166+
6. Hover any shortened path-like duplicate project title and confirm the tooltip shows the full project path, not only the friendly label.
4167+
7. Switch to dark theme and repeat steps 1-6.
4168+
4169+
#### Expected Results
4170+
- Managed worktree threads with the same leaf folder name are not split into hidden path-like project groups.
4171+
- Generic `.git/worktrees` rows are not treated as managed Codex worktrees for project-root grouping.
4172+
- The canonical `codex-web-local` project shows both main-root and worktree threads.
4173+
- Path-like project tooltips expose the full project path.
4174+
- Project rows and worktree icons remain readable in light and dark themes.
4175+
4176+
#### Rollback/Cleanup
4177+
- None.

0 commit comments

Comments
 (0)