kanna-code 0.1.3 → 0.2.0
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/LICENSE +21 -0
- package/README.md +46 -19
- package/dist/client/assets/index-C-sGbl7X.js +409 -0
- package/dist/client/assets/index-gld9RxCU.css +1 -0
- package/dist/client/index.html +2 -2
- package/package.json +18 -2
- package/src/server/agent.test.ts +415 -0
- package/src/server/agent.ts +483 -194
- package/src/server/codex-app-server-protocol.ts +440 -0
- package/src/server/codex-app-server.test.ts +1303 -0
- package/src/server/codex-app-server.ts +1277 -0
- package/src/server/event-store.ts +81 -34
- package/src/server/events.ts +25 -17
- package/src/server/harness-types.ts +19 -0
- package/src/server/provider-catalog.test.ts +34 -0
- package/src/server/provider-catalog.ts +77 -0
- package/src/server/read-models.test.ts +63 -0
- package/src/server/read-models.ts +5 -1
- package/src/shared/protocol.ts +12 -2
- package/src/shared/tools.test.ts +88 -0
- package/src/shared/tools.ts +233 -0
- package/src/shared/types.ts +404 -5
- package/dist/client/assets/index-BRiM6Nxc.css +0 -1
- package/dist/client/assets/index-DelZ0MyD.js +0 -418
|
@@ -2,7 +2,7 @@ import { appendFile, mkdir } from "node:fs/promises"
|
|
|
2
2
|
import { homedir } from "node:os"
|
|
3
3
|
import path from "node:path"
|
|
4
4
|
import { getDataDir, LOG_PREFIX } from "../shared/branding"
|
|
5
|
-
import type { TranscriptEntry } from "../shared/types"
|
|
5
|
+
import type { AgentProvider, TranscriptEntry } from "../shared/types"
|
|
6
6
|
import { STORE_VERSION } from "../shared/types"
|
|
7
7
|
import {
|
|
8
8
|
type ChatEvent,
|
|
@@ -29,6 +29,7 @@ export class EventStore {
|
|
|
29
29
|
readonly dataDir = DATA_DIR
|
|
30
30
|
readonly state: StoreState = createEmptyState()
|
|
31
31
|
private writeChain = Promise.resolve()
|
|
32
|
+
private storageReset = false
|
|
32
33
|
|
|
33
34
|
async initialize() {
|
|
34
35
|
await mkdir(DATA_DIR, { recursive: true })
|
|
@@ -50,14 +51,31 @@ export class EventStore {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
private async clearStorage() {
|
|
55
|
+
if (this.storageReset) return
|
|
56
|
+
this.storageReset = true
|
|
57
|
+
this.resetState()
|
|
58
|
+
await Promise.all([
|
|
59
|
+
Bun.write(SNAPSHOT_PATH, ""),
|
|
60
|
+
Bun.write(PROJECTS_LOG, ""),
|
|
61
|
+
Bun.write(CHATS_LOG, ""),
|
|
62
|
+
Bun.write(MESSAGES_LOG, ""),
|
|
63
|
+
Bun.write(TURNS_LOG, ""),
|
|
64
|
+
])
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
private async loadSnapshot() {
|
|
54
68
|
const file = Bun.file(SNAPSHOT_PATH)
|
|
55
69
|
if (!(await file.exists())) return
|
|
56
70
|
|
|
57
71
|
try {
|
|
58
|
-
const
|
|
72
|
+
const text = await file.text()
|
|
73
|
+
if (!text.trim()) return
|
|
74
|
+
const parsed = JSON.parse(text) as SnapshotFile
|
|
59
75
|
if (parsed.v !== STORE_VERSION) {
|
|
60
|
-
|
|
76
|
+
console.warn(`${LOG_PREFIX} Resetting local chat history for store version ${STORE_VERSION}`)
|
|
77
|
+
await this.clearStorage()
|
|
78
|
+
return
|
|
61
79
|
}
|
|
62
80
|
for (const project of parsed.projects) {
|
|
63
81
|
this.state.projectsById.set(project.id, { ...project })
|
|
@@ -70,8 +88,8 @@ export class EventStore {
|
|
|
70
88
|
this.state.messagesByChatId.set(messageSet.chatId, cloneTranscriptEntries(messageSet.entries))
|
|
71
89
|
}
|
|
72
90
|
} catch (error) {
|
|
73
|
-
console.warn(`${LOG_PREFIX} Failed to load snapshot,
|
|
74
|
-
this.
|
|
91
|
+
console.warn(`${LOG_PREFIX} Failed to load snapshot, resetting local history:`, error)
|
|
92
|
+
await this.clearStorage()
|
|
75
93
|
}
|
|
76
94
|
}
|
|
77
95
|
|
|
@@ -83,9 +101,13 @@ export class EventStore {
|
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
private async replayLogs() {
|
|
104
|
+
if (this.storageReset) return
|
|
86
105
|
await this.replayLog<ProjectEvent>(PROJECTS_LOG)
|
|
106
|
+
if (this.storageReset) return
|
|
87
107
|
await this.replayLog<ChatEvent>(CHATS_LOG)
|
|
108
|
+
if (this.storageReset) return
|
|
88
109
|
await this.replayLog<MessageEvent>(MESSAGES_LOG)
|
|
110
|
+
if (this.storageReset) return
|
|
89
111
|
await this.replayLog<TurnEvent>(TURNS_LOG)
|
|
90
112
|
}
|
|
91
113
|
|
|
@@ -108,14 +130,21 @@ export class EventStore {
|
|
|
108
130
|
const line = lines[index].trim()
|
|
109
131
|
if (!line) continue
|
|
110
132
|
try {
|
|
111
|
-
const event = JSON.parse(line) as
|
|
112
|
-
|
|
133
|
+
const event = JSON.parse(line) as Partial<StoreEvent>
|
|
134
|
+
if (event.v !== STORE_VERSION) {
|
|
135
|
+
console.warn(`${LOG_PREFIX} Resetting local history from incompatible event log`)
|
|
136
|
+
await this.clearStorage()
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
this.applyEvent(event as StoreEvent)
|
|
113
140
|
} catch (error) {
|
|
114
141
|
if (index === lastNonEmpty) {
|
|
115
142
|
console.warn(`${LOG_PREFIX} Ignoring corrupt trailing line in ${path.basename(filePath)}`)
|
|
116
143
|
return
|
|
117
144
|
}
|
|
118
|
-
|
|
145
|
+
console.warn(`${LOG_PREFIX} Failed to replay ${path.basename(filePath)}, resetting local history:`, error)
|
|
146
|
+
await this.clearStorage()
|
|
147
|
+
return
|
|
119
148
|
}
|
|
120
149
|
}
|
|
121
150
|
}
|
|
@@ -150,8 +179,9 @@ export class EventStore {
|
|
|
150
179
|
title: event.title,
|
|
151
180
|
createdAt: event.timestamp,
|
|
152
181
|
updatedAt: event.timestamp,
|
|
182
|
+
provider: null,
|
|
153
183
|
planMode: false,
|
|
154
|
-
|
|
184
|
+
sessionToken: null,
|
|
155
185
|
lastTurnOutcome: null,
|
|
156
186
|
}
|
|
157
187
|
this.state.chatsById.set(chat.id, chat)
|
|
@@ -171,6 +201,13 @@ export class EventStore {
|
|
|
171
201
|
chat.updatedAt = event.timestamp
|
|
172
202
|
break
|
|
173
203
|
}
|
|
204
|
+
case "chat_provider_set": {
|
|
205
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
206
|
+
if (!chat) break
|
|
207
|
+
chat.provider = event.provider
|
|
208
|
+
chat.updatedAt = event.timestamp
|
|
209
|
+
break
|
|
210
|
+
}
|
|
174
211
|
case "chat_plan_mode_set": {
|
|
175
212
|
const chat = this.state.chatsById.get(event.chatId)
|
|
176
213
|
if (!chat) break
|
|
@@ -181,15 +218,8 @@ export class EventStore {
|
|
|
181
218
|
case "message_appended": {
|
|
182
219
|
const chat = this.state.chatsById.get(event.chatId)
|
|
183
220
|
if (chat) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const parsed = JSON.parse(event.entry.message)
|
|
188
|
-
if (parsed.type === "user_prompt") {
|
|
189
|
-
chat.lastMessageAt = event.entry.createdAt
|
|
190
|
-
}
|
|
191
|
-
} catch {
|
|
192
|
-
// non-JSON entry, skip
|
|
221
|
+
if (event.entry.kind === "user_prompt") {
|
|
222
|
+
chat.lastMessageAt = event.entry.createdAt
|
|
193
223
|
}
|
|
194
224
|
chat.updatedAt = Math.max(chat.updatedAt, event.entry.createdAt)
|
|
195
225
|
}
|
|
@@ -225,10 +255,10 @@ export class EventStore {
|
|
|
225
255
|
chat.lastTurnOutcome = "cancelled"
|
|
226
256
|
break
|
|
227
257
|
}
|
|
228
|
-
case "
|
|
258
|
+
case "session_token_set": {
|
|
229
259
|
const chat = this.state.chatsById.get(event.chatId)
|
|
230
260
|
if (!chat) break
|
|
231
|
-
chat.
|
|
261
|
+
chat.sessionToken = event.sessionToken
|
|
232
262
|
chat.updatedAt = event.timestamp
|
|
233
263
|
break
|
|
234
264
|
}
|
|
@@ -326,6 +356,19 @@ export class EventStore {
|
|
|
326
356
|
await this.append(CHATS_LOG, event)
|
|
327
357
|
}
|
|
328
358
|
|
|
359
|
+
async setChatProvider(chatId: string, provider: AgentProvider) {
|
|
360
|
+
const chat = this.requireChat(chatId)
|
|
361
|
+
if (chat.provider === provider) return
|
|
362
|
+
const event: ChatEvent = {
|
|
363
|
+
v: STORE_VERSION,
|
|
364
|
+
type: "chat_provider_set",
|
|
365
|
+
timestamp: Date.now(),
|
|
366
|
+
chatId,
|
|
367
|
+
provider,
|
|
368
|
+
}
|
|
369
|
+
await this.append(CHATS_LOG, event)
|
|
370
|
+
}
|
|
371
|
+
|
|
329
372
|
async setPlanMode(chatId: string, planMode: boolean) {
|
|
330
373
|
const chat = this.requireChat(chatId)
|
|
331
374
|
if (chat.planMode === planMode) return
|
|
@@ -396,15 +439,15 @@ export class EventStore {
|
|
|
396
439
|
await this.append(TURNS_LOG, event)
|
|
397
440
|
}
|
|
398
441
|
|
|
399
|
-
async
|
|
442
|
+
async setSessionToken(chatId: string, sessionToken: string | null) {
|
|
400
443
|
const chat = this.requireChat(chatId)
|
|
401
|
-
if (chat.
|
|
444
|
+
if (chat.sessionToken === sessionToken) return
|
|
402
445
|
const event: TurnEvent = {
|
|
403
446
|
v: STORE_VERSION,
|
|
404
|
-
type: "
|
|
447
|
+
type: "session_token_set",
|
|
405
448
|
timestamp: Date.now(),
|
|
406
449
|
chatId,
|
|
407
|
-
|
|
450
|
+
sessionToken,
|
|
408
451
|
}
|
|
409
452
|
await this.append(TURNS_LOG, event)
|
|
410
453
|
}
|
|
@@ -460,19 +503,23 @@ export class EventStore {
|
|
|
460
503
|
entries: cloneTranscriptEntries(entries),
|
|
461
504
|
})),
|
|
462
505
|
}
|
|
506
|
+
|
|
463
507
|
await Bun.write(SNAPSHOT_PATH, JSON.stringify(snapshot, null, 2))
|
|
464
|
-
await
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
508
|
+
await Promise.all([
|
|
509
|
+
Bun.write(PROJECTS_LOG, ""),
|
|
510
|
+
Bun.write(CHATS_LOG, ""),
|
|
511
|
+
Bun.write(MESSAGES_LOG, ""),
|
|
512
|
+
Bun.write(TURNS_LOG, ""),
|
|
513
|
+
])
|
|
468
514
|
}
|
|
469
515
|
|
|
470
516
|
private async shouldCompact() {
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
517
|
+
const sizes = await Promise.all([
|
|
518
|
+
Bun.file(PROJECTS_LOG).size,
|
|
519
|
+
Bun.file(CHATS_LOG).size,
|
|
520
|
+
Bun.file(MESSAGES_LOG).size,
|
|
521
|
+
Bun.file(TURNS_LOG).size,
|
|
522
|
+
])
|
|
523
|
+
return sizes.reduce((total, size) => total + size, 0) >= COMPACTION_THRESHOLD_BYTES
|
|
477
524
|
}
|
|
478
525
|
}
|
package/src/server/events.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProjectSummary, TranscriptEntry } from "../shared/types"
|
|
1
|
+
import type { AgentProvider, ProjectSummary, TranscriptEntry } from "../shared/types"
|
|
2
2
|
|
|
3
3
|
export interface ProjectRecord extends ProjectSummary {
|
|
4
4
|
deletedAt?: number
|
|
@@ -11,8 +11,9 @@ export interface ChatRecord {
|
|
|
11
11
|
createdAt: number
|
|
12
12
|
updatedAt: number
|
|
13
13
|
deletedAt?: number
|
|
14
|
+
provider: AgentProvider | null
|
|
14
15
|
planMode: boolean
|
|
15
|
-
|
|
16
|
+
sessionToken: string | null
|
|
16
17
|
lastMessageAt?: number
|
|
17
18
|
lastTurnOutcome: "success" | "failed" | "cancelled" | null
|
|
18
19
|
}
|
|
@@ -25,7 +26,7 @@ export interface StoreState {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface SnapshotFile {
|
|
28
|
-
v:
|
|
29
|
+
v: 2
|
|
29
30
|
generatedAt: number
|
|
30
31
|
projects: ProjectRecord[]
|
|
31
32
|
chats: ChatRecord[]
|
|
@@ -33,14 +34,14 @@ export interface SnapshotFile {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export type ProjectEvent = {
|
|
36
|
-
v:
|
|
37
|
+
v: 2
|
|
37
38
|
type: "project_opened"
|
|
38
39
|
timestamp: number
|
|
39
40
|
projectId: string
|
|
40
41
|
localPath: string
|
|
41
42
|
title: string
|
|
42
43
|
} | {
|
|
43
|
-
v:
|
|
44
|
+
v: 2
|
|
44
45
|
type: "project_removed"
|
|
45
46
|
timestamp: number
|
|
46
47
|
projectId: string
|
|
@@ -48,7 +49,7 @@ export type ProjectEvent = {
|
|
|
48
49
|
|
|
49
50
|
export type ChatEvent =
|
|
50
51
|
| {
|
|
51
|
-
v:
|
|
52
|
+
v: 2
|
|
52
53
|
type: "chat_created"
|
|
53
54
|
timestamp: number
|
|
54
55
|
chatId: string
|
|
@@ -56,20 +57,27 @@ export type ChatEvent =
|
|
|
56
57
|
title: string
|
|
57
58
|
}
|
|
58
59
|
| {
|
|
59
|
-
v:
|
|
60
|
+
v: 2
|
|
60
61
|
type: "chat_renamed"
|
|
61
62
|
timestamp: number
|
|
62
63
|
chatId: string
|
|
63
64
|
title: string
|
|
64
65
|
}
|
|
65
66
|
| {
|
|
66
|
-
v:
|
|
67
|
+
v: 2
|
|
67
68
|
type: "chat_deleted"
|
|
68
69
|
timestamp: number
|
|
69
70
|
chatId: string
|
|
70
71
|
}
|
|
71
72
|
| {
|
|
72
|
-
v:
|
|
73
|
+
v: 2
|
|
74
|
+
type: "chat_provider_set"
|
|
75
|
+
timestamp: number
|
|
76
|
+
chatId: string
|
|
77
|
+
provider: AgentProvider
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
v: 2
|
|
73
81
|
type: "chat_plan_mode_set"
|
|
74
82
|
timestamp: number
|
|
75
83
|
chatId: string
|
|
@@ -77,7 +85,7 @@ export type ChatEvent =
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
export type MessageEvent = {
|
|
80
|
-
v:
|
|
88
|
+
v: 2
|
|
81
89
|
type: "message_appended"
|
|
82
90
|
timestamp: number
|
|
83
91
|
chatId: string
|
|
@@ -86,36 +94,36 @@ export type MessageEvent = {
|
|
|
86
94
|
|
|
87
95
|
export type TurnEvent =
|
|
88
96
|
| {
|
|
89
|
-
v:
|
|
97
|
+
v: 2
|
|
90
98
|
type: "turn_started"
|
|
91
99
|
timestamp: number
|
|
92
100
|
chatId: string
|
|
93
101
|
}
|
|
94
102
|
| {
|
|
95
|
-
v:
|
|
103
|
+
v: 2
|
|
96
104
|
type: "turn_finished"
|
|
97
105
|
timestamp: number
|
|
98
106
|
chatId: string
|
|
99
107
|
}
|
|
100
108
|
| {
|
|
101
|
-
v:
|
|
109
|
+
v: 2
|
|
102
110
|
type: "turn_failed"
|
|
103
111
|
timestamp: number
|
|
104
112
|
chatId: string
|
|
105
113
|
error: string
|
|
106
114
|
}
|
|
107
115
|
| {
|
|
108
|
-
v:
|
|
116
|
+
v: 2
|
|
109
117
|
type: "turn_cancelled"
|
|
110
118
|
timestamp: number
|
|
111
119
|
chatId: string
|
|
112
120
|
}
|
|
113
121
|
| {
|
|
114
|
-
v:
|
|
115
|
-
type: "
|
|
122
|
+
v: 2
|
|
123
|
+
type: "session_token_set"
|
|
116
124
|
timestamp: number
|
|
117
125
|
chatId: string
|
|
118
|
-
|
|
126
|
+
sessionToken: string | null
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
export type StoreEvent = ProjectEvent | ChatEvent | MessageEvent | TurnEvent
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AccountInfo, AgentProvider, NormalizedToolCall, TranscriptEntry } from "../shared/types"
|
|
2
|
+
|
|
3
|
+
export interface HarnessEvent {
|
|
4
|
+
type: "transcript" | "session_token"
|
|
5
|
+
entry?: TranscriptEntry
|
|
6
|
+
sessionToken?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface HarnessToolRequest {
|
|
10
|
+
tool: NormalizedToolCall & { toolKind: "ask_user_question" | "exit_plan_mode" }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface HarnessTurn {
|
|
14
|
+
provider: AgentProvider
|
|
15
|
+
stream: AsyncIterable<HarnessEvent>
|
|
16
|
+
getAccountInfo?: () => Promise<AccountInfo | null>
|
|
17
|
+
interrupt: () => Promise<void>
|
|
18
|
+
close: () => void
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
codexServiceTierFromModelOptions,
|
|
4
|
+
normalizeClaudeModelOptions,
|
|
5
|
+
normalizeCodexModelOptions,
|
|
6
|
+
} from "./provider-catalog"
|
|
7
|
+
|
|
8
|
+
describe("provider catalog normalization", () => {
|
|
9
|
+
test("maps legacy Claude effort into shared model options", () => {
|
|
10
|
+
expect(normalizeClaudeModelOptions(undefined, "max")).toEqual({
|
|
11
|
+
reasoningEffort: "max",
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("normalizes Codex model options and fast mode defaults", () => {
|
|
16
|
+
expect(normalizeCodexModelOptions(undefined)).toEqual({
|
|
17
|
+
reasoningEffort: "high",
|
|
18
|
+
fastMode: false,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const normalized = normalizeCodexModelOptions({
|
|
22
|
+
codex: {
|
|
23
|
+
reasoningEffort: "xhigh",
|
|
24
|
+
fastMode: true,
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(normalized).toEqual({
|
|
29
|
+
reasoningEffort: "xhigh",
|
|
30
|
+
fastMode: true,
|
|
31
|
+
})
|
|
32
|
+
expect(codexServiceTierFromModelOptions(normalized)).toBe("fast")
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentProvider,
|
|
3
|
+
ClaudeModelOptions,
|
|
4
|
+
CodexModelOptions,
|
|
5
|
+
ModelOptions,
|
|
6
|
+
ProviderCatalogEntry,
|
|
7
|
+
ProviderModelOption,
|
|
8
|
+
ServiceTier,
|
|
9
|
+
} from "../shared/types"
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_CLAUDE_MODEL_OPTIONS,
|
|
12
|
+
DEFAULT_CODEX_MODEL_OPTIONS,
|
|
13
|
+
PROVIDERS,
|
|
14
|
+
isClaudeReasoningEffort,
|
|
15
|
+
isCodexReasoningEffort,
|
|
16
|
+
} from "../shared/types"
|
|
17
|
+
|
|
18
|
+
const HARD_CODED_CODEX_MODELS: ProviderModelOption[] = [
|
|
19
|
+
{ id: "gpt-5.4", label: "GPT-5.4", supportsEffort: false },
|
|
20
|
+
{ id: "gpt-5.3-codex", label: "GPT-5.3 Codex", supportsEffort: false },
|
|
21
|
+
{ id: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark", supportsEffort: false },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
export const SERVER_PROVIDERS: ProviderCatalogEntry[] = PROVIDERS.map((provider) =>
|
|
25
|
+
provider.id === "codex"
|
|
26
|
+
? {
|
|
27
|
+
...provider,
|
|
28
|
+
defaultModel: "gpt-5.4",
|
|
29
|
+
models: HARD_CODED_CODEX_MODELS,
|
|
30
|
+
}
|
|
31
|
+
: provider
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export function getServerProviderCatalog(provider: AgentProvider): ProviderCatalogEntry {
|
|
35
|
+
const entry = SERVER_PROVIDERS.find((candidate) => candidate.id === provider)
|
|
36
|
+
if (!entry) {
|
|
37
|
+
throw new Error(`Unknown provider: ${provider}`)
|
|
38
|
+
}
|
|
39
|
+
return entry
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalizeServerModel(provider: AgentProvider, model?: string): string {
|
|
43
|
+
const catalog = getServerProviderCatalog(provider)
|
|
44
|
+
if (model && catalog.models.some((candidate) => candidate.id === model)) {
|
|
45
|
+
return model
|
|
46
|
+
}
|
|
47
|
+
return catalog.defaultModel
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeClaudeModelOptions(modelOptions?: ModelOptions, legacyEffort?: string): ClaudeModelOptions {
|
|
51
|
+
const reasoningEffort = modelOptions?.claude?.reasoningEffort
|
|
52
|
+
return {
|
|
53
|
+
reasoningEffort: isClaudeReasoningEffort(reasoningEffort)
|
|
54
|
+
? reasoningEffort
|
|
55
|
+
: isClaudeReasoningEffort(legacyEffort)
|
|
56
|
+
? legacyEffort
|
|
57
|
+
: DEFAULT_CLAUDE_MODEL_OPTIONS.reasoningEffort,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeCodexModelOptions(modelOptions?: ModelOptions, legacyEffort?: string): CodexModelOptions {
|
|
62
|
+
const reasoningEffort = modelOptions?.codex?.reasoningEffort
|
|
63
|
+
return {
|
|
64
|
+
reasoningEffort: isCodexReasoningEffort(reasoningEffort)
|
|
65
|
+
? reasoningEffort
|
|
66
|
+
: isCodexReasoningEffort(legacyEffort)
|
|
67
|
+
? legacyEffort
|
|
68
|
+
: DEFAULT_CODEX_MODEL_OPTIONS.reasoningEffort,
|
|
69
|
+
fastMode: typeof modelOptions?.codex?.fastMode === "boolean"
|
|
70
|
+
? modelOptions.codex.fastMode
|
|
71
|
+
: DEFAULT_CODEX_MODEL_OPTIONS.fastMode,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function codexServiceTierFromModelOptions(modelOptions: CodexModelOptions): ServiceTier | undefined {
|
|
76
|
+
return modelOptions.fastMode ? "fast" : undefined
|
|
77
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { deriveChatSnapshot, deriveSidebarData } from "./read-models"
|
|
3
|
+
import { createEmptyState } from "./events"
|
|
4
|
+
|
|
5
|
+
describe("read models", () => {
|
|
6
|
+
test("include provider data in sidebar rows", () => {
|
|
7
|
+
const state = createEmptyState()
|
|
8
|
+
state.projectsById.set("project-1", {
|
|
9
|
+
id: "project-1",
|
|
10
|
+
localPath: "/tmp/project",
|
|
11
|
+
title: "Project",
|
|
12
|
+
createdAt: 1,
|
|
13
|
+
updatedAt: 1,
|
|
14
|
+
})
|
|
15
|
+
state.projectIdsByPath.set("/tmp/project", "project-1")
|
|
16
|
+
state.chatsById.set("chat-1", {
|
|
17
|
+
id: "chat-1",
|
|
18
|
+
projectId: "project-1",
|
|
19
|
+
title: "Chat",
|
|
20
|
+
createdAt: 1,
|
|
21
|
+
updatedAt: 1,
|
|
22
|
+
provider: "codex",
|
|
23
|
+
planMode: false,
|
|
24
|
+
sessionToken: "thread-1",
|
|
25
|
+
lastTurnOutcome: null,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const sidebar = deriveSidebarData(state, new Map())
|
|
29
|
+
expect(sidebar.projectGroups[0]?.chats[0]?.provider).toBe("codex")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("includes available providers in chat snapshots", () => {
|
|
33
|
+
const state = createEmptyState()
|
|
34
|
+
state.projectsById.set("project-1", {
|
|
35
|
+
id: "project-1",
|
|
36
|
+
localPath: "/tmp/project",
|
|
37
|
+
title: "Project",
|
|
38
|
+
createdAt: 1,
|
|
39
|
+
updatedAt: 1,
|
|
40
|
+
})
|
|
41
|
+
state.projectIdsByPath.set("/tmp/project", "project-1")
|
|
42
|
+
state.chatsById.set("chat-1", {
|
|
43
|
+
id: "chat-1",
|
|
44
|
+
projectId: "project-1",
|
|
45
|
+
title: "Chat",
|
|
46
|
+
createdAt: 1,
|
|
47
|
+
updatedAt: 1,
|
|
48
|
+
provider: "claude",
|
|
49
|
+
planMode: true,
|
|
50
|
+
sessionToken: "session-1",
|
|
51
|
+
lastTurnOutcome: null,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const chat = deriveChatSnapshot(state, new Map(), "chat-1")
|
|
55
|
+
expect(chat?.runtime.provider).toBe("claude")
|
|
56
|
+
expect(chat?.availableProviders.length).toBeGreaterThan(1)
|
|
57
|
+
expect(chat?.availableProviders.find((provider) => provider.id === "codex")?.models.map((model) => model.id)).toEqual([
|
|
58
|
+
"gpt-5.4",
|
|
59
|
+
"gpt-5.3-codex",
|
|
60
|
+
"gpt-5.3-codex-spark",
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
import type { ChatRecord, StoreState } from "./events"
|
|
11
11
|
import { cloneTranscriptEntries } from "./events"
|
|
12
12
|
import { resolveLocalPath } from "./paths"
|
|
13
|
+
import { SERVER_PROVIDERS } from "./provider-catalog"
|
|
13
14
|
|
|
14
15
|
export function deriveStatus(chat: ChatRecord, activeStatus?: KannaStatus): KannaStatus {
|
|
15
16
|
if (activeStatus) return activeStatus
|
|
@@ -36,6 +37,7 @@ export function deriveSidebarData(
|
|
|
36
37
|
title: chat.title,
|
|
37
38
|
status: deriveStatus(chat, activeStatuses.get(chat.id)),
|
|
38
39
|
localPath: project.localPath,
|
|
40
|
+
provider: chat.provider,
|
|
39
41
|
lastMessageAt: chat.lastMessageAt,
|
|
40
42
|
hasAutomation: false,
|
|
41
43
|
}))
|
|
@@ -109,12 +111,14 @@ export function deriveChatSnapshot(
|
|
|
109
111
|
localPath: project.localPath,
|
|
110
112
|
title: chat.title,
|
|
111
113
|
status: deriveStatus(chat, activeStatuses.get(chat.id)),
|
|
114
|
+
provider: chat.provider,
|
|
112
115
|
planMode: chat.planMode,
|
|
113
|
-
|
|
116
|
+
sessionToken: chat.sessionToken,
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
return {
|
|
117
120
|
runtime,
|
|
118
121
|
messages: cloneTranscriptEntries(state.messagesByChatId.get(chat.id) ?? []),
|
|
122
|
+
availableProviders: [...SERVER_PROVIDERS],
|
|
119
123
|
}
|
|
120
124
|
}
|
package/src/shared/protocol.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatSnapshot, LocalProjectsSnapshot, SidebarData } from "./types"
|
|
1
|
+
import type { AgentProvider, ChatSnapshot, LocalProjectsSnapshot, ModelOptions, SidebarData } from "./types"
|
|
2
2
|
|
|
3
3
|
export type SubscriptionTopic =
|
|
4
4
|
| { type: "sidebar" }
|
|
@@ -13,7 +13,17 @@ export type ClientCommand =
|
|
|
13
13
|
| { type: "chat.create"; projectId: string }
|
|
14
14
|
| { type: "chat.rename"; chatId: string; title: string }
|
|
15
15
|
| { type: "chat.delete"; chatId: string }
|
|
16
|
-
| {
|
|
16
|
+
| {
|
|
17
|
+
type: "chat.send"
|
|
18
|
+
chatId?: string
|
|
19
|
+
projectId?: string
|
|
20
|
+
provider?: AgentProvider
|
|
21
|
+
content: string
|
|
22
|
+
model?: string
|
|
23
|
+
modelOptions?: ModelOptions
|
|
24
|
+
effort?: string
|
|
25
|
+
planMode?: boolean
|
|
26
|
+
}
|
|
17
27
|
| { type: "chat.cancel"; chatId: string }
|
|
18
28
|
| { type: "chat.respondTool"; chatId: string; toolUseId: string; result: unknown }
|
|
19
29
|
|