kanna-code 0.41.5 → 0.41.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,7 +25,7 @@
25
25
  })()
26
26
  </script>
27
27
  <title>Kanna</title>
28
- <script type="module" crossorigin src="/assets/index-CTT6qioa.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-DqIqYlzq.js"></script>
29
29
  <link rel="stylesheet" crossorigin href="/assets/index-BujIAIt0.css">
30
30
  </head>
31
31
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kanna-code",
3
3
  "type": "module",
4
- "version": "0.41.5",
4
+ "version": "0.41.6",
5
5
  "description": "A beautiful web UI for Claude Code",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -48,7 +48,8 @@
48
48
  "prepublishOnly": "bun run build"
49
49
  },
50
50
  "dependencies": {
51
- "@anthropic-ai/claude-agent-sdk": "^0.2.126",
51
+ "@anthropic-ai/claude-agent-sdk": "^0.3.170",
52
+ "@anthropic-ai/sdk": "^0.104.0",
52
53
  "@legendapp/list": "3.0.0-beta.44",
53
54
  "@pierre/diffs": "^1.1.12",
54
55
  "@radix-ui/react-context-menu": "^2.2.16",
@@ -63,7 +64,8 @@
63
64
  "file-type": "^22.0.0",
64
65
  "openai": "^6.34.0",
65
66
  "react-resizable-panels": "^4.7.3",
66
- "uqr": "^0.1.3"
67
+ "uqr": "^0.1.3",
68
+ "zod": "^4.4.3"
67
69
  },
