Skip to content

Feature/support multi env#2512

Open
fuzi1996 wants to merge 12 commits intofarion1231:mainfrom
fuzi1996:feature/support_multi_env
Open

Feature/support multi env#2512
fuzi1996 wants to merge 12 commits intofarion1231:mainfrom
fuzi1996:feature/support_multi_env

Conversation

@fuzi1996
Copy link
Copy Markdown

@fuzi1996 fuzi1996 commented May 1, 2026

Summary / 概述

支持多环境

Related Issue / 关联 Issue

如何同时管理windows环境与WSL环境的切换

Screenshots / 截图

image image

Checklist / 检查清单

  • pnpm typecheck passes / 通过 TypeScript 类型检查
  • pnpm format:check passes / 通过代码格式检查
  • cargo clippy passes (if Rust code changed) / 通过 Clippy 检查(如修改了 Rust 代码)
  • Updated i18n files if user-facing text changed / 如修改了用户可见文本,已更新国际化文件

fuzi1996 and others added 8 commits May 1, 2026 14:07
…ions

Introduce profile-based config directory system where each profile can
have its own config dirs and current provider IDs per tool. Adds:
- ConfigDirProfile struct with 18 fields (id, name, 6 config dirs, 6 current providers)
- Profile management functions (get, upsert, delete, set_active)
- 4 new Tauri commands exposed to frontend
- Unit tests for struct serialization and default fields

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When an active profile exists, read/write current_provider_* fields
from/to the profile instead of top-level AppSettings fields. Falls back
to top-level fields for backward compatibility when no profile is active.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ration

Config directory getter functions now read from active profile first,
falling back to top-level legacy fields. Adds automatic migration that
creates a "默认" profile from existing top-level fields on first load.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add TypeScript ConfigDirProfile type with 14 fields (config dirs +
current provider IDs per tool). Add 4 API methods for profile CRUD:
getConfigDirProfiles, upsertConfigDirProfile, deleteConfigDirProfile,
setActiveConfigDirProfile.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add profile state management to useDirectorySettings hook with
createProfile, updateProfile, deleteProfile, and switchProfile functions.
Add profile selector UI (dropdown + new/delete buttons) to
DirectorySettings component. Wire profile props through SettingsPage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Profile CRUD operations now update settings state to ensure changes
are persisted when settings are saved:
- createProfile: sets activeConfigDirProfileId and configDirProfiles
- updateProfile: syncs configDirProfiles to settings
- deleteProfile: syncs configDirProfiles and clears activeProfileId
- switchProfile: syncs activeConfigDirProfileId to settings
- updateDirectoryState: when active profile exists, writes to profile
  fields instead of top-level settings fields

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add ProfileSwitcher component for quick environment profile switching
on the home page. Button shows icon + current profile name, with popover
menu listing all profiles with radio-style selection indicator.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@farion1231
Copy link
Copy Markdown
Owner

@codex review
@claude review

@claude
Copy link
Copy Markdown

claude Bot commented May 1, 2026

Claude finished @farion1231's task in 2m 4s —— View job


Code Review: Feature/support multi env

  • Gather context and read changed files
  • Review Rust backend changes (settings.rs, config.rs, lib.rs)
  • Review TypeScript/frontend changes (types.ts, api/settings.ts, hooks, components)
  • Review i18n changes
  • Summarize findings

Summary

This PR adds multi-environment profile support — users can define named ConfigDirProfile entries (each holding per-app config-dir paths + current provider IDs) and switch between them. The overall design is solid and the backward-compat migration logic is well thought-out. There are several issues to address before merging, ranging from a few functional bugs to UX problems and code smells.


Issues

🔴 Functional / Correctness

1. Stale closure in updateDirectoryState (useDirectorySettings.ts:294)

When updating a profile directory, onUpdateSettings is called with profiles.map(...), but this uses the stale profiles captured in the closure rather than the result of the immediately preceding setProfiles call:

setProfiles((prev) =>
  prev.map((p) => (p.id === activeProfileId ? updatedProfile : p)),
);
// ⚠️ 'profiles' here is still the old value — stale closure
onUpdateSettings({
  configDirProfiles: profiles.map((p) =>
    p.id === activeProfileId ? updatedProfile : p,
  ),
});

This should derive the new list once and use it for both:

const newProfiles = profiles.map((p) => (p.id === activeProfileId ? updatedProfile : p));
setProfiles(newProfiles);
onUpdateSettings({ configDirProfiles: newProfiles });

The same stale-closure pattern appears in createProfile (useDirectorySettings.ts:438): profiles hasn't yet been updated when onUpdateSettings is called.

Fix this →


2. Directory inputs show top-level values, not active-profile values

