feat(sessions): org-scope aware session grouping and workspace picker#402
Conversation
Under an active cloud org sidebar scope: - The new-session workspace picker (WorkspaceDropdown + WorkspacePalette) only offers repos whose git-remote scope key falls inside the org's repo scope — out-of-scope workspaces can no longer be selected. A stale out-of-scope selection (persisted default from another scope) is swapped to the first in-scope repo, so a session created under an org scope always lands visible in that org (previously it silently fell to Personal when autoTag's scope gate failed). - Personal scope now hides sessions explicitly tagged into a cloud org (the mirror of extraSessionIds on the cloud side); untagging via MoveToOrgDialog or the engine's out-of-scope invalidation moves them back. - Imported CLI sessions (claude/codex/cursor/…) whose repo sits inside exactly ONE member org's repo scope are stamped with that org at list-materialization time (in-memory overlay over the existing load path) — they group under the org instead of piling up in Personal. Ambiguous coverage (multiple orgs) stays Personal. No new polling anywhere: the picker predicate re-evaluates via the shared scope-key cache subscription, the fallback guard runs on scope / repo-list changes only, and the overlay rides the existing list loads (cache-only peek + fire-and-forget prime). Pre-commit hook ran. Total eslint: 0, total circular: 0
Replace the load-time single-org stamp with view-layer matching: an imported CLI session whose repo is covered by an org's repo scope now appears under EVERY covering org's view (no exactly-one-org ambiguity rule), and Personal hides it as soon as ANY member org covers it. The load-time overlay could only write one orgId per session, which forced the multi-org case to stay in Personal — the wrong default. The sidebar connector now derives two id-sets per render pass from lists it already holds (extraSessionIds for the active cloud scope, exclusions for Personal); async scope-key resolutions re-derive through the shared cache subscription. loaders.ts returns to untouched pre-feature state. Pre-commit hook ran. Total eslint: 0, total circular: 1
Multi-repo workspaces and the recent section bypassed the repoFilter. They are judged by their PRIMARY folder now (the path a session launched from the workspace reports as repoPath), matching autoTag's semantics. Pre-commit hook ran. Total eslint: 0, total circular: 0
Three picker-filter corrections from live E2E: - Scope keys now come from peekShareableScopeKeys (ALL remotes) instead of the primary-only peek — a fork checkout whose upstream hits the org scope (VantaNode/ORG2 origin + yorgai/ORG2 upstream) was hidden. - Multi-repo workspaces stay visible when ANY member folder is eligible, not just the primary. - The picker predicate is now OPTIMISTIC for still-resolving checkouts (visible + primed until the cache lands); the repos table stores no repo_url, so every row depends on async remote resolution and a cold cache used to blank genuinely-in-scope repos. Session grouping keeps the strict resolved-evidence rule; a scope-less org no longer filters the picker at all. Pre-commit hook ran. Total eslint: 0, total circular: 0
|
Follow-up fix from live E2E with real repo/workspace data (b102332):
Re-verified on the real app: ORG2 (fork), ORG2-claude_code, ORGII-cloud-infra-ORGII and ORGII Workspace all appear under the org scope; out-of-scope repos/workspaces stay filtered. |
Per user decision: a session lives in Personal AND every org whose repo scope covers it, simultaneously. Org membership no longer auto-removes a session from Personal. - MoveToOrgDialog: a "Personal" checkbox (default checked, always enabled, local-only — no server sync) sits above the org rows, making the multi-select model visible. Unchecking stores PERSONAL_EXCLUDED_TOKEN. - Personal scope now hides ONLY sessions the user explicitly excluded, not everything tagged/scope-matched to an org (reverts the mirror rule). - Drop the now-unused collectImportedSessionIdsCoveredByAnyScope helper. The personal marker rides the same tag atom but is not a cloud org, so cloud parsing/sync skip it. Pre-commit hook ran. Total eslint: 0, total circular: 0
|
Per user decision (9b52c59): Personal is a default-on membership — a session lives in Personal AND every covering org simultaneously; org membership never auto-removes it from Personal.
Live-verified: dialog shows Personal + org checked together; unchecking Personal removes the session from the Personal view instantly while it stays in the org view; state persists across dialog reopen; re-checking restores it. |
Pre-commit hook ran. Total eslint: 0, total circular: 0
- session creator guard swaps drafts via onRepoSelect only, no longer persists an org repo as the global default - orphan double-guard: Personal cannot be unchecked without another org membership; sync engine clears personal:excluded with the last cloud tag - MoveToOrgDialog truth: personalUnavailable badge for org-stamped sessions, autoInScope badge for scope-matched imported sessions - workspace picker: system-path rows exempt from scope filtering, shared workspaceMatchesRepoFilter, eligibility split from search filtering, intermediate eligible* memos inlined - sidebar scope matching dedupes per repoPath and normalizes org scopes once per pass; shared useShareableScopeKeyVersion hook - orgScopeRepoFilter resolves fs_uri (all remotes) before repo_url and strips file:// scheme; predicates merged - drop dead orgId guard + stale header in importedSessionScopeMatch - strip branch-added narrative comments; translate 3 new i18n keys across 13 locales; extend unit tests (engine, tags atom, filters) Pre-commit hook ran. Total eslint: 0, total circular: 0
Manual org-uncheck and add-rollback in MoveToOrgDialog used raw withoutTag, so untagging the last org of a Personal-excluded session orphaned it out of every scope. Both paths now use withoutCloudOrgTag, which clears personal:excluded with the last cloud tag - a session always keeps at least one membership. Also fix Checkbox: checked+disabled rendered as a near-background gray box indistinguishable from unchecked (which is what made the dialog look like it had zero memberships). Checked/indeterminate now wins over disabled in getCheckboxIconClassName, keeping the accent fill dimmed by the existing label opacity. Pre-commit hook ran. Total eslint: 0, total circular: 0
|
审计修复 + 归属不变量修复已推送(56cd55c3f, 5d6500f): 审计修复(15 项 finding 全部处理)
归属不变量(会话任何时刻 ≥1 归属)
验证:tsc 零错误、eslint 干净、vitest 262 用例全过;真机 computer-use 实测「排除 Personal → 移除最后一个 org → 自动回落 Personal(禁用+唯一归属)」链路通过。 |
What
When a cloud org is the active sidebar scope, sessions and the new-session workspace picker now follow the org's repo scope instead of everything defaulting to Personal.
1. Workspace picker filters to the org scope
SessionInfoLine's repo picker (bothWorkspaceDropdownandWorkspacePalette, incl. the recent + multi-repo-workspace sections) only offers repos whose git-remote scope key is inside the active org's repo scope. Multi-repo workspaces are judged by their primary folder. A stale out-of-scope selection is swapped to the first in-scope repo, so a session created under an org scope always lands visible in that org — never silently in Personal.2. New sessions default into the active org
Unchanged
autoTagNewSessiontags an in-scope new session into the active cloud org; Personal now hides any session that lives in a cloud org (explicit tag OR repo-scope match), so it shows up in the org view and not both.3. Imported CLI sessions match by repo scope at filter time
Imported claude/codex/cursor sessions carry no persisted org_id. Rather than stamping one org at load time (which forced the multi-org case into Personal), the sidebar matches them against repo scopes at filter time: a session whose repo is covered by an org's scope appears under every covering org's view, and is hidden from Personal as soon as any member org covers it. No exactly-one-org ambiguity rule.
No new polling
All derivation rides lists the sidebar already holds; async git-remote scope-key resolutions re-derive through the shared cache subscription (
subscribeShareableScopeKeys). The picker fallback runs on scope/repo-list changes only. Mirrors the focus-adaptive / cache-bounded posture of the recent perf work.Verification (macOS, computer-use, real app)
Org scope =
github.com/yorgai/org2(the ORGII checkout's remote):tsc, eslint, and the TeamCollaboration + sidebar suites (184 tests, incl. 12 new for the scope matchers) are green.🤖 Generated with Claude Code