kanna-code 0.26.7 → 0.26.9

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.
@@ -5,7 +5,7 @@
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-CH3ZbXse.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-C6vlAZYX.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-C5RBxQW4.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kanna-code",
3
3
  "type": "module",
4
- "version": "0.26.7",
4
+ "version": "0.26.9",
5
5
  "description": "A beautiful web UI for Claude Code",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -19,6 +19,22 @@ import { resolveLocalPath } from "./paths"
19
19
 
20
20
  const COMPACTION_THRESHOLD_BYTES = 2 * 1024 * 1024
21
21
  const STALE_EMPTY_CHAT_MAX_AGE_MS = 30 * 60 * 1000
22
+
23
+ function isSendToStartingProfilingEnabled() {
24
+ return process.env.KANNA_PROFILE_SEND_TO_STARTING === "1"
25
+ }
26
+
27
+ function logSendToStartingProfile(stage: string, details?: Record<string, unknown>) {
28
+ if (!isSendToStartingProfilingEnabled()) {
29
+ return
30
+ }
31
+
32
+ console.log("[kanna/send->starting][server]", JSON.stringify({
33
+ stage,
34
+ ...details,
35
+ }))
36
+ }
37
+
22
38
  interface LegacyTranscriptStats {
23
39
  hasLegacyData: boolean
24
40
  sources: Array<"snapshot" | "messages_log">
@@ -589,13 +605,27 @@ export class EventStore {
589
605
  this.requireChat(chatId)
590
606
  const payload = `${JSON.stringify(entry)}\n`
591
607
  const transcriptPath = this.transcriptPath(chatId)
608
+ const queuedAt = performance.now()
592
609
  this.writeChain = this.writeChain.then(async () => {
610
+ const startedAt = performance.now()
611
+ const queueDelayMs = Number((startedAt - queuedAt).toFixed(1))
593
612
  await mkdir(this.transcriptsDir, { recursive: true })
613
+ const beforeAppendAt = performance.now()
594
614
  await appendFile(transcriptPath, payload, "utf8")
615
+ const afterAppendAt = performance.now()
595
616
  this.applyMessageMetadata(chatId, entry)
596
617
  if (this.cachedTranscript?.chatId === chatId) {
597
618
  this.cachedTranscript.entries.push({ ...entry })
598
619
  }
620
+ logSendToStartingProfile("event_store.append_message", {
621
+ chatId,
622
+ entryId: entry._id,
623
+ kind: entry.kind,
624
+ payloadBytes: payload.length,
625
+ queueDelayMs,
626
+ appendMs: Number((afterAppendAt - beforeAppendAt).toFixed(1)),
627
+ totalMs: Number((afterAppendAt - queuedAt).toFixed(1)),
628
+ })
599
629
  })
600
630
  return this.writeChain
601
631
  }
@@ -37,6 +37,53 @@ function logSendToStartingProfile(
37
37
  }))
38
38
  }
39
39
 