In SettingsPage.tsx:330-335, the DirectorySettings component is passed:

onUpdateProfile={updateProfile}

But in DirectorySettings.tsx, the claudeDir, codexDir, etc. props come from the top-level settings.claudeConfigDir, settings.codexConfigDir, etc. — not from the active profile's fields. So when a profile is active, the input fields display the wrong (top-level) values rather than the profile's actual directory overrides.


3. TOCTOU in set_active_config_dir_profile (settings.rs:1029-1039)

pub fn set_active_config_dir_profile(id: &str) -> Result<(), AppError> {
    let settings = get_settings();           // read lock released here
    if !settings.config_dir_profiles.iter().any(|p| p.id == id) {
        return Err(...);
    }
    mutate_settings(|settings| { ... })     // write lock acquired here
}

Between the read and the write, a concurrent call could delete the profile. On a single-user desktop app this is unlikely, but the check could be moved inside mutate_settings for correctness:

pub fn set_active_config_dir_profile(id: &str) -> Result<(), AppError> {
    let id = id.to_string();
    mutate_settings(|settings| {
        if settings.config_dir_profiles.iter().any(|p| p.id == id) {
            settings.active_config_dir_profile_id = Some(id.clone());
        }
    })
}

🟡 UX / Design

4. prompt() used for profile creation (DirectorySettings.tsx:128)

const name = prompt("Profile 名称:");
if (name) onCreateProfile?.(name);

Using the browser's native prompt() is inconsistent with the rest of the UI, not i18n-compatible (hardcoded Chinese), and blocks the thread. This should use a proper inline dialog or popover, similar to how other naming operations work in the app.

Fix this →


5. onUpdateProfile prop is declared but never destructured or used (DirectorySettings.tsx:29, 54)

interface DirectorySettingsProps {
  ...
  onUpdateProfile?: (profile: ConfigDirProfile) => Promise<void>; // declared ✓
}

