Skip to content

fix(task-input): prefill repo from sidebar "+" in both local and cloud modes#2703

Closed
mp-hog wants to merge 1 commit into
mainfrom
posthog-code/prefill-newtask-repo-from-sidebar-plus
Closed

fix(task-input): prefill repo from sidebar "+" in both local and cloud modes#2703
mp-hog wants to merge 1 commit into
mainfrom
posthog-code/prefill-newtask-repo-from-sidebar-plus

Conversation

@mp-hog

@mp-hog mp-hog commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Replaced by #2712 as PH code couldn't continue committing to this PR

…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
@github-actions

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit c78c485.

@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix 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,

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.

P1 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.

Suggested change
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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).

Comment on lines +23 to +131
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,
});
});
});

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.

P2 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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

mp-hog commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

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.

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.

2 participants