kanna-code 0.14.0 → 0.16.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/README.md +28 -2
- package/dist/client/assets/index-BET6YVYr.css +32 -0
- package/dist/client/assets/index-DMxCijCf.js +533 -0
- package/dist/client/index.html +2 -2
- package/package.json +3 -2
- package/src/server/agent.ts +8 -36
- package/src/server/cli-runtime.test.ts +55 -0
- package/src/server/cli-runtime.ts +30 -9
- package/src/server/codex-app-server-protocol.ts +8 -1
- package/src/server/codex-app-server.ts +1 -1
- package/src/server/event-store.test.ts +112 -1
- package/src/server/event-store.ts +170 -26
- package/src/server/events.ts +1 -3
- package/src/server/generate-title.ts +0 -13
- package/src/server/provider-catalog.test.ts +29 -1
- package/src/server/provider-catalog.ts +8 -1
- package/src/server/quick-response.ts +0 -9
- package/src/server/read-models.test.ts +1 -1
- package/src/server/read-models.ts +3 -3
- package/src/server/server.ts +5 -0
- package/src/server/ws-router.ts +1 -1
- package/src/shared/dev-ports.test.ts +52 -0
- package/src/shared/dev-ports.ts +43 -0
- package/src/shared/ports.ts +0 -1
- package/src/shared/types.ts +41 -3
- package/dist/client/assets/index-CVu2H5bX.js +0 -523
- package/dist/client/assets/index-D5tYLJi-.css +0 -32
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Kanna</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DMxCijCf.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BET6YVYr.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kanna-code",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"description": "A beautiful web UI for Claude Code",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"check": "tsc --noEmit && vite build",
|
|
38
38
|
"dev": "bun run ./scripts/dev.ts",
|
|
39
39
|
"dev:client": "vite --host 0.0.0.0 --port 5174",
|
|
40
|
-
"dev:server": "bun run ./scripts/dev-server.ts --no-open --port
|
|
40
|
+
"dev:server": "bun run ./scripts/dev-server.ts --no-open --port 5175",
|
|
41
|
+
"install:dev": "bun install && bun run build && bun install -g .",
|
|
41
42
|
"start": "bun run ./src/server/cli.ts",
|
|
42
43
|
"test": "bun test",
|
|
43
44
|
"prepublishOnly": "vite build"
|
package/src/server/agent.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
normalizeCodexModelOptions,
|
|
20
20
|
normalizeServerModel,
|
|
21
21
|
} from "./provider-catalog"
|
|
22
|
+
import { resolveClaudeApiModelId } from "../shared/types"
|
|
22
23
|
|
|
23
24
|
const CLAUDE_TOOLSET = [
|
|
24
25
|
"Skill",
|
|
@@ -39,8 +40,6 @@ const CLAUDE_TOOLSET = [
|
|
|
39
40
|
"ExitPlanMode",
|
|
40
41
|
] as const
|
|
41
42
|
|
|
42
|
-
const TITLE_LOG_PREFIX = "[kanna:title]"
|
|
43
|
-
|
|
44
43
|
interface PendingToolRequest {
|
|
45
44
|
toolUseId: string
|
|
46
45
|
tool: NormalizedToolCall & { toolKind: "ask_user_question" | "exit_plan_mode" }
|
|
@@ -367,9 +366,10 @@ export class AgentCoordinator {
|
|
|
367
366
|
private getProviderSettings(provider: AgentProvider, command: Extract<ClientCommand, { type: "chat.send" }>) {
|
|
368
367
|
const catalog = getServerProviderCatalog(provider)
|
|
369
368
|
if (provider === "claude") {
|
|
370
|
-
const
|
|
369
|
+
const model = normalizeServerModel(provider, command.model)
|
|
370
|
+
const modelOptions = normalizeClaudeModelOptions(model, command.modelOptions, command.effort)
|
|
371
371
|
return {
|
|
372
|
-
model:
|
|
372
|
+
model: resolveClaudeApiModelId(model, modelOptions.contextWindow),
|
|
373
373
|
effort: modelOptions.reasoningEffort,
|
|
374
374
|
serviceTier: undefined,
|
|
375
375
|
planMode: catalog.supportsPlanMode ? Boolean(command.planMode) : false,
|
|
@@ -408,14 +408,6 @@ export class AgentCoordinator {
|
|
|
408
408
|
const existingMessages = this.store.getMessages(args.chatId)
|
|
409
409
|
const shouldGenerateTitle = args.appendUserPrompt && chat.title === "New Chat" && existingMessages.length === 0
|
|
410
410
|
|
|
411
|
-
console.warn(`${TITLE_LOG_PREFIX} title generation check`, {
|
|
412
|
-
chatId: args.chatId,
|
|
413
|
-
appendUserPrompt: args.appendUserPrompt,
|
|
414
|
-
currentTitle: chat.title,
|
|
415
|
-
existingMessageCount: existingMessages.length,
|
|
416
|
-
shouldGenerateTitle,
|
|
417
|
-
})
|
|
418
|
-
|
|
419
411
|
if (args.appendUserPrompt) {
|
|
420
412
|
await this.store.appendMessage(args.chatId, timestamped({ kind: "user_prompt", content: args.content }, Date.now()))
|
|
421
413
|
}
|
|
@@ -428,10 +420,6 @@ export class AgentCoordinator {
|
|
|
428
420
|
|
|
429
421
|
if (shouldGenerateTitle) {
|
|
430
422
|
void this.generateTitleInBackground(args.chatId, args.content, project.localPath)
|
|
431
|
-
} else {
|
|
432
|
-
console.warn(`${TITLE_LOG_PREFIX} not starting background title generation`, {
|
|
433
|
-
chatId: args.chatId,
|
|
434
|
-
})
|
|
435
423
|
}
|
|
436
424
|
|
|
437
425
|
const onToolRequest = async (request: HarnessToolRequest): Promise<unknown> => {
|
|
@@ -543,32 +531,16 @@ export class AgentCoordinator {
|
|
|
543
531
|
|
|
544
532
|
private async generateTitleInBackground(chatId: string, messageContent: string, cwd: string) {
|
|
545
533
|
try {
|
|
546
|
-
console.log(`${TITLE_LOG_PREFIX} background generation started`, {
|
|
547
|
-
chatId,
|
|
548
|
-
cwd,
|
|
549
|
-
messagePreview: messageContent.replace(/\s+/g, " ").trim().slice(0, 120),
|
|
550
|
-
})
|
|
551
534
|
const title = await this.generateTitle(messageContent, cwd)
|
|
552
|
-
if (!title)
|
|
553
|
-
console.warn(`${TITLE_LOG_PREFIX} background generation produced no title`, { chatId })
|
|
554
|
-
return
|
|
555
|
-
}
|
|
535
|
+
if (!title) return
|
|
556
536
|
|
|
557
537
|
const chat = this.store.requireChat(chatId)
|
|
558
|
-
if (chat.title !== "New Chat")
|
|
559
|
-
console.log(`${TITLE_LOG_PREFIX} skipping rename because chat title already changed`, {
|
|
560
|
-
chatId,
|
|
561
|
-
currentTitle: chat.title,
|
|
562
|
-
generatedTitle: title,
|
|
563
|
-
})
|
|
564
|
-
return
|
|
565
|
-
}
|
|
538
|
+
if (chat.title !== "New Chat") return
|
|
566
539
|
|
|
567
540
|
await this.store.renameChat(chatId, title)
|
|
568
|
-
console.log(`${TITLE_LOG_PREFIX} renamed chat from generated title`, { chatId, title })
|
|
569
541
|
this.onStateChange()
|
|
570
|
-
} catch
|
|
571
|
-
|
|
542
|
+
} catch {
|
|
543
|
+
// Ignore background title generation failures.
|
|
572
544
|
}
|
|
573
545
|
}
|
|
574
546
|
|
|
@@ -22,6 +22,7 @@ function createDeps(overrides: Partial<Parameters<typeof runCli>[1]> = {}) {
|
|
|
22
22
|
const calls = {
|
|
23
23
|
startServer: [] as Array<{
|
|
24
24
|
port: number
|
|
25
|
+
host: string
|
|
25
26
|
openBrowser: boolean
|
|
26
27
|
strictPort: boolean
|
|
27
28
|
update: {
|
|
@@ -81,6 +82,7 @@ describe("parseArgs", () => {
|
|
|
81
82
|
kind: "run",
|
|
82
83
|
options: {
|
|
83
84
|
port: 4000,
|
|
85
|
+
host: "127.0.0.1",
|
|
84
86
|
openBrowser: false,
|
|
85
87
|
strictPort: false,
|
|
86
88
|
},
|
|
@@ -92,12 +94,54 @@ describe("parseArgs", () => {
|
|
|
92
94
|
kind: "run",
|
|
93
95
|
options: {
|
|
94
96
|
port: 3210,
|
|
97
|
+
host: "127.0.0.1",
|
|
95
98
|
openBrowser: true,
|
|
96
99
|
strictPort: true,
|
|
97
100
|
},
|
|
98
101
|
})
|
|
99
102
|
})
|
|
100
103
|
|
|
104
|
+
test("--remote without value binds all interfaces", () => {
|
|
105
|
+
expect(parseArgs(["--remote"])).toEqual({
|
|
106
|
+
kind: "run",
|
|
107
|
+
options: {
|
|
108
|
+
port: 3210,
|
|
109
|
+
host: "0.0.0.0",
|
|
110
|
+
openBrowser: true,
|
|
111
|
+
strictPort: false,
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("--host with IP binds to that address", () => {
|
|
117
|
+
expect(parseArgs(["--host", "100.64.0.1"])).toEqual({
|
|
118
|
+
kind: "run",
|
|
119
|
+
options: {
|
|
120
|
+
port: 3210,
|
|
121
|
+
host: "100.64.0.1",
|
|
122
|
+
openBrowser: true,
|
|
123
|
+
strictPort: false,
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test("--host with hostname binds to that name", () => {
|
|
129
|
+
expect(parseArgs(["--host", "dev-box"])).toEqual({
|
|
130
|
+
kind: "run",
|
|
131
|
+
options: {
|
|
132
|
+
port: 3210,
|
|
133
|
+
host: "dev-box",
|
|
134
|
+
openBrowser: true,
|
|
135
|
+
strictPort: false,
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test("--host without a value throws", () => {
|
|
141
|
+
expect(() => parseArgs(["--host"])).toThrow("Missing value for --host")
|
|
142
|
+
expect(() => parseArgs(["--host", "--no-open"])).toThrow("Missing value for --host")
|
|
143
|
+
})
|
|
144
|
+
|
|
101
145
|
test("returns version and help actions without running startup", () => {
|
|
102
146
|
expect(parseArgs(["--version"])).toEqual({ kind: "version" })
|
|
103
147
|
expect(parseArgs(["--help"])).toEqual({ kind: "help" })
|
|
@@ -146,6 +190,7 @@ describe("runCli", () => {
|
|
|
146
190
|
expect(calls.startServer).toHaveLength(1)
|
|
147
191
|
expect(calls.startServer[0]).toMatchObject({
|
|
148
192
|
port: 4000,
|
|
193
|
+
host: "127.0.0.1",
|
|
149
194
|
openBrowser: false,
|
|
150
195
|
strictPort: false,
|
|
151
196
|
update: {
|
|
@@ -180,6 +225,7 @@ describe("runCli", () => {
|
|
|
180
225
|
})
|
|
181
226
|
|
|
182
227
|
test("opens the root route in the browser", async () => {
|
|
228
|
+
delete process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR]
|
|
183
229
|
const { calls, deps } = createDeps()
|
|
184
230
|
|
|
185
231
|
await runCli(["--port", "4000"], deps)
|
|
@@ -187,6 +233,15 @@ describe("runCli", () => {
|
|
|
187
233
|
expect(calls.openUrl).toEqual(["http://localhost:4000"])
|
|
188
234
|
})
|
|
189
235
|
|
|
236
|
+
test("opens browser at hostname when --host <host> is given", async () => {
|
|
237
|
+
delete process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR]
|
|
238
|
+
const { calls, deps } = createDeps()
|
|
239
|
+
|
|
240
|
+
await runCli(["--host", "dev-box", "--port", "4000"], deps)
|
|
241
|
+
|
|
242
|
+
expect(calls.openUrl).toEqual(["http://dev-box:4000"])
|
|
243
|
+
})
|
|
244
|
+
|
|
190
245
|
test("suppresses browser open for a ui-triggered restarted child", async () => {
|
|
191
246
|
process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR] = "1"
|
|
192
247
|
const { calls, deps } = createDeps()
|
|
@@ -8,6 +8,7 @@ import { CLI_SUPPRESS_OPEN_ONCE_ENV_VAR } from "./restart"
|
|
|
8
8
|
|
|
9
9
|
export interface CliOptions {
|
|
10
10
|
port: number
|
|
11
|
+
host: string
|
|
11
12
|
openBrowser: boolean
|
|
12
13
|
strictPort: boolean
|
|
13
14
|
}
|
|
@@ -40,7 +41,10 @@ export type CliRunResult = StartedCli | RestartingCli | ExitedCli
|
|
|
40
41
|
export interface CliRuntimeDeps {
|
|
41
42
|
version: string
|
|
42
43
|
bunVersion: string
|
|
43
|
-
startServer: (options: CliOptions & {
|
|
44
|
+
startServer: (options: CliOptions & {
|
|
45
|
+
update: CliUpdateOptions
|
|
46
|
+
onMigrationProgress?: (message: string) => void
|
|
47
|
+
}) => Promise<{ port: number; stop: () => Promise<void> }>
|
|
44
48
|
fetchLatestVersion: (packageName: string) => Promise<string>
|
|
45
49
|
installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
|
|
46
50
|
openUrl: (url: string) => void
|
|
@@ -69,15 +73,18 @@ Usage:
|
|
|
69
73
|
${CLI_COMMAND} [options]
|
|
70
74
|
|
|
71
75
|
Options:
|
|
72
|
-
--port <number>
|
|
73
|
-
--
|
|
74
|
-
--
|
|
75
|
-
--
|
|
76
|
-
--
|
|
76
|
+
--port <number> Port to listen on (default: ${PROD_SERVER_PORT})
|
|
77
|
+
--host <host> Bind to a specific host or IP
|
|
78
|
+
--remote Shortcut for --host 0.0.0.0
|
|
79
|
+
--strict-port Fail instead of trying another port
|
|
80
|
+
--no-open Don't open browser automatically
|
|
81
|
+
--version Print version and exit
|
|
82
|
+
--help Show this help message`)
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
export function parseArgs(argv: string[]): ParsedArgs {
|
|
80
86
|
let port = PROD_SERVER_PORT
|
|
87
|
+
let host = "127.0.0.1"
|
|
81
88
|
let openBrowser = true
|
|
82
89
|
let strictPort = false
|
|
83
90
|
|
|
@@ -96,6 +103,17 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
96
103
|
index += 1
|
|
97
104
|
continue
|
|
98
105
|
}
|
|
106
|
+
if (arg === "--host") {
|
|
107
|
+
const next = argv[index + 1]
|
|
108
|
+
if (!next || next.startsWith("-")) throw new Error("Missing value for --host")
|
|
109
|
+
host = next
|
|
110
|
+
index += 1
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (arg === "--remote") {
|
|
114
|
+
host = "0.0.0.0"
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
99
117
|
if (arg === "--no-open") {
|
|
100
118
|
openBrowser = false
|
|
101
119
|
continue
|
|
@@ -111,6 +129,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
111
129
|
kind: "run",
|
|
112
130
|
options: {
|
|
113
131
|
port,
|
|
132
|
+
host,
|
|
114
133
|
openBrowser,
|
|
115
134
|
strictPort,
|
|
116
135
|
},
|
|
@@ -202,6 +221,7 @@ export async function runCli(argv: string[], deps: CliRuntimeDeps): Promise<CliR
|
|
|
202
221
|
|
|
203
222
|
const { port, stop } = await deps.startServer({
|
|
204
223
|
...parsedArgs.options,
|
|
224
|
+
onMigrationProgress: deps.log,
|
|
205
225
|
update: {
|
|
206
226
|
version: deps.version,
|
|
207
227
|
fetchLatestVersion: deps.fetchLatestVersion,
|
|
@@ -210,10 +230,11 @@ export async function runCli(argv: string[], deps: CliRuntimeDeps): Promise<CliR
|
|
|
210
230
|
command: CLI_COMMAND,
|
|
211
231
|
},
|
|
212
232
|
})
|
|
213
|
-
const
|
|
214
|
-
const
|
|
233
|
+
const bindHost = parsedArgs.options.host
|
|
234
|
+
const displayHost = bindHost === "127.0.0.1" || bindHost === "0.0.0.0" ? "localhost" : bindHost
|
|
235
|
+
const launchUrl = `http://${displayHost}:${port}`
|
|
215
236
|
|
|
216
|
-
deps.log(`${LOG_PREFIX} listening on
|
|
237
|
+
deps.log(`${LOG_PREFIX} listening on http://${bindHost}:${port}`)
|
|
217
238
|
deps.log(`${LOG_PREFIX} data dir: ${getDataDirDisplay()}`)
|
|
218
239
|
|
|
219
240
|
const suppressOpenBrowser = process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR] === "1"
|
|
@@ -394,7 +394,14 @@ export interface ItemCompletedNotification {
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
export interface ErrorNotification {
|
|
397
|
-
|
|
397
|
+
error: {
|
|
398
|
+
message: string
|
|
399
|
+
codexErrorInfo?: string
|
|
400
|
+
additionalDetails?: unknown
|
|
401
|
+
}
|
|
402
|
+
willRetry: boolean
|
|
403
|
+
threadId?: string
|
|
404
|
+
turnId?: string
|
|
398
405
|
}
|
|
399
406
|
|
|
400
407
|
export type ServerNotification =
|
|
@@ -1114,7 +1114,7 @@ export class CodexAppServerManager {
|
|
|
1114
1114
|
this.handleContextCompacted(pendingTurn, notification.params)
|
|
1115
1115
|
return
|
|
1116
1116
|
case "error":
|
|
1117
|
-
this.failContext(context, notification.params.message)
|
|
1117
|
+
this.failContext(context, notification.params.error.message)
|
|
1118
1118
|
return
|
|
1119
1119
|
default:
|
|
1120
1120
|
return
|
|
@@ -1,16 +1,39 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
3
|
+
import { existsSync } from "node:fs"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import type { TranscriptEntry } from "../shared/types"
|
|
7
|
+
import type { SnapshotFile } from "./events"
|
|
2
8
|
import { EventStore } from "./event-store"
|
|
3
9
|
|
|
4
10
|
const originalRuntimeProfile = process.env.KANNA_RUNTIME_PROFILE
|
|
11
|
+
const tempDirs: string[] = []
|
|
5
12
|
|
|
6
|
-
afterEach(() => {
|
|
13
|
+
afterEach(async () => {
|
|
7
14
|
if (originalRuntimeProfile === undefined) {
|
|
8
15
|
delete process.env.KANNA_RUNTIME_PROFILE
|
|
9
16
|
} else {
|
|
10
17
|
process.env.KANNA_RUNTIME_PROFILE = originalRuntimeProfile
|
|
11
18
|
}
|
|
19
|
+
|
|
20
|
+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
|
|
12
21
|
})
|
|
13
22
|
|
|
23
|
+
async function createTempDataDir() {
|
|
24
|
+
const dir = await mkdtemp(join(tmpdir(), "kanna-event-store-"))
|
|
25
|
+
tempDirs.push(dir)
|
|
26
|
+
return dir
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function entry(kind: "user_prompt" | "assistant_text", createdAt: number, extra: Record<string, unknown> = {}): TranscriptEntry {
|
|
30
|
+
const base = { _id: `${kind}-${createdAt}`, createdAt }
|
|
31
|
+
if (kind === "user_prompt") {
|
|
32
|
+
return { ...base, kind, content: String(extra.content ?? "") }
|
|
33
|
+
}
|
|
34
|
+
return { ...base, kind, text: String(extra.content ?? extra.text ?? "") }
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
describe("EventStore", () => {
|
|
15
38
|
test("uses the runtime profile for the default data dir", () => {
|
|
16
39
|
process.env.KANNA_RUNTIME_PROFILE = "dev"
|
|
@@ -19,4 +42,92 @@ describe("EventStore", () => {
|
|
|
19
42
|
|
|
20
43
|
expect(store.dataDir).toEndWith("/.kanna-dev/data")
|
|
21
44
|
})
|
|
45
|
+
|
|
46
|
+
test("migrates legacy snapshot and messages log transcripts into per-chat files", async () => {
|
|
47
|
+
const dataDir = await createTempDataDir()
|
|
48
|
+
const snapshotPath = join(dataDir, "snapshot.json")
|
|
49
|
+
const messagesLogPath = join(dataDir, "messages.jsonl")
|
|
50
|
+
const chatId = "chat-1"
|
|
51
|
+
|
|
52
|
+
const snapshot: SnapshotFile = {
|
|
53
|
+
v: 2,
|
|
54
|
+
generatedAt: 10,
|
|
55
|
+
projects: [{
|
|
56
|
+
id: "project-1",
|
|
57
|
+
localPath: "/tmp/project",
|
|
58
|
+
title: "Project",
|
|
59
|
+
createdAt: 1,
|
|
60
|
+
updatedAt: 5,
|
|
61
|
+
}],
|
|
62
|
+
chats: [{
|
|
63
|
+
id: chatId,
|
|
64
|
+
projectId: "project-1",
|
|
65
|
+
title: "Chat",
|
|
66
|
+
createdAt: 1,
|
|
67
|
+
updatedAt: 5,
|
|
68
|
+
provider: null,
|
|
69
|
+
planMode: false,
|
|
70
|
+
sessionToken: null,
|
|
71
|
+
lastTurnOutcome: null,
|
|
72
|
+
}],
|
|
73
|
+
messages: [{
|
|
74
|
+
chatId,
|
|
75
|
+
entries: [
|
|
76
|
+
entry("user_prompt", 100, { content: "hello" }),
|
|
77
|
+
],
|
|
78
|
+
}],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await writeFile(snapshotPath, JSON.stringify(snapshot, null, 2), "utf8")
|
|
82
|
+
await writeFile(messagesLogPath, `${JSON.stringify({
|
|
83
|
+
v: 2,
|
|
84
|
+
type: "message_appended",
|
|
85
|
+
timestamp: 101,
|
|
86
|
+
chatId,
|
|
87
|
+
entry: entry("assistant_text", 101, { content: "world" }),
|
|
88
|
+
})}\n`, "utf8")
|
|
89
|
+
|
|
90
|
+
const store = new EventStore(dataDir)
|
|
91
|
+
await store.initialize()
|
|
92
|
+
|
|
93
|
+
const progress: string[] = []
|
|
94
|
+
const migrated = await store.migrateLegacyTranscripts((message) => {
|
|
95
|
+
progress.push(message)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
expect(migrated).toBe(true)
|
|
99
|
+
expect(progress.some((message) => message.includes("transcript migration detected"))).toBe(true)
|
|
100
|
+
expect(progress.at(-1)).toContain("transcript migration complete")
|
|
101
|
+
expect(store.getMessages(chatId)).toEqual([
|
|
102
|
+
entry("user_prompt", 100, { content: "hello" }),
|
|
103
|
+
entry("assistant_text", 101, { text: "world" }),
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
const migratedSnapshot = JSON.parse(await readFile(snapshotPath, "utf8")) as SnapshotFile
|
|
107
|
+
expect(migratedSnapshot.messages).toBeUndefined()
|
|
108
|
+
expect(await readFile(messagesLogPath, "utf8")).toBe("")
|
|
109
|
+
expect(await readFile(join(dataDir, "transcripts", `${chatId}.jsonl`), "utf8")).toContain('"kind":"assistant_text"')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test("appends new transcript entries only to the per-chat transcript file", async () => {
|
|
113
|
+
const dataDir = await createTempDataDir()
|
|
114
|
+
const store = new EventStore(dataDir)
|
|
115
|
+
await store.initialize()
|
|
116
|
+
|
|
117
|
+
const project = await store.openProject("/tmp/project")
|
|
118
|
+
const chat = await store.createChat(project.id)
|
|
119
|
+
await store.appendMessage(chat.id, entry("user_prompt", 200, { content: "hello" }))
|
|
120
|
+
await store.appendMessage(chat.id, entry("assistant_text", 201, { content: "world" }))
|
|
121
|
+
await store.compact()
|
|
122
|
+
|
|
123
|
+
expect(store.getMessages(chat.id)).toEqual([
|
|
124
|
+
entry("user_prompt", 200, { content: "hello" }),
|
|
125
|
+
entry("assistant_text", 201, { text: "world" }),
|
|
126
|
+
])
|
|
127
|
+
expect(await readFile(join(dataDir, "messages.jsonl"), "utf8")).toBe("")
|
|
128
|
+
|
|
129
|
+
const snapshot = JSON.parse(await readFile(join(dataDir, "snapshot.json"), "utf8")) as SnapshotFile
|
|
130
|
+
expect(snapshot.messages).toBeUndefined()
|
|
131
|
+
expect(existsSync(join(dataDir, "transcripts", `${chat.id}.jsonl`))).toBe(true)
|
|
132
|
+
})
|
|
22
133
|
})
|