export function DirectorySettings({
  ...,
  onDeleteProfile,   // destructured
  onSwitchProfile,   // destructured
  // onUpdateProfile is NOT destructured → silently dropped
}: DirectorySettingsProps) {

This means there is no way for the user to rename an existing profile from the settings UI.


6. Missing delete confirmation

The delete button in DirectorySettings.tsx:136-147 directly invokes onDeleteProfile without any confirmation dialog. Deleting a profile with configured paths could be destructive if accidental.


7. Hardcoded Chinese in migration default profile name (settings.rs:494)

name: "默认".to_string(),

This profile name is baked into settings.json at migration time and will appear in the UI for all users regardless of their language preference. Consider using an ASCII/English fallback like "Default" or deriving it from an i18n key at display time rather than at storage time.


8. switchProfile's type-unsafe cast (useDirectorySettings.ts:489-492)

const dirPath = (newActive as unknown as Record<string, string | undefined>)[meta.key];

ConfigDirProfile has named optional string fields. This cast is fragile — a rename of any field would silently return undefined at runtime. Use direct property access:

const key = meta.key as keyof ConfigDirProfile;
const dirPath = newActive[key] as string | undefined;

🟢 Minor / Polish

9. select onChange doesn't await in DirectorySettings.tsx:116

onChange={(e) => onSwitchProfile?.(e.target.value)}

The promise is not awaited and errors are silently swallowed. Consider adding error handling or disabling the select during the async operation.

10. Double onUpdateSettings call in deleteProfile (useDirectorySettings.ts:455-465)

Two separate onUpdateSettings calls are made sequentially (one for configDirProfiles, one for activeConfigDirProfileId). They could be merged into one call to avoid a potential extra re-render:

onUpdateSettings({
  configDirProfiles: newProfiles,
  ...(activeProfileId === id ? { activeConfigDirProfileId: undefined } : {}),
});

11. Missing i18n key for "环境配置集" (DirectorySettings.tsx:114)

The section header "环境配置集" is hardcoded Chinese and has no i18n entry. Add a key to en.json/zh.json.

12. Unused manage key in en.json:102

"profileSwitcher": {
  "title": "Environment Profile",
  "manage": "Manage Profiles",   // ← no component uses this key
  ...
}

What's Well Done

  • Migration logic in migrate_legacy_fields_to_profile is clean and safe — only runs when no profiles exist and at least one legacy field is populated, with comprehensive unit tests.
  • Priority fallback chain (active profile → top-level field) in get_*_override_dir and get_current_provider / set_current_provider is consistent throughout.
  • ProfileSwitcher component is well-structured with good accessibility patterns and correctly short-circuits when no profiles exist.
  • Command registration in lib.rs and the API layer in settings.ts are complete and consistent.
  • Tests in settings.rs cover migration scenarios thoroughly.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c254ddda56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/useDirectorySettings.ts
Comment thread src/App.tsx
@farion1231
Copy link
Copy Markdown
Owner

@claude review

(Re-running with the updated review prompt for comparison.)

@claude
Copy link
Copy Markdown

claude Bot commented May 2, 2026

Claude finished @farion1231's task in 2m 25s —— View job


Code Review (re-run with updated prompt)

Three Important findings, plus a couple of nits. No verdict — leaving the call to you.


Important

1. Directory inputs in Settings show top-level values, not the active profile's values  confidence ~90

SettingsPage.tsx:321-326 passes claudeDir={settings.claudeConfigDir} (and the other five) to DirectorySettings. But once a profile is active, updateDirectoryState (useDirectorySettings.ts:273-299) writes only into configDirProfiles[active].claude and leaves the top-level claudeConfigDir untouched. After the migration step copies the legacy top-level fields into the default profile (settings.rs:492-506), the two values are identical — but as soon as the user edits any directory while a profile is active, they diverge, and the input keeps showing the stale top-level path instead of what the profile actually resolves to. This also makes profile switching invisible in the input boxes (the displayed value never changes when activeProfileId changes).

Fix: derive the displayed values from the active profile when one exists, falling back to top-level fields, e.g. compute claudeDir = activeProfile?.claude ?? settings.claudeConfigDir in useSettings/SettingsPage before passing to DirectorySettings.

2. deleteProfile clobbers the backend's active-profile reassignment  confidence ~85

delete_config_dir_profile in settings.rs:1015-1025 intentionally reassigns active_config_dir_profile_id to the first remaining profile when the deleted one was active. But the frontend (useDirectorySettings.ts:462-467) unconditionally writes activeConfigDirProfileId: undefined into form state:

if (activeProfileId === id) {
  setActiveProfileId(undefined);
  onUpdateSettings({ activeConfigDirProfileId: undefined });
}

onUpdateSettings is just local form state (useSettingsForm.setSettingsState). The next time settings get saved, that undefined is persisted and overwrites the reassignment the backend made — directory/provider resolution then silently falls back to the legacy top-level fields instead of the next remaining profile. (This is the same issue codex flagged at line 466, which I'm seconding.)

3. App-level useSettings() instance has stale profiles  confidence ~85

App.tsx:178-182 calls useSettings() to feed ProfileSwitcher in the header, while SettingsPage calls it independently. In useDirectorySettings.ts:248-257, profiles is loaded once into local React state via getConfigDirProfiles() and is not subscribed to any shared cache. So when the user creates or deletes a profile inside Settings, only that hook instance's local state updates — the App-level switcher continues to show the pre-mutation list (deleted profiles still listed, newly created ones missing) until a full reload. (Codex's P2 finding — agreed.)

Cheapest fix: hoist profile state into a react-query key (e.g. ["configDirProfiles"]) so all useDirectorySettings consumers share a single source of truth and invalidate on mutation.


Nits

  • DirectorySettings.tsx:128 uses native prompt() for naming a profile — hardcoded Chinese, not i18n'd, and Tauri's webviews aren't a guaranteed-consistent host for window.prompt. Worth swapping for an inline dialog.
  • onUpdateProfile is declared on DirectorySettingsProps (DirectorySettings.tsx:29) but never destructured (:54) and never wired to a UI affordance — either implement rename or delete the prop. Per CLAUDE.md "if you are certain that something is unused, you can delete it completely."
    · feature/support_multi_env

fuzi1996 and others added 4 commits May 2, 2026 11:19
…tion sync

Add a new Tauri command to retrieve the currently active config dir profile.
Fix the profile deletion logic to fetch the actual active profile from backend
instead of setting it to undefined, ensuring proper sync when a profile is deleted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Fix directory inputs showing top-level values instead of active profile's values
  - Add getActiveProfileDirectoryValues getter to derive display values from active profile
- Replace native prompt() with Dialog component for profile creation
  - Add i18n support for profile dialogs (zh/en/ja)
- Implement onUpdateProfile usage for rename functionality
  - Add rename button and dialog for profile renaming

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ture

- Fix stale closure bug in updateDirectoryState and createProfile
- Fix TOCTOU race condition in set_active_config_dir_profile
- Replace unsafe type cast with proper keyof access in switchProfile
- Add delete confirmation dialog for profile deletion
- Change hardcoded Chinese "默认" to "Default" in migration
- Properly handle async in profile select onChange

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add icons (Pencil, Plus, Trash2, AlertTriangle) to profile action buttons
- Improve button styling with inline-flex layout and proper spacing
- Style delete button with destructive colors for better UX
- Enhance delete confirmation dialog with warning styling
- Add i18n translations for delete dialog title and confirmation message

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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