diff --git a/packages/desktop/src-tauri/src/commands/settings.rs b/packages/desktop/src-tauri/src/commands/settings.rs index 8bf58b38..35d2d633 100644 --- a/packages/desktop/src-tauri/src/commands/settings.rs +++ b/packages/desktop/src-tauri/src/commands/settings.rs @@ -257,6 +257,9 @@ fn sanitize_settings_update(payload: &Value) -> Value { if let Some(Value::Bool(b)) = obj.get("queueModeEnabled") { result_obj.insert("queueModeEnabled".to_string(), json!(b)); } + if let Some(Value::Bool(b)) = obj.get("autoCreateWorktree") { + result_obj.insert("autoCreateWorktree".to_string(), json!(b)); + } // Number fields if let Some(Value::Number(n)) = obj.get("autoDeleteAfterDays") { diff --git a/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx b/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx index 70412574..c55e5e91 100644 --- a/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx @@ -7,6 +7,7 @@ import { updateDesktopSettings } from '@/lib/persistence'; import { isDesktopRuntime, getDesktopSettings } from '@/lib/desktop'; import { useConfigStore } from '@/stores/useConfigStore'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; +import { getModifierLabel } from '@/lib/utils'; export const DefaultsSettings: React.FC = () => { const setProvider = useConfigStore((state) => state.setProvider); @@ -14,6 +15,8 @@ export const DefaultsSettings: React.FC = () => { const setAgent = useConfigStore((state) => state.setAgent); const setSettingsDefaultModel = useConfigStore((state) => state.setSettingsDefaultModel); const setSettingsDefaultAgent = useConfigStore((state) => state.setSettingsDefaultAgent); + const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree); + const setSettingsAutoCreateWorktree = useConfigStore((state) => state.setSettingsAutoCreateWorktree); const providers = useConfigStore((state) => state.providers); const [defaultModel, setDefaultModel] = React.useState(); @@ -126,6 +129,18 @@ export const DefaultsSettings: React.FC = () => { } }, [setAgent, setSettingsDefaultAgent]); + const handleAutoWorktreeChange = React.useCallback(async (e: React.ChangeEvent) => { + const enabled = e.target.checked; + setSettingsAutoCreateWorktree(enabled); + try { + await updateDesktopSettings({ + autoCreateWorktree: enabled, + }); + } catch (error) { + console.warn('Failed to save auto create worktree setting:', error); + } + }, [setSettingsAutoCreateWorktree]); + if (isLoading) { return null; } @@ -134,14 +149,13 @@ export const DefaultsSettings: React.FC = () => {
-

Default model & agent

+

Session Defaults

- Set the default model and agent for new sessions.
- When not set, uses agent's preferred model or opencode/big-pickle as fallback. + Configure default behaviors for new sessions.
@@ -174,6 +188,25 @@ export const DefaultsSettings: React.FC = () => { {defaultAgent && {defaultAgent}}
)} + +
+ +

+ {settingsAutoCreateWorktree + ? `New session (Worktree): ${getModifierLabel()} + N • New session (Standard): Shift + ${getModifierLabel()} + N` + : `New session (Standard): ${getModifierLabel()} + N • New session (Worktree): Shift + ${getModifierLabel()} + N`} +

