diff --git a/src/main/java/com/github/claudecodegui/handler/core/HandlerContext.java b/src/main/java/com/github/claudecodegui/handler/core/HandlerContext.java
index 4eb1a989..ca5182a6 100644
--- a/src/main/java/com/github/claudecodegui/handler/core/HandlerContext.java
+++ b/src/main/java/com/github/claudecodegui/handler/core/HandlerContext.java
@@ -17,7 +17,7 @@ import java.util.function.Supplier;
*/
public class HandlerContext {
- public static final String DEFAULT_MODEL = "claude-sonnet-4-7";
+ public static final String DEFAULT_MODEL = "claude-sonnet-5";
public static final String DEFAULT_PROVIDER = "claude";
private final Project project;
diff --git a/src/main/java/com/github/claudecodegui/provider/pricing/ClaudePricingTable.java b/src/main/java/com/github/claudecodegui/provider/pricing/ClaudePricingTable.java
index a7695750..9faee7a2 100644
--- a/src/main/java/com/github/claudecodegui/provider/pricing/ClaudePricingTable.java
+++ b/src/main/java/com/github/claudecodegui/provider/pricing/ClaudePricingTable.java
@@ -13,7 +13,7 @@ import java.util.Map;
*/
public final class ClaudePricingTable {
- public static final String DEFAULT_MODEL = "claude-sonnet-4-7";
+ public static final String DEFAULT_MODEL = "claude-sonnet-5";
private static final ClaudePricing DEFAULT_PRICING = new ClaudePricing(3.0, 15.0, 3.75, 0.30);
private static final ClaudePricing TIERED_SONNET_PRICING = new ClaudePricing(3.0, 15.0, 3.75, 0.30, 6.0, 22.5, 7.5, 0.60);
diff --git a/src/main/java/com/github/claudecodegui/session/SessionState.java b/src/main/java/com/github/claudecodegui/session/SessionState.java
index 650ddd30..b327fa33 100644
--- a/src/main/java/com/github/claudecodegui/session/SessionState.java
+++ b/src/main/java/com/github/claudecodegui/session/SessionState.java
@@ -83,7 +83,7 @@ public class SessionState {
// explicit, informed opt-in — see security remediation A: shipping bypass as the
// out-of-the-box default removed the only confirmation gate for AI-issued commands.
private volatile String permissionMode = "default";
- private volatile String model = "claude-sonnet-4-7";
+ private volatile String model = "claude-sonnet-5";
private volatile String provider = "claude";
// Reasoning effort (thinking depth). Null means "do not override SDK/settings".
private volatile String reasoningEffort = null;
@@ -213,7 +213,49 @@ public class SessionState {
}
public void setModel(String model) {
- this.model = model;
+ this.model = normalizeRetiredModelId(model);
+ }
+
+ /**
+ * Migrate retired Claude model ids to their live replacement on write.
+ *
+ *
Persisted tab state (.idea/claudeCodeTabState.xml) and history sessions keep
+ * whatever model id was saved forever. When a model is retired from the API
+ * (sonnet-4-6, sonnet-4-7, ...), restoring such a tab would otherwise spawn a CLI
+ * pinned to a dead model that fails on every send ("It may not exist or you may
+ * not have access to it") - see #1678. Migrating here self-heals restored tabs
+ * without touching the persisted XML.
+ *
+ * @param model raw model id (may be null, blank, carry a [1m] suffix, or be retired)
+ * @return the model id to store - retired ids mapped to their live replacement,
+ * anything else (including non-Claude ids) passed through unchanged
+ */
+ static String normalizeRetiredModelId(String model) {
+ if (model == null) {
+ return null;
+ }
+ String trimmed = model.trim();
+ if (trimmed.isEmpty()) {
+ return model;
+ }
+ String base = trimmed;
+ boolean oneM = false;
+ if (base.endsWith("[1m]")) {
+ base = base.substring(0, base.length() - "[1m]".length());
+ oneM = true;
+ }
+ switch (base) {
+ case "claude-sonnet-4-6":
+ case "claude-sonnet-4-7":
+ base = "claude-sonnet-5";
+ break;
+ case "claude-opus-4-6":
+ base = "claude-opus-4-8";
+ break;
+ default:
+ return trimmed;
+ }
+ return oneM ? base + "[1m]" : base;
}
public void setProvider(String provider) {
diff --git a/src/main/java/com/github/claudecodegui/ui/ChatWindowDelegate.java b/src/main/java/com/github/claudecodegui/ui/ChatWindowDelegate.java
index 6c53c571..23266586 100644
--- a/src/main/java/com/github/claudecodegui/ui/ChatWindowDelegate.java
+++ b/src/main/java/com/github/claudecodegui/ui/ChatWindowDelegate.java
@@ -440,7 +440,7 @@ public class ChatWindowDelegate {
String mode = session != null ? session.getPermissionMode() : "default";
com.github.claudecodegui.notifications.ClaudeNotifier.setMode(project, mode);
- String model = session != null ? session.getModel() : "claude-sonnet-4-7";
+ String model = session != null ? session.getModel() : "claude-sonnet-5";
com.github.claudecodegui.notifications.ClaudeNotifier.setModel(project, model);
try {
diff --git a/src/test/java/com/github/claudecodegui/session/SessionStateTest.java b/src/test/java/com/github/claudecodegui/session/SessionStateTest.java
new file mode 100644
index 00000000..af3b8dee
--- /dev/null
+++ b/src/test/java/com/github/claudecodegui/session/SessionStateTest.java
@@ -0,0 +1,75 @@
+package com.github.claudecodegui.session;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Regression tests for retired Claude model id migration on session state writes
+ * (persisted tab / history restore self-heal) - see #1678.
+ */
+public class SessionStateTest {
+
+ @Test
+ public void setModelMigratesRetiredSonnet47ToSonnet5() {
+ SessionState state = new SessionState();
+ // Saved by versions <= 0.5.2 where sonnet-4-7 was the default model.
+ state.setModel("claude-sonnet-4-7");
+ Assert.assertEquals("claude-sonnet-5", state.getModel());
+ }
+
+ @Test
+ public void setModelMigratesRetiredSonnet46ToSonnet5() {
+ SessionState state = new SessionState();
+ state.setModel("claude-sonnet-4-6");
+ Assert.assertEquals("claude-sonnet-5", state.getModel());
+ }
+
+ @Test
+ public void setModelMigratesRetiredOpus46ToOpus48() {
+ SessionState state = new SessionState();
+ state.setModel("claude-opus-4-6");
+ Assert.assertEquals("claude-opus-4-8", state.getModel());
+ }
+
+ @Test
+ public void setModelPreserves1MSuffixWhenMigrating() {
+ SessionState state = new SessionState();
+ state.setModel("claude-sonnet-4-7[1m]");
+ Assert.assertEquals("claude-sonnet-5[1m]", state.getModel());
+ }
+
+ @Test
+ public void setModelLeavesLiveModelsUntouched() {
+ SessionState state = new SessionState();
+ state.setModel("claude-sonnet-5");
+ Assert.assertEquals("claude-sonnet-5", state.getModel());
+ state.setModel("claude-opus-4-8[1m]");
+ Assert.assertEquals("claude-opus-4-8[1m]", state.getModel());
+ }
+
+ @Test
+ public void setModelLeavesNonClaudeAndUnknownIdsUntouched() {
+ SessionState state = new SessionState();
+ // Non-Claude provider models must pass through unchanged.
+ state.setModel("gpt-5.6-sol");
+ Assert.assertEquals("gpt-5.6-sol", state.getModel());
+ state.setModel("qwen3.5-plus");
+ Assert.assertEquals("qwen3.5-plus", state.getModel());
+ }
+
+ @Test
+ public void setModelHandlesNullAndBlank() {
+ SessionState state = new SessionState();
+ state.setModel(null);
+ Assert.assertNull(state.getModel());
+ state.setModel(" ");
+ Assert.assertEquals(" ", state.getModel());
+ }
+
+ @Test
+ public void defaultModelIsTheLiveSonnet5() {
+ SessionState state = new SessionState();
+ // The initial value must never be a retired id (#1678).
+ Assert.assertEquals("claude-sonnet-5", state.getModel());
+ }
+}
diff --git a/webview/src/components/ChatInputBox/ButtonArea.tsx b/webview/src/components/ChatInputBox/ButtonArea.tsx
index 99c74349..04b26885 100644
--- a/webview/src/components/ChatInputBox/ButtonArea.tsx
+++ b/webview/src/components/ChatInputBox/ButtonArea.tsx
@@ -1,6 +1,7 @@
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import type { ButtonAreaProps, CodexFastMode, ModelInfo, PermissionMode, ReasoningEffort } from './types';
+import { DEFAULT_CLAUDE_MODEL_ID } from './types';
import { CodexFastModeSelect, ConfigSelect, ModelSelect, ModeSelect, ProviderSelect, ReasoningSelect } from './selectors';
import { STORAGE_KEYS, validateCodexCustomModels } from '../../types/provider';
import type { CodexCustomModel } from '../../types/provider';
@@ -73,7 +74,7 @@ export const ButtonArea = ({
hasInputContent = false,
isLoading = false,
isEnhancing = false,
- selectedModel = 'claude-sonnet-4-7',
+ selectedModel = DEFAULT_CLAUDE_MODEL_ID,
permissionMode = 'default',
currentProvider = 'claude',
reasoningEffort = 'high',
diff --git a/webview/src/components/ChatInputBox/ChatInputBox.tsx b/webview/src/components/ChatInputBox/ChatInputBox.tsx
index 084f2eb2..53a82e9f 100644
--- a/webview/src/components/ChatInputBox/ChatInputBox.tsx
+++ b/webview/src/components/ChatInputBox/ChatInputBox.tsx
@@ -14,6 +14,7 @@ import type {
ChatInputBoxProps,
PermissionMode,
} from './types.js';
+import { DEFAULT_CLAUDE_MODEL_ID } from './types.js';
import { ChatInputBoxHeader } from './ChatInputBoxHeader.js';
import { ChatInputBoxFooter } from './ChatInputBoxFooter.js';
import { ResizeHandles } from './ResizeHandles.js';
@@ -76,7 +77,7 @@ export const ChatInputBox = memo(forwardRef {
'claude-opus-5',
'claude-opus-4-8',
'claude-sonnet-5',
- 'claude-sonnet-4-7',
'claude-haiku-4-5',
]);
const ids = CLAUDE_MODELS.map((model) => model.id);
expect(ids).not.toContain('claude-opus-4-7');
expect(ids).not.toContain('claude-opus-4-6');
expect(ids).not.toContain('claude-sonnet-4-6');
+ expect(ids).not.toContain('claude-sonnet-4-7');
expect(ids.some((id) => id.endsWith('[1m]'))).toBe(false);
});
diff --git a/webview/src/components/ChatInputBox/selectors/ModelSelect.tsx b/webview/src/components/ChatInputBox/selectors/ModelSelect.tsx
index e33bb29f..4ade8efa 100644
--- a/webview/src/components/ChatInputBox/selectors/ModelSelect.tsx
+++ b/webview/src/components/ChatInputBox/selectors/ModelSelect.tsx
@@ -69,7 +69,6 @@ const DEFAULT_MODEL_MAP: Record = AVAILABLE_MODELS.reduce(
const MODEL_LABEL_KEYS: Record = {
'claude-opus-5': 'models.claude.opus5.label',
'claude-sonnet-5': 'models.claude.sonnet5.label',
- 'claude-sonnet-4-7': 'models.claude.sonnet47.label',
'claude-sonnet-4-6': 'models.claude.sonnet46.label',
'claude-fable-5': 'models.claude.fable5.label',
'claude-opus-4-8': 'models.claude.opus48.label',
@@ -89,7 +88,6 @@ const MODEL_LABEL_KEYS: Record = {
const MODEL_DESCRIPTION_KEYS: Record = {
'claude-opus-5': 'models.claude.opus5.description',
'claude-sonnet-5': 'models.claude.sonnet5.description',
- 'claude-sonnet-4-7': 'models.claude.sonnet47.description',
'claude-sonnet-4-6': 'models.claude.sonnet46.description',
'claude-fable-5': 'models.claude.fable5.description',
'claude-opus-4-8': 'models.claude.opus48.description',
diff --git a/webview/src/components/ChatInputBox/types.test.ts b/webview/src/components/ChatInputBox/types.test.ts
index 364e0fd5..a5839d94 100644
--- a/webview/src/components/ChatInputBox/types.test.ts
+++ b/webview/src/components/ChatInputBox/types.test.ts
@@ -20,7 +20,12 @@ describe('normalizeClaudeModelId', () => {
it('migrates retired Sonnet 4.6 to the current default', () => {
// Saved by versions <= 0.4.7 where sonnet-4-6 was the default model.
- expect(normalizeClaudeModelId('claude-sonnet-4-6')).toBe('claude-sonnet-4-7');
+ expect(normalizeClaudeModelId('claude-sonnet-4-6')).toBe('claude-sonnet-5');
+ });
+
+ it('migrates retired Sonnet 4.7 to the current default', () => {
+ // Saved by versions <= 0.5.2 where sonnet-4-7 was the default model (#1678).
+ expect(normalizeClaudeModelId('claude-sonnet-4-7')).toBe('claude-sonnet-5');
});
it('migrates retired Opus 4.6 to Opus 4.8', () => {
@@ -28,7 +33,8 @@ describe('normalizeClaudeModelId', () => {
});
it('migrates retired IDs carrying a [1m] suffix', () => {
- expect(normalizeClaudeModelId('claude-sonnet-4-6[1m]')).toBe('claude-sonnet-4-7');
+ expect(normalizeClaudeModelId('claude-sonnet-4-6[1m]')).toBe('claude-sonnet-5');
+ expect(normalizeClaudeModelId('claude-sonnet-4-7[1m]')).toBe('claude-sonnet-5');
expect(normalizeClaudeModelId('claude-opus-4-6[1m]')).toBe('claude-opus-4-8');
});
diff --git a/webview/src/components/ChatInputBox/types.ts b/webview/src/components/ChatInputBox/types.ts
index 625f2963..4490902b 100644
--- a/webview/src/components/ChatInputBox/types.ts
+++ b/webview/src/components/ChatInputBox/types.ts
@@ -301,15 +301,18 @@ export function strip1MContextSuffix(modelId: string | undefined | null): string
* CLAUDE_MODELS[0], which is the newest tier and the most likely to be missing
* from a user's API relay.
*/
-export const DEFAULT_CLAUDE_MODEL_ID = 'claude-sonnet-4-7';
+export const DEFAULT_CLAUDE_MODEL_ID = 'claude-sonnet-5';
/**
- * Retired model IDs → their current-generation replacement. Lookup happens after
+ * Retired model IDs -> their current-generation replacement. Lookup happens after
* the [1m] suffix is stripped, so keys must be base IDs. Without an entry here a
* saved retired model fails validation and silently resets to the fallback.
+ * Retired ids must always map to a LIVE model - mapping one retired id to another
+ * (sonnet-4-6 -> sonnet-4-7) kept restoring tabs pinned to a dead model (#1678).
*/
const LEGACY_CLAUDE_MODEL_ID_ALIASES: Record = {
- 'claude-sonnet-4-6': 'claude-sonnet-4-7',
+ 'claude-sonnet-4-6': 'claude-sonnet-5',
+ 'claude-sonnet-4-7': 'claude-sonnet-5',
'claude-opus-4-6': 'claude-opus-4-8',
};
@@ -345,12 +348,7 @@ export const CLAUDE_MODELS: ModelInfo[] = [
{
id: 'claude-sonnet-5',
label: 'Sonnet 5',
- description: 'Sonnet 5 · Upgraded Sonnet model',
- },
- {
- id: 'claude-sonnet-4-7',
- label: 'Sonnet 4.7',
- description: 'Sonnet 4.7 · Use the default model',
+ description: 'Sonnet 5 · Use the default model',
},
{
id: 'claude-haiku-4-5',
diff --git a/webview/src/hooks/providers/useModelStatePersistence.test.ts b/webview/src/hooks/providers/useModelStatePersistence.test.ts
index b3aad1bf..b8f8faa7 100644
--- a/webview/src/hooks/providers/useModelStatePersistence.test.ts
+++ b/webview/src/hooks/providers/useModelStatePersistence.test.ts
@@ -207,9 +207,9 @@ describe('useModelStatePersistence — retired model migration', () => {
renderHook(() => useModelStatePersistence(makeOptions({ setSelectedClaudeModel })));
vi.advanceTimersByTime(200);
- expect(setSelectedClaudeModel).toHaveBeenCalledWith('claude-sonnet-4-7');
+ expect(setSelectedClaudeModel).toHaveBeenCalledWith('claude-sonnet-5');
expect(setSelectedClaudeModel).not.toHaveBeenCalledWith('claude-fable-5');
- expect(bridgeEventsFor('set_model')).toEqual([['set_model', 'claude-sonnet-4-7']]);
+ expect(bridgeEventsFor('set_model')).toEqual([['set_model', 'claude-sonnet-5']]);
});
it('migrates a backend-supplied retired model via __INITIAL_TAB_MODEL__', () => {
@@ -225,8 +225,8 @@ describe('useModelStatePersistence — retired model migration', () => {
renderHook(() => useModelStatePersistence(makeOptions({ setSelectedClaudeModel })));
vi.advanceTimersByTime(200);
- expect(setSelectedClaudeModel).toHaveBeenCalledWith('claude-sonnet-4-7');
- expect(bridgeEventsFor('set_model')).toEqual([['set_model', 'claude-sonnet-4-7']]);
+ expect(setSelectedClaudeModel).toHaveBeenCalledWith('claude-sonnet-5');
+ expect(bridgeEventsFor('set_model')).toEqual([['set_model', 'claude-sonnet-5']]);
});
it('falls back to the default model (not the list head) for unrecognized saved models', () => {
diff --git a/webview/src/i18n/locales/en.json b/webview/src/i18n/locales/en.json
index 25641556..08062640 100644
--- a/webview/src/i18n/locales/en.json
+++ b/webview/src/i18n/locales/en.json
@@ -1915,11 +1915,7 @@
},
"sonnet5": {
"label": "Sonnet 5",
- "description": "Sonnet 5 · Upgraded Sonnet model"
- },
- "sonnet47": {
- "label": "Sonnet 4.7",
- "description": "Sonnet 4.7 · Use the default model"
+ "description": "Sonnet 5 · Use the default model"
},
"opus5": {
"label": "Opus 5",
diff --git a/webview/src/i18n/locales/es.json b/webview/src/i18n/locales/es.json
index 02d6b1d9..e53f4bcb 100644
--- a/webview/src/i18n/locales/es.json
+++ b/webview/src/i18n/locales/es.json
@@ -1539,11 +1539,7 @@
},
"sonnet5": {
"label": "Sonnet 5",
- "description": "Sonnet 5 · Modelo mejorado de Sonnet 4.6"
- },
- "sonnet47": {
- "label": "Sonnet 4.7",
- "description": "Sonnet 4.7 · Modelo recomendado por defecto"
+ "description": "Sonnet 5 · Modelo recomendado por defecto"
},
"opus5": {
"label": "Opus 5",
diff --git a/webview/src/i18n/locales/fr.json b/webview/src/i18n/locales/fr.json
index c09907b5..149d0636 100644
--- a/webview/src/i18n/locales/fr.json
+++ b/webview/src/i18n/locales/fr.json
@@ -1539,11 +1539,7 @@
},
"sonnet5": {
"label": "Sonnet 5",
- "description": "Sonnet 5 · Modèle amélioré de Sonnet 4.6"
- },
- "sonnet47": {
- "label": "Sonnet 4.7",
- "description": "Sonnet 4.7 · Modèle recommandé par défaut"
+ "description": "Sonnet 5 · Modèle recommandé par défaut"
},
"opus5": {
"label": "Opus 5",
diff --git a/webview/src/i18n/locales/hi.json b/webview/src/i18n/locales/hi.json
index e92bd858..a6144d5c 100644
--- a/webview/src/i18n/locales/hi.json
+++ b/webview/src/i18n/locales/hi.json
@@ -1538,11 +1538,7 @@
},
"sonnet5": {
"label": "Sonnet 5",
- "description": "Sonnet 5 · Sonnet 4.6 का अपग्रेड मॉडल"
- },
- "sonnet47": {
- "label": "Sonnet 4.7",
- "description": "Sonnet 4.7 · डिफ़ॉल्ट अनुशंसित मॉडल"
+ "description": "Sonnet 5 · डिफ़ॉल्ट अनुशंसित मॉडल"
},
"opus5": {
"label": "Opus 5",
diff --git a/webview/src/i18n/locales/ja.json b/webview/src/i18n/locales/ja.json
index 75d24055..3153c783 100644
--- a/webview/src/i18n/locales/ja.json
+++ b/webview/src/i18n/locales/ja.json
@@ -1550,11 +1550,7 @@
},
"sonnet5": {
"label": "Sonnet 5",
- "description": "Sonnet 5 · Sonnet 4.6 のアップグレードモデル"
- },
- "sonnet47": {
- "label": "Sonnet 4.7",
- "description": "Sonnet 4.7 · デフォルトモデルを使用"
+ "description": "Sonnet 5 · デフォルトモデルを使用"
},
"opus5": {
"label": "Opus 5",
diff --git a/webview/src/i18n/locales/ko.json b/webview/src/i18n/locales/ko.json
index 3cd78903..2724b8df 100644
--- a/webview/src/i18n/locales/ko.json
+++ b/webview/src/i18n/locales/ko.json
@@ -1634,11 +1634,7 @@
},
"sonnet5": {
"label": "Sonnet 5",
- "description": "Sonnet 5 · Sonnet 4.6 업그레이드 모델"
- },
- "sonnet47": {
- "label": "Sonnet 4.7",
- "description": "Sonnet 4.7 · 기본 모델 사용"
+ "description": "Sonnet 5 · 기본 모델 사용"
},
"opus5": {
"label": "Opus 5",
diff --git a/webview/src/i18n/locales/pt-BR.json b/webview/src/i18n/locales/pt-BR.json
index 4a3d3aae..f2729d0b 100644
--- a/webview/src/i18n/locales/pt-BR.json
+++ b/webview/src/i18n/locales/pt-BR.json
@@ -1689,11 +1689,7 @@
},
"sonnet5": {
"label": "Sonnet 5",
- "description": "Sonnet 5 · Modelo aprimorado do Sonnet 4.6"
- },
- "sonnet47": {
- "label": "Sonnet 4.7",
- "description": "Sonnet 4.7 · Usar o modelo padrão"
+ "description": "Sonnet 5 · Usar o modelo padrão"
},
"opus5": {
"label": "Opus 5",
diff --git a/webview/src/i18n/locales/ru.json b/webview/src/i18n/locales/ru.json
index 9d26b015..33f73794 100644
--- a/webview/src/i18n/locales/ru.json
+++ b/webview/src/i18n/locales/ru.json
@@ -1565,11 +1565,7 @@
},
"sonnet5": {
"label": "Sonnet 5",
- "description": "Sonnet 5 · Обновленная модель Sonnet 4.6"
- },
- "sonnet47": {
- "label": "Sonnet 4.7",
- "description": "Sonnet 4.7 · Модель по умолчанию"
+ "description": "Sonnet 5 · Модель по умолчанию"
},
"opus5": {
"label": "Opus 5",
diff --git a/webview/src/i18n/locales/zh-TW.json b/webview/src/i18n/locales/zh-TW.json
index 971c7f67..e6a046b7 100644
--- a/webview/src/i18n/locales/zh-TW.json
+++ b/webview/src/i18n/locales/zh-TW.json
@@ -1544,11 +1544,7 @@
},
"sonnet5": {
"label": "Sonnet 5",
- "description": "Sonnet 5 · Sonnet 4.6 升級模型"
- },
- "sonnet47": {
- "label": "Sonnet 4.7",
- "description": "Sonnet 4.7 · 預設推薦模型"
+ "description": "Sonnet 5 · 預設推薦模型"
},
"opus5": {
"label": "Opus 5",
diff --git a/webview/src/i18n/locales/zh.json b/webview/src/i18n/locales/zh.json
index 7292a409..96b76035 100644
--- a/webview/src/i18n/locales/zh.json
+++ b/webview/src/i18n/locales/zh.json
@@ -1920,11 +1920,7 @@
},
"sonnet5": {
"label": "Sonnet 5",
- "description": "Sonnet 5 · Sonnet 4.6 升级模型"
- },
- "sonnet47": {
- "label": "Sonnet 4.7",
- "description": "Sonnet 4.7 · 默认推荐模型"
+ "description": "Sonnet 5 · 默认推荐模型"
},
"opus5": {
"label": "Opus 5",