fix(client): merge No project filters across environments#17050

merged4
main
GitHub fork: pixpmusic/fix-no-project-filter
opened20d agobyPPixPMusic
View on GitHubTwo-way sync enabledPR last imported 7d ago

Problem

Connecting multiple environments produces multiple “No project” entries in the thread filter because each scratch workspace has a different project ID and path. Selecting one hides scratch threads from the other environments. This reproduces in web and Android; the report includes desktop.

Closes #17049.

Change

Identify scratch projects from each environment’s configured scratch root and combine them into one “No project” group for thread filters in the shared client runtime. Keep every environment-scoped project reference so the filter includes all matching threads. Ordinary projects, including a project merely named “No project”, retain their existing grouping behavior. Scratch merging is enabled by thread-filter callers; other consumers keep their existing groups. Settings shortcuts from the merged filter or a thread open the corresponding project’s existing settings scope.

Preserve a selected scratch filter when environment config arrives after projects. Web migrates saved physical or repository filter keys and waits for enabled environments’ configs before clearing a missing selection. Both mobile filter owners save the migrated logical key so it survives removal of the original host. Existing ordinary repository scopes take precedence over scratch aliases.

Scope and approval

This is a focused fix for the obvious duplicate-entry bug tracked in #17049 and aligns the filter with the existing single “No project” picker choice. It changes client grouping and selection handling, with no new setting or server contract. The issue is newly filed; no prior maintainer approval is claimed.

Verification

  • 65 tests passed across projectEntities.test.ts, projectGrouping.test.ts, environmentGrouping.test.ts, homeThreadList.test.ts, and ProjectSettingsPanel.test.tsx. Coverage includes late config arrival, stable project identity, all grouping modes, duplicate project references, same-title ordinary projects, saved filter migration in all grouping modes, disabled-environment readiness, host removal after migration, environment-scoped thread filtering, and host-specific settings ownership and shortcuts.
  • Scoped typechecks passed for client-runtime, web, and mobile. Targeted lint passed with existing React/sidebar warnings; git diff --check passed.
  • Reproduced the baseline in an isolated web client and Android 15 emulator connected to two disposable environments. Before: two choices, each filtering to one host. After: one choice includes both scratch threads and excludes ordinary project threads. Clearing the filter restores the full list; the web selection survives reload. Opening Settings from the merged filter targets a single project entry, with single-entry removal copy.
  • Desktop uses the changed web code; iOS uses the changed mobile code. Electron, iOS, relay, and tunnel were not separately exercised. Provider behavior and wire contracts are unaffected.

Both captures use the same fixture, theme, and viewport/device. Android captures crop to the menu and use an equal canvas with blank padding where the shorter menu removed a row.

Model: GPT-6.1 Sol | Harness: Codex in T3 Code

Checks

1 pending · 24 successful · 9 skipped · 1 neutral
Checks in progress

In progress1

