fix(task-input): prefill repo from sidebar "+" in both local and cloud modes#2703
fix(task-input): prefill repo from sidebar "+" in both local and cloud modes#2703mp-hog wants to merge 1 commit into
Conversation
…d modes The TASKS sidebar "+" button (and the command menu's "new task in folder") passes a `folderId` to the new-task screen, but `folderId` only ever prefilled the local directory selector — never the cloud repo dropdown. In Cloud mode the prefill was invisible, so clicking "+" on a repo left the cloud dropdown on the last-used (wrong) repo, and tasks could be started against the wrong repo. Make `folderId` drive both selectors. A registered folder already carries both `path` (local) and `remoteUrl` (the resolved `owner/repo` slug), so it is a single source of truth for the local directory and the cloud repo. New `useInitialRepoSelectionFromFolderId` hook + pure `resolveRepoSelectionForFolder` resolver: - always prefill the local directory from the folder path; - prefill the cloud repo when the folder's remote is a real `owner/repo` that is a connected GitHub integration; - keep the user's current workspace mode when it can represent the repo, and only switch (Cloud -> last-used local mode) when it can't (local-only repo while in Cloud). The mode switch uses the non-persisting setter so it does not overwrite the user's mode preference. The cloud/mode decision is gated on the integrations list having loaded, and runs once per `folderId` so it never clobbers a manual change. Replaces the directory-only `useInitialDirectoryFromFolderId` (removed). Call sites are unchanged, so every folder-scoped new-task entry point benefits. Known limits (out of scope): a cloud-only group with no registered local folder falls back to defaults; in Cloud mode, "+" on a repo whose org is not a connected integration intentionally switches to Local. Generated-By: PostHog Code Task-Id: 50f73c6f-d922-4d90-b547-a450102c770c
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
packages/ui/src/features/task-detail/components/TaskInput.tsx:482
The `repositories.length > 0` guard conflates "the fetch has finished" with "the fetch returned results." When a user has no connected GitHub integrations the list finishes loading empty, so `reposLoaded` stays `false` permanently. The effect's `repoModeInitRef` block is therefore never entered, and a user in Cloud mode who clicks "+" on a local-only folder will never be switched to Local — the whole mode-switch branch this PR adds is silently skipped for that cohort.
```suggestion
reposLoaded: !isLoadingRepos,
```
### Issue 2 of 2
packages/ui/src/features/task-detail/hooks/useInitialRepoSelectionFromFolderId.test.ts:23-131
**Prefer parameterised tests for the pure resolver.** The six `resolveRepoSelectionForFolder` cases all call the same function with a different set of inputs and assert a different output — a textbook fit for `it.each`. Flattening them into a table would also expose that there is no case for `reposLoaded: true` with an empty `repositories` array, which is where the `reposLoaded` condition in `TaskInput.tsx` goes wrong.
Reviews (1): Last reviewed commit: "fix(task-input): prefill repo from sideb..." | Re-trigger Greptile |
| folderId: view.folderId, | ||
| folders, | ||
| repositories, | ||
| reposLoaded: !isLoadingRepos && repositories.length > 0, |
There was a problem hiding this comment.
The
repositories.length > 0 guard conflates "the fetch has finished" with "the fetch returned results." When a user has no connected GitHub integrations the list finishes loading empty, so reposLoaded stays false permanently. The effect's repoModeInitRef block is therefore never entered, and a user in Cloud mode who clicks "+" on a local-only folder will never be switched to Local — the whole mode-switch branch this PR adds is silently skipped for that cohort.
| reposLoaded: !isLoadingRepos && repositories.length > 0, | |
| reposLoaded: !isLoadingRepos, |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/task-detail/components/TaskInput.tsx
Line: 482
Comment:
The `repositories.length > 0` guard conflates "the fetch has finished" with "the fetch returned results." When a user has no connected GitHub integrations the list finishes loading empty, so `reposLoaded` stays `false` permanently. The effect's `repoModeInitRef` block is therefore never entered, and a user in Cloud mode who clicks "+" on a local-only folder will never be switched to Local — the whole mode-switch branch this PR adds is silently skipped for that cohort.
```suggestion
reposLoaded: !isLoadingRepos,
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Valid catch on the diagnosis — a user can be in Cloud mode with zero GitHub integrations (the mode toggle gates on the twig-cloud-mode-toggle flag, not hasGithubIntegration, and lastUsedWorkspaceMode: cloud persists after disconnecting). For that cohort repositories.length > 0 never becomes true, so the Cloud→Local switch never fires and "+" on a local-only folder silently does nothing.
The proposed fix (!isLoadingRepos alone) isn't quite right though — it reintroduces the transient-empty race the length check guarded: right after the integrations query resolves there's a window where isLoadingRepos === false while repositories is still [] (per-installation repo queries freshly spawned, same window the validation effect documents). A user with integrations clicking a cloud-capable repo in that window would be wrongly judged not-cloud-capable and switched to Local on the common path.
Fixed by distinguishing settled-empty from transiently-empty: extracted a pure areReposReady({ isLoadingRepos, repositoriesCount, hasGithubIntegration }) → ready when !isLoadingRepos && (repositoriesCount > 0 || !hasGithubIntegration). Fixes the no-integration cohort, keeps the window guard for the common path. Done in #2712 (this branch is locked to further commits).
| describe("resolveRepoSelectionForFolder", () => { | ||
| it("prefills both selectors for a cloud-capable folder and keeps cloud mode", () => { | ||
| expect( | ||
| resolveRepoSelectionForFolder({ | ||
| folder: folder("a", "/repos/a", "posthog/posthog"), | ||
| repositories: ["posthog/posthog"], | ||
| reposLoaded: true, | ||
| currentMode: "cloud", | ||
| lastUsedLocalMode: "local", | ||
| }), | ||
| ).toEqual({ | ||
| directory: "/repos/a", | ||
| cloudRepository: "posthog/posthog", | ||
| nextMode: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it("prefills the cloud repo while keeping local mode (no switch)", () => { | ||
| expect( | ||
| resolveRepoSelectionForFolder({ | ||
| folder: folder("a", "/repos/a", "posthog/posthog"), | ||
| repositories: ["posthog/posthog"], | ||
| reposLoaded: true, | ||
| currentMode: "local", | ||
| lastUsedLocalMode: "local", | ||
| }), | ||
| ).toEqual({ | ||
| directory: "/repos/a", | ||
| cloudRepository: "posthog/posthog", | ||
| nextMode: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it("lower-cases the remote slug before matching", () => { | ||
| expect( | ||
| resolveRepoSelectionForFolder({ | ||
| folder: folder("a", "/repos/a", "PostHog/PostHog"), | ||
| repositories: ["posthog/posthog"], | ||
| reposLoaded: true, | ||
| currentMode: "cloud", | ||
| lastUsedLocalMode: "local", | ||
| }).cloudRepository, | ||
| ).toBe("posthog/posthog"); | ||
| }); | ||
|
|
||
| it("switches to the last-used local mode for a local-only folder while in cloud", () => { | ||
| expect( | ||
| resolveRepoSelectionForFolder({ | ||
| folder: folder("a", "/repos/a", null), | ||
| repositories: ["posthog/posthog"], | ||
| reposLoaded: true, | ||
| currentMode: "cloud", | ||
| lastUsedLocalMode: "worktree", | ||
| }), | ||
| ).toEqual({ | ||
| directory: "/repos/a", | ||
| cloudRepository: undefined, | ||
| nextMode: "worktree", | ||
| }); | ||
| }); | ||
|
|
||
| it("treats a remote not in the integrations list as not cloud-capable", () => { | ||
| expect( | ||
| resolveRepoSelectionForFolder({ | ||
| folder: folder("a", "/repos/a", "acme/private"), | ||
| repositories: ["posthog/posthog"], | ||
| reposLoaded: true, | ||
| currentMode: "cloud", | ||
| lastUsedLocalMode: "local", | ||
| }), | ||
| ).toEqual({ | ||
| directory: "/repos/a", | ||
| cloudRepository: undefined, | ||
| nextMode: "local", | ||
| }); | ||
| }); | ||
|
|
||
| it("ignores legacy single-segment remote values", () => { | ||
| expect( | ||
| resolveRepoSelectionForFolder({ | ||
| folder: folder("a", "/repos/a", "posthog"), | ||
| repositories: ["posthog"], | ||
| reposLoaded: true, | ||
| currentMode: "cloud", | ||
| lastUsedLocalMode: "local", | ||
| }), | ||
| ).toEqual({ | ||
| directory: "/repos/a", | ||
| cloudRepository: undefined, | ||
| nextMode: "local", | ||
| }); | ||
| }); | ||
|
|
||
| it("never switches mode before the integrations list has loaded", () => { | ||
| expect( | ||
| resolveRepoSelectionForFolder({ | ||
| folder: folder("a", "/repos/a", null), | ||
| repositories: [], | ||
| reposLoaded: false, | ||
| currentMode: "cloud", | ||
| lastUsedLocalMode: "local", | ||
| }), | ||
| ).toEqual({ | ||
| directory: "/repos/a", | ||
| cloudRepository: undefined, | ||
| nextMode: undefined, | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Prefer parameterised tests for the pure resolver. The six
resolveRepoSelectionForFolder cases all call the same function with a different set of inputs and assert a different output — a textbook fit for it.each. Flattening them into a table would also expose that there is no case for reposLoaded: true with an empty repositories array, which is where the reposLoaded condition in TaskInput.tsx goes wrong.
Context Used: Do not attempt to comment on incorrect alphabetica... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/task-detail/hooks/useInitialRepoSelectionFromFolderId.test.ts
Line: 23-131
Comment:
**Prefer parameterised tests for the pure resolver.** The six `resolveRepoSelectionForFolder` cases all call the same function with a different set of inputs and assert a different output — a textbook fit for `it.each`. Flattening them into a table would also expose that there is no case for `reposLoaded: true` with an empty `repositories` array, which is where the `reposLoaded` condition in `TaskInput.tsx` goes wrong.
**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Done — folded the resolver cases into an it.each table per CLAUDE.md guidance, and added a parameterised areReposReady table covering the readiness predicate directly (incl. the transient-window and no-integration cases the original tests missed). In #2712.
|
Heads up: PR #2703's branch became locked to further signed commits (the signing tool flagged it as a base branch), so I couldn't push the review-feedback fixes here. They live in #2712, which carries this same first commit plus the fix for the P1/P2 review comments. Recommend reviewing #2712 and closing this one as superseded. |
Replaced by #2712 as PH code couldn't continue committing to this PR