feat(settings): add option to skip Claude Code first-run confirmation

Add a new setting to automatically skip Claude Code's onboarding screen
by writing hasCompletedOnboarding=true to ~/.claude.json. The setting
defaults to enabled for better user experience.

- Add set/clear_has_completed_onboarding functions in claude_mcp.rs
- Add Tauri commands and frontend API integration
- Add toggle in WindowSettings with i18n support (en/zh/ja)
- Fix hardcoded Chinese text in tests to use i18n keys
This commit is contained in:
Jason
2025-12-20 23:55:10 +08:00
parent ca7cb398c2
commit ddbff070d5
18 changed files with 243 additions and 9 deletions
@@ -63,7 +63,7 @@ describe("ImportExportSection Component", () => {
fireEvent.click(importButton);
expect(baseProps.onImport).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole("button", { name: "Clear selection" }));
fireEvent.click(screen.getByRole("button", { name: "common.clear" }));
expect(baseProps.onClear).toHaveBeenCalledTimes(1);
});
+3 -3
View File
@@ -305,7 +305,7 @@ describe("SettingsPage Component", () => {
});
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("数据管理"));
fireEvent.click(screen.getByText("settings.advanced.data.title"));
// 有文件时,点击导入按钮执行 importConfig
fireEvent.click(
@@ -319,7 +319,7 @@ describe("SettingsPage Component", () => {
expect(importExportMock.exportConfig).toHaveBeenCalled();
// 清除选择按钮
fireEvent.click(screen.getByRole("button", { name: "Clear selection" }));
fireEvent.click(screen.getByRole("button", { name: "common.clear" }));
expect(importExportMock.clearSelection).toHaveBeenCalled();
});
@@ -412,7 +412,7 @@ describe("SettingsPage Component", () => {
render(<SettingsPage open={true} onOpenChange={vi.fn()} />);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("配置文件目录"));
fireEvent.click(screen.getByText("settings.advanced.configDir.title"));
fireEvent.click(screen.getByText("browse-directory"));
expect(settingsMock.browseDirectory).toHaveBeenCalledWith("claude");
+68
View File
@@ -7,6 +7,8 @@ const mutateAsyncMock = vi.fn();
const useSettingsQueryMock = vi.fn();
const setAppConfigDirOverrideMock = vi.fn();
const applyClaudePluginConfigMock = vi.fn();
const applyClaudeOnboardingSkipMock = vi.fn();
const clearClaudeOnboardingSkipMock = vi.fn();
const syncCurrentProvidersLiveMock = vi.fn();
const updateTrayMenuMock = vi.fn();
const toastErrorMock = vi.fn();
@@ -50,6 +52,10 @@ vi.mock("@/lib/api", () => ({
setAppConfigDirOverrideMock(...args),
applyClaudePluginConfig: (...args: unknown[]) =>
applyClaudePluginConfigMock(...args),
applyClaudeOnboardingSkip: (...args: unknown[]) =>
applyClaudeOnboardingSkipMock(...args),
clearClaudeOnboardingSkip: (...args: unknown[]) =>
clearClaudeOnboardingSkipMock(...args),
syncCurrentProvidersLive: (...args: unknown[]) =>
syncCurrentProvidersLiveMock(...args),
},
@@ -63,6 +69,7 @@ const createSettingsFormMock = (overrides: Record<string, unknown> = {}) => ({
showInTray: true,
minimizeToTrayOnClose: true,
enableClaudePluginIntegration: false,
skipClaudeOnboarding: true,
claudeConfigDir: "/claude",
codexConfigDir: "/codex",
language: "zh",
@@ -111,6 +118,8 @@ describe("useSettings hook", () => {
useSettingsQueryMock.mockReset();
setAppConfigDirOverrideMock.mockReset();
applyClaudePluginConfigMock.mockReset();
applyClaudeOnboardingSkipMock.mockReset();
clearClaudeOnboardingSkipMock.mockReset();
syncCurrentProvidersLiveMock.mockReset();
toastErrorMock.mockReset();
toastSuccessMock.mockReset();
@@ -120,6 +129,7 @@ describe("useSettings hook", () => {
showInTray: true,
minimizeToTrayOnClose: true,
enableClaudePluginIntegration: false,
skipClaudeOnboarding: true,
claudeConfigDir: "/server/claude",
codexConfigDir: "/server/codex",
language: "zh",
@@ -142,6 +152,64 @@ describe("useSettings hook", () => {
mutateAsyncMock.mockResolvedValue(true);
setAppConfigDirOverrideMock.mockResolvedValue(true);
applyClaudePluginConfigMock.mockResolvedValue(true);
applyClaudeOnboardingSkipMock.mockResolvedValue(true);
clearClaudeOnboardingSkipMock.mockResolvedValue(true);
});
it("auto-saves and applies Claude onboarding skip when toggled on", async () => {
serverSettings = {
...serverSettings,
skipClaudeOnboarding: false,
};
useSettingsQueryMock.mockReturnValue({
data: serverSettings,
isLoading: false,
});
settingsFormMock = createSettingsFormMock({
settings: {
...serverSettings,
language: "zh",
skipClaudeOnboarding: false,
},
});
const { result } = renderHook(() => useSettings());
await act(async () => {
await result.current.autoSaveSettings({ skipClaudeOnboarding: true });
});
expect(applyClaudeOnboardingSkipMock).toHaveBeenCalledTimes(1);
expect(toastErrorMock).not.toHaveBeenCalled();
});
it("auto-saves and clears Claude onboarding skip when toggled off", async () => {
serverSettings = {
...serverSettings,
skipClaudeOnboarding: true,
};
useSettingsQueryMock.mockReturnValue({
data: serverSettings,
isLoading: false,
});
settingsFormMock = createSettingsFormMock({
settings: {
...serverSettings,
language: "zh",
skipClaudeOnboarding: true,
},
});
const { result } = renderHook(() => useSettings());
await act(async () => {
await result.current.autoSaveSettings({ skipClaudeOnboarding: false });
});
expect(clearClaudeOnboardingSkipMock).toHaveBeenCalledTimes(1);
expect(toastErrorMock).not.toHaveBeenCalled();
});
it("saves settings and flags restart when app config directory changes", async () => {
+5 -5
View File
@@ -150,7 +150,7 @@ describe("SettingsPage integration", () => {
expect(screen.getByText("language:zh")).toBeInTheDocument(),
);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("配置文件目录"));
fireEvent.click(screen.getByText("settings.advanced.configDir.title"));
const appInput = await screen.findByPlaceholderText(
"settings.browsePlaceholderApp",
);
@@ -166,7 +166,7 @@ describe("SettingsPage integration", () => {
);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("数据管理"));
fireEvent.click(screen.getByText("settings.advanced.data.title"));
fireEvent.click(screen.getByText("settings.selectConfigFile"));
await waitFor(() =>
expect(screen.getByTestId("selected-file").textContent).toContain(
@@ -190,7 +190,7 @@ describe("SettingsPage integration", () => {
);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("配置文件目录"));
fireEvent.click(screen.getByText("settings.advanced.configDir.title"));
const appInput = await screen.findByPlaceholderText(
"settings.browsePlaceholderApp",
);
@@ -217,7 +217,7 @@ describe("SettingsPage integration", () => {
);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("配置文件目录"));
fireEvent.click(screen.getByText("settings.advanced.configDir.title"));
const browseButtons = screen.getAllByTitle("settings.browseDirectory");
const resetButtons = screen.getAllByTitle("settings.resetDefault");
@@ -257,7 +257,7 @@ describe("SettingsPage integration", () => {
expect(screen.getByText("language:zh")).toBeInTheDocument(),
);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("数据管理"));
fireEvent.click(screen.getByText("settings.advanced.data.title"));
server.use(
http.post("http://tauri.local/save_file_dialog", () =>
+4
View File
@@ -176,6 +176,10 @@ export const handlers = [
},
),
http.post(`${TAURI_ENDPOINT}/apply_claude_onboarding_skip`, () => success(true)),
http.post(`${TAURI_ENDPOINT}/clear_claude_onboarding_skip`, () => success(true)),
http.post(`${TAURI_ENDPOINT}/get_config_dir`, async ({ request }) => {
const { app } = await withJson<{ app: AppId }>(request);
return success(app === "claude" ? "/default/claude" : "/default/codex");