kanna-code 0.32.1 → 0.32.2
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.
- package/dist/client/assets/{index-EVYrrkP5.js → index-CdBLcsbf.js} +189 -184
- package/dist/client/index.html +1 -1
- package/package.json +1 -1
- package/src/server/codex-app-server.test.ts +155 -0
- package/src/server/codex-app-server.ts +49 -1
- package/src/server/diff-store.test.ts +22 -0
- package/src/server/diff-store.ts +2 -1
- package/src/server/event-store.test.ts +1 -1
- package/src/server/provider-catalog.test.ts +5 -5
- package/src/server/quick-response.ts +1 -1
- package/src/server/read-models.test.ts +1 -1
- package/src/shared/types.ts +32 -4
package/dist/client/index.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
})()
|
|
31
31
|
</script>
|
|
32
32
|
<title>Kanna</title>
|
|
33
|
-
<script type="module" crossorigin src="/assets/index-
|
|
33
|
+
<script type="module" crossorigin src="/assets/index-CdBLcsbf.js"></script>
|
|
34
34
|
<link rel="stylesheet" crossorigin href="/assets/index-B3EUEMfY.css">
|
|
35
35
|
</head>
|
|
36
36
|
<body>
|
package/package.json
CHANGED
|
@@ -672,6 +672,161 @@ describe("CodexAppServerManager", () => {
|
|
|
672
672
|
})
|
|
673
673
|
})
|
|
674
674
|
|
|
675
|
+
test("maps plain-text fileChange adds into write_file tool calls", async () => {
|
|
676
|
+
const process = new FakeCodexProcess((message, child) => {
|
|
677
|
+
if (message.method === "initialize") {
|
|
678
|
+
child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
|
|
679
|
+
} else if (message.method === "thread/start") {
|
|
680
|
+
child.writeServerMessage({
|
|
681
|
+
id: message.id,
|
|
682
|
+
result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
|
|
683
|
+
})
|
|
684
|
+
} else if (message.method === "turn/start") {
|
|
685
|
+
child.writeServerMessage({
|
|
686
|
+
id: message.id,
|
|
687
|
+
result: { turn: { id: "turn-1", status: "inProgress", error: null } },
|
|
688
|
+
})
|
|
689
|
+
child.writeServerMessage({
|
|
690
|
+
method: "item/completed",
|
|
691
|
+
params: {
|
|
692
|
+
threadId: "thread-1",
|
|
693
|
+
turnId: "turn-1",
|
|
694
|
+
item: {
|
|
695
|
+
type: "fileChange",
|
|
696
|
+
id: "call-1",
|
|
697
|
+
changes: [
|
|
698
|
+
{
|
|
699
|
+
path: "/tmp/project/test.md",
|
|
700
|
+
kind: {
|
|
701
|
+
type: "add",
|
|
702
|
+
move_path: null,
|
|
703
|
+
},
|
|
704
|
+
diff: "hello\nworld\n",
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
status: "completed",
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
})
|
|
711
|
+
child.writeServerMessage({
|
|
712
|
+
method: "turn/completed",
|
|
713
|
+
params: {
|
|
714
|
+
threadId: "thread-1",
|
|
715
|
+
turn: { id: "turn-1", status: "completed", error: null },
|
|
716
|
+
},
|
|
717
|
+
})
|
|
718
|
+
}
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
const manager = new CodexAppServerManager({
|
|
722
|
+
spawnProcess: () => process as never,
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
await manager.startSession({
|
|
726
|
+
chatId: "chat-1",
|
|
727
|
+
cwd: "/tmp/project",
|
|
728
|
+
model: "gpt-5.4",
|
|
729
|
+
sessionToken: null,
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
const turn = await manager.startTurn({
|
|
733
|
+
chatId: "chat-1",
|
|
734
|
+
model: "gpt-5.4",
|
|
735
|
+
content: "write a file",
|
|
736
|
+
planMode: false,
|
|
737
|
+
onToolRequest: async () => ({}),
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
const events = await collectStream(turn.stream)
|
|
741
|
+
const toolCall = events.find((event) => event.type === "transcript" && event.entry.kind === "tool_call")
|
|
742
|
+
|
|
743
|
+
expect(toolCall?.entry.kind).toBe("tool_call")
|
|
744
|
+
if (!toolCall || toolCall.entry.kind !== "tool_call") throw new Error("missing tool call")
|
|
745
|
+
expect(toolCall.entry.tool.toolKind).toBe("write_file")
|
|
746
|
+
expect(toolCall.entry.tool.input).toEqual({
|
|
747
|
+
filePath: "/tmp/project/test.md",
|
|
748
|
+
content: "hello\nworld\n",
|
|
749
|
+
})
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
test("maps plain-text fileChange deletes into delete_file tool calls", async () => {
|
|
753
|
+
const process = new FakeCodexProcess((message, child) => {
|
|
754
|
+
if (message.method === "initialize") {
|
|
755
|
+
child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
|
|
756
|
+
} else if (message.method === "thread/start") {
|
|
757
|
+
child.writeServerMessage({
|
|
758
|
+
id: message.id,
|
|
759
|
+
result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
|
|
760
|
+
})
|
|
761
|
+
} else if (message.method === "turn/start") {
|
|
762
|
+
child.writeServerMessage({
|
|
763
|
+
id: message.id,
|
|
764
|
+
result: { turn: { id: "turn-1", status: "inProgress", error: null } },
|
|
765
|
+
})
|
|
766
|
+
child.writeServerMessage({
|
|
767
|
+
method: "item/completed",
|
|
768
|
+
params: {
|
|
769
|
+
threadId: "thread-1",
|
|
770
|
+
turnId: "turn-1",
|
|
771
|
+
item: {
|
|
772
|
+
type: "fileChange",
|
|
773
|
+
id: "call-1",
|
|
774
|
+
changes: [
|
|
775
|
+
{
|
|
776
|
+
path: "/tmp/project/test.md",
|
|
777
|
+
kind: {
|
|
778
|
+
type: "delete",
|
|
779
|
+
move_path: null,
|
|
780
|
+
},
|
|
781
|
+
diff: "hello\nworld\n",
|
|
782
|
+
},
|
|
783
|
+
],
|
|
784
|
+
status: "completed",
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
})
|
|
788
|
+
child.writeServerMessage({
|
|
789
|
+
method: "turn/completed",
|
|
790
|
+
params: {
|
|
791
|
+
threadId: "thread-1",
|
|
792
|
+
turn: { id: "turn-1", status: "completed", error: null },
|
|
793
|
+
},
|
|
794
|
+
})
|
|
795
|
+
}
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
const manager = new CodexAppServerManager({
|
|
799
|
+
spawnProcess: () => process as never,
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
await manager.startSession({
|
|
803
|
+
chatId: "chat-1",
|
|
804
|
+
cwd: "/tmp/project",
|
|
805
|
+
model: "gpt-5.4",
|
|
806
|
+
sessionToken: null,
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
const turn = await manager.startTurn({
|
|
810
|
+
chatId: "chat-1",
|
|
811
|
+
model: "gpt-5.4",
|
|
812
|
+
content: "delete a file",
|
|
813
|
+
planMode: false,
|
|
814
|
+
onToolRequest: async () => ({}),
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
const events = await collectStream(turn.stream)
|
|
818
|
+
const toolCall = events.find((event) => event.type === "transcript" && event.entry.kind === "tool_call")
|
|
819
|
+
|
|
820
|
+
expect(toolCall?.entry.kind).toBe("tool_call")
|
|
821
|
+
if (!toolCall || toolCall.entry.kind !== "tool_call") throw new Error("missing tool call")
|
|
822
|
+
expect(toolCall.entry.tool.toolKind).toBe("delete_file")
|
|
823
|
+
expect(toolCall.entry.tool.toolName).toBe("Delete")
|
|
824
|
+
expect(toolCall.entry.tool.input).toEqual({
|
|
825
|
+
filePath: "/tmp/project/test.md",
|
|
826
|
+
content: "hello\nworld\n",
|
|
827
|
+
})
|
|
828
|
+
})
|
|
829
|
+
|
|
675
830
|
test("splits multi-change fileChange items into multiple tool calls and results", async () => {
|
|
676
831
|
const process = new FakeCodexProcess((message, child) => {
|
|
677
832
|
if (message.method === "initialize") {
|
|
@@ -430,6 +430,18 @@ function parseUnifiedDiff(diff: string): { oldString: string; newString: string
|
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
+
function isUnifiedDiff(diff: string) {
|
|
434
|
+
return diff.includes("@@")
|
|
435
|
+
|| diff.startsWith("---")
|
|
436
|
+
|| diff.startsWith("+++")
|
|
437
|
+
|| diff.split(/\r?\n/).some((line) => (
|
|
438
|
+
line.startsWith("+")
|
|
439
|
+
|| line.startsWith("-")
|
|
440
|
+
|| line.startsWith(" ")
|
|
441
|
+
|| line === "\"
|
|
442
|
+
))
|
|
443
|
+
}
|
|
444
|
+
|
|
433
445
|
function fileChangeToToolCalls(item: Extract<ThreadItem, { type: "fileChange" }>): TranscriptEntry[] {
|
|
434
446
|
return item.changes.map((change, index) => {
|
|
435
447
|
const payload = fileChangePayload(item, change)
|
|
@@ -453,7 +465,10 @@ function fileChangeToToolCalls(item: Extract<ThreadItem, { type: "fileChange" }>
|
|
|
453
465
|
}
|
|
454
466
|
|
|
455
467
|
if (typeof change.diff === "string") {
|
|
456
|
-
const
|
|
468
|
+
const diffIsUnified = isUnifiedDiff(change.diff)
|
|
469
|
+
const { oldString, newString } = diffIsUnified
|
|
470
|
+
? parseUnifiedDiff(change.diff)
|
|
471
|
+
: { oldString: change.diff, newString: change.diff }
|
|
457
472
|
|
|
458
473
|
if (normalizedKind.type === "add") {
|
|
459
474
|
return timestamped({
|
|
@@ -473,6 +488,22 @@ function fileChangeToToolCalls(item: Extract<ThreadItem, { type: "fileChange" }>
|
|
|
473
488
|
}
|
|
474
489
|
|
|
475
490
|
if (normalizedKind.type === "update") {
|
|
491
|
+
if (!diffIsUnified) {
|
|
492
|
+
return timestamped({
|
|
493
|
+
kind: "tool_call",
|
|
494
|
+
tool: {
|
|
495
|
+
kind: "tool",
|
|
496
|
+
toolKind: "unknown_tool",
|
|
497
|
+
toolName: "FileChange",
|
|
498
|
+
toolId,
|
|
499
|
+
input: {
|
|
500
|
+
payload,
|
|
501
|
+
},
|
|
502
|
+
rawInput: payload,
|
|
503
|
+
},
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
|
|
476
507
|
return timestamped({
|
|
477
508
|
kind: "tool_call",
|
|
478
509
|
tool: {
|
|
@@ -489,6 +520,23 @@ function fileChangeToToolCalls(item: Extract<ThreadItem, { type: "fileChange" }>
|
|
|
489
520
|
},
|
|
490
521
|
})
|
|
491
522
|
}
|
|
523
|
+
|
|
524
|
+
if (normalizedKind.type === "delete") {
|
|
525
|
+
return timestamped({
|
|
526
|
+
kind: "tool_call",
|
|
527
|
+
tool: {
|
|
528
|
+
kind: "tool",
|
|
529
|
+
toolKind: "delete_file",
|
|
530
|
+
toolName: "Delete",
|
|
531
|
+
toolId,
|
|
532
|
+
input: {
|
|
533
|
+
filePath: change.path,
|
|
534
|
+
content: oldString,
|
|
535
|
+
},
|
|
536
|
+
rawInput: payload,
|
|
537
|
+
},
|
|
538
|
+
})
|
|
539
|
+
}
|
|
492
540
|
}
|
|
493
541
|
|
|
494
542
|
return timestamped({
|
|
@@ -270,6 +270,28 @@ describe("DiffStore", () => {
|
|
|
270
270
|
})
|
|
271
271
|
})
|
|
272
272
|
|
|
273
|
+
test("refreshSnapshot tolerates tracked files replaced by directories", async () => {
|
|
274
|
+
const repoRoot = await createRepo()
|
|
275
|
+
tempDirs.push(repoRoot)
|
|
276
|
+
await writeFile(path.join(repoRoot, "thing"), "base\n", "utf8")
|
|
277
|
+
await run(["git", "add", "."], repoRoot)
|
|
278
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
279
|
+
|
|
280
|
+
await rm(path.join(repoRoot, "thing"), { force: true })
|
|
281
|
+
await mkdir(path.join(repoRoot, "thing"), { recursive: true })
|
|
282
|
+
await writeFile(path.join(repoRoot, "thing", "file.txt"), "nested\n", "utf8")
|
|
283
|
+
|
|
284
|
+
const store = new DiffStore(repoRoot)
|
|
285
|
+
await store.initialize()
|
|
286
|
+
await expect(store.refreshSnapshot("project-1", repoRoot)).resolves.toBe(true)
|
|
287
|
+
|
|
288
|
+
const snapshot = store.getProjectSnapshot("project-1")
|
|
289
|
+
expect(snapshot.status).toBe("ready")
|
|
290
|
+
expect(snapshot.files).toHaveLength(2)
|
|
291
|
+
expect(snapshot.files.map((file) => file.path)).toEqual(["thing", "thing/file.txt"])
|
|
292
|
+
expect(snapshot.files.map((file) => file.changeType)).toEqual(["deleted", "added"])
|
|
293
|
+
})
|
|
294
|
+
|
|
273
295
|
test("discardFile reverts a tracked modified file", async () => {
|
|
274
296
|
const repoRoot = await createRepo()
|
|
275
297
|
tempDirs.push(repoRoot)
|
package/src/server/diff-store.ts
CHANGED
|
@@ -856,7 +856,8 @@ async function listDirtyPaths(repoRoot: string) {
|
|
|
856
856
|
|
|
857
857
|
async function readWorktreeFile(repoRoot: string, relativePath: string): Promise<string | null> {
|
|
858
858
|
const absolutePath = path.join(repoRoot, relativePath)
|
|
859
|
-
|
|
859
|
+
const fileInfo = await stat(absolutePath).catch(() => null)
|
|
860
|
+
if (!fileInfo?.isFile()) {
|
|
860
861
|
return null
|
|
861
862
|
}
|
|
862
863
|
|
|
@@ -8,14 +8,14 @@ import { resolveClaudeApiModelId } from "../shared/types"
|
|
|
8
8
|
|
|
9
9
|
describe("provider catalog normalization", () => {
|
|
10
10
|
test("maps legacy Claude effort into shared model options", () => {
|
|
11
|
-
expect(normalizeClaudeModelOptions("opus", undefined, "max")).toEqual({
|
|
11
|
+
expect(normalizeClaudeModelOptions("claude-opus-4-7", undefined, "max")).toEqual({
|
|
12
12
|
reasoningEffort: "max",
|
|
13
13
|
contextWindow: "200k",
|
|
14
14
|
})
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
test("normalizes Claude context window only for supported models", () => {
|
|
18
|
-
expect(normalizeClaudeModelOptions("sonnet", {
|
|
18
|
+
expect(normalizeClaudeModelOptions("claude-sonnet-4-6", {
|
|
19
19
|
claude: {
|
|
20
20
|
reasoningEffort: "medium",
|
|
21
21
|
contextWindow: "1m",
|
|
@@ -25,7 +25,7 @@ describe("provider catalog normalization", () => {
|
|
|
25
25
|
contextWindow: "1m",
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
expect(normalizeClaudeModelOptions("haiku", {
|
|
28
|
+
expect(normalizeClaudeModelOptions("claude-haiku-4-5-20251001", {
|
|
29
29
|
claude: {
|
|
30
30
|
reasoningEffort: "medium",
|
|
31
31
|
contextWindow: "1m",
|
|
@@ -56,7 +56,7 @@ describe("provider catalog normalization", () => {
|
|
|
56
56
|
})
|
|
57
57
|
|
|
58
58
|
test("resolves Claude API model ids for 1m context window", () => {
|
|
59
|
-
expect(resolveClaudeApiModelId("opus", "1m")).toBe("opus[1m]")
|
|
60
|
-
expect(resolveClaudeApiModelId("sonnet", "200k")).toBe("sonnet")
|
|
59
|
+
expect(resolveClaudeApiModelId("claude-opus-4-7", "1m")).toBe("claude-opus-4-7[1m]")
|
|
60
|
+
expect(resolveClaudeApiModelId("claude-sonnet-4-6", "200k")).toBe("claude-sonnet-4-6")
|
|
61
61
|
})
|
|
62
62
|
})
|
package/src/shared/types.ts
CHANGED
|
@@ -119,6 +119,26 @@ export function isClaudeContextWindow(value: unknown): value is ClaudeContextWin
|
|
|
119
119
|
return CLAUDE_CONTEXT_WINDOW_OPTIONS.some((option) => option.id === value)
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
export function normalizeClaudeModelId(modelId?: string): string {
|
|
123
|
+
switch (modelId) {
|
|
124
|
+
case "opus":
|
|
125
|
+
case "claude-opus-4-7":
|
|
126
|
+
return "claude-opus-4-7"
|
|
127
|
+
case "sonnet":
|
|
128
|
+
case "claude-sonnet-4-6":
|
|
129
|
+
return "claude-sonnet-4-6"
|
|
130
|
+
case "haiku":
|
|
131
|
+
case "claude-haiku-4-5-20251001":
|
|
132
|
+
return "claude-haiku-4-5-20251001"
|
|
133
|
+
default:
|
|
134
|
+
return modelId ?? "claude-opus-4-7"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function isClaudeOpusModelId(modelId: string): boolean {
|
|
139
|
+
return normalizeClaudeModelId(modelId).startsWith("claude-opus-")
|
|
140
|
+
}
|
|
141
|
+
|
|
122
142
|
export interface ProviderCatalogEntry {
|
|
123
143
|
id: AgentProvider
|
|
124
144
|
label: string
|
|
@@ -133,13 +153,13 @@ export const PROVIDERS: ProviderCatalogEntry[] = [
|
|
|
133
153
|
{
|
|
134
154
|
id: "claude",
|
|
135
155
|
label: "Claude",
|
|
136
|
-
defaultModel: "sonnet",
|
|
156
|
+
defaultModel: "claude-sonnet-4-6",
|
|
137
157
|
defaultEffort: "high",
|
|
138
158
|
supportsPlanMode: true,
|
|
139
159
|
models: [
|
|
140
|
-
{ id: "opus", label: "Opus", supportsEffort: true, contextWindowOptions: [...CLAUDE_CONTEXT_WINDOW_OPTIONS] },
|
|
141
|
-
{ id: "sonnet", label: "Sonnet", supportsEffort: true, contextWindowOptions: [...CLAUDE_CONTEXT_WINDOW_OPTIONS] },
|
|
142
|
-
{ id: "haiku", label: "Haiku", supportsEffort: true },
|
|
160
|
+
{ id: "claude-opus-4-7", label: "Opus 4.7", supportsEffort: true, contextWindowOptions: [...CLAUDE_CONTEXT_WINDOW_OPTIONS] },
|
|
161
|
+
{ id: "claude-sonnet-4-6", label: "Sonnet 4.6", supportsEffort: true, contextWindowOptions: [...CLAUDE_CONTEXT_WINDOW_OPTIONS] },
|
|
162
|
+
{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5", supportsEffort: true },
|
|
143
163
|
],
|
|
144
164
|
efforts: [...CLAUDE_REASONING_OPTIONS],
|
|
145
165
|
},
|
|
@@ -420,6 +440,9 @@ export interface WriteFileToolCall
|
|
|
420
440
|
export interface EditFileToolCall
|
|
421
441
|
extends ToolCallBase<"edit_file", { filePath: string; oldString: string; newString: string }> { }
|
|
422
442
|
|
|
443
|
+
export interface DeleteFileToolCall
|
|
444
|
+
extends ToolCallBase<"delete_file", { filePath: string; content: string }> { }
|
|
445
|
+
|
|
423
446
|
export interface SubagentTaskToolCall
|
|
424
447
|
extends ToolCallBase<"subagent_task", { subagentType?: string }> { }
|
|
425
448
|
|
|
@@ -441,6 +464,7 @@ export type NormalizedToolCall =
|
|
|
441
464
|
| ReadFileToolCall
|
|
442
465
|
| WriteFileToolCall
|
|
443
466
|
| EditFileToolCall
|
|
467
|
+
| DeleteFileToolCall
|
|
444
468
|
| SubagentTaskToolCall
|
|
445
469
|
| McpGenericToolCall
|
|
446
470
|
| UnknownToolCall
|
|
@@ -783,6 +807,9 @@ export type HydratedWriteFileToolCall =
|
|
|
783
807
|
export type HydratedEditFileToolCall =
|
|
784
808
|
HydratedToolCallBase<"edit_file", EditFileToolCall["input"], unknown>
|
|
785
809
|
|
|
810
|
+
export type HydratedDeleteFileToolCall =
|
|
811
|
+
HydratedToolCallBase<"delete_file", DeleteFileToolCall["input"], unknown>
|
|
812
|
+
|
|
786
813
|
export type HydratedSubagentTaskToolCall =
|
|
787
814
|
HydratedToolCallBase<"subagent_task", SubagentTaskToolCall["input"], unknown>
|
|
788
815
|
|
|
@@ -804,6 +831,7 @@ export type HydratedToolCall =
|
|
|
804
831
|
| HydratedReadFileToolCall
|
|
805
832
|
| HydratedWriteFileToolCall
|
|
806
833
|
| HydratedEditFileToolCall
|
|
834
|
+
| HydratedDeleteFileToolCall
|
|
807
835
|
| HydratedSubagentTaskToolCall
|
|
808
836
|
| HydratedMcpGenericToolCall
|
|
809
837
|
| HydratedUnknownToolCall
|