+
); }; diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index 7a115a9e..3eb25a31 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -698,6 +698,18 @@ export const SessionSidebar: React.FC = ({ const handleDeleteSession = React.useCallback( async (session: Session) => { const descendants = collectDescendants(session.id); + + // Check if this is a worktree session - if so, show confirmation dialog + const worktree = worktreeMetadata.get(session.id); + if (worktree) { + sessionEvents.requestDelete({ + sessions: [session, ...descendants], + mode: 'worktree', + worktree, + }); + return; + } + if (descendants.length === 0) { const success = await deleteSession(session.id); @@ -718,7 +730,7 @@ export const SessionSidebar: React.FC = ({ } } }, - [collectDescendants, deleteSession, deleteSessions], + [collectDescendants, deleteSession, deleteSessions, worktreeMetadata], ); const handleCreateSessionInGroup = React.useCallback( diff --git a/packages/ui/src/components/ui/CommandPalette.tsx b/packages/ui/src/components/ui/CommandPalette.tsx index 208119e9..448df09a 100644 --- a/packages/ui/src/components/ui/CommandPalette.tsx +++ b/packages/ui/src/components/ui/CommandPalette.tsx @@ -12,6 +12,7 @@ import { import { useUIStore } from '@/stores/useUIStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; +import { useConfigStore } from '@/stores/useConfigStore'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useDeviceInfo } from '@/lib/device'; import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine, RiTimeLine } from '@remixicon/react'; @@ -37,6 +38,8 @@ export const CommandPalette: React.FC = () => { getSessionsByDirectory, } = useSessionStore(); + const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree); + const { currentDirectory } = useDirectoryStore(); const { themeMode, setThemeMode } = useThemeSystem(); @@ -133,12 +136,16 @@ export const CommandPalette: React.FC = () => { New Session - {getModifierLabel()} + N + + {settingsAutoCreateWorktree ? `Shift + ${getModifierLabel()} + N` : `${getModifierLabel()} + N`} + New Session with Worktree - Shift + {getModifierLabel()} + N + + {settingsAutoCreateWorktree ? `${getModifierLabel()} + N` : `Shift + ${getModifierLabel()} + N`} + diff --git a/packages/ui/src/components/ui/HelpDialog.tsx b/packages/ui/src/components/ui/HelpDialog.tsx index f05c821b..89c3ece2 100644 --- a/packages/ui/src/components/ui/HelpDialog.tsx +++ b/packages/ui/src/components/ui/HelpDialog.tsx @@ -7,6 +7,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { useUIStore } from "@/stores/useUIStore"; +import { useConfigStore } from "@/stores/useConfigStore"; import { RiAddLine, RiArrowUpSLine, @@ -82,6 +83,7 @@ type ShortcutSection = { export const HelpDialog: React.FC = () => { const { isHelpDialogOpen, setHelpDialogOpen } = useUIStore(); + const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree); const mod = getModifierLabel(); @@ -116,13 +118,13 @@ export const HelpDialog: React.FC = () => { items: [ { keys: [`${mod} + N`], - description: "Create New Session", - icon: RiAddLine, + description: settingsAutoCreateWorktree ? "Create new session in worktree" : "Create New Session", + icon: settingsAutoCreateWorktree ? RiGitBranchLine : RiAddLine, }, { keys: [`Shift + ${mod} + N`], - description: "Open Worktree Creator", - icon: RiGitBranchLine, + description: settingsAutoCreateWorktree ? "Create New Session" : "Create new session in worktree", + icon: settingsAutoCreateWorktree ? RiAddLine : RiGitBranchLine, }, { keys: [`${mod} + I`], description: "Focus Chat Input", icon: RiText }, { diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts index c2a7b486..8d9cc5be 100644 --- a/packages/ui/src/hooks/useKeyboardShortcuts.ts +++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts @@ -6,6 +6,7 @@ import { useAssistantStatus } from '@/hooks/useAssistantStatus'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { hasModifier } from '@/lib/utils'; import { createWorktreeSession } from '@/lib/worktreeSessionCreator'; +import { useConfigStore } from '@/stores/useConfigStore'; export const useKeyboardShortcuts = () => { const { openNewSessionDraft, abortCurrentOperation, armAbortPrompt, clearAbortPrompt, currentSessionId } = useSessionStore(); @@ -81,14 +82,20 @@ export const useKeyboardShortcuts = () => { if (hasModifier(e) && e.key.toLowerCase() === 'n') { e.preventDefault(); - if (e.shiftKey) { - // Shift+Cmd/Ctrl+N creates a new session with auto-generated worktree + + const autoWorktree = useConfigStore.getState().settingsAutoCreateWorktree; + // If autoWorktree is true: Cmd+N -> Worktree, Cmd+Shift+N -> Standard + // If autoWorktree is false: Cmd+N -> Standard, Cmd+Shift+N -> Worktree + const shouldCreateWorktree = autoWorktree ? !e.shiftKey : e.shiftKey; + + if (shouldCreateWorktree) { + // Create new session with auto-generated worktree setActiveMainTab('chat'); setSessionSwitcherOpen(false); createWorktreeSession(); return; } - // Cmd/Ctrl+N opens a new session without worktree + // Open a new session without worktree setActiveMainTab('chat'); setSessionSwitcherOpen(false); openNewSessionDraft(); diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index 75c33ac8..3455ca5f 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -55,6 +55,7 @@ export type DesktopSettings = { autoDeleteAfterDays?: number; defaultModel?: string; // format: "provider/model" defaultAgent?: string; + autoCreateWorktree?: boolean; queueModeEnabled?: boolean; // User-added skills catalogs (persisted to ~/.config/openchamber/settings.json) diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index 8686a2d3..ff05eb3d 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -255,6 +255,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { if (typeof candidate.defaultAgent === 'string' && candidate.defaultAgent.length > 0) { result.defaultAgent = candidate.defaultAgent; } + if (typeof candidate.autoCreateWorktree === 'boolean') { + result.autoCreateWorktree = candidate.autoCreateWorktree; + } if (typeof candidate.queueModeEnabled === 'boolean') { result.queueModeEnabled = candidate.queueModeEnabled; } diff --git a/packages/ui/src/lib/worktreeSessionCreator.ts b/packages/ui/src/lib/worktreeSessionCreator.ts index 695fede6..74ce4e63 100644 --- a/packages/ui/src/lib/worktreeSessionCreator.ts +++ b/packages/ui/src/lib/worktreeSessionCreator.ts @@ -8,6 +8,7 @@ import { toast } from 'sonner'; import { useSessionStore } from '@/stores/useSessionStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useConfigStore } from '@/stores/useConfigStore'; +import { useContextStore } from '@/stores/contextStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { checkIsGitRepository } from '@/lib/gitApi'; import { generateUniqueBranchName } from '@/lib/git/branchNameGenerator'; @@ -116,11 +117,57 @@ export async function createWorktreeSession(): Promise<{ id: string } | null> { } // Initialize the session - const agents = useConfigStore.getState().agents; + const configState = useConfigStore.getState(); + const agents = configState.agents; sessionStore.initializeNewOpenChamberSession(session.id, agents); sessionStore.setSessionDirectory(session.id, metadata.path); sessionStore.setWorktreeMetadata(session.id, createdMetadata); + // Apply default agent and model settings + try { + const visibleAgents = configState.getVisibleAgents(); + let agentName: string | undefined; + + // Priority: settingsDefaultAgent → build → first visible + if (configState.settingsDefaultAgent) { + const settingsAgent = visibleAgents.find((a) => a.name === configState.settingsDefaultAgent); + if (settingsAgent) { + agentName = settingsAgent.name; + } + } + if (!agentName) { + agentName = + visibleAgents.find((agent) => agent.name === 'build')?.name || + visibleAgents[0]?.name; + } + + if (agentName) { + // 1. Update global UI state + configState.setAgent(agentName); + + // 2. Persist to session context so it sticks after reload/switch + useContextStore.getState().saveSessionAgentSelection(session.id, agentName); + + // 3. Handle default model for the agent if set in global settings + const settingsDefaultModel = configState.settingsDefaultModel; + if (settingsDefaultModel) { + const parts = settingsDefaultModel.split('/'); + if (parts.length === 2) { + const [providerId, modelId] = parts; + // Validate model exists (optional, but good practice) + const modelMetadata = configState.getModelMetadata(providerId, modelId); + if (modelMetadata) { + useContextStore.getState().saveSessionModelSelection(session.id, providerId, modelId); + // Also save the specific agent's model preference for this session + useContextStore.getState().saveAgentModelForSession(session.id, agentName, providerId, modelId); + } + } + } + } + } catch { + // Ignore errors setting default agent + } + // Update directory useDirectoryStore.getState().setDirectory(metadata.path, { showOverlay: false }); diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index e648df99..fac0160c 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -23,6 +23,7 @@ const FALLBACK_MODEL_ID = "big-pickle"; interface OpenChamberDefaults { defaultModel?: string; defaultAgent?: string; + autoCreateWorktree?: boolean; } const fetchOpenChamberDefaults = async (): Promise => { @@ -33,6 +34,7 @@ const fetchOpenChamberDefaults = async (): Promise => { return { defaultModel: settings?.defaultModel, defaultAgent: settings?.defaultAgent, + autoCreateWorktree: settings?.autoCreateWorktree, }; } @@ -46,6 +48,7 @@ const fetchOpenChamberDefaults = async (): Promise => { return { defaultModel: typeof data?.defaultModel === 'string' ? data.defaultModel : undefined, defaultAgent: typeof data?.defaultAgent === 'string' ? data.defaultAgent : undefined, + autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined, }; } } catch { @@ -65,6 +68,7 @@ const fetchOpenChamberDefaults = async (): Promise => { return { defaultModel: typeof data?.defaultModel === 'string' ? data.defaultModel : undefined, defaultAgent: typeof data?.defaultAgent === 'string' ? data.defaultAgent : undefined, + autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined, }; } catch { return {}; @@ -351,6 +355,7 @@ interface ConfigStore { // OpenChamber settings-based defaults (take precedence over agent preferences) settingsDefaultModel: string | undefined; // format: "provider/model" settingsDefaultAgent: string | undefined; + settingsAutoCreateWorktree: boolean; activateDirectory: (directory: string | null | undefined) => Promise; @@ -362,6 +367,7 @@ interface ConfigStore { setSelectedProvider: (providerId: string) => void; setSettingsDefaultModel: (model: string | undefined) => void; setSettingsDefaultAgent: (agent: string | undefined) => void; + setSettingsAutoCreateWorktree: (enabled: boolean) => void; saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void; getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null; checkConnection: () => Promise; @@ -402,6 +408,7 @@ export const useConfigStore = create()( modelsMetadata: new Map(), settingsDefaultModel: undefined, settingsDefaultAgent: undefined, + settingsAutoCreateWorktree: false, activateDirectory: async (directory) => { const directoryKey = toDirectoryKey(directory); @@ -730,6 +737,7 @@ export const useConfigStore = create()( const nextState: Partial = { settingsDefaultModel: openChamberDefaults.defaultModel, settingsDefaultAgent: openChamberDefaults.defaultAgent, + settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, @@ -1111,6 +1119,10 @@ export const useConfigStore = create()( set({ settingsDefaultAgent: agent }); }, + setSettingsAutoCreateWorktree: (enabled: boolean) => { + set({ settingsAutoCreateWorktree: enabled }); + }, + checkConnection: async () => { const maxAttempts = 5; let attempt = 0; diff --git a/packages/web/server/index.js b/packages/web/server/index.js index e870aff0..fe22f131 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -599,6 +599,9 @@ const sanitizeSettingsUpdate = (payload) => { if (typeof candidate.queueModeEnabled === 'boolean') { result.queueModeEnabled = candidate.queueModeEnabled; } + if (typeof candidate.autoCreateWorktree === 'boolean') { + result.autoCreateWorktree = candidate.autoCreateWorktree; + } const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs); if (skillCatalogs) {