fix(desktop): stop dual main lanes for non-git project workspaces
Live overlay always placed sessions under `::branch::main`, while the backend non-git heuristic keys the lane by folder path (label = basename). Overlay missed that lane by id/label and forked a phantom `main` group with the same sessions. Match existing path-keyed isMain lanes before creating a branch-style main lane. Covers the codex-research-guardian-style drill-in duplicate.
This commit is contained in:
@@ -697,6 +697,77 @@ describe('overlayLiveLanes', () => {
|
||||
expect(overlaid.repos[0].groups.flatMap(g => g.sessions.map(s => s.id))).toEqual(['dup'])
|
||||
})
|
||||
|
||||
it('does not fork a phantom main lane for a non-git backend workspace lane', () => {
|
||||
// Backend non-git heuristic (`project_tree._place_by_heuristic`): lane id =
|
||||
// folder path, label = basename, isMain=true. Live overlay used to always
|
||||
// place under `::branch::main` / label "main", miss that lane by id+label,
|
||||
// and CREATE a second main lane with the same sessions — dual lanes in the
|
||||
// project drill-in (e.g. main + codex-research-guardian).
|
||||
const root = '/home/hermes/hermes-workspace/codex-research-guardian'
|
||||
const a = makeSession(root, { id: 's1' }) // empty git_branch / git_repo_root
|
||||
const b = makeSession(root, { id: 's2' })
|
||||
|
||||
const project = projectNode({
|
||||
id: root,
|
||||
isAuto: true,
|
||||
path: root,
|
||||
repos: [
|
||||
{
|
||||
id: root,
|
||||
label: 'codex-research-guardian',
|
||||
path: root,
|
||||
sessionCount: 2,
|
||||
groups: [
|
||||
lane({
|
||||
id: root,
|
||||
label: 'codex-research-guardian',
|
||||
isMain: true,
|
||||
path: root,
|
||||
sessions: [a, b]
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const overlaid = overlayLiveLanes(project, [a, b])
|
||||
const groups = overlaid.repos[0].groups
|
||||
|
||||
expect(groups).toHaveLength(1)
|
||||
expect(groups[0].id).toBe(root)
|
||||
expect(groups[0].label).toBe('codex-research-guardian')
|
||||
expect(groups[0].sessions.map(s => s.id).sort()).toEqual(['s1', 's2'])
|
||||
expect(groups.some(g => g.label === 'main' || g.id.endsWith('::branch::main'))).toBe(false)
|
||||
})
|
||||
|
||||
it('joins a fresh live session into an existing non-git workspace lane (no branch id)', () => {
|
||||
const root = '/work/notes'
|
||||
const existing = makeSession(root, { id: 'old' })
|
||||
|
||||
const project = projectNode({
|
||||
id: root,
|
||||
isAuto: true,
|
||||
path: root,
|
||||
repos: [
|
||||
{
|
||||
id: root,
|
||||
label: 'notes',
|
||||
path: root,
|
||||
sessionCount: 1,
|
||||
groups: [lane({ id: root, label: 'notes', isMain: true, path: root, sessions: [existing] })]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const fresh = makeSession(root, { id: 'fresh' })
|
||||
const overlaid = overlayLiveLanes(project, [existing, fresh])
|
||||
const groups = overlaid.repos[0].groups
|
||||
|
||||
expect(groups).toHaveLength(1)
|
||||
expect(groups[0].id).toBe(root)
|
||||
expect(groups[0].sessions.map(s => s.id).sort()).toEqual(['fresh', 'old'])
|
||||
})
|
||||
|
||||
it('adds a new session to an existing worktree lane keyed by a divergent id (matches by path)', () => {
|
||||
// Backend keyed the worktree lane off a branch-style id (no live git probe),
|
||||
// but the lane PATH is the worktree dir. A new session under that worktree
|
||||
|
||||
@@ -563,6 +563,21 @@ export function overlayRepoLanes(
|
||||
(placed.isMain
|
||||
? lanes.find(g => g.isMain && g.label.toLowerCase() === placed.label.toLowerCase())
|
||||
: undefined) ??
|
||||
// Non-git backend heuristic (`project_tree._place_by_heuristic`): one
|
||||
// isMain lane keyed by the folder path itself (id === path, label =
|
||||
// basename) — not `::branch::<name>`. Live placement always emits
|
||||
// `::branch::main` / label "main", so id+label miss and used to FORK a
|
||||
// phantom second main lane with the same sessions. Prefer the existing
|
||||
// path-keyed main lane when present.
|
||||
(placed.isMain && placedKey
|
||||
? lanes.find(
|
||||
g =>
|
||||
g.isMain &&
|
||||
pathKey(g.path) === placedKey &&
|
||||
!g.id.includes('::branch::') &&
|
||||
!g.id.includes('::kanban')
|
||||
)
|
||||
: undefined) ??
|
||||
(!placed.isMain && placedKey ? lanes.find(g => pathKey(g.path) === placedKey) : undefined)
|
||||
|
||||
if (!lane) {
|
||||
|
||||
Reference in New Issue
Block a user