68
70
  "devDependencies": {
69
71
  "@dnd-kit/core": "^6.3.1",
@@ -20,6 +20,8 @@ import { CodexAppServerManager } from "./codex-app-server"
20
20
  import { type GenerateChatTitleResult, generateTitleForChatDetailed } from "./generate-title"
21
21
  import type { HarnessEvent, HarnessToolRequest, HarnessTurn } from "./harness-types"
22
22
  import {
23
+ applyClaudeSdkModels,
24
+ type ClaudeSdkModelInfo,
23
25
  codexServiceTierFromModelOptions,
24
26
  getServerProviderCatalog,
25
27
  normalizeClaudeModelOptions,
@@ -82,6 +84,7 @@ interface ClaudeSessionHandle {
82
84
  sendPrompt: (content: string) => Promise<void>
83
85
  setModel: (model: string) => Promise<void>
84
86
  setPermissionMode: (planMode: boolean) => Promise<void>
87
+ supportedModels?: () => Promise<ClaudeSdkModelInfo[]>
85
88
  }
86
89
 
87
90
  interface ClaudeSessionState {
@@ -664,6 +667,7 @@ async function startClaudeSession(args: {
664
667
  setPermissionMode: async (planMode: boolean) => {
665
668
  await q.setPermissionMode(planMode ? "plan" : "acceptEdits")
666
669
  },
670
+ supportedModels: async () => await q.supportedModels(),
667
671
  close: () => {
668
672
  promptQueue.close()
669
673
  q.close()
@@ -718,6 +722,20 @@ export class AgentCoordinator {
718
722
  this.onStateChange(chatId, options)
719
723
  }
720
724
 
725
+ private refreshClaudeModelCatalog(session: ClaudeSessionHandle) {
726
+ if (!session.supportedModels) return
727
+ void session.supportedModels()
728
+ .then((models) => {
729
+ if (applyClaudeSdkModels(models)) {
730
+ this.emitStateChange(undefined, { immediate: true })
731
+ }
732
+ })
733
+ .catch((error) => {
734
+ const message = error instanceof Error ? error.message : String(error)
735
+ this.reportBackgroundError?.(`[claude-models] failed to refresh Claude model catalog: ${message}`)
736
+ })
737
+ }
738
+
721
739
  getActiveTurnProfile(chatId: string): SendToStartingProfile | null {
722
740
  const active = this.activeTurns.get(chatId)
723
741
  if (!active?.clientTraceId || active.profilingStartedAt === undefined) {
@@ -1078,6 +1096,7 @@ export class AgentCoordinator {
1078
1096
  forkSession: args.forkSession,
1079
1097
  onToolRequest: args.onToolRequest,
1080
1098
  })
1099
+ this.refreshClaudeModelCatalog(started)
1081
1100
 
1082
1101
  session = {
1083
1102
  id: crypto.randomUUID(),
@@ -36,7 +36,7 @@ function expectedSettingsSnapshot(filePath: string, overrides: Partial<AppSettin
36
36
  defaultProvider: "last_used",
37
37
  providerDefaults: {
38
38
  claude: {
39
- model: "claude-opus-4-7",
39
+ model: "claude-opus-4-8",
40
40
  modelOptions: {
41
41
  reasoningEffort: "high",
42
42
  contextWindow: "200k",
@@ -99,7 +99,7 @@ function getDefaultEditorCommandTemplate(preset: EditorPreset) {
99
99
  function createDefaultProviderDefaults(): ChatProviderPreferences {
100
100
  return {
101
101
  claude: {
102
- model: "claude-opus-4-7",
102
+ model: "claude-opus-4-8",
103
103
  modelOptions: { ...DEFAULT_CLAUDE_MODEL_OPTIONS },
104
104
  planMode: false,
105
105
  },
@@ -1,15 +1,22 @@
1
- import { describe, expect, test } from "bun:test"
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
2
  import {
3
+ SERVER_PROVIDERS,
4
+ applyClaudeSdkModels,
3
5
  codexServiceTierFromModelOptions,
4
6
  normalizeClaudeModelOptions,
5
7
  normalizeCodexModelOptions,
6
8
  normalizeServerModel,
9
+ resetServerProvidersForTests,
7
10
  } from "./provider-catalog"
8
11
  import { resolveClaudeApiModelId } from "../shared/types"
9
12
 
10
13
  describe("provider catalog normalization", () => {
14
+ afterEach(() => {
15
+ resetServerProvidersForTests()
16
+ })
17
+
11
18
  test("maps legacy Claude effort into shared model options", () => {
12
- expect(normalizeClaudeModelOptions("claude-opus-4-7", undefined, "max")).toEqual({
19
+ expect(normalizeClaudeModelOptions("claude-opus-4-8", undefined, "max")).toEqual({
13
20
  reasoningEffort: "max",
14
21
  contextWindow: "200k",
15
22
  })
@@ -58,12 +65,26 @@ describe("provider catalog normalization", () => {
58
65
 
59
66
  test("normalizes server model ids through the shared alias catalog", () => {
60
67
  expect(normalizeServerModel("codex")).toBe("gpt-5.5")
61
- expect(normalizeServerModel("claude", "opus")).toBe("claude-opus-4-7")
68
+ expect(normalizeServerModel("claude", "fable")).toBe("fable")
69
+ expect(normalizeServerModel("claude", "opus")).toBe("claude-opus-4-8")
62
70
  expect(normalizeServerModel("codex", "gpt-5-codex")).toBe("gpt-5.3-codex")
63
71
  })
64
72
 
65
73
  test("resolves Claude API model ids for 1m context window", () => {
66
- expect(resolveClaudeApiModelId("claude-opus-4-7", "1m")).toBe("claude-opus-4-7[1m]")
74
+ expect(resolveClaudeApiModelId("claude-opus-4-8", "1m")).toBe("claude-opus-4-8[1m]")
75
+ expect(resolveClaudeApiModelId("fable", "200k")).toBe("fable")
67
76
  expect(resolveClaudeApiModelId("claude-sonnet-4-6", "200k")).toBe("claude-sonnet-4-6")
68
77
  })
78
+
79
+ test("overlays Claude model labels from the Agent SDK model catalog", () => {
80
+ expect(applyClaudeSdkModels([
81
+ { value: "claude-fable-5[1m]", displayName: "Fable from SDK", supportsEffort: true },
82
+ { value: "claude-opus-4-7", displayName: "Opus 4.7", supportsEffort: true },
83
+ { value: "claude-opus-4-8", displayName: "Opus from SDK", supportsEffort: true },
84
+ ])).toBe(true)
85
+
86
+ const claude = SERVER_PROVIDERS.find((provider) => provider.id === "claude")
87
+ expect(claude?.models.find((model) => model.id === "fable")?.label).toBe("Fable from SDK")
88
+ expect(claude?.models.find((model) => model.id === "claude-opus-4-8")?.label).toBe("Opus from SDK")
89
+ })
69
90
  })
@@ -25,15 +25,84 @@ const HARD_CODED_CODEX_MODELS: ProviderModelOption[] = [
25
25
  { id: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark", supportsEffort: false },
26
26
  ]
27
27
 
28
- export const SERVER_PROVIDERS: ProviderCatalogEntry[] = PROVIDERS.map((provider) =>
29
- provider.id === "codex"
30
- ? {
31
- ...provider,
32
- defaultModel: "gpt-5.5",
33
- models: HARD_CODED_CODEX_MODELS,
34
- }
35
- : provider
36
- )
28
+ export interface ClaudeSdkModelInfo {
29
+ value: string
30
+ displayName?: string
31
+ description?: string
32
+ supportsEffort?: boolean
33
+ supportedEffortLevels?: readonly string[]
34
+ supportsAdaptiveThinking?: boolean
35
+ }
36
+
37
+ function createServerProviders(): ProviderCatalogEntry[] {
38
+ return PROVIDERS.map((provider) =>
39
+ provider.id === "codex"
40
+ ? {
41
+ ...provider,
42
+ defaultModel: "gpt-5.5",
43
+ models: HARD_CODED_CODEX_MODELS,
44
+ }
45
+ : provider
46
+ )
47
+ }
48
+
49
+ export const SERVER_PROVIDERS: ProviderCatalogEntry[] = createServerProviders()
50
+
51
+ export function resetServerProvidersForTests() {
52
+ SERVER_PROVIDERS.splice(0, SERVER_PROVIDERS.length, ...createServerProviders())
53
+ }
54
+
55
+ function modelFamily(value: string) {
56
+ const match = value.match(/^(?:claude-)?([a-z]+)(?:-|$)/i)
57
+ return match?.[1]?.toLowerCase() ?? value.toLowerCase()
58
+ }
59
+
60
+ function sdkModelMatchScore(model: ClaudeSdkModelInfo, option: ProviderModelOption) {
61
+ const modelValue = model.value.toLowerCase()
62
+ if (modelValue === option.id.toLowerCase()) return 3
63
+ if (option.aliases?.some((alias) => alias.toLowerCase() === modelValue)) return 2
64
+ const optionKeys = [option.id, ...(option.aliases ?? [])].map(modelFamily)
65
+ return optionKeys.includes(modelFamily(model.value)) ? 1 : 0
66
+ }
67
+
68
+ function findSdkModelForOption(models: readonly ClaudeSdkModelInfo[], option: ProviderModelOption) {
69
+ let bestModel: ClaudeSdkModelInfo | undefined
70
+ let bestScore = 0
71
+ for (const model of models) {
72
+ const score = sdkModelMatchScore(model, option)
73
+ if (score > bestScore) {
74
+ bestModel = model
75
+ bestScore = score
76
+ }
77
+ }
78
+ return bestModel
79
+ }
80
+
81
+ export function applyClaudeSdkModels(models: readonly ClaudeSdkModelInfo[]) {
82
+ const claudeIndex = SERVER_PROVIDERS.findIndex((provider) => provider.id === "claude")
83
+ const claudeProvider = SERVER_PROVIDERS[claudeIndex]
84
+ if (!claudeProvider) return false
85
+
86
+ const nextModels = claudeProvider.models.map((option) => {
87
+ const sdkModel = findSdkModelForOption(models, option)
88
+ if (!sdkModel) return option
89
+ return {
90
+ ...option,
91
+ label: sdkModel.displayName?.trim() || option.label,
92
+ supportsEffort: sdkModel.supportsEffort ?? option.supportsEffort,
93
+ }
94
+ })
95
+
96
+ if (JSON.stringify(nextModels) === JSON.stringify(claudeProvider.models)) {
97
+ return false
98
+ }
99
+
100
+ SERVER_PROVIDERS.splice(claudeIndex, 1, {
101
+ ...claudeProvider,
102
+ models: nextModels,
103
+ })
104
+ return true
105
+ }
37
106
 
38
107
  export function getServerProviderCatalog(provider: AgentProvider): ProviderCatalogEntry {
39
108
  const entry = SERVER_PROVIDERS.find((candidate) => candidate.id === provider)
@@ -87,7 +87,7 @@ const DEFAULT_APP_SETTINGS_SNAPSHOT: AppSettingsSnapshot = {
87
87
  defaultProvider: "last_used",
88
88
  providerDefaults: {
89
89
  claude: {
90
- model: "claude-opus-4-7",
90
+ model: "claude-opus-4-8",
91
91
  modelOptions: {
92
92
  reasoningEffort: "high",
93
93
  contextWindow: "200k",
@@ -465,7 +465,7 @@ export function createWsRouter({
465
465
  defaultProvider: "last_used",
466
466
  providerDefaults: {
467
467
  claude: {
468
- model: "claude-opus-4-7",
468
+ model: "claude-opus-4-8",
469
469
  modelOptions: {
470
470
  reasoningEffort: "high",
471
471
  contextWindow: "200k",
@@ -1,13 +1,21 @@
1
1
  import { describe, expect, test } from "bun:test"
2
2
  import {
3
+ deriveClaudeModelLabel,
3
4
  normalizeClaudeModelId,
4
5
  normalizeCodexModelId,
5
6
  supportsClaudeMaxReasoningEffort,
6
7
  } from "./types"
7
8
 
8
9
  describe("shared model normalization", () => {
10
+ test("derives fallback Claude model labels from model ids", () => {
11
+ expect(deriveClaudeModelLabel("fable")).toBe("Fable")
12
+ expect(deriveClaudeModelLabel("claude-opus-4-8")).toBe("Opus")
13
+ expect(deriveClaudeModelLabel("claude-haiku-4-5-20251001")).toBe("Haiku")
14
+ })
15
+
9
16
  test("normalizes Claude aliases via the provider catalog", () => {
10
- expect(normalizeClaudeModelId("opus")).toBe("claude-opus-4-7")
17
+ expect(normalizeClaudeModelId("fable")).toBe("fable")
18
+ expect(normalizeClaudeModelId("opus")).toBe("claude-opus-4-8")
11
19
  expect(normalizeClaudeModelId("sonnet")).toBe("claude-sonnet-4-6")
12
20
  expect(normalizeClaudeModelId("haiku")).toBe("claude-haiku-4-5-20251001")
13
21
  })
@@ -18,8 +26,9 @@ describe("shared model normalization", () => {
18
26
  })
19
27
 
20
28
  test("uses declarative metadata for Claude max-effort support", () => {
21
- expect(supportsClaudeMaxReasoningEffort("claude-opus-4-7")).toBe(true)
29
+ expect(supportsClaudeMaxReasoningEffort("claude-opus-4-8")).toBe(true)
22
30
  expect(supportsClaudeMaxReasoningEffort("opus")).toBe(true)
31
+ expect(supportsClaudeMaxReasoningEffort("fable")).toBe(false)
23
32
  expect(supportsClaudeMaxReasoningEffort("claude-sonnet-4-6")).toBe(false)
24
33
  })
25
34
  })
@@ -228,6 +228,16 @@ export function isClaudeContextWindow(value: unknown): value is ClaudeContextWin
228
228
  return CLAUDE_CONTEXT_WINDOW_OPTIONS.some((option) => option.id === value)
229
229
  }
230
230
 
231
+ function titleCaseWord(value: string) {
232
+ return value.length === 0 ? value : `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`
233
+ }
234
+
235
+ export function deriveClaudeModelLabel(modelId: string): string {
236
+ const parts = modelId.replace(/^claude-/, "").split("-").filter(Boolean)
237
+ if (parts.length === 0) return modelId
238
+ return titleCaseWord(parts[0] ?? modelId)
239
+ }
240
+
231
241
  export interface ProviderCatalogEntry {
232
242
  id: AgentProvider
233
243
  label: string
@@ -247,8 +257,13 @@ export const PROVIDERS: ProviderCatalogEntry[] = [
247
257
  supportsPlanMode: true,
248
258
  models: [
249
259
  {
250
- id: "claude-opus-4-7",
251
- label: "Opus 4.7",
260
+ id: "fable",
261
+ label: deriveClaudeModelLabel("fable"),
262
+ supportsEffort: true,
263
+ },
264
+ {
265
+ id: "claude-opus-4-8",
266
+ label: deriveClaudeModelLabel("claude-opus-4-8"),
252
267
  supportsEffort: true,
253
268
  aliases: ["opus"],
254
269
  contextWindowOptions: [...CLAUDE_CONTEXT_WINDOW_OPTIONS],
@@ -256,14 +271,14 @@ export const PROVIDERS: ProviderCatalogEntry[] = [
256
271
  },
257
272
  {
258
273
  id: "claude-sonnet-4-6",
259
- label: "Sonnet 4.6",
274
+ label: deriveClaudeModelLabel("claude-sonnet-4-6"),
260
275
  supportsEffort: true,
261
276
  aliases: ["sonnet"],
262
277
  contextWindowOptions: [...CLAUDE_CONTEXT_WINDOW_OPTIONS],
263
278
  },
264
279
  {
265
280
  id: "claude-haiku-4-5-20251001",
266
- label: "Haiku 4.5",
281
+ label: deriveClaudeModelLabel("claude-haiku-4-5-20251001"),
267
282
  supportsEffort: true,
268
283
  aliases: ["haiku"],
269
284
  },
@@ -311,7 +326,7 @@ export function normalizeProviderModelId(
311
326
  ?? getProviderCatalog(provider).defaultModel
312
327
  }
313
328
 
314
- export function normalizeClaudeModelId(modelId?: string, fallbackModelId = "claude-opus-4-7"): string {
329
+ export function normalizeClaudeModelId(modelId?: string, fallbackModelId = "claude-opus-4-8"): string {
315
330
  return normalizeProviderModelId("claude", modelId, fallbackModelId)
316
331
  }
317
332