Skip to content

Commit 83da4fe

Browse files
authored
Allow making a toolset the default for plan mode (#238)
* Allow making a toolset the default for plan mode Closes #187 * Reorder the indicators * Format * Changelog
1 parent 6794611 commit 83da4fe

7 files changed

Lines changed: 204 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Allow making a toolset the default for plan mode (#238) - @bl-ue
11+
1012
## [v3.1.3](https://github.com/Piebald-AI/tweakcc/releases/tag/v3.1.3) - 2025-11-26
1113

1214
- Add paths for mise npm backend (#234) - @coryzibell

src/components/ToolsetEditView.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,19 @@ export function ToolsetEditView({
4747
useEffect(() => {
4848
if (toolset) {
4949
updateSettings(settings => {
50+
const oldName = settings.toolsets[toolsetIndex].name;
5051
settings.toolsets[toolsetIndex].name = name;
5152
settings.toolsets[toolsetIndex].allowedTools = allowedTools;
53+
54+
// Update references if name changed
55+
if (oldName !== name) {
56+
if (settings.defaultToolset === oldName) {
57+
settings.defaultToolset = name;
58+
}
59+
if (settings.planModeToolset === oldName) {
60+
settings.planModeToolset = name;
61+
}
62+
}
5263
});
5364
}
5465
}, [name, allowedTools]);

src/components/ToolsetsView.tsx

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
import React, { useState, useContext } from 'react';
22
import { Box, Text, useInput } from 'ink';
3-
import { Toolset } from '../utils/types.js';
3+
import { Toolset, DEFAULT_SETTINGS } from '../utils/types.js';
44
import { SettingsContext } from '../App.js';
55
import { ToolsetEditView } from './ToolsetEditView.js';
66
import Header from './Header.js';
7+
import { getCurrentClaudeCodeTheme } from '../utils/misc.js';
78

89
interface ToolsetsViewProps {
910
onBack: () => void;
1011
}
1112

1213
export function ToolsetsView({ onBack }: ToolsetsViewProps) {
1314
const {
14-
settings: { toolsets, defaultToolset },
15+
settings: { toolsets, defaultToolset, planModeToolset, themes },
1516
updateSettings,
1617
} = useContext(SettingsContext);
1718

19+
// Get current theme colors
20+
const currentThemeId = getCurrentClaudeCodeTheme();
21+
const currentTheme = themes.find(t => t.id === currentThemeId) || themes[0];
22+
23+
const defaultTheme = DEFAULT_SETTINGS.themes[0]; // Dark mode theme
24+
const planModeColor =
25+
currentTheme?.colors.planMode || defaultTheme.colors.planMode;
26+
const autoAcceptColor =
27+
currentTheme?.colors.autoAccept || defaultTheme.colors.autoAccept;
28+
1829
const [selectedIndex, setSelectedIndex] = useState(0);
1930
const [editingToolsetIndex, setEditingToolsetIndex] = useState<number | null>(
2031
null
@@ -43,6 +54,10 @@ export function ToolsetsView({ onBack }: ToolsetsViewProps) {
4354
if (settings.defaultToolset === toolsetToDelete.name) {
4455
settings.defaultToolset = null;
4556
}
57+
// Clear plan mode if we're deleting the plan mode toolset
58+
if (settings.planModeToolset === toolsetToDelete.name) {
59+
settings.planModeToolset = null;
60+
}
4661
});
4762

4863
if (selectedIndex >= toolsets.length - 1) {
@@ -57,6 +72,13 @@ export function ToolsetsView({ onBack }: ToolsetsViewProps) {
5772
});
5873
};
5974

75+
const handleSetPlanModeToolset = (index: number) => {
76+
const toolset = toolsets[index];
77+
updateSettings(settings => {
78+
settings.planModeToolset = toolset.name;
79+
});
80+
};
81+
6082
useInput(
6183
(input, key) => {
6284
if (key.escape) {
@@ -70,10 +92,12 @@ export function ToolsetsView({ onBack }: ToolsetsViewProps) {
7092
setInputActive(false);
7193
} else if (input === 'n') {
7294
handleCreateToolset();
73-
} else if (input === 'd' && toolsets.length > 0) {
95+
} else if (input === 'x' && toolsets.length > 0) {
7496
handleDeleteToolset(selectedIndex);
75-
} else if (input === 's' && toolsets.length > 0) {
97+
} else if (input === 'd' && toolsets.length > 0) {
7698
handleSetDefaultToolset(selectedIndex);
99+
} else if (input === 'p' && toolsets.length > 0) {
100+
handleSetPlanModeToolset(selectedIndex);
77101
}
78102
},
79103
{ isActive: inputActive }
@@ -108,9 +132,12 @@ export function ToolsetsView({ onBack }: ToolsetsViewProps) {
108132
<Box marginBottom={1} flexDirection="column">
109133
<Text dimColor>n to create a new toolset</Text>
110134
{toolsets.length > 0 && (
111-
<Text dimColor>s to set as default toolset</Text>
135+
<Text dimColor>d to set as default toolset</Text>
136+
)}
137+
{toolsets.length > 0 && (
138+
<Text dimColor>p to set as plan mode toolset</Text>
112139
)}
113-
{toolsets.length > 0 && <Text dimColor>d to delete a toolset</Text>}
140+
{toolsets.length > 0 && <Text dimColor>x to delete a toolset</Text>}
114141
{toolsets.length > 0 && <Text dimColor>enter to edit toolset</Text>}
115142
<Text dimColor>esc to go back</Text>
116143
</Box>
@@ -121,17 +148,31 @@ export function ToolsetsView({ onBack }: ToolsetsViewProps) {
121148
<Box flexDirection="column">
122149
{toolsets.map((toolset, index) => {
123150
const isDefault = toolset.name === defaultToolset;
151+
const isPlanMode = toolset.name === planModeToolset;
124152
const isSelected = selectedIndex === index;
153+
154+
// Determine the color for the entire line
155+
let lineColor: string | undefined = undefined;
156+
if (isSelected) {
157+
lineColor = 'yellow';
158+
}
159+
125160
return (
126-
<Text
127-
key={index}
128-
color={isSelected ? 'yellow' : undefined}
129-
bold={isDefault}
130-
>
131-
{isSelected ? '❯ ' : ' '}
132-
{toolset.name}
133-
{isDefault && ' [DEFAULT]'} ({getToolsetDescription(toolset)})
134-
</Text>
161+
<Box key={index} flexDirection="row">
162+
<Text color={lineColor}>
163+
{isSelected ? '❯ ' : ' '}
164+
{toolset.name}{' '}
165+
</Text>
166+
167+
<Text color={lineColor}>
168+
({getToolsetDescription(toolset)})
169+
</Text>
170+
171+
{isDefault && (
172+
<Text color={autoAcceptColor}> ⏵⏵ accept edits</Text>
173+
)}
174+
{isPlanMode && <Text color={planModeColor}> ⏸ plan mode</Text>}
175+
</Box>
135176
);
136177
})}
137178
</Box>

src/utils/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ export const readConfigFile = async (): Promise<TweakccConfig> => {
104104
if (!Object.hasOwn(readConfig.settings, 'defaultToolset')) {
105105
readConfig.settings.defaultToolset = DEFAULT_SETTINGS.defaultToolset;
106106
}
107+
if (!Object.hasOwn(readConfig.settings, 'planModeToolset')) {
108+
readConfig.settings.planModeToolset = DEFAULT_SETTINGS.planModeToolset;
109+
}
107110

108111
// Add any colors that the user doesn't have to any built-in themes.
109112
for (const defaultTheme of DEFAULT_SETTINGS.themes) {

src/utils/patches/index.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ import { writeThinkingVisibility } from './thinkingVisibility.js';
5050
import { writePatchesAppliedIndication } from './patchesAppliedIndication.js';
5151
import { applySystemPrompts } from './systemPrompts.js';
5252
import { writeFixLspSupport } from './fixLspSupport.js';
53-
import { writeToolsets } from './toolsets.js';
53+
import {
54+
writeToolsets,
55+
writeModeChangeUpdateToolset,
56+
addSetStateFnAccessAtToolChangeComponentScope,
57+
} from './toolsets.js';
5458
import { writeConversationTitle } from './conversationTitle.js';
5559

5660
export interface LocationResult {
@@ -612,6 +616,23 @@ export const applyCustomization = async (
612616
content = result;
613617
}
614618

619+
// Apply mode-change toolset switching (if both toolsets are configured)
620+
if (config.settings.planModeToolset && config.settings.defaultToolset) {
621+
// First, add setState access at the tool change component scope
622+
if ((result = addSetStateFnAccessAtToolChangeComponentScope(content)))
623+
content = result;
624+
625+
// Then, inject the mode change toolset switching code
626+
if (
627+
(result = writeModeChangeUpdateToolset(
628+
content,
629+
config.settings.planModeToolset,
630+
config.settings.defaultToolset
631+
))
632+
)
633+
content = result;
634+
}
635+
615636
// Apply conversation title management (always enabled)
616637
if ((result = writeConversationTitle(content))) content = result;
617638

src/utils/patches/toolsets.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,114 @@ export const writeSlashCommandDefinition = (oldFile: string): string | null => {
739739
return writeSlashCommandDefinitionToArray(oldFile, commandDef);
740740
};
741741

742+
// ============================================================================
743+
// MODE CHANGE TOOLSET FUNCTIONS
744+
// ============================================================================
745+
746+
/**
747+
* Find the tool change component scope
748+
* Pattern: X(Y,function(Z){W("tengu_ext_at_mentioned",{});
749+
* Returns the start index
750+
*/
751+
export const findToolChangeComponentScope = (
752+
fileContents: string
753+
): number | null => {
754+
const pattern =
755+
/[\w$]+\([\w$]+,function\([\w$]+\)\{[\w$]+\("tengu_ext_at_mentioned",\{\}\);/;
756+
const match = fileContents.match(pattern);
757+
758+
if (!match || match.index === undefined) {
759+
console.error(
760+
'patch: findToolChangeComponentScope: failed to find tool change component scope'
761+
);
762+
return null;
763+
}
764+
765+
return match.index;
766+
};
767+
768+
/**
769+
* Add setState function access at the tool change component scope
770+
* Injects: const [state, setState] = appStateGetterFn();
771+
*/
772+
export const addSetStateFnAccessAtToolChangeComponentScope = (
773+
oldFile: string
774+
): string | null => {
775+
const scopeIndex = findToolChangeComponentScope(oldFile);
776+
if (scopeIndex === null) {
777+
return null;
778+
}
779+
780+
const stateInfo = getAppStateVarAndGetterFunction(oldFile);
781+
if (!stateInfo) {
782+
console.error(
783+
'patch: addSetStateFnAccessAtToolChangeComponentScope: failed to get app state getter function'
784+
);
785+
return null;
786+
}
787+
788+
const { appStateGetterFunction } = stateInfo;
789+
790+
// Inject the setState access right at the start of the component scope
791+
const injectionCode = `const [state, setState] = ${appStateGetterFunction}();`;
792+
793+
const newFile =
794+
oldFile.slice(0, scopeIndex) + injectionCode + oldFile.slice(scopeIndex);
795+
796+
return newFile;
797+
};
798+
799+
/**
800+
* Find the mode change location in the code
801+
* Pattern: let X=Y(Z,{type:"setMode",mode:W,destination:"session"});
802+
* Returns the start index and the mode variable (W)
803+
*/
804+
export const findModeChange = (
805+
fileContents: string
806+
): { index: number; modeVar: string } | null => {
807+
const pattern =
808+
/let [\w$]+=[\w$]+\([\w$]+,\{type:"setMode",mode:([\w$]+),destination:"session"\}\);/;
809+
const match = fileContents.match(pattern);
810+
811+
if (!match || match.index === undefined) {
812+
console.error('patch: findModeChange: failed to find mode change location');
813+
return null;
814+
}
815+
816+
return {
817+
index: match.index,
818+
modeVar: match[1],
819+
};
820+
};
821+
822+
/**
823+
* Write the mode change toolset update code
824+
* This injects code before the mode change to automatically switch toolsets
825+
*/
826+
export const writeModeChangeUpdateToolset = (
827+
oldFile: string,
828+
planModeToolset: string,
829+
defaultToolset: string
830+
): string | null => {
831+
const modeChangeResult = findModeChange(oldFile);
832+
if (!modeChangeResult) {
833+
return null;
834+
}
835+
836+
const { index: modeChangeIndex, modeVar } = modeChangeResult;
837+
838+
// Build the injection code using setState directly
839+
const injectionCode = `if(${modeVar}==="plan"){setState((prev)=>({...prev,toolset:${JSON.stringify(planModeToolset)}}));}else{setState((prev)=>({...prev,toolset:${JSON.stringify(defaultToolset)}}));}`;
840+
841+
// Inject right before the mode change
842+
const newFile =
843+
oldFile.slice(0, modeChangeIndex) +
844+
injectionCode +
845+
oldFile.slice(modeChangeIndex);
846+
847+
return newFile;
848+
};
849+
742850
// ============================================================================
743851
// MAIN ORCHESTRATOR
744852
// ============================================================================

src/utils/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export interface Settings {
118118
misc: MiscConfig;
119119
toolsets: Toolset[];
120120
defaultToolset: string | null;
121+
planModeToolset: string | null;
121122
}
122123

123124
export interface TweakccConfig {
@@ -910,6 +911,7 @@ export const DEFAULT_SETTINGS: Settings = {
910911
},
911912
toolsets: [],
912913
defaultToolset: null,
914+
planModeToolset: null,
913915
};
914916

915917
// Support XDG Base Directory Specification with backward compatibility

0 commit comments

Comments
 (0)