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.

Cherry-picked from main (ddbff07) with conflict resolution.
This commit is contained in:
Jason
2025-12-23 22:39:57 +08:00
parent 9ea7952246
commit 21ceb819b9
16 changed files with 234 additions and 1 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);
});
+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 () => {
+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");