Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- Add search box functionality to /models with new patch (#462) - @brrock
- Fix Linux native installation support (#644) - @signadou

## [v4.0.11](https://github.com/Piebald-AI/tweakcc/releases/tag/v4.0.11) - 2026-03-05
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ $ pnpm dlx tweakcc
- Increase the max size in tokens for files read via `Read`
- Remove the border from the message input box
- Add all models to `/model`
- [Searchable `/model` picker](#feature-searchable-model-picker)
- tweakcc patches applied indicator
- Show more items in select menus
- Subagent models
Expand Down Expand Up @@ -862,6 +863,42 @@ claude --model opusplan[1m]
| Plan mode (Shift+Tab twice) | Opus 4.5 | 200k |
| Execution mode (default) | Sonnet 4.5 | **1M** |

## Feature: Searchable `/model` picker

Claude Code's `/model` menu is much easier to use once tweakcc patches it to show the full model list and adds a searchable picker at the bottom of the screen.

This patch is **enabled by default** in tweakcc, and it currently applies to the Claude Code **2.1.86** and **2.1.87** `/model` bundle shapes. On other Claude Code versions, tweakcc will leave this specific patch unapplied rather than trying to force a partial match.

The search box uses the same rounded bordered styling as older Claude Code versions, stays visually wide even when empty, and lets you quickly filter large model lists without relying on numeric shortcuts or scrolling through everything manually.

Here's what the searchable `/model` picker looks like:

![Searchable /model picker with bordered search box](./assets/modelSel.png)

### Configuration

**Via the UI:** Run `npx tweakcc`, go to **Misc**. These options are on by default:

- **Enable model customizations (/model shows all models)**
- **Enable searchable /model picker**

The searchable picker depends on model customizations being enabled, since the main benefit is searching the expanded `/model` list instead of Claude Code's default short list.

If you're not on Claude Code `2.1.86` or `2.1.87`, you can still leave the setting enabled in tweakcc, but the searchable picker patch itself will be skipped until its bundle shape is explicitly supported.

**Via `config.json`:**

```json
{
"settings": {
"misc": {
"enableModelCustomizations": true,
"enableModelSelectorSearch": true
}
}
}
```

## Feature: MCP startup optimization

If you use multiple MCP servers, Claude Code's startup can be slow—waiting 10-15+ seconds for all servers to connect before you can start typing.
Expand Down
Binary file added assets/modelSel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/defaultSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ export const DEFAULT_SETTINGS: Settings = {
allowCustomAgentModels: false,
enableContextLimitOverride: false,
enableModelCustomizations: true,
enableModelSelectorSearch: true,
enableVoiceMode: false,
enableVoiceConciseOutput: true,
},
Expand Down
13 changes: 13 additions & 0 deletions src/patches/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { writeUserMessageDisplay } from './userMessageDisplay';
import { writeInputPatternHighlighters } from './inputPatternHighlighters';
import { writeVerboseProperty } from './verboseProperty';
import { writeModelCustomizations } from './modelSelector';
import { writeModelSelectorSearch } from './modelSelectorSearch';
import { writeOpusplan1m } from './opusplan1m';
import { writeThinkingVisibility } from './thinkingVisibility';
import { writeSubagentModels } from './subagentModels';
Expand Down Expand Up @@ -186,6 +187,12 @@ const PATCH_DEFINITIONS = [
group: PatchGroup.MISC_CONFIGURABLE,
description: 'Show 25 items in select menus instead of default 5',
},
{
id: 'search-model-selector',
name: 'Searchable model selector',
group: PatchGroup.MISC_CONFIGURABLE,
description: 'Makes model selector searchable, with fuzzy matching',
},
{
id: 'context-limit',
name: 'Context limit',
Expand Down Expand Up @@ -625,6 +632,8 @@ export const applyCustomization = async (
// Disabling model customizations should restore both selectors to vanilla CC behavior.
const modelCustomizationsEnabled =
config.settings.misc?.enableModelCustomizations ?? true;
const modelSelectorSearchEnabled =
config.settings.misc?.enableModelSelectorSearch ?? true;
const patchImplementations: Record<PatchId, PatchImplementation> = {
// Always Applied
'verbose-property': {
Expand Down Expand Up @@ -674,6 +683,10 @@ export const applyCustomization = async (
fn: c => writeShowMoreItemsInSelectMenus(c, 25),
condition: modelCustomizationsEnabled,
},
'search-model-selector': {
fn: c => writeModelSelectorSearch(c),
condition: modelCustomizationsEnabled && modelSelectorSearchEnabled,
},
'table-format': {
fn: c => writeTableFormat(c, tableFormat),
condition: tableFormat !== 'default',
Expand Down
38 changes: 38 additions & 0 deletions src/patches/modelCustomizationsToggle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { updateConfigFile } from '../config';
import { replaceFileBreakingHardLinks } from '../utils';
import { restoreClijsFromBackup } from '../installationBackup';
import { writeModelCustomizations } from './modelSelector';
import { writeModelSelectorSearch } from './modelSelectorSearch';
import { writeShowMoreItemsInSelectMenus } from './showMoreItemsInSelectMenus';
import { applySystemPrompts } from './systemPrompts';
import { applyCustomization } from './index';
Expand Down Expand Up @@ -53,6 +54,10 @@ vi.mock('./showMoreItemsInSelectMenus', () => ({
),
}));

vi.mock('./modelSelectorSearch', () => ({
writeModelSelectorSearch: vi.fn((content: string) => `${content}|search`),
}));

vi.mock('./systemPrompts', () => ({
applySystemPrompts: vi.fn(async (content: string) => ({
newContent: content,
Expand All @@ -63,6 +68,7 @@ vi.mock('./systemPrompts', () => ({
const PATCH_IDS = [
'model-customizations',
'show-more-items-in-select-menus',
'search-model-selector',
] as const;

const baseConfig = (): TweakccConfig => ({
Expand Down Expand Up @@ -102,11 +108,14 @@ describe('model customization toggle patch conditions', () => {
const showMoreResult = results.find(
r => r.id === 'show-more-items-in-select-menus'
);
const searchResult = results.find(r => r.id === 'search-model-selector');

expect(modelResult).toMatchObject({ applied: false, skipped: true });
expect(showMoreResult).toMatchObject({ applied: false, skipped: true });
expect(searchResult).toMatchObject({ applied: false, skipped: true });
expect(vi.mocked(writeModelCustomizations)).not.toHaveBeenCalled();
expect(vi.mocked(writeShowMoreItemsInSelectMenus)).not.toHaveBeenCalled();
expect(vi.mocked(writeModelSelectorSearch)).not.toHaveBeenCalled();
expect(vi.mocked(replaceFileBreakingHardLinks)).toHaveBeenCalledWith(
'/tmp/claude-cli.js',
'base-content',
Expand All @@ -126,24 +135,51 @@ describe('model customization toggle patch conditions', () => {
const showMoreResult = results.find(
r => r.id === 'show-more-items-in-select-menus'
);
const searchResult = results.find(r => r.id === 'search-model-selector');

expect(modelResult).toMatchObject({ applied: true, failed: false });
expect(showMoreResult).toMatchObject({ applied: true, failed: false });
expect(searchResult).toMatchObject({ applied: true, failed: false });
expect(vi.mocked(writeModelCustomizations)).toHaveBeenCalledTimes(1);
expect(vi.mocked(writeShowMoreItemsInSelectMenus)).toHaveBeenCalledTimes(1);
expect(vi.mocked(writeModelSelectorSearch)).toHaveBeenCalledTimes(1);
expect(vi.mocked(replaceFileBreakingHardLinks)).toHaveBeenCalledWith(
'/tmp/claude-cli.js',
expect.stringContaining('base-content'),
'patch'
);
});

it('skips only the searchable model picker patch when its toggle is disabled', async () => {
const config = baseConfig();
config.settings.misc.enableModelCustomizations = true;
config.settings.misc.enableModelSelectorSearch = false;

const { results } = await applyCustomization(config, ccInstInfo, [
...PATCH_IDS,
]);

const modelResult = results.find(r => r.id === 'model-customizations');
const showMoreResult = results.find(
r => r.id === 'show-more-items-in-select-menus'
);
const searchResult = results.find(r => r.id === 'search-model-selector');

expect(modelResult).toMatchObject({ applied: true, failed: false });
expect(showMoreResult).toMatchObject({ applied: true, failed: false });
expect(searchResult).toMatchObject({ applied: false, skipped: true });
expect(vi.mocked(writeModelCustomizations)).toHaveBeenCalledTimes(1);
expect(vi.mocked(writeShowMoreItemsInSelectMenus)).toHaveBeenCalledTimes(1);
expect(vi.mocked(writeModelSelectorSearch)).not.toHaveBeenCalled();
});

it('marks patches as failed when patch functions return null', async () => {
const config = baseConfig();
config.settings.misc.enableModelCustomizations = true;

vi.mocked(writeModelCustomizations).mockReturnValue(null);
vi.mocked(writeShowMoreItemsInSelectMenus).mockReturnValue(null);
vi.mocked(writeModelSelectorSearch).mockReturnValue(null);

const { results } = await applyCustomization(config, ccInstInfo, [
...PATCH_IDS,
Expand All @@ -153,9 +189,11 @@ describe('model customization toggle patch conditions', () => {
const showMoreResult = results.find(
r => r.id === 'show-more-items-in-select-menus'
);
const searchResult = results.find(r => r.id === 'search-model-selector');

expect(modelResult).toMatchObject({ applied: false, failed: true });
expect(showMoreResult).toMatchObject({ applied: false, failed: true });
expect(searchResult).toMatchObject({ applied: false, failed: true });
expect(vi.mocked(replaceFileBreakingHardLinks)).toHaveBeenCalledWith(
'/tmp/claude-cli.js',
'base-content',
Expand Down
106 changes: 106 additions & 0 deletions src/patches/modelSelectorSearch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { describe, expect, it, vi } from 'vitest';

import { writeModelSelectorSearch } from './modelSelectorSearch';

describe('writeModelSelectorSearch', () => {
it('supports the Claude Code 2.1.86 model picker shape', () => {
const file =
'[M,D]=SB8.useState(X),P=M8(IYz),[W,Z]=SB8.useState(!1),f=M8(bYz),G;if(K[0]!==f)G=f!==void 0?n26(f):void 0,K[0]=f,K[1]=G;else G=K[1];let[T,V]=SB8.useState(G),N=P??!1,L;' +
'let p=I,B;if(K[14]!==X||K[15]!==p)B=p.some((A6)=>A6.value===X)?X:p[0]?.value??void 0,K[14]=X,K[15]=p,K[16]=B;else B=K[16];' +
'let C=B,F=Math.min(10,p.length),g=Math.max(0,p.length-F),Q;' +
'let E6=A??SYz,T6;if(K[49]!==J6||K[50]!==a||K[51]!==C||K[52]!==X||K[53]!==p||K[54]!==E6||K[55]!==F)T6=zK.createElement(m,{flexDirection:"column"},zK.createElement(J1,{defaultValue:X,defaultFocusValue:C,options:p,onChange:a,onFocus:J6,onCancel:E6,visibleOptionCount:F})),K[49]=J6,K[50]=a,K[51]=C,K[52]=X,K[53]=p,K[54]=E6,K[55]=F,K[56]=T6;else T6=K[56];let R6;if(K[57]!==g)R6=g>0&&zK.createElement(m,{paddingLeft:3},zK.createElement(v,{dimColor:!0},"and ",g," more…")),K[57]=g,K[58]=R6;else R6=K[58];let y6;if(K[59]!==T6||K[60]!==R6)y6=zK.createElement(m,{flexDirection:"column",marginBottom:1},T6,R6),K[59]=T6,K[60]=R6,K[61]=y6;else y6=K[61];' +
'let K8;if(K[69]!==f6||K[70]!==y6||K[71]!==S6||K[72]!==s6)K8=zK.createElement(m,{flexDirection:"column"},f6,y6,S6,s6),K[69]=f6,K[70]=y6,K[71]=S6,K[72]=s6,K[73]=K8;else K8=K[73];';

const result = writeModelSelectorSearch(file);

expect(result).not.toBeNull();
expect(result).toContain('[L7,b7]=SB8.useState("")');
expect(result).toContain('let p=L7.trim()?I.map((A6)=>{');
expect(result).toContain('highlightText:L7');
expect(result).toContain('replace(/[^a-z0-9]/g,"")');
expect(result).toContain('Math.min(25,Math.max(1,p.length))');
expect(result).toContain(
'let n6=zK.createElement(m,{marginTop:1,width:"100%",minWidth:48,flexGrow:1,borderStyle:"round",borderColor:"suggestion",paddingX:1,flexDirection:"row"}'
);
expect(result).toContain('zK.createElement(v,{dimColor:!0},"⌕ ")');
expect(result).toContain(
'createElement(x3,{value:L7,onChange:b7,onSubmit:()=>{if(C!==void 0)a(C)},onExit:E6,placeholder:"Search models..."+" ".repeat(48)'
);
expect(result).toContain(
'onChangeCursorOffset:()=>{},columns:Math.max(42,(process.stdout.columns||80)-10)}'
);
expect(result).toContain(
'let K8=zK.createElement(m,{flexDirection:"column",width:"100%"},f6,y6,S6,s6,n6);'
);
expect(result).not.toContain('searchable:!0');
expect(result).not.toContain('onSearchTextChange:b7');
});

it('supports the Claude Code 2.1.87 model picker shape', () => {
const file =
'[M,D]=SB8.useState(X),P=M8(bYz),[W,Z]=SB8.useState(!1),f=M8(CYz),G;if(K[0]!==f)G=f!==void 0?n26(f):void 0,K[0]=f,K[1]=G;else G=K[1];let[T,V]=SB8.useState(G),N=P??!1,L;' +
'let p=I,B;if(K[14]!==X||K[15]!==p)B=p.some((A6)=>A6.value===X)?X:p[0]?.value??void 0,K[14]=X,K[15]=p,K[16]=B;else B=K[16];' +
'let C=B,F=Math.min(10,p.length),g=Math.max(0,p.length-F),Q;' +
'let E6=A??hYz,T6;if(K[49]!==J6||K[50]!==a||K[51]!==C||K[52]!==X||K[53]!==p||K[54]!==E6||K[55]!==F)T6=zK.createElement(m,{flexDirection:"column"},zK.createElement(J1,{defaultValue:X,defaultFocusValue:C,options:p,onChange:a,onFocus:J6,onCancel:E6,visibleOptionCount:F})),K[49]=J6,K[50]=a,K[51]=C,K[52]=X,K[53]=p,K[54]=E6,K[55]=F,K[56]=T6;else T6=K[56];let R6;if(K[57]!==g)R6=g>0&&zK.createElement(m,{paddingLeft:3},zK.createElement(v,{dimColor:!0},"and ",g," more…")),K[57]=g,K[58]=R6;else R6=K[58];let y6;if(K[59]!==T6||K[60]!==R6)y6=zK.createElement(m,{flexDirection:"column",marginBottom:1},T6,R6),K[59]=T6,K[60]=R6,K[61]=y6;else y6=K[61];' +
'let K8;if(K[69]!==f6||K[70]!==y6||K[71]!==S6||K[72]!==s6)K8=zK.createElement(m,{flexDirection:"column"},f6,y6,S6,s6),K[69]=f6,K[70]=y6,K[71]=S6,K[72]=s6,K[73]=K8;else K8=K[73];';

const result = writeModelSelectorSearch(file);

expect(result).not.toBeNull();
expect(result).toContain('[L7,b7]=SB8.useState("")');
expect(result).toContain(
'[W,Z]=SB8.useState(!1),[L7,b7]=SB8.useState(""),f=M8(CYz),G;if('
);
expect(result).toContain('let p=L7.trim()?I.map((A6)=>{');
expect(result).toContain('highlightText:L7');
expect(result).toContain(
'let E6=A??hYz,T6=zK.createElement(m,{flexDirection:"column"},p.length===0?zK.createElement(v,{dimColor:!0,italic:!0},"No models match'
);
expect(result).toContain(
'let n6=zK.createElement(m,{marginTop:1,width:"100%",minWidth:48,flexGrow:1,borderStyle:"round",borderColor:"suggestion",paddingX:1,flexDirection:"row"}'
);
expect(result).toContain('zK.createElement(v,{dimColor:!0},"⌕ ")');
expect(result).toContain(
'onChangeCursorOffset:()=>{},columns:Math.max(42,(process.stdout.columns||80)-10)}'
);
expect(result).toContain(
'let K8=zK.createElement(m,{flexDirection:"column",width:"100%"},f6,y6,S6,s6,n6);'
);
});

it('returns null for the older pre-2.1.86 model picker shape', () => {
const consoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});

const file =
'[W,Z]=HB8.useState(!1),G=M8(F3Y),f;' +
'let p=I,u;if(K[14]!==X||K[15]!==p)u=p.some((Z6)=>Z6.value===X)?X:p[0]?.value??void 0,K[14]=X,K[15]=p,K[16]=u;else u=K[16];' +
'let C=u,g=Math.min(10,p.length),F=Math.max(0,p.length-g),Q;' +
'let k6=$??p3Y,f6;if(K[49]!==J6||K[50]!==s||K[51]!==C||K[52]!==X||K[53]!==p||K[54]!==k6||K[55]!==g)f6=KK.createElement(B,{flexDirection:"column"},KK.createElement(J1,{defaultValue:X,defaultFocusValue:C,options:p,onChange:s,onFocus:J6,onCancel:k6,visibleOptionCount:g})),K[49]=J6,K[50]=s,K[51]=C,K[52]=X,K[53]=p,K[54]=k6,K[55]=g,K[56]=f6;else f6=K[56];';

try {
expect(writeModelSelectorSearch(file)).toBeNull();
expect(consoleError).toHaveBeenCalledWith(
'patch: modelSelectorSearch: only supported on Claude Code 2.1.86 or 2.1.87'
);
} finally {
consoleError.mockRestore();
}
});

it('returns null when the model picker callsite is missing', () => {
const consoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});

try {
expect(writeModelSelectorSearch('const nope=1;')).toBeNull();
expect(consoleError).toHaveBeenCalledWith(
'patch: modelSelectorSearch: only supported on Claude Code 2.1.86 or 2.1.87'
);
} finally {
consoleError.mockRestore();
}
});
});
Loading