Skip to content

fix(desktop): keep a nested folder as its own project - #3070

Merged
Astro-Han merged 4 commits into
apache:mainfrom
1625567290:fix/desktop-nested-project-root
Aug 16, 2026
Merged

Astro-Han merged 4 commits into
apache:mainfrom
1625567290:fix/desktop-nested-project-root

Conversation

@1625567290

Copy link
Copy Markdown
Contributor

Summary

Adding a project from a folder that lives inside an existing Git project remapped the selection to the repository root. register used resolveProjectLocation, which walks to the enclosing worktree, so parent/child became parent and the already-registered parent project was reopened.

User-chosen add/relink paths now keep the exact selected folder when it is not the worktree root. A repository root and a linked worktree still share one Git identity. Historical session cwds still resolve to the enclosing repository, so a session that ran in repo/src does not become a new project.

Fixes #2660

Verification

  • npm --workspace @maka/storage run build
  • npm --workspace @maka/storage run typecheck
  • node --test packages/storage/dist/__tests__/project-catalog.test.js19/19 (includes the new parent/child case)
  • npx tsx --test apps/desktop/src/main/__tests__/project-management-service.test.ts6/6
  • npx tsx --test apps/desktop/src/main/__tests__/new-session-project.test.ts7/7
  • Not run: @maka/desktop build:main — current main fails typecheck on unrelated files (assistant-stream.test.ts, chat-readiness.ts, conversation-copy.ts missing oauth_subscription_not_wired)

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope:

Grok authored the implementation, tests, and this PR description.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f9fbeb5-5804-42ad-b167-a0b33db41d15

📥 Commits

Reviewing files that changed from the base of the PR and between 66778b4 and c2e26a9.

📒 Files selected for processing (1)
  • packages/storage/src/__tests__/project-catalog.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/storage/src/tests/project-catalog.test.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 0 remain after this review.


📝 Walkthrough

What problem this solves

This PR fixes registration of folders nested inside existing Git projects. Selecting parent/child now registers and selects parent/child instead of reopening parent.

Repository roots and linked worktrees continue to share one Git identity. Historical session working directories continue to resolve to the enclosing repository.

Source of truth

The PR extends the existing project-location resolution in packages/storage/src/project-catalog.ts.

It does not create a parallel project model or duplicate registration logic. User-selected paths and historical session paths use separate resolution rules because they have different requirements.

Scope and complexity

This is the smallest coherent solution. It routes register, relink, and relinkWithSessions through the user-selection resolver.

touch now canonicalizes an explicitly supplied path, matches it against registered locations[], and raises ProjectPathMismatchError for mismatched paths.

No code or tests can be removed without weakening behavior or regression coverage.

Validation

Coverage includes:

  • Separate parent and nested projects.
  • Preservation of nested paths and names during registration, touch, and relinking.
  • Shared Git identity for the parent repository.
  • Rejection of mismatched parent paths.
  • Desktop registration and selection of nested folders.
  • Relinking through relink() and relinkWithSessions().
  • Session reassignment and session-header cwd updates during nested relinking.

Reported verification includes 20/20 project-catalog and authority tests, 13/13 desktop add/select and new-session tests, and successful project usage recording through HostWorkspaceResolver.

The desktop main build was not run because the current main branch has unrelated typecheck failures. Required check status remains unverified without direct current check output.

Review-relevant risks

The diff changes user-visible project registration, relinking, and touch behavior for folders inside Git worktrees. Material changes in this area require independent human review under repository policy.

No public entity declaration changes were identified. No security, licensing, release, or governance effect was identified in the current diff.

Implementation children in nested projects may still start at the worktree root. This is pre-existing behavior and is outside this PR.

The person performing the merge reviews the final diff. A maintainer makes the final determination.

Walkthrough

The project catalog preserves explicitly selected Git subdirectories as separate folder projects. Registration, relinking, and explicit path touching use the updated resolution behavior. Desktop and storage tests cover nested project identities, paths, selection, relinking, and Git setup.

Changes

Nested project selection

Layer / File(s) Summary
User-selected path resolution and catalog wiring
packages/storage/src/project-catalog.ts
Added resolveUserSelectedProjectLocation. Registration and relinking use it so Git subdirectories remain folder projects, while Git roots and non-Git paths retain existing behavior.
Explicit touch path matching
packages/storage/src/project-catalog.ts, packages/storage/src/__tests__/project-catalog.test.ts
touch now canonicalizes explicit paths and matches them directly against stored locations. Tests cover child-path touches and ProjectPathMismatchError for the parent path.
Nested project integration coverage
apps/desktop/src/main/__tests__/project-management-service.test.ts, packages/storage/src/__tests__/project-catalog.test.ts
Tests cover separate parent and child projects, child selection, relinking, Git repository setup, path assertions, and temporary-resource cleanup.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to c2e26

