Skip to content

Commit 888beb4

Browse files
committed
Implement extended thinking chat toggle
1 parent 953eda8 commit 888beb4

File tree

5 files changed

+191
-16
lines changed

5 files changed

+191
-16
lines changed

apps/array/src/renderer/features/message-editor/components/EditorToolbar.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { ModelSelector } from "@features/sessions/components/ModelSelector";
2-
import { Paperclip } from "@phosphor-icons/react";
2+
import { useSessionForTask } from "@features/sessions/stores/sessionStore";
3+
import { useThinkingStore } from "@features/sessions/stores/thinkingStore";
4+
import { useSettingsStore } from "@features/settings/stores/settingsStore";
5+
import { Brain, Paperclip } from "@phosphor-icons/react";
36
import { Flex, IconButton, Tooltip } from "@radix-ui/themes";
7+
import { AVAILABLE_MODELS } from "@shared/types/models";
48
import { useRef } from "react";
59
import type { MentionChip } from "../utils/content";
610

@@ -22,6 +26,20 @@ export function EditorToolbar({
2226
iconSize = 14,
2327
}: EditorToolbarProps) {
2428
const fileInputRef = useRef<HTMLInputElement>(null);
29+
const session = useSessionForTask(taskId);
30+
const defaultModel = useSettingsStore((state) => state.defaultModel);
31+
const activeModel = session?.model ?? defaultModel;
32+
33+
// Check if current model is Anthropic
34+
const isAnthropicModel = AVAILABLE_MODELS.some(
35+
(m) => m.id === activeModel && m.provider === "anthropic"
36+
);
37+
38+
// Thinking state for this task
39+
const thinkingEnabled = useThinkingStore((state) =>
40+
taskId ? state.getThinking(taskId) : false
41+
);
42+
const toggleThinking = useThinkingStore((state) => state.toggleThinking);
2543

2644
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
2745
const files = e.target.files;
@@ -66,6 +84,32 @@ export function EditorToolbar({
6684
</IconButton>
6785
</Tooltip>
6886
<ModelSelector taskId={taskId} disabled={disabled} />
87+
{isAnthropicModel && taskId && (
88+
<Tooltip
89+
content={
90+
thinkingEnabled
91+
? "Extended thinking enabled"
92+
: "Extended thinking disabled"
93+
}
94+
>
95+
<IconButton
96+
size="1"
97+
variant="ghost"
98+
color={thinkingEnabled ? "red" : "gray"}
99+
onClick={(e) => {
100+
e.stopPropagation();
101+
toggleThinking(taskId);
102+
}}
103+
disabled={disabled}
104+
style={{ marginLeft: "8px" }}
105+
>
106+
<Brain
107+
size={iconSize}
108+
weight={thinkingEnabled ? "fill" : "regular"}
109+
/>
110+
</IconButton>
111+
</Tooltip>
112+
)}
69113
</Flex>
70114
);
71115
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useSettingsStore } from "@features/settings/stores/settingsStore";
2+
import { create } from "zustand";
3+
import { persist } from "zustand/middleware";
4+
5+
interface ThinkingState {
6+
// Tracks thinking enabled state per task
7+
thinkingByTask: Record<string, boolean>;
8+
}
9+
10+
interface ThinkingActions {
11+
// Get thinking state for a task (defaults to settings default)
12+
getThinking: (taskId: string) => boolean;
13+
// Set thinking state for a task
14+
setThinking: (taskId: string, enabled: boolean) => void;
15+
// Toggle thinking state for a task
16+
toggleThinking: (taskId: string) => void;
17+
// Initialize thinking for a new task from settings default
18+
initializeThinking: (taskId: string) => void;
19+
}
20+
21+
export const useThinkingStore = create<ThinkingState & ThinkingActions>()(
22+
persist(
23+
(set, get) => ({
24+
thinkingByTask: {},
25+
26+
getThinking: (taskId: string) => {
27+
const state = get().thinkingByTask[taskId];
28+
if (state === undefined) {
29+
// Default to settings value
30+
return useSettingsStore.getState().defaultThinkingEnabled;
31+
}
32+
return state;
33+
},
34+
35+
setThinking: (taskId: string, enabled: boolean) => {
36+
set((state) => ({
37+
thinkingByTask: { ...state.thinkingByTask, [taskId]: enabled },
38+
}));
39+
},
40+
41+
toggleThinking: (taskId: string) => {
42+
const current = get().getThinking(taskId);
43+
get().setThinking(taskId, !current);
44+
},
45+
46+
initializeThinking: (taskId: string) => {
47+
// Only initialize if not already set
48+
if (get().thinkingByTask[taskId] === undefined) {
49+
const defaultEnabled =
50+
useSettingsStore.getState().defaultThinkingEnabled;
51+
set((state) => ({
52+
thinkingByTask: { ...state.thinkingByTask, [taskId]: defaultEnabled },
53+
}));
54+
}
55+
},
56+
}),
57+
{
58+
name: "thinking-storage",
59+
}
60+
)
61+
);
62+
63+
// Hook to get thinking state for a specific task
64+
export function useThinkingForTask(taskId: string | undefined) {
65+
return useThinkingStore((state) =>
66+
taskId ? state.getThinking(taskId) : false
67+
);
68+
}

apps/array/src/renderer/features/settings/components/SettingsView.tsx

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { formatHotkey } from "@renderer/constants/keyboard-shortcuts";
2121
import { track } from "@renderer/lib/analytics";
2222
import { clearApplicationStorage } from "@renderer/lib/clearStorage";
2323
import { logger } from "@renderer/lib/logger";
24+
import { AVAILABLE_MODELS } from "@shared/types/models";
2425
import type { CloudRegion } from "@shared/types/oauth";
2526
import { useSettingsStore as useTerminalLayoutStore } from "@stores/settingsStore";
2627
import { useShortcutsSheetStore } from "@stores/shortcutsSheetStore";
@@ -55,10 +56,13 @@ export function SettingsView() {
5556
autoRunTasks,
5657
createPR,
5758
cursorGlow,
59+
defaultModel,
60+
defaultThinkingEnabled,
5861
desktopNotifications,
5962
setAutoRunTasks,
6063
setCreatePR,
6164
setCursorGlow,
65+
setDefaultThinkingEnabled,
6266
setDesktopNotifications,
6367
} = useSettingsStore();
6468
const terminalLayoutMode = useTerminalLayoutStore(
@@ -160,6 +164,18 @@ export function SettingsView() {
160164
[terminalLayoutMode, setTerminalLayout],
161165
);
162166

167+
const handleThinkingEnabledChange = useCallback(
168+
(checked: boolean) => {
169+
track(ANALYTICS_EVENTS.SETTING_CHANGED, {
170+
setting_name: "default_thinking_enabled",
171+
new_value: checked,
172+
old_value: defaultThinkingEnabled,
173+
});
174+
setDefaultThinkingEnabled(checked);
175+
},
176+
[defaultThinkingEnabled, setDefaultThinkingEnabled],
177+
);
178+
163179
const handleWorktreeLocationChange = async (newLocation: string) => {
164180
setLocalWorktreeLocation(newLocation);
165181
try {
@@ -341,20 +357,44 @@ export function SettingsView() {
341357
<Flex direction="column" gap="3">
342358
<Heading size="3">Chat</Heading>
343359
<Card>
344-
<Flex align="center" justify="between">
345-
<Flex direction="column" gap="1">
346-
<Text size="1" weight="medium">
347-
Desktop notifications
348-
</Text>
349-
<Text size="1" color="gray">
350-
Show notifications when the agent finishes working on a task
351-
</Text>
360+
<Flex direction="column" gap="4">
361+
{/* Thinking toggle - only for Anthropic models */}
362+
{AVAILABLE_MODELS.find(
363+
(m) => m.id === defaultModel && m.provider === "anthropic",
364+
) && (
365+
<Flex align="center" justify="between">
366+
<Flex direction="column" gap="1">
367+
<Text size="1" weight="medium">
368+
Extended thinking
369+
</Text>
370+
<Text size="1" color="gray">
371+
Enable extended thinking for all chats.
372+
</Text>
373+
</Flex>
374+
<Switch
375+
checked={defaultThinkingEnabled}
376+
onCheckedChange={handleThinkingEnabledChange}
377+
size="1"
378+
/>
379+
</Flex>
380+
)}
381+
382+
<Flex align="center" justify="between">
383+
<Flex direction="column" gap="1">
384+
<Text size="1" weight="medium">
385+
Desktop notifications
386+
</Text>
387+
<Text size="1" color="gray">
388+
Show notifications when the agent finishes working on a
389+
task
390+
</Text>
391+
</Flex>
392+
<Switch
393+
checked={desktopNotifications}
394+
onCheckedChange={setDesktopNotifications}
395+
size="1"
396+
/>
352397
</Flex>
353-
<Switch
354-
checked={desktopNotifications}
355-
onCheckedChange={setDesktopNotifications}
356-
size="1"
357-
/>
358398
</Flex>
359399
</Card>
360400
</Flex>

apps/array/src/renderer/features/settings/stores/settingsStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface SettingsStore {
1414
lastUsedWorkspaceMode: WorkspaceMode;
1515
createPR: boolean;
1616
defaultModel: string;
17+
defaultThinkingEnabled: boolean;
1718
desktopNotifications: boolean;
1819
cursorGlow: boolean;
1920

@@ -24,6 +25,7 @@ interface SettingsStore {
2425
setLastUsedWorkspaceMode: (mode: WorkspaceMode) => void;
2526
setCreatePR: (createPR: boolean) => void;
2627
setDefaultModel: (model: string) => void;
28+
setDefaultThinkingEnabled: (enabled: boolean) => void;
2729
setDesktopNotifications: (enabled: boolean) => void;
2830
setCursorGlow: (enabled: boolean) => void;
2931
}
@@ -38,6 +40,7 @@ export const useSettingsStore = create<SettingsStore>()(
3840
lastUsedWorkspaceMode: "worktree",
3941
createPR: true,
4042
defaultModel: DEFAULT_MODEL,
43+
defaultThinkingEnabled: false,
4144
desktopNotifications: true,
4245
cursorGlow: false,
4346

@@ -49,6 +52,8 @@ export const useSettingsStore = create<SettingsStore>()(
4952
setLastUsedWorkspaceMode: (mode) => set({ lastUsedWorkspaceMode: mode }),
5053
setCreatePR: (createPR) => set({ createPR }),
5154
setDefaultModel: (model) => set({ defaultModel: model }),
55+
setDefaultThinkingEnabled: (enabled) =>
56+
set({ defaultThinkingEnabled: enabled }),
5257
setDesktopNotifications: (enabled) =>
5358
set({ desktopNotifications: enabled }),
5459
setCursorGlow: (enabled) => set({ cursorGlow: enabled }),

apps/array/src/renderer/features/task-detail/components/TaskLogsPanel.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
useSessionActions,
66
useSessionForTask,
77
} from "@features/sessions/stores/sessionStore";
8+
import { useThinkingStore } from "@features/sessions/stores/thinkingStore";
89
import { useSettingsStore } from "@features/settings/stores/settingsStore";
910
import { useTaskViewedStore } from "@features/sidebar/stores/taskViewedStore";
1011
import { useTaskData } from "@features/task-detail/hooks/useTaskData";
@@ -87,11 +88,21 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
8788
sessionStatus: session?.status ?? "none",
8889
});
8990

91+
// Initialize thinking state for this task from settings default
92+
useThinkingStore.getState().initializeThinking(task.id);
93+
94+
// Prepend "ultrathink" to initial prompt if extended thinking is enabled
95+
const thinkingEnabled = useThinkingStore.getState().getThinking(task.id);
96+
const initialPromptText =
97+
hasInitialPrompt && thinkingEnabled
98+
? `ultrathink ${task.description}`
99+
: task.description;
100+
90101
connectToTask({
91102
task,
92103
repoPath,
93104
initialPrompt: hasInitialPrompt
94-
? [{ type: "text", text: task.description }]
105+
? [{ type: "text", text: initialPromptText }]
95106
: undefined,
96107
}).finally(() => {
97108
isConnecting.current = false;
@@ -103,7 +114,14 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
103114
try {
104115
markAsViewed(taskId);
105116

106-
const result = await sendPrompt(taskId, text);
117+
// Prepend "ultrathink" if thinking is enabled and not already present
118+
const thinkingEnabled = useThinkingStore.getState().getThinking(taskId);
119+
const promptText =
120+
thinkingEnabled && !text.trim().toLowerCase().startsWith("ultrathink")
121+
? `ultrathink ${text}`
122+
: text;
123+
124+
const result = await sendPrompt(taskId, promptText);
107125
log.info("Prompt completed", { stopReason: result.stopReason });
108126

109127
markActivity(taskId);

0 commit comments

Comments
 (0)