diff --git a/apps/array/src/renderer/features/message-editor/components/EditorToolbar.tsx b/apps/array/src/renderer/features/message-editor/components/EditorToolbar.tsx index 72a2e35a..02dc8bf5 100644 --- a/apps/array/src/renderer/features/message-editor/components/EditorToolbar.tsx +++ b/apps/array/src/renderer/features/message-editor/components/EditorToolbar.tsx @@ -1,6 +1,10 @@ import { ModelSelector } from "@features/sessions/components/ModelSelector"; -import { Paperclip } from "@phosphor-icons/react"; +import { useSessionForTask } from "@features/sessions/stores/sessionStore"; +import { useThinkingStore } from "@features/sessions/stores/thinkingStore"; +import { useSettingsStore } from "@features/settings/stores/settingsStore"; +import { Brain, Paperclip } from "@phosphor-icons/react"; import { Flex, IconButton, Tooltip } from "@radix-ui/themes"; +import { AVAILABLE_MODELS } from "@shared/types/models"; import { useRef } from "react"; import type { MentionChip } from "../utils/content"; @@ -22,6 +26,20 @@ export function EditorToolbar({ iconSize = 14, }: EditorToolbarProps) { const fileInputRef = useRef(null); + const session = useSessionForTask(taskId); + const defaultModel = useSettingsStore((state) => state.defaultModel); + const activeModel = session?.model ?? defaultModel; + + // Check if current model is Anthropic + const isAnthropicModel = AVAILABLE_MODELS.some( + (m) => m.id === activeModel && m.provider === "anthropic", + ); + + // Thinking state for this task + const thinkingEnabled = useThinkingStore((state) => + taskId ? state.getThinking(taskId) : false, + ); + const toggleThinking = useThinkingStore((state) => state.toggleThinking); const handleFileSelect = (e: React.ChangeEvent) => { const files = e.target.files; @@ -66,6 +84,32 @@ export function EditorToolbar({ + {isAnthropicModel && taskId && ( + + { + e.stopPropagation(); + toggleThinking(taskId); + }} + disabled={disabled} + style={{ marginLeft: "8px" }} + > + + + + )} ); } diff --git a/apps/array/src/renderer/features/sessions/stores/thinkingStore.ts b/apps/array/src/renderer/features/sessions/stores/thinkingStore.ts new file mode 100644 index 00000000..5501da14 --- /dev/null +++ b/apps/array/src/renderer/features/sessions/stores/thinkingStore.ts @@ -0,0 +1,71 @@ +import { useSettingsStore } from "@features/settings/stores/settingsStore"; +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface ThinkingState { + // Tracks thinking enabled state per task + thinkingByTask: Record; +} + +interface ThinkingActions { + // Get thinking state for a task (defaults to settings default) + getThinking: (taskId: string) => boolean; + // Set thinking state for a task + setThinking: (taskId: string, enabled: boolean) => void; + // Toggle thinking state for a task + toggleThinking: (taskId: string) => void; + // Initialize thinking for a new task from settings default + initializeThinking: (taskId: string) => void; +} + +export const useThinkingStore = create()( + persist( + (set, get) => ({ + thinkingByTask: {}, + + getThinking: (taskId: string) => { + const state = get().thinkingByTask[taskId]; + if (state === undefined) { + // Default to settings value + return useSettingsStore.getState().defaultThinkingEnabled; + } + return state; + }, + + setThinking: (taskId: string, enabled: boolean) => { + set((state) => ({ + thinkingByTask: { ...state.thinkingByTask, [taskId]: enabled }, + })); + }, + + toggleThinking: (taskId: string) => { + const current = get().getThinking(taskId); + get().setThinking(taskId, !current); + }, + + initializeThinking: (taskId: string) => { + // Only initialize if not already set + if (get().thinkingByTask[taskId] === undefined) { + const defaultEnabled = + useSettingsStore.getState().defaultThinkingEnabled; + set((state) => ({ + thinkingByTask: { + ...state.thinkingByTask, + [taskId]: defaultEnabled, + }, + })); + } + }, + }), + { + name: "thinking-storage", + }, + ), +); + +// Hook to get thinking state for a specific task +export function useThinkingForTask(taskId: string | undefined) { + return useThinkingStore((state) => + taskId ? state.getThinking(taskId) : false, + ); +} diff --git a/apps/array/src/renderer/features/settings/components/SettingsView.tsx b/apps/array/src/renderer/features/settings/components/SettingsView.tsx index ccec761a..95c1d7c4 100644 --- a/apps/array/src/renderer/features/settings/components/SettingsView.tsx +++ b/apps/array/src/renderer/features/settings/components/SettingsView.tsx @@ -21,6 +21,7 @@ import { formatHotkey } from "@renderer/constants/keyboard-shortcuts"; import { track } from "@renderer/lib/analytics"; import { clearApplicationStorage } from "@renderer/lib/clearStorage"; import { logger } from "@renderer/lib/logger"; +import { AVAILABLE_MODELS } from "@shared/types/models"; import type { CloudRegion } from "@shared/types/oauth"; import { useSettingsStore as useTerminalLayoutStore } from "@stores/settingsStore"; import { useShortcutsSheetStore } from "@stores/shortcutsSheetStore"; @@ -55,10 +56,13 @@ export function SettingsView() { autoRunTasks, createPR, cursorGlow, + defaultModel, + defaultThinkingEnabled, desktopNotifications, setAutoRunTasks, setCreatePR, setCursorGlow, + setDefaultThinkingEnabled, setDesktopNotifications, } = useSettingsStore(); const terminalLayoutMode = useTerminalLayoutStore( @@ -160,6 +164,18 @@ export function SettingsView() { [terminalLayoutMode, setTerminalLayout], ); + const handleThinkingEnabledChange = useCallback( + (checked: boolean) => { + track(ANALYTICS_EVENTS.SETTING_CHANGED, { + setting_name: "default_thinking_enabled", + new_value: checked, + old_value: defaultThinkingEnabled, + }); + setDefaultThinkingEnabled(checked); + }, + [defaultThinkingEnabled, setDefaultThinkingEnabled], + ); + const handleWorktreeLocationChange = async (newLocation: string) => { setLocalWorktreeLocation(newLocation); try { @@ -341,20 +357,44 @@ export function SettingsView() { Chat - - - - Desktop notifications - - - Show notifications when the agent finishes working on a task - + + {/* Thinking toggle - only for Anthropic models */} + {AVAILABLE_MODELS.find( + (m) => m.id === defaultModel && m.provider === "anthropic", + ) && ( + + + + Extended thinking + + + Enable extended thinking for all chats. + + + + + )} + + + + + Desktop notifications + + + Show notifications when the agent finishes working on a + task + + + - diff --git a/apps/array/src/renderer/features/settings/stores/settingsStore.ts b/apps/array/src/renderer/features/settings/stores/settingsStore.ts index 7051ae7b..dbe49067 100644 --- a/apps/array/src/renderer/features/settings/stores/settingsStore.ts +++ b/apps/array/src/renderer/features/settings/stores/settingsStore.ts @@ -14,6 +14,7 @@ interface SettingsStore { lastUsedWorkspaceMode: WorkspaceMode; createPR: boolean; defaultModel: string; + defaultThinkingEnabled: boolean; desktopNotifications: boolean; cursorGlow: boolean; @@ -24,6 +25,7 @@ interface SettingsStore { setLastUsedWorkspaceMode: (mode: WorkspaceMode) => void; setCreatePR: (createPR: boolean) => void; setDefaultModel: (model: string) => void; + setDefaultThinkingEnabled: (enabled: boolean) => void; setDesktopNotifications: (enabled: boolean) => void; setCursorGlow: (enabled: boolean) => void; } @@ -38,6 +40,7 @@ export const useSettingsStore = create()( lastUsedWorkspaceMode: "worktree", createPR: true, defaultModel: DEFAULT_MODEL, + defaultThinkingEnabled: false, desktopNotifications: true, cursorGlow: false, @@ -49,6 +52,8 @@ export const useSettingsStore = create()( setLastUsedWorkspaceMode: (mode) => set({ lastUsedWorkspaceMode: mode }), setCreatePR: (createPR) => set({ createPR }), setDefaultModel: (model) => set({ defaultModel: model }), + setDefaultThinkingEnabled: (enabled) => + set({ defaultThinkingEnabled: enabled }), setDesktopNotifications: (enabled) => set({ desktopNotifications: enabled }), setCursorGlow: (enabled) => set({ cursorGlow: enabled }), diff --git a/apps/array/src/renderer/features/task-detail/components/TaskLogsPanel.tsx b/apps/array/src/renderer/features/task-detail/components/TaskLogsPanel.tsx index 19644580..4f84bf9b 100644 --- a/apps/array/src/renderer/features/task-detail/components/TaskLogsPanel.tsx +++ b/apps/array/src/renderer/features/task-detail/components/TaskLogsPanel.tsx @@ -5,6 +5,7 @@ import { useSessionActions, useSessionForTask, } from "@features/sessions/stores/sessionStore"; +import { useThinkingStore } from "@features/sessions/stores/thinkingStore"; import { useSettingsStore } from "@features/settings/stores/settingsStore"; import { useTaskViewedStore } from "@features/sidebar/stores/taskViewedStore"; import { useTaskData } from "@features/task-detail/hooks/useTaskData"; @@ -87,11 +88,21 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) { sessionStatus: session?.status ?? "none", }); + // Initialize thinking state for this task from settings default + useThinkingStore.getState().initializeThinking(task.id); + + // Prepend "ultrathink" to initial prompt if extended thinking is enabled + const thinkingEnabled = useThinkingStore.getState().getThinking(task.id); + const initialPromptText = + hasInitialPrompt && thinkingEnabled + ? `ultrathink ${task.description}` + : task.description; + connectToTask({ task, repoPath, initialPrompt: hasInitialPrompt - ? [{ type: "text", text: task.description }] + ? [{ type: "text", text: initialPromptText }] : undefined, }).finally(() => { isConnecting.current = false; @@ -103,7 +114,14 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) { try { markAsViewed(taskId); - const result = await sendPrompt(taskId, text); + // Prepend "ultrathink" if thinking is enabled and not already present + const thinkingEnabled = useThinkingStore.getState().getThinking(taskId); + const promptText = + thinkingEnabled && !text.trim().toLowerCase().startsWith("ultrathink") + ? `ultrathink ${text}` + : text; + + const result = await sendPrompt(taskId, promptText); log.info("Prompt completed", { stopReason: result.stopReason }); markActivity(taskId);