This change preserves user-selected nested folders as independent projects while retaining repository identity behavior for roots and historical sessions; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant DesktopProjectService
  participant ProjectCatalog
  participant Git
  DesktopProjectService->>ProjectCatalog: register selected child directory
  ProjectCatalog->>Git: resolve repository context
  Git-->>ProjectCatalog: repository root and child path
  ProjectCatalog-->>DesktopProjectService: child folder project
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary fix: preserving nested folders as separate projects.
Description check ✅ Passed The description includes the required summary, issue reference, verification results, AI disclosure, and checklist.
Linked Issues check ✅ Passed The changes satisfy issue #2660 by preserving selected nested folders as independent project paths.
Out of Scope Changes check ✅ Passed The relink, session, touch, and regression-test changes support the stated nested-project behavior and linked issue.
Ai Use Disclosure ✅ Passed The PR selects generative tooling, names Grok, and states its scope. All four PR commits contain standalone Generated-by: Grok trailers consistent with that disclosure.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@1625567290
1625567290 force-pushed the fix/desktop-nested-project-root branch from eab34ea to 6a995f8 Compare August 15, 2026 13:40

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing the distinction between an explicitly selected folder and a historical cwd. Keeping repo/child as its own project is the right direction, but there is one lifecycle gap to address before merge.

[P1] Keep registered project locations authoritative in touch()

After the child project is registered, creating a session for it still fails:

session.createHostWorkspaceResolver.runWithUsageRecorded()catalog.touch(projectId, workspace.cwd)

Here, workspace.cwd is the child project’s stored preferredPath. However, touch() runs it through resolveProjectLocation() again, which maps /repo/child back to the enclosing Git root /repo. That path is not one of the child project’s locations, so touch() throws ProjectPathMismatchError and session creation returns operation_conflict.

In other words, the PR currently allows the nested project to be registered and selected, but its primary runtime flow cannot start a session.

I suggest making the registered locations[] authoritative in touch(): canonicalize the supplied path, match it directly against the project’s stored locations, and reject it if no exact match exists. The only production caller passes the stored preferredPath, and touch is not exposed as a protocol operation, so Git-root fallback does not appear necessary here.

A focused regression test should cover either:

  • catalog.touch(childProject.id, childProject.preferredPath) succeeding after registering the parent and child; or
  • the full session.create path for the nested project.

Two optional simplifications would also keep the contract smaller:

  • Keep resolveUserSelectedProjectLocation module-private and test it through catalog.register() rather than exporting and directly testing the helper.
  • Remove the new assertion that resolveHistoricalPath(child) returns the parent project. That API currently has no production caller, so this PR does not need to freeze that future behavior.

I do not think a broader project-catalog redesign is needed in this PR; fixing touch() at the ownership boundary and adding one production-shaped regression test should complete the lifecycle.

中文对照

感谢修正“用户明确选择的目录”和“历史 cwd”之间的区别。让 repo/child 保持为独立项目的方向是正确的,但合并前还需要补齐一个生命周期缺口。

[P1] touch() 应以项目已经登记的 location 为归属依据

child 项目注册完成后,为它创建 Session 仍然会失败:

session.createHostWorkspaceResolver.runWithUsageRecorded()catalog.touch(projectId, workspace.cwd)

此时 workspace.cwd 就是 child 项目已经保存的 preferredPath。但是 touch() 又调用 resolveProjectLocation(),把 /repo/child 重新解析成父 Git 根目录 /repo。父目录不在 child 项目的 locations 中,因此会抛出 ProjectPathMismatchError,最终令 Session 创建返回 operation_conflict

也就是说,当前 PR 可以注册和选择嵌套项目,但该项目的主要运行流程无法真正创建 Session。

建议让 touch() 直接以已登记的 locations[] 为权威来源:canonicalize 输入路径后,在该项目的 locations 中精确匹配;不存在则拒绝。唯一的生产调用者传入的就是已保存的 preferredPath,而且 touch 也不是对外的 protocol operation,因此这里不需要保留 Git-root fallback。

回归测试可以选择以下任意一条真实 seam:

  • 注册父项目和 child 后,验证 catalog.touch(childProject.id, childProject.preferredPath) 成功;或
  • 完整验证嵌套项目的 session.create

另外有两个可选简化:

  • resolveUserSelectedProjectLocation 保持为模块私有,通过 catalog.register() 测试行为,不扩大公开 API。
  • 删除新增的 resolveHistoricalPath(child) 必须返回父项目的断言。该接口目前没有生产调用者,本 PR 没有必要冻结其未来契约。

