Skip to content

Commit d6a3196

Browse files
committed
fix: resolve plan-mode crash and make model customizations configurable
- Fix UNKNOWN_N identifier mapping in systemPromptSync that caused "UNKNOWN_3 is not defined" ReferenceError when entering plan mode - Add settings.misc.enableModelCustomizations toggle (default true) - Move model-customizations and show-more-items-in-select-menus to Misc Configurable group, gated by the new setting - Update model catalog (add Sonnet 4.6, Haiku 4.5) and model descriptions from 4.5 to 4.6 - Widen modelSelector lookback window and generalize declaration pattern
1 parent ac348f5 commit d6a3196

9 files changed

Lines changed: 242 additions & 45 deletions

File tree

src/defaultSettings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ export const DEFAULT_SETTINGS: Settings = {
713713
suppressNativeInstallerWarning: false,
714714
filterScrollEscapeSequences: false,
715715
enableWorktreeMode: true,
716+
enableModelCustomizations: true,
716717
},
717718
toolsets: [],
718719
defaultToolset: null,

src/patches/index.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -153,25 +153,13 @@ const PATCH_DEFINITIONS = [
153153
description:
154154
'Set the CLAUDE_CODE_CONTEXT_LIMIT env var to change 200k max for custom models',
155155
},
156-
{
157-
id: 'model-customizations',
158-
name: 'Model customizations',
159-
group: PatchGroup.ALWAYS_APPLIED,
160-
description: 'Access all Claude models with /model, not just latest 3',
161-
},
162156
{
163157
id: 'opusplan1m',
164158
name: 'Opusplan[1m] support',
165159
group: PatchGroup.ALWAYS_APPLIED,
166160
description:
167161
'Use the "Opus Plan 1M" model: Opus for planning, Sonnet 1M context for building',
168162
},
169-
{
170-
id: 'show-more-items-in-select-menus',
171-
name: 'Show more items in select menus',
172-
group: PatchGroup.ALWAYS_APPLIED,
173-
description: 'Show 25 items in select menus instead of default 5',
174-
},
175163
{
176164
id: 'thinking-block-styling',
177165
name: 'Thinking block styling',
@@ -191,6 +179,18 @@ const PATCH_DEFINITIONS = [
191179
description: `Statusline updates will be properly throttled instead of queued (or debounced)`,
192180
},
193181
// Misc Configurable
182+
{
183+
id: 'model-customizations',
184+
name: 'Model customizations',
185+
group: PatchGroup.MISC_CONFIGURABLE,
186+
description: 'Access all Claude models with /model, not just latest 3',
187+
},
188+
{
189+
id: 'show-more-items-in-select-menus',
190+
name: 'Show more items in select menus',
191+
group: PatchGroup.MISC_CONFIGURABLE,
192+
description: 'Show 25 items in select menus instead of default 5',
193+
},
194194
{
195195
id: 'patches-applied-indication',
196196
name: 'Patches applied indication',
@@ -605,6 +605,10 @@ export const applyCustomization = async (
605605
// ==========================================================================
606606
// Define patch implementations (keyed by PatchId)
607607
// ==========================================================================
608+
// Keep model list customization and select-menu size behavior in sync.
609+
// Disabling model customizations should restore both selectors to vanilla CC behavior.
610+
const modelCustomizationsEnabled =
611+
config.settings.misc?.enableModelCustomizations ?? true;
608612
const patchImplementations: Record<PatchId, PatchImplementation> = {
609613
// Always Applied
610614
'verbose-property': {
@@ -613,15 +617,9 @@ export const applyCustomization = async (
613617
'context-limit': {
614618
fn: c => writeContextLimit(c),
615619
},
616-
'model-customizations': {
617-
fn: c => writeModelCustomizations(c),
618-
},
619620
opusplan1m: {
620621
fn: c => writeOpusplan1m(c),
621622
},
622-
'show-more-items-in-select-menus': {
623-
fn: c => writeShowMoreItemsInSelectMenus(c, 25),
624-
},
625623
'thinking-block-styling': {
626624
fn: c => writeThinkingBlockStyling(c),
627625
condition:
@@ -651,6 +649,14 @@ export const applyCustomization = async (
651649
showPatchesApplied
652650
),
653651
},
652+
'model-customizations': {
653+
fn: c => writeModelCustomizations(c),
654+
condition: modelCustomizationsEnabled,
655+
},
656+
'show-more-items-in-select-menus': {
657+
fn: c => writeShowMoreItemsInSelectMenus(c, 25),
658+
condition: modelCustomizationsEnabled,
659+
},
654660
'table-format': {
655661
fn: c => writeTableFormat(c, tableFormat),
656662
condition: tableFormat !== 'default',
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import * as fs from 'node:fs/promises';
2+
3+
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
import { DEFAULT_SETTINGS } from '../defaultSettings';
6+
import { ClaudeCodeInstallationInfo, TweakccConfig } from '../types';
7+
import { updateConfigFile } from '../config';
8+
import { replaceFileBreakingHardLinks } from '../utils';
9+
import { restoreClijsFromBackup } from '../installationBackup';
10+
import { writeModelCustomizations } from './modelSelector';
11+
import { writeShowMoreItemsInSelectMenus } from './showMoreItemsInSelectMenus';
12+
import { applySystemPrompts } from './systemPrompts';
13+
import { applyCustomization } from './index';
14+
15+
const mockReadFile = vi.hoisted(() => vi.fn());
16+
17+
vi.mock('node:fs/promises', () => ({
18+
readFile: mockReadFile,
19+
}));
20+
21+
vi.mock('../config', () => ({
22+
CONFIG_DIR: '/tmp/tweakcc-test-config',
23+
NATIVE_BINARY_BACKUP_FILE: '/tmp/tweakcc-test-config/native.backup',
24+
updateConfigFile: vi.fn(async updateFn => {
25+
const config = { changesApplied: false } as TweakccConfig;
26+
updateFn(config);
27+
return config;
28+
}),
29+
}));
30+
31+
vi.mock('../utils', () => ({
32+
debug: vi.fn(),
33+
replaceFileBreakingHardLinks: vi.fn(),
34+
}));
35+
36+
vi.mock('../installationBackup', () => ({
37+
restoreNativeBinaryFromBackup: vi.fn(),
38+
restoreClijsFromBackup: vi.fn(async () => true),
39+
}));
40+
41+
vi.mock('../nativeInstallationLoader', () => ({
42+
extractClaudeJsFromNativeInstallation: vi.fn(),
43+
repackNativeInstallation: vi.fn(),
44+
}));
45+
46+
vi.mock('./modelSelector', () => ({
47+
writeModelCustomizations: vi.fn((content: string) => `${content}|model`),
48+
}));
49+
50+
vi.mock('./showMoreItemsInSelectMenus', () => ({
51+
writeShowMoreItemsInSelectMenus: vi.fn(
52+
(content: string) => `${content}|show`
53+
),
54+
}));
55+
56+
vi.mock('./systemPrompts', () => ({
57+
applySystemPrompts: vi.fn(async (content: string) => ({
58+
newContent: content,
59+
results: [],
60+
})),
61+
}));
62+
63+
const baseConfig = (): TweakccConfig => ({
64+
ccVersion: '',
65+
ccInstallationPath: null,
66+
lastModified: '2026-01-01T00:00:00.000Z',
67+
changesApplied: false,
68+
settings: {
69+
...DEFAULT_SETTINGS,
70+
misc: {
71+
...DEFAULT_SETTINGS.misc,
72+
},
73+
},
74+
});
75+
76+
const ccInstInfo: ClaudeCodeInstallationInfo = {
77+
cliPath: '/tmp/claude-cli.js',
78+
version: '2.1.63',
79+
source: 'search-paths',
80+
};
81+
82+
describe('model customization toggle patch conditions', () => {
83+
beforeEach(() => {
84+
vi.clearAllMocks();
85+
vi.mocked(fs.readFile).mockResolvedValue('base-content');
86+
});
87+
88+
it('skips both model customization patches when disabled', async () => {
89+
const config = baseConfig();
90+
config.settings.misc.enableModelCustomizations = false;
91+
92+
const { results } = await applyCustomization(config, ccInstInfo, [
93+
'model-customizations',
94+
'show-more-items-in-select-menus',
95+
]);
96+
97+
const modelResult = results.find(r => r.id === 'model-customizations');
98+
const showMoreResult = results.find(
99+
r => r.id === 'show-more-items-in-select-menus'
100+
);
101+
102+
expect(modelResult).toMatchObject({ applied: false, skipped: true });
103+
expect(showMoreResult).toMatchObject({ applied: false, skipped: true });
104+
expect(vi.mocked(writeModelCustomizations)).not.toHaveBeenCalled();
105+
expect(vi.mocked(writeShowMoreItemsInSelectMenus)).not.toHaveBeenCalled();
106+
expect(vi.mocked(replaceFileBreakingHardLinks)).toHaveBeenCalledWith(
107+
'/tmp/claude-cli.js',
108+
'base-content',
109+
'patch'
110+
);
111+
});
112+
113+
it('applies both model customization patches when enabled', async () => {
114+
const config = baseConfig();
115+
config.settings.misc.enableModelCustomizations = true;
116+
117+
const { results } = await applyCustomization(config, ccInstInfo, [
118+
'model-customizations',
119+
'show-more-items-in-select-menus',
120+
]);
121+
122+
const modelResult = results.find(r => r.id === 'model-customizations');
123+
const showMoreResult = results.find(
124+
r => r.id === 'show-more-items-in-select-menus'
125+
);
126+
127+
expect(modelResult).toMatchObject({ applied: true, failed: false });
128+
expect(showMoreResult).toMatchObject({ applied: true, failed: false });
129+
expect(vi.mocked(writeModelCustomizations)).toHaveBeenCalledTimes(1);
130+
expect(vi.mocked(writeShowMoreItemsInSelectMenus)).toHaveBeenCalledTimes(1);
131+
expect(vi.mocked(replaceFileBreakingHardLinks)).toHaveBeenCalledWith(
132+
'/tmp/claude-cli.js',
133+
expect.stringContaining('base-content'),
134+
'patch'
135+
);
136+
});
137+
138+
it('runs plumbing required for apply customization', async () => {
139+
await applyCustomization(baseConfig(), ccInstInfo, [
140+
'model-customizations',
141+
'show-more-items-in-select-menus',
142+
]);
143+
144+
expect(vi.mocked(restoreClijsFromBackup)).toHaveBeenCalledTimes(1);
145+
expect(vi.mocked(applySystemPrompts)).toHaveBeenCalledTimes(1);
146+
expect(vi.mocked(updateConfigFile)).toHaveBeenCalledTimes(1);
147+
});
148+
});

src/patches/modelSelector.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ import { escapeIdent, showDiff } from './index';
55
// Models to inject/make available.
66
// prettier-ignore
77
export const CUSTOM_MODELS: { value: string; label: string; description: string }[] = [
8-
{ value: 'claude-opus-4-6', label: 'Opus 4.6', description: "Claude Opus 4.6 (February 2026)" },
9-
{ value: 'claude-opus-4-5-20251101', label: 'Opus 4.5', description: "Claude Opus 4.5 (November 2025)" },
10-
{ value: 'claude-sonnet-4-5-20250929', label: 'Sonnet 4.5', description: "Claude Sonnet 4.5 (September 2025)" },
11-
{ value: 'claude-opus-4-1-20250805', label: 'Opus 4.1', description: "Claude Opus 4.1 (August 2025)" },
12-
{ value: 'claude-opus-4-20250514', label: 'Opus 4', description: "Claude Opus 4 (May 2025)" },
13-
{ value: 'claude-sonnet-4-20250514', label: 'Sonnet 4', description: "Claude Sonnet 4 (May 2025)" },
14-
{ value: 'claude-3-7-sonnet-20250219', label: 'Sonnet 3.7', description: "Claude 3.7 Sonnet (February 2025)" },
15-
{ value: 'claude-3-5-sonnet-20241022', label: 'Sonnet 3.5 (October)', description: "Claude 3.5 Sonnet (October 2024)" },
16-
{ value: 'claude-3-5-haiku-20241022', label: 'Haiku 3.5', description: "Claude 3.5 Haiku (October 2024)" },
17-
{ value: 'claude-3-5-sonnet-20240620', label: 'Sonnet 3.5 (June)', description: "Claude 3.5 Sonnet (June 2024)" },
18-
{ value: 'claude-3-haiku-20240307', label: 'Haiku 3', description: "Claude 3 Haiku (March 2024)" },
19-
{ value: 'claude-3-opus-20240229', label: 'Opus 3', description: "Claude 3 Opus (February 2024)" },
8+
{ value: 'claude-opus-4-6', label: 'Opus 4.6', description: "Claude Opus 4.6 (February 2026)" },
9+
{ value: 'claude-sonnet-4-6', label: 'Sonnet 4.6', description: "Claude Sonnet 4.6 (February 2026)" },
10+
{ value: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5', description: "Claude Haiku 4.5 (October 2025)" },
11+
{ value: 'claude-opus-4-5-20251101', label: 'Opus 4.5', description: "Claude Opus 4.5 (November 2025)" },
12+
{ value: 'claude-sonnet-4-5-20250929', label: 'Sonnet 4.5', description: "Claude Sonnet 4.5 (September 2025)" },
13+
{ value: 'claude-opus-4-1-20250805', label: 'Opus 4.1', description: "Claude Opus 4.1 (August 2025)" },
14+
{ value: 'claude-opus-4-20250514', label: 'Opus 4', description: "Claude Opus 4 (May 2025)" },
15+
{ value: 'claude-sonnet-4-20250514', label: 'Sonnet 4', description: "Claude Sonnet 4 (May 2025)" },
16+
{ value: 'claude-3-7-sonnet-20250219', label: 'Sonnet 3.7', description: "Claude 3.7 Sonnet (February 2025)" },
17+
{ value: 'claude-3-5-sonnet-20241022', label: 'Sonnet 3.5 (October)', description: "Claude 3.5 Sonnet (October 2024)" },
18+
{ value: 'claude-3-5-haiku-20241022', label: 'Haiku 3.5', description: "Claude 3.5 Haiku (October 2024)" },
19+
{ value: 'claude-3-5-sonnet-20240620', label: 'Sonnet 3.5 (June)', description: "Claude 3.5 Sonnet (June 2024)" },
20+
{ value: 'claude-3-haiku-20240307', label: 'Haiku 3', description: "Claude 3 Haiku (March 2024)" },
21+
{ value: 'claude-3-opus-20240229', label: 'Opus 3', description: "Claude 3 Opus (February 2024)" },
2022
];
2123

2224
const findCustomModelListInsertionPoint = (
@@ -36,13 +38,15 @@ const findCustomModelListInsertionPoint = (
3638
// 2. Extract the model list variable name
3739
const modelListVar = pushMatch[1];
3840

39-
// 3. Look back 600 chars from the push match
40-
const searchStart = Math.max(0, pushMatch.index - 600);
41+
// The declaration/function head can move farther from the push site across CC builds
42+
// and when other patches expand this block, so keep a wider lookback window.
43+
const searchStart = Math.max(0, pushMatch.index - 1500);
4144
const chunk = fileContents.slice(searchStart, pushMatch.index);
4245

43-
// 4. Find the LAST occurrence of the function with let modelListVar=...;
46+
// Declaration can be emitted as let/var/const depending on minifier output.
47+
const declPattern = `(?:let|var|const) ${escapeIdent(modelListVar)}=.+?;`;
4448
const funcPattern = new RegExp(
45-
`function [$\\w]+\\([^)]*\\)\\{let ${escapeIdent(modelListVar)}=.+?;`,
49+
`function [$\\w]+\\([^)]*\\)\\{${declPattern}`,
4650
'g'
4751
);
4852
let lastMatch: RegExpExecArray | null = null;
@@ -53,7 +57,7 @@ const findCustomModelListInsertionPoint = (
5357

5458
if (!lastMatch) {
5559
console.error(
56-
`patch: findCustomModelListInsertionPoint: failed to find function with let ${modelListVar}`
60+
`patch: findCustomModelListInsertionPoint: failed to find function with ${modelListVar}`
5761
);
5862
return null;
5963
}

src/patches/opusplan1m.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@ const patchModelAliasesList = (oldFile: string): string | null => {
102102
* Patch 3: Fix the description function (Zm3) to handle opusplan[1m]
103103
*
104104
* Original:
105-
* if (A === "opusplan") return "Opus 4.5 in plan mode, else Sonnet 4.5";
105+
* if (A === "opusplan") return "Opus 4.6 in plan mode, else Sonnet 4.6";
106106
*
107107
* Patched:
108-
* if (A === "opusplan") return "Opus 4.5 in plan mode, else Sonnet 4.5";
109-
* if (A === "opusplan[1m]") return "Opus 4.5 in plan mode, else Sonnet 4.5 (1M context)";
108+
* if (A === "opusplan") return "Opus 4.6 in plan mode, else Sonnet 4.6";
109+
* if (A === "opusplan[1m]") return "Opus 4.6 in plan mode, else Sonnet 4.6 (1M context)";
110110
*/
111111
const patchDescriptionFunction = (oldFile: string): string | null => {
112-
// Pattern matches: if (VAR === "opusplan") return "Opus 4.5 in plan mode, else Sonnet 4.5";
112+
// Pattern matches: if (VAR === "opusplan") return "Opus 4.6 in plan mode, else Sonnet 4.6";
113113
const pattern =
114114
/(if\s*\(\s*([$\w]+)\s*===\s*"opusplan"\s*\)\s*return\s*"Opus .{0,20} in plan mode, else Sonnet .{0,20}";)/;
115115

@@ -126,7 +126,7 @@ const patchDescriptionFunction = (oldFile: string): string | null => {
126126
// Add the opusplan[1m] case right after the opusplan case
127127
const replacement =
128128
fullMatch +
129-
`if(${varName}==="opusplan[1m]")return"Opus 4.5 in plan mode, else Sonnet 4.5 (1M context)";`;
129+
`if(${varName}==="opusplan[1m]")return"Opus 4.6 in plan mode, else Sonnet 4.6 (1M context)";`;
130130

131131
const newFile =
132132
oldFile.slice(0, match.index) +
@@ -198,7 +198,7 @@ const patchLabelFunction = (oldFile: string): string | null => {
198198
* return {
199199
* value: "opusplan",
200200
* label: "Opus Plan Mode",
201-
* description: "Use Opus 4.5 in plan mode, Sonnet 4.5 otherwise",
201+
* description: "Use Opus 4.6 in plan mode, Sonnet 4.6 otherwise",
202202
* };
203203
* };
204204
*
@@ -227,7 +227,7 @@ const patchModelSelectorOptions = (oldFile: string): string | null => {
227227
// since we don't want to modify the function definitions area
228228
const replacement =
229229
fullMatch +
230-
`if(${varName}==="opusplan[1m]")return[...${listVar},{value:"opusplan[1m]",label:"Opus Plan Mode 1M",description:"Use Opus 4.5 in plan mode, Sonnet 4.5 (1M context) otherwise"}];`;
230+
`if(${varName}==="opusplan[1m]")return[...${listVar},{value:"opusplan[1m]",label:"Opus Plan Mode 1M",description:"Use Opus 4.6 in plan mode, Sonnet 4.6 (1M context) otherwise"}];`;
231231

232232
const newFile =
233233
oldFile.slice(0, match.index) +
@@ -272,8 +272,8 @@ const patchAlwaysShowInModelSelector = (oldFile: string): string | null => {
272272
// Inject pushes BEFORE the conditional return
273273
// This ensures opusplan and opusplan[1m] are always in the list
274274
const inject =
275-
`${listVar}.push({value:"opusplan",label:"Opus Plan Mode",description:"Use Opus 4.5 in plan mode, Sonnet 4.5 otherwise"});` +
276-
`${listVar}.push({value:"opusplan[1m]",label:"Opus Plan Mode 1M",description:"Use Opus 4.5 in plan mode, Sonnet 4.5 (1M context) otherwise"});`;
275+
`${listVar}.push({value:"opusplan",label:"Opus Plan Mode",description:"Use Opus 4.6 in plan mode, Sonnet 4.6 otherwise"});` +
276+
`${listVar}.push({value:"opusplan[1m]",label:"Opus Plan Mode 1M",description:"Use Opus 4.6 in plan mode, Sonnet 4.6 (1M context) otherwise"});`;
277277

278278
const newFile =
279279
oldFile.slice(0, match.index) + inject + oldFile.slice(match.index);

src/systemPromptSync.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1345,8 +1345,9 @@ const applyIdentifierMapping = (
13451345
const humanName = identifierMap[labelIndex];
13461346

13471347
if (humanName) {
1348-
// Skip empty mappings
13491348
reverseMap[humanName] = capturedVar;
1349+
} else {
1350+
reverseMap[`UNKNOWN_${labelIndex}`] = capturedVar;
13501351
}
13511352
}
13521353

0 commit comments

Comments
 (0)