2a8d4be
apps/mobile/src/features/home/HomeRouteScreen.tsxline 107Outdated · dd3eb10Open thread
……@@ -102,7 +103,8 @@ export function HomeRouteScreen() {
104105 const selectedEnvironmentId = listOptions.selectedEnvironmentId;
105- const [selectedProjectKey, setSelectedProjectKey] = useState<string | null>(null);
106+ const [projectScopeKey, setSelectedProjectKey] = useState<string | null>(null);
107+ const selectedProjectKey = resolveScratchProjectScopeKey(projectScopeKey, projects);
MmacroscopeappBoton line 10720d ago

🟡 Medium home/HomeRouteScreen.tsx:107

Removing or disabling environment A clears the selected project filter and exposes ordinary-project threads, even when environment B still provides the combined scratch scope. selectedProjectKey resolves A's physical key for display but never updates projectScopeKey, so the missing-option effect later clears the stale selection. Persist the resolved key when it changes to keep the logical scratch selection.

Also found in 1 other location(s)

apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx:210

The resolved scratch key is never written back to projectScopeKey. If a user selects environment A's physical scratch filter before config arrives, it later displays the combined “No project” filter, but state still holds A's physical key. Removing/disabling A then removes its projects from projectsAtom (which aggregates only enabled environments); the resolver returns the obsolete physical key even when environment B still supplies the combined scratch scope. The missing-option effect clears the selection, unexpectedly showing all projects. Persist the migrated logical key so the combined filter survives removal of its original host.

No longer relevant as of a6d00ed33ffbfbc169ed7a4f6754c7724fd5d825

Fixed in a6d00ed in both mobile filter owners. When the resolved key changes, they save the logical scratch key using a guarded render update, so removing the original environment cannot revert it to the old physical key. Regression coverage migrates a pre-config selection in each grouping mode, removes its original host, and verifies the surviving scratch scope and project references.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

packages/client-runtime/src/state/projectGrouping.tsline 171Outdated · dd3eb10Open thread
……@@ -156,6 +160,19 @@ export function deriveLogicalProjectKeyFromSettings(
168+ >,
169+): string | null {
170+ if (key === null || key === SCRATCH_PROJECT_SCOPE_KEY) return key;
171+ return projects.some((project) => project.isScratch && derivePhysicalProjectKey(project) === key)
MmacroscopeappBoton line 17120d ago

🟡 Medium state/projectGrouping.ts:171

When a scratch project was selected before config arrived in repository or repository_path mode, this returns its stale repository-derived key instead of SCRATCH_PROJECT_SCOPE_KEY, so the mobile and web selection effects clear the filter after config marks it scratch. The lookup only matches derivePhysicalProjectKey; preserve the pre-config logical key as well when resolving the scratch scope.

No longer relevant as of a6d00ed33ffbfbc169ed7a4f6754c7724fd5d825

Fixed in a6d00ed. The resolver now checks the pre-config repository key using the active grouping mode and overrides, as well as the physical key. Both clients pass their grouping settings. Existing ordinary repository scopes take precedence. Regression tests cover repository, repository_path, and separate migration, plus the ordinary-scope case.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

apps/web/src/components/Sidebar.tsxline 2655Outdated · dd3eb10Open thread
……@@ -2644,15 +2649,28 @@ export default function Sidebar() {
2652+ // Configs identify scratch projects, so both they and live project snapshots
2653+ // must arrive before a persisted scope can be considered missing.
26502654 const allProjectSnapshotsReady = useAllEnvironmentProjectSnapshotsReady();
2655+ const allProjectConfigsReady = environments.every((environment) =>
MmacroscopeappBoton line 265520d ago

🟡 Medium components/Sidebar.tsx:2655

A saved scope for a removed project is never cleared when any catalog environment is switched off, so the picker shows “All projects” while retaining a stale selection that can reactivate later. allProjectConfigsReady checks every environment from useEnvironmentIdentities(), but serverConfigs only contains enabled environments; restrict this readiness check to enabled environments, as the snapshot readiness check does.

🤖 Copy this AI Prompt to have your agent fix this:
text
1In file @apps/web/src/components/Sidebar.tsx around line 2655:
2
3A saved scope for a removed project is never cleared when any catalog environment is switched off, so the picker shows “All projects” while retaining a stale selection that can reactivate later. `allProjectConfigsReady` checks every `environment` from `useEnvironmentIdentities()`, but `serverConfigs` only contains enabled environments; restrict this readiness check to enabled environments, as the snapshot readiness check does.

Fixed in a6d00ed. Config readiness now derives from enabledEnvironmentIds(catalog), matching project aggregation and snapshot readiness. A missing config from a disabled environment no longer prevents stale-filter cleanup. The atom regression test verifies disabled, re-enabled, and not-yet-ready catalog transitions.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change alters the default cross-environment “No project” filtering behavior across web/desktop and mobile, while adding shared config-derived project identity and persisted-selection migration. The implementation is covered by substantial targeted tests, but the product-wide default behavior change warrants human review.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Addressed all three correctness findings in a6d00ed: pre-config repository-key migration, config readiness limited to enabled environments, and durable mobile scratch selection after host removal. Added focused regression coverage; 51 tests and client-runtime/web/mobile typechecks pass. Rechecked the single filter and combined scratch-thread result in web and Android.

All three review threads are now resolved and outdated. All 8 required checks and Macroscope’s latest correctness check passed on a6d00ed. The approvability summary still describes these findings as unresolved, despite the current thread state; its check is neutral. CodeRabbit remains pending. The PR body records the verification gaps.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cf7b2d2a-810f-420b-abd7-61dddd8e331f

📥 Commits

Reviewing files that changed from the base of the PR and between 9f4c301590ccdd5a7dc09b43b42821de7549472f and a647e48fab12abfdf188c5c5a9809b254405beb9.


📒 Files selected for processing (3)
  • apps/mobile/src/features/home/HomeRouteScreen.tsx
  • apps/mobile/src/features/home/homeThreadList.test.ts
  • apps/web/src/components/Sidebar.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.



📝 Walkthrough

Walkthrough

This change identifies scratch projects from environment configuration, groups them under a shared “No project” scope, and resolves saved project selections in web and mobile views. It also adds project-configuration readiness checks and tests for cross-environment grouping and thread filtering.

Changes

Scratch project grouping and scope

Layer / File(s)Summary
Derive scratch identity from environment config
packages/client-runtime/src/state/models.ts, packages/client-runtime/src/state/projectEntities.ts, packages/client-runtime/src/state/projectEntities.test.ts, apps/web/src/state/projects.ts, apps/mobile/src/state/projects.ts
Environment project atoms mark projects whose workspace root matches the configured scratch root. They expose project-configuration readiness, and web and mobile supply the server configuration atom.
Group scratch projects and resolve their scope key
packages/client-runtime/src/state/projectGrouping.ts, packages/client-runtime/src/state/projectGrouping.test.ts
Scratch projects can share a reserved logical key and use the “No project” label. Tests cover grouping modes, scope resolution, and keeping ordinary projects distinct.
Resolve saved scopes in web and mobile
apps/web/src/sidebarProjectGrouping.ts, apps/web/src/components/Sidebar.tsx, apps/web/src/environmentGrouping.test.ts, apps/mobile/src/features/home/HomeRouteScreen.tsx, apps/mobile/src/features/home/homeThreadList.ts, apps/mobile/src/features/home/homeThreadList.test.ts, apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
The web sidebar resolves persisted keys against project groups and delays clearing missing scopes until snapshots and configurations are ready. Project settings navigation can target a group member. Mobile views resolve stored keys against current projects, and tests cover cross-environment thread filtering.

Priority: ⬇️ Low

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

Change: Bug fix · Severity of issue fixed: Low

Possibly related PRs

  • pingdotgg/t3code#14666: Establishes scratch-project identity and a shared logical key, which this PR uses for cross-environment grouping and saved-scope resolution.

Suggested reviewers: juliusmarminge, t3dotgg


Merge Risk | ⚪ Minimal · up to a647e

Merge Risk: ⚪ Minimal · up to a647e

No actionable merge-blocking risk is established for the shared “No project” filter. The change is mergeable after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to a647e

The shared filter preserves environment-specific project references, while settings shortcuts resolve to existing project scopes. No new authorization bypass was established. Remaining uncertainty concerns selection timing and recovery, rather than expanded privileges.

Retained concerns No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed display scope can include scratch threads across enabled environments already represented in the client. Membership still carries environment/project pairs; the shared key itself does not grant access to an additional environment or project.

Trust Boundaries and Controls

  • observed — Settings navigation continues through the existing route gate, which accepts authenticated or hosted-static states. The settings panel resolves an existing project group and its members. These client controls do not establish independent server-side mutation authorization.

Resilience and Maintainability Implications

  • observed — Mobile already cleared a selection absent from its current filter options before this PR. Existing disconnect and stream-error transitions retain the shell snapshot, and configuration resolution can fall back to retained configuration. This counters routine reconnect-induced catalog loss, but does not verify every teardown or concurrent selection state. No security-boundary regression was established from the filter-clearing behavior.

Pre-merge checks | Passed 4
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the primary change: merging “No project” filters across environments.
Description checkPassedThe description covers the problem, implementation, scope rationale, verification results, test coverage, screenshots, limitations, and agent details. It explains why approval was not required for thi…
Linked Issues checkPassedThe changes meet the coding requirements in [#17049]. They identify scratch projects from each environment's configured scratch root. They merge scratch projects only for thread-filter callers and ret…
Out of Scope Changes checkPassedThe changes stay within [#17049]. The shared grouping change, opt-in filter behavior, selection migration, and settings-target resolution support the requested filter behavior. The reserved scratch ke…

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Synced from GitHub

Addressed the retained architecture concern in CodeRabbit’s summary in 9f4c301. Scratch merging is now an explicit opt-in used only by thread-filter callers. The default grouping used by web/mobile Settings, creation flows, and other project consumers retains its prior scopes. Settings shortcuts from the merged web filter and thread/draft menus resolve the corresponding host’s existing project group, including an explicitly selected remote thread.

Added regression coverage for default scratch ownership across grouping modes and settings-shortcut targets. All 65 focused tests (including the existing ProjectSettingsPanel tests), three scoped typechecks, and targeted lint pass. Web and Android still show one filter including both scratch threads; opening Settings from the web filter shows single-project removal copy. Updated the PR body while preserving bot blocks. New-head CI and reviews are pending.

Added 3 commits

  • fix(client): merge No project filters across environments992a7b0
  • fix(client): limit scratch merging to thread filter scopesa647e48
  • fix(client): retain scratch filters through config and host changesacf4a91
Synced from GitHub

@juliusmarminge @maria-rcks This is rebased onto current main and ready for maintainer review. Focused filter/grouping regression tests and client-runtime, web, and mobile typechecks pass. Existing web and Android evidence is in the description.

Added 1 commit

  • fix(mobile): integrate scratch filters with shared list state2a8d4be