本 PR 不需要扩大成 project catalog 的整体重构;在 touch() 这个自然 owner 处修正归属判断,再补一条命中真实调用链的测试,就足以完整收口。

AI-assisted review disclosure: Codex, Claude, and Kimi K3-256k assisted with tracing the call chain. I verified the finding against the current head and made the final review decision.

Registering a directory inside an existing Git project used to walk
up to the worktree root, so adding repo/child silently reopened repo.
Keep the exact folder the user picked. Historical session cwds still
resolve to the enclosing repository.

Fixes apache#2660

Generated-by: Grok
session.create records usage through catalog.touch(projectId, cwd).
Resolving that cwd through Git walked a nested folder back to the
parent worktree and rejected the child's stored preferredPath.
Canonicalize the supplied path and match it exactly against the
project's registered locations.

Generated-by: Grok
@1625567290
1625567290 force-pushed the fix/desktop-nested-project-root branch from 6a995f8 to 9b677f9 Compare August 15, 2026 16:30
@1625567290

Copy link
Copy Markdown
Contributor Author

Addressed in 9b677f9.

P1 touch(): it no longer walks the path through Git. The supplied path is canonicalized (realpath) and matched exactly against the project's registered locations[]. catalog.touch(child.id, child.preferredPath) succeeds after parent+child register; touch(child.id, parentPath) throws ProjectPathMismatchError.

Optional cleanups from the review:

  • resolveUserSelectedProjectLocation is module-private again
  • the resolveHistoricalPath(child) assertion is gone

Verified:

  • project-catalog + authority — 20/20
  • Desktop add/select nested folder + new-session-project — 13/13
  • HostWorkspaceResolver.runWithUsageRecorded({ kind: "project", projectId: child.id }) — ok (the session.create usage-recording seam)

Generated-by: Grok

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for following up on the earlier review. I rechecked the current head, and the original touch() lifecycle issue is fixed: registered locations[] are now authoritative, the helper remains module-private, and the unnecessary historical-path assertion has been removed.

I found two follow-up items. Neither needs to block this PR, but they seem worth recording:

[P2] Preserve the nested Project cwd for worktree children

A Project registered at /repo/child now correctly starts its normal Session there. However, an Implementation child still resolves that path to /repo, provisions a repository-wide worktree, and starts at the worktree root rather than <worktree>/child.

I reproduced this with the actual executor at the current head. The behavior was already reachable through host_path, so this is not a regression introduced solely by this PR; #3070 simply makes it reachable through the normal Desktop Project flow as well.

I think a separate follow-up is appropriate. A compact solution would keep the durable binding rooted at the managed worktree while returning a separate childCwd derived from the source path’s repository-relative location.

[P2] Add coverage for the changed relink paths

Both relink() and relinkWithSessions() now use the explicit-selection resolver, but no test currently relinks a Project to a nested repository directory. Reverting either call site would leave the suite green.

A focused storage test could relink an unrelated Project to childPath and verify that the exact path is preserved rather than collapsing into the repository Project.

Two optional simplifications:

  • The new desktop test’s dynamic currentSelection mock is not used by add(), so it could be a constant stub.
  • The values compared in resolveUserSelectedProjectLocation() are already normalized, making the two additional normalize() calls unnecessary.

I would keep both newly added tests: the storage test covers the catalog/session lifecycle, while the desktop test uniquely verifies the user-visible selected path.

Overall, the Project Catalog change is well-scoped and uses the right ownership boundary. I’m comfortable with this PR moving forward while the worktree behavior is tracked separately.

中文对照

感谢继续跟进上一轮 review。我重新检查了当前 head,之前发现的 touch() 生命周期问题已经修复:现在以已登记的 locations[] 为权威来源,helper 也保持为模块私有,同时删除了不必要的 historical-path 断言。

还有两项值得后续处理。它们都不需要阻塞本 PR,但建议明确记录:

[P2] worktree 子会话应保留嵌套 Project 的 cwd

注册在 /repo/child 的 Project,现在可以正确地从该目录启动普通 Session。不过,Implementation 子会话仍会把这个路径解析回 /repo,为整个仓库创建 worktree,并从 worktree 根目录启动,而不是从 <worktree>/child 启动。

我使用当前 head 的真实 executor 复现了该行为。它此前已经可以通过 host_path 路线触发,因此并不是完全由本 PR 引入的回归;#3070 只是让正常的 Desktop Project 流程也可以触达这条路径。