40
+ function countSubscriptionsByTopic(ws: ServerWebSocket<ClientState>) {
41
+ let sidebar = 0
42
+ let chat = 0
43
+ let projectGit = 0
44
+ let localProjects = 0
45
+ let update = 0
46
+ let keybindings = 0
47
+ let terminal = 0
48
+
49
+ for (const topic of ws.data.subscriptions.values()) {
50
+ switch (topic.type) {
51
+ case "sidebar":
52
+ sidebar += 1
53
+ break
54
+ case "chat":
55
+ chat += 1
56
+ break
57
+ case "project-git":
58
+ projectGit += 1
59
+ break
60
+ case "local-projects":
61
+ localProjects += 1
62
+ break
63
+ case "update":
64
+ update += 1
65
+ break
66
+ case "keybindings":
67
+ keybindings += 1
68
+ break
69
+ case "terminal":
70
+ terminal += 1
71
+ break
72
+ }
73
+ }
74
+
75
+ return {
76
+ total: ws.data.subscriptions.size,
77
+ sidebar,
78
+ chat,
79
+ projectGit,
80
+ localProjects,
81
+ update,
82
+ keybindings,
83
+ terminal,
84
+ }
85
+ }
86
+
40
87
  export interface ClientState {
41
88
  subscriptions: Map<string, SubscriptionTopic>
42
89
  snapshotSignatures: Map<string, string>
@@ -56,7 +103,9 @@ interface CreateWsRouterArgs {
56
103
  }
57
104
 
58
105
  function send(ws: ServerWebSocket<ClientState>, message: ServerEnvelope) {
59
- ws.send(JSON.stringify(message))
106
+ const payload = JSON.stringify(message)
107
+ ws.send(payload)
108
+ return payload.length
60
109
  }
61
110
 
62
111
  function ensureSnapshotSignatures(ws: ServerWebSocket<ClientState>) {
@@ -100,9 +149,13 @@ export function createWsRouter({
100
149
  }
101
150
 
102
151
  function getProtectedChatIds() {
152
+ const activeStatuses = agent.getActiveStatuses()
153
+ const drainingChatIds = typeof agent.getDrainingChatIds === "function"
154
+ ? agent.getDrainingChatIds()
155
+ : new Set<string>()
103
156
  return new Set([
104
- ...agent.getActiveStatuses().keys(),
105
- ...agent.getDrainingChatIds().values(),
157
+ ...activeStatuses.keys(),
158
+ ...drainingChatIds.values(),
106
159
  ])
107
160
  }
108
161
 
@@ -125,21 +178,48 @@ export function createWsRouter({
125
178
  }
126
179
 
127
180
  async function maybePruneStaleEmptyChats(extraSockets?: Iterable<ServerWebSocket<ClientState>>) {
128
- await store.pruneStaleEmptyChats?.({
129
- activeChatIds: getProtectedChatIds(),
130
- protectedChatIds: getProtectedDraftChatIds(extraSockets),
181
+ const startedAt = performance.now()
182
+ const activeChatIds = getProtectedChatIds()
183
+ const protectedDraftChatIds = getProtectedDraftChatIds(extraSockets)
184
+ const prunedChatIds = await store.pruneStaleEmptyChats?.({
185
+ activeChatIds,
186
+ protectedChatIds: protectedDraftChatIds,
131
187
  })
188
+ if (isSendToStartingProfilingEnabled()) {
189
+ console.log("[kanna/send->starting][server]", JSON.stringify({
190
+ stage: "ws.prune_stale_empty_chats",
191
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
192
+ activeChatCount: activeChatIds.size,
193
+ protectedDraftChatCount: protectedDraftChatIds.size,
194
+ prunedCount: prunedChatIds?.length ?? 0,
195
+ totalChatCount: store.state.chatsById.size,
196
+ totalProjectCount: store.state.projectsById.size,
197
+ }))
198
+ }
132
199
  }
133
200
 
134
201
  function createEnvelope(id: string, topic: SubscriptionTopic): ServerEnvelope {
135
202
  if (topic.type === "sidebar") {
203
+ const startedAt = performance.now()
204
+ const data = deriveSidebarData(store.state, agent.getActiveStatuses())
205
+ if (isSendToStartingProfilingEnabled()) {
206
+ const totalChats = data.projectGroups.reduce((count, group) => count + group.chats.length, 0)
207
+ console.log("[kanna/send->starting][server]", JSON.stringify({
208
+ stage: "ws.sidebar_snapshot_built",
209
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
210
+ projectGroupCount: data.projectGroups.length,
211
+ chatCount: totalChats,
212
+ totalChatCount: store.state.chatsById.size,
213
+ totalProjectCount: store.state.projectsById.size,
214
+ }))
215
+ }
136
216
  return {
137
217
  v: PROTOCOL_VERSION,
138
218
  type: "snapshot",
139
219
  id,
140
220
  snapshot: {
141
221
  type: "sidebar",
142
- data: deriveSidebarData(store.state, agent.getActiveStatuses()),
222
+ data,
143
223
  },
144
224
  }
145
225
  }
@@ -235,15 +315,22 @@ export function createWsRouter({
235
315
  }
236
316
 
237
317
  async function pushSnapshots(ws: ServerWebSocket<ClientState>, options?: { skipPrune?: boolean }) {
318
+ const pushStartedAt = performance.now()
238
319
  if (!options?.skipPrune) {
239
320
  await maybePruneStaleEmptyChats([ws])
240
321
  }
241
322
  const snapshotSignatures = ensureSnapshotSignatures(ws)
323
+ let sentCount = 0
324
+ let skippedCount = 0
242
325
  for (const [id, topic] of ws.data.subscriptions.entries()) {
326
+ const envelopeStartedAt = performance.now()
243
327
  const envelope = createEnvelope(id, topic)
328
+ const createdAt = performance.now()
244
329
  if (envelope.type !== "snapshot") continue
245
330
  const signature = JSON.stringify(envelope.snapshot)
331
+ const signatureReadyAt = performance.now()
246
332
  if (snapshotSignatures.get(id) === signature) {
333
+ skippedCount += 1
247
334
  continue
248
335
  }
249
336
  snapshotSignatures.set(id, signature)
@@ -253,17 +340,52 @@ export function createWsRouter({
253
340
  chatId: topic.chatId,
254
341
  status: envelope.snapshot.data.runtime.status,
255
342
  messageCount: envelope.snapshot.data.messages.length,
343
+ buildMs: Number((createdAt - envelopeStartedAt).toFixed(1)),
344
+ signatureMs: Number((signatureReadyAt - createdAt).toFixed(1)),
345
+ signatureBytes: signature.length,
346
+ })
347
+ }
348
+ const payloadBytes = send(ws, envelope)
349
+ sentCount += 1
350
+ if (topic.type === "chat" && envelope.snapshot.type === "chat" && envelope.snapshot.data?.runtime.status === "starting") {
351
+ const profile = agent.getActiveTurnProfile(topic.chatId)
352
+ logSendToStartingProfile(profile?.traceId, profile?.startedAt, "ws.snapshot_send_completed", {
353
+ chatId: topic.chatId,
354
+ payloadBytes,
256
355
  })
257
356
  }
258
- send(ws, envelope)
357
+ }
358
+ if (isSendToStartingProfilingEnabled()) {
359
+ console.log("[kanna/send->starting][server]", JSON.stringify({
360
+ stage: "ws.push_snapshots_completed",
361
+ elapsedMs: Number((performance.now() - pushStartedAt).toFixed(1)),
362
+ skipPrune: Boolean(options?.skipPrune),
363
+ sentCount,
364
+ skippedCount,
365
+ ...countSubscriptionsByTopic(ws),
366
+ }))
259
367
  }
260
368
  }
261
369
 
262
370
  async function broadcastSnapshots() {
371
+ const startedAt = performance.now()
263
372
  await maybePruneStaleEmptyChats()
373
+ const afterPruneAt = performance.now()
374
+ let socketCount = 0
264
375
  for (const ws of sockets) {
376
+ socketCount += 1
265
377
  await pushSnapshots(ws, { skipPrune: true })
266
378
  }
379
+ if (isSendToStartingProfilingEnabled()) {
380
+ console.log("[kanna/send->starting][server]", JSON.stringify({
381
+ stage: "ws.broadcast_snapshots_completed",
382
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
383
+ pruneMs: Number((afterPruneAt - startedAt).toFixed(1)),
384
+ socketCount,
385
+ totalChatCount: store.state.chatsById.size,
386
+ totalProjectCount: store.state.projectsById.size,
387
+ }))
388
+ }
267
389
  }
268
390
 
269
391
  function broadcastError(message: string) {
@@ -473,7 +595,11 @@ export function createWsRouter({
473
595
  logSendToStartingProfile(profile?.traceId ?? command.clientTraceId, profile?.startedAt, "ws.chat_send_ack", {
474
596
  chatId: result.chatId ?? null,
475
597
  })
476
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
598
+ const payloadBytes = send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
599
+ logSendToStartingProfile(profile?.traceId ?? command.clientTraceId, profile?.startedAt, "ws.chat_send_ack_completed", {
600
+ chatId: result.chatId ?? null,
601
+ payloadBytes,
602
+ })
477
603
  break
478
604
  }
479
605
  case "chat.refreshDiffs": {