kanna-code 0.41.5 → 0.41.7

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-BWrXLfF8.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.7",
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
  },
@@ -633,6 +633,53 @@ describe("DiffStore", () => {
633
633
  expect((await run(["git", "branch", "--show-current"], repoRoot)).trim()).toBe("feature/new")
634
634
  })
635
635
 
636
+ test("syncBranch pull rebases divergent local commits onto the upstream branch", async () => {
637
+ const repoRoot = await createRepo()
638
+ const remoteRoot = await createBareRemote()
639
+ const remoteWorktree = await mkdtemp(path.join(tmpdir(), "kanna-diff-remote-worktree-"))
640
+ tempDirs.push(repoRoot, remoteRoot, remoteWorktree)
641
+
642
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
643
+ await run(["git", "add", "."], repoRoot)
644
+ await run(["git", "commit", "-m", "base"], repoRoot)
645
+ await run(["git", "branch", "-M", "main"], repoRoot)
646
+ await run(["git", "remote", "add", "origin", remoteRoot], repoRoot)
647
+ await run(["git", "push", "-u", "origin", "main"], repoRoot)
648
+
649
+ await run(["git", "clone", "-b", "main", remoteRoot, remoteWorktree], tmpdir())
650
+ await run(["git", "config", "user.email", "kanna@example.com"], remoteWorktree)
651
+ await run(["git", "config", "user.name", "Kanna"], remoteWorktree)
652
+ await writeFile(path.join(remoteWorktree, "remote.txt"), "remote\n", "utf8")
653
+ await run(["git", "add", "."], remoteWorktree)
654
+ await run(["git", "commit", "-m", "remote change"], remoteWorktree)
655
+ await run(["git", "push"], remoteWorktree)
656
+
657
+ await writeFile(path.join(repoRoot, "local.txt"), "local\n", "utf8")
658
+ await run(["git", "add", "."], repoRoot)
659
+ await run(["git", "commit", "-m", "local change"], repoRoot)
660
+
661
+ const store = new DiffStore(repoRoot)
662
+ await store.initialize()
663
+ const result = await store.syncBranch({
664
+ projectId: "project-1",
665
+ projectPath: repoRoot,
666
+ action: "pull",
667
+ })
668
+
669
+ expect(result).toMatchObject({
670
+ ok: true,
671
+ action: "pull",
672
+ branchName: "main",
673
+ aheadCount: 1,
674
+ behindCount: 0,
675
+ snapshotChanged: true,
676
+ })
677
+ expect((await run(["git", "log", "--format=%s", "-2"], repoRoot)).trim().split("\n")).toEqual([
678
+ "local change",
679
+ "remote change",
680
+ ])
681
+ })
682
+
636
683
  test("previewMergeBranch reports up-to-date and mergeable states", async () => {
637
684
  const repoRoot = await createRepo()
638
685
  tempDirs.push(repoRoot)
@@ -1903,7 +1903,7 @@ export class DiffStore {
1903
1903
  }
1904
1904
 
1905
1905
  const syncResult = args.action === "pull"
1906
- ? await runGit(["pull", "--ff-only"], repo.repoRoot)
1906
+ ? await runGit(["pull", "--rebase", "--autostash"], repo.repoRoot)
1907
1907
  : await runGit(["fetch", "--all", "--prune"], repo.repoRoot)
1908
1908
 
1909
1909
  if (syncResult.exitCode !== 0) {
@@ -1912,10 +1912,7 @@ export class DiffStore {
1912
1912
  let title = args.action === "pull" ? "Pull failed" : "Fetch failed"
1913
1913
  let message = summarizeGitFailure(detail, args.action === "pull" ? "Git could not pull the latest changes." : "Git could not fetch the latest changes.")
1914
1914
 
1915
- if (args.action === "pull" && normalized.includes("not possible to fast-forward")) {
1916
- title = "Pull requires merge or rebase"
1917
- message = "Your branch cannot be fast-forwarded. Rebase or merge manually, then try again."
1918
- } else if (normalized.includes("could not read from remote repository") || normalized.includes("authentication failed") || normalized.includes("permission denied")) {
1915
+ if (normalized.includes("could not read from remote repository") || normalized.includes("authentication failed") || normalized.includes("permission denied")) {
1919
1916
  title = "Remote authentication failed"
1920
1917
  message = "Git could not authenticate with the remote repository."
1921
1918
  }
@@ -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