我认为这适合单独跟进。一个比较紧凑的方案是:持久化 binding 仍以受管 worktree 为根,同时额外返回根据原始路径在仓库中的相对位置计算出的 childCwd

[P2] 为本次修改涉及的 relink 路径补充覆盖

relink()relinkWithSessions() 现在都改用了显式选择 resolver,但目前没有测试覆盖“把 Project relink 到 Git 仓库内的嵌套目录”。即使把其中任意一个调用点改回旧 resolver,现有测试仍然会全绿。

可以在 storage 层增加一条聚焦测试:把一个无关 Project relink 到 childPath,并验证精确路径得到保留,而不是重新折叠为父仓库 Project。

另外有两项可选简化:

  • Desktop 新测试中的动态 currentSelection mock 并未被 add() 调用,可以改成常量 stub。
  • resolveUserSelectedProjectLocation() 中参与比较的两个值已经规范化,因此额外的两次 normalize() 可以删除。

我建议保留两条新增测试:storage 测试覆盖 catalog/session 生命周期,desktop 测试则独立验证用户最终选中的可见路径。

总体而言,Project Catalog 的改动范围合理,也放在了正确的权威边界上。我认为本 PR 可以继续推进,同时把 worktree 行为作为独立事项跟进。

AI-assisted review disclosure: Codex performed the primary lifecycle trace and executor reproduction; an independent Codex reviewer and Claude performed adversarial review. The final assessment above was checked against the current PR head.

relink() and relinkWithSessions() now have a nested-folder regression
test so the explicit-selection resolver stays on the critical path. The
chooser comparison already sees normalized paths, so the extra
normalize() calls are gone.

Generated-by: Grok
@1625567290

Copy link
Copy Markdown
Contributor Author

Addressed the in-scope P2 follow-ups in 66778b4.

Relink coverage

  • Storage test relinks unrelated projects onto nested repository paths via both relink() and relinkWithSessions(), asserting the exact child path is kept (not collapsed to the parent repo).

Optional cleanups

  • Desktop nested-add test uses a constant currentSelection stub (add() only uses chooseDirectory / setSelection).
  • resolveUserSelectedProjectLocation compares already-normalized paths without extra normalize() calls.

Worktree child cwd (P2)
Leaving as a separate follow-up as you suggested: Implementation children still resolve through the repo-wide worktree path. That path was already reachable via host_path; this PR does not change that executor binding. Happy to open a focused PR for childCwd next.

Verified: project-catalog 20/20.

Generated-by: Grok

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cf5b8ee4-a5a4-49a5-b08c-1ed4a279cc39

📥 Commits

Reviewing files that changed from the base of the PR and between 9b677f9 and 66778b4.

📒 Files selected for processing (3)
  • apps/desktop/src/main/__tests__/project-management-service.test.ts
  • packages/storage/src/__tests__/project-catalog.test.ts
  • packages/storage/src/project-catalog.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/main/tests/project-management-service.test.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review.

Comment thread packages/storage/src/__tests__/project-catalog.test.ts Outdated
The nested-path test now creates a Session on the source project and
checks that relinkWithSessions reports that id and rewrites the header
cwd to the nested folder.

Generated-by: Grok

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the follow-up. I re-reviewed the latest head (c2e26a9) and the original concerns are now addressed.

The touch() path matches registered project locations directly, and the nested relinkWithSessions coverage now creates a real Session and verifies both the reported Session ID and the persisted projectId/cwd.

I found no remaining P0–P3 issues. The implementation stays within the existing Project Catalog authority and is the smallest complete fix for #2660. The repository-root behavior of Implementation children remains a separate, pre-existing executor concern and does not block this PR.

Targeted storage tests pass (20/20), CI is green, and the PR is merge-clean. Approved from a code-review perspective.

Reviewed with Codex as an AI-assisted adversarial review; I verified the final diff, tests, feedback resolutions, CI, and merge state.

中文版

感谢继续完善。我重新审查了最新提交 c2e26a9,此前的问题均已解决。

touch() 现在会直接匹配已登记的项目路径;嵌套目录的 relinkWithSessions 测试也已创建真实 Session,并验证返回的 Session ID 以及持久化后的 projectIdcwd

当前没有剩余的 P0–P3 问题。实现沿用现有 Project Catalog 权威边界,是解决 #2660 的最小完整方案。Implementation child 从仓库 worktree 根目录启动属于既有 executor 行为,可单独跟进,不阻塞本 PR。

定向测试 20/20 通过,CI 全绿,且当前可干净合并。从代码审查角度同意合并。

@Astro-Han
Astro-Han merged commit 9fbdc99 into apache:main Aug 16, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(desktop): creating a project from a subfolder selects the existing parent project instead

2 participants