kanna-code 0.26.8 → 0.26.10
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-BaucHYoH.js → index-DmyPGZQY.js} +1 -1
- package/dist/client/assets/{index-C5RBxQW4.css → index-Dx_Ruzm5.css} +1 -1
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/agent.ts +21 -17
- package/src/server/event-store.ts +8 -2
- package/src/server/events.ts +1 -0
- package/src/server/server.ts +8 -3
- package/src/server/ws-router.test.ts +10 -2
- package/src/server/ws-router.ts +259 -18
package/src/server/ws-router.ts
CHANGED
|
@@ -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>
|
|
@@ -55,6 +102,16 @@ interface CreateWsRouterArgs {
|
|
|
55
102
|
updateManager: UpdateManager | null
|
|
56
103
|
}
|
|
57
104
|
|
|
105
|
+
interface SnapshotBroadcastFilter {
|
|
106
|
+
includeSidebar?: boolean
|
|
107
|
+
includeLocalProjects?: boolean
|
|
108
|
+
includeUpdate?: boolean
|
|
109
|
+
includeKeybindings?: boolean
|
|
110
|
+
chatIds?: Set<string>
|
|
111
|
+
projectIds?: Set<string>
|
|
112
|
+
terminalIds?: Set<string>
|
|
113
|
+
}
|
|
114
|
+
|
|
58
115
|
function send(ws: ServerWebSocket<ClientState>, message: ServerEnvelope) {
|
|
59
116
|
const payload = JSON.stringify(message)
|
|
60
117
|
ws.send(payload)
|
|
@@ -81,6 +138,9 @@ export function createWsRouter({
|
|
|
81
138
|
updateManager,
|
|
82
139
|
}: CreateWsRouterArgs) {
|
|
83
140
|
const sockets = new Set<ServerWebSocket<ClientState>>()
|
|
141
|
+
let pendingBroadcastTimer: ReturnType<typeof setTimeout> | null = null
|
|
142
|
+
let pendingBroadcastAll = false
|
|
143
|
+
const pendingBroadcastChatIds = new Set<string>()
|
|
84
144
|
const resolvedDiffStore = diffStore ?? {
|
|
85
145
|
getProjectSnapshot: () => ({ status: "unknown", branchName: undefined, defaultBranchName: undefined, hasOriginRemote: undefined, originRepoSlug: undefined, hasUpstream: undefined, aheadCount: undefined, behindCount: undefined, lastFetchedAt: undefined, files: [] as const, branchHistory: { entries: [] as const } }),
|
|
86
146
|
refreshSnapshot: async () => false,
|
|
@@ -102,9 +162,13 @@ export function createWsRouter({
|
|
|
102
162
|
}
|
|
103
163
|
|
|
104
164
|
function getProtectedChatIds() {
|
|
165
|
+
const activeStatuses = agent.getActiveStatuses()
|
|
166
|
+
const drainingChatIds = typeof agent.getDrainingChatIds === "function"
|
|
167
|
+
? agent.getDrainingChatIds()
|
|
168
|
+
: new Set<string>()
|
|
105
169
|
return new Set([
|
|
106
|
-
...
|
|
107
|
-
...
|
|
170
|
+
...activeStatuses.keys(),
|
|
171
|
+
...drainingChatIds.values(),
|
|
108
172
|
])
|
|
109
173
|
}
|
|
110
174
|
|
|
@@ -127,21 +191,78 @@ export function createWsRouter({
|
|
|
127
191
|
}
|
|
128
192
|
|
|
129
193
|
async function maybePruneStaleEmptyChats(extraSockets?: Iterable<ServerWebSocket<ClientState>>) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
194
|
+
const startedAt = performance.now()
|
|
195
|
+
const activeChatIds = getProtectedChatIds()
|
|
196
|
+
const protectedDraftChatIds = getProtectedDraftChatIds(extraSockets)
|
|
197
|
+
const prunedChatIds = await store.pruneStaleEmptyChats?.({
|
|
198
|
+
activeChatIds,
|
|
199
|
+
protectedChatIds: protectedDraftChatIds,
|
|
133
200
|
})
|
|
201
|
+
if (isSendToStartingProfilingEnabled()) {
|
|
202
|
+
console.log("[kanna/send->starting][server]", JSON.stringify({
|
|
203
|
+
stage: "ws.prune_stale_empty_chats",
|
|
204
|
+
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
|
|
205
|
+
activeChatCount: activeChatIds.size,
|
|
206
|
+
protectedDraftChatCount: protectedDraftChatIds.size,
|
|
207
|
+
prunedCount: prunedChatIds?.length ?? 0,
|
|
208
|
+
totalChatCount: store.state.chatsById.size,
|
|
209
|
+
totalProjectCount: store.state.projectsById.size,
|
|
210
|
+
}))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function shouldIncludeTopic(topic: SubscriptionTopic, filter?: SnapshotBroadcastFilter) {
|
|
215
|
+
if (!filter) {
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (topic.type === "sidebar") {
|
|
220
|
+
return Boolean(filter.includeSidebar)
|
|
221
|
+
}
|
|
222
|
+
if (topic.type === "local-projects") {
|
|
223
|
+
return Boolean(filter.includeLocalProjects)
|
|
224
|
+
}
|
|
225
|
+
if (topic.type === "update") {
|
|
226
|
+
return Boolean(filter.includeUpdate)
|
|
227
|
+
}
|
|
228
|
+
if (topic.type === "keybindings") {
|
|
229
|
+
return Boolean(filter.includeKeybindings)
|
|
230
|
+
}
|
|
231
|
+
if (topic.type === "chat") {
|
|
232
|
+
return filter.chatIds?.has(topic.chatId) ?? false
|
|
233
|
+
}
|
|
234
|
+
if (topic.type === "project-git") {
|
|
235
|
+
return filter.projectIds?.has(topic.projectId) ?? false
|
|
236
|
+
}
|
|
237
|
+
if (topic.type === "terminal") {
|
|
238
|
+
return filter.terminalIds?.has(topic.terminalId) ?? false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return true
|
|
134
242
|
}
|
|
135
243
|
|
|
136
244
|
function createEnvelope(id: string, topic: SubscriptionTopic): ServerEnvelope {
|
|
137
245
|
if (topic.type === "sidebar") {
|
|
246
|
+
const startedAt = performance.now()
|
|
247
|
+
const data = deriveSidebarData(store.state, agent.getActiveStatuses())
|
|
248
|
+
if (isSendToStartingProfilingEnabled()) {
|
|
249
|
+
const totalChats = data.projectGroups.reduce((count, group) => count + group.chats.length, 0)
|
|
250
|
+
console.log("[kanna/send->starting][server]", JSON.stringify({
|
|
251
|
+
stage: "ws.sidebar_snapshot_built",
|
|
252
|
+
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
|
|
253
|
+
projectGroupCount: data.projectGroups.length,
|
|
254
|
+
chatCount: totalChats,
|
|
255
|
+
totalChatCount: store.state.chatsById.size,
|
|
256
|
+
totalProjectCount: store.state.projectsById.size,
|
|
257
|
+
}))
|
|
258
|
+
}
|
|
138
259
|
return {
|
|
139
260
|
v: PROTOCOL_VERSION,
|
|
140
261
|
type: "snapshot",
|
|
141
262
|
id,
|
|
142
263
|
snapshot: {
|
|
143
264
|
type: "sidebar",
|
|
144
|
-
data
|
|
265
|
+
data,
|
|
145
266
|
},
|
|
146
267
|
}
|
|
147
268
|
}
|
|
@@ -236,12 +357,21 @@ export function createWsRouter({
|
|
|
236
357
|
}
|
|
237
358
|
}
|
|
238
359
|
|
|
239
|
-
async function pushSnapshots(
|
|
360
|
+
async function pushSnapshots(
|
|
361
|
+
ws: ServerWebSocket<ClientState>,
|
|
362
|
+
options?: { skipPrune?: boolean; filter?: SnapshotBroadcastFilter }
|
|
363
|
+
) {
|
|
364
|
+
const pushStartedAt = performance.now()
|
|
240
365
|
if (!options?.skipPrune) {
|
|
241
366
|
await maybePruneStaleEmptyChats([ws])
|
|
242
367
|
}
|
|
243
368
|
const snapshotSignatures = ensureSnapshotSignatures(ws)
|
|
369
|
+
let sentCount = 0
|
|
370
|
+
let skippedCount = 0
|
|
244
371
|
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
372
|
+
if (!shouldIncludeTopic(topic, options?.filter)) {
|
|
373
|
+
continue
|
|
374
|
+
}
|
|
245
375
|
const envelopeStartedAt = performance.now()
|
|
246
376
|
const envelope = createEnvelope(id, topic)
|
|
247
377
|
const createdAt = performance.now()
|
|
@@ -249,6 +379,7 @@ export function createWsRouter({
|
|
|
249
379
|
const signature = JSON.stringify(envelope.snapshot)
|
|
250
380
|
const signatureReadyAt = performance.now()
|
|
251
381
|
if (snapshotSignatures.get(id) === signature) {
|
|
382
|
+
skippedCount += 1
|
|
252
383
|
continue
|
|
253
384
|
}
|
|
254
385
|
snapshotSignatures.set(id, signature)
|
|
@@ -264,6 +395,7 @@ export function createWsRouter({
|
|
|
264
395
|
})
|
|
265
396
|
}
|
|
266
397
|
const payloadBytes = send(ws, envelope)
|
|
398
|
+
sentCount += 1
|
|
267
399
|
if (topic.type === "chat" && envelope.snapshot.type === "chat" && envelope.snapshot.data?.runtime.status === "starting") {
|
|
268
400
|
const profile = agent.getActiveTurnProfile(topic.chatId)
|
|
269
401
|
logSendToStartingProfile(profile?.traceId, profile?.startedAt, "ws.snapshot_send_completed", {
|
|
@@ -272,13 +404,112 @@ export function createWsRouter({
|
|
|
272
404
|
})
|
|
273
405
|
}
|
|
274
406
|
}
|
|
407
|
+
if (isSendToStartingProfilingEnabled()) {
|
|
408
|
+
console.log("[kanna/send->starting][server]", JSON.stringify({
|
|
409
|
+
stage: "ws.push_snapshots_completed",
|
|
410
|
+
elapsedMs: Number((performance.now() - pushStartedAt).toFixed(1)),
|
|
411
|
+
skipPrune: Boolean(options?.skipPrune),
|
|
412
|
+
sentCount,
|
|
413
|
+
skippedCount,
|
|
414
|
+
...countSubscriptionsByTopic(ws),
|
|
415
|
+
}))
|
|
416
|
+
}
|
|
275
417
|
}
|
|
276
418
|
|
|
277
419
|
async function broadcastSnapshots() {
|
|
278
|
-
|
|
420
|
+
const startedAt = performance.now()
|
|
421
|
+
let socketCount = 0
|
|
279
422
|
for (const ws of sockets) {
|
|
423
|
+
socketCount += 1
|
|
280
424
|
await pushSnapshots(ws, { skipPrune: true })
|
|
281
425
|
}
|
|
426
|
+
if (isSendToStartingProfilingEnabled()) {
|
|
427
|
+
console.log("[kanna/send->starting][server]", JSON.stringify({
|
|
428
|
+
stage: "ws.broadcast_snapshots_completed",
|
|
429
|
+
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
|
|
430
|
+
pruneMs: 0,
|
|
431
|
+
socketCount,
|
|
432
|
+
totalChatCount: store.state.chatsById.size,
|
|
433
|
+
totalProjectCount: store.state.projectsById.size,
|
|
434
|
+
}))
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function broadcastFilteredSnapshots(filter: SnapshotBroadcastFilter) {
|
|
439
|
+
const startedAt = performance.now()
|
|
440
|
+
let socketCount = 0
|
|
441
|
+
for (const ws of sockets) {
|
|
442
|
+
socketCount += 1
|
|
443
|
+
await pushSnapshots(ws, { skipPrune: true, filter })
|
|
444
|
+
}
|
|
445
|
+
if (isSendToStartingProfilingEnabled()) {
|
|
446
|
+
console.log("[kanna/send->starting][server]", JSON.stringify({
|
|
447
|
+
stage: "ws.broadcast_filtered_snapshots_completed",
|
|
448
|
+
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
|
|
449
|
+
socketCount,
|
|
450
|
+
includeSidebar: Boolean(filter.includeSidebar),
|
|
451
|
+
chatCount: filter.chatIds?.size ?? 0,
|
|
452
|
+
projectCount: filter.projectIds?.size ?? 0,
|
|
453
|
+
}))
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function scheduleBroadcast() {
|
|
458
|
+
pendingBroadcastAll = true
|
|
459
|
+
pendingBroadcastChatIds.clear()
|
|
460
|
+
if (pendingBroadcastTimer) {
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
pendingBroadcastTimer = setTimeout(() => {
|
|
464
|
+
pendingBroadcastTimer = null
|
|
465
|
+
const shouldBroadcastAll = pendingBroadcastAll
|
|
466
|
+
const chatIds = new Set(pendingBroadcastChatIds)
|
|
467
|
+
pendingBroadcastAll = false
|
|
468
|
+
pendingBroadcastChatIds.clear()
|
|
469
|
+
if (shouldBroadcastAll) {
|
|
470
|
+
void broadcastSnapshots()
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
if (chatIds.size > 0) {
|
|
474
|
+
void broadcastFilteredSnapshots({
|
|
475
|
+
includeSidebar: true,
|
|
476
|
+
chatIds,
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
}, 16)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function scheduleChatStateBroadcast(chatId: string) {
|
|
483
|
+
if (!pendingBroadcastAll) {
|
|
484
|
+
pendingBroadcastChatIds.add(chatId)
|
|
485
|
+
}
|
|
486
|
+
if (pendingBroadcastTimer) {
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
pendingBroadcastTimer = setTimeout(() => {
|
|
490
|
+
pendingBroadcastTimer = null
|
|
491
|
+
const shouldBroadcastAll = pendingBroadcastAll
|
|
492
|
+
const chatIds = new Set(pendingBroadcastChatIds)
|
|
493
|
+
pendingBroadcastAll = false
|
|
494
|
+
pendingBroadcastChatIds.clear()
|
|
495
|
+
if (shouldBroadcastAll) {
|
|
496
|
+
void broadcastSnapshots()
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
if (chatIds.size > 0) {
|
|
500
|
+
void broadcastFilteredSnapshots({
|
|
501
|
+
includeSidebar: true,
|
|
502
|
+
chatIds,
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
}, 16)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function broadcastChatAndSidebar(chatId: string) {
|
|
509
|
+
await broadcastFilteredSnapshots({
|
|
510
|
+
includeSidebar: true,
|
|
511
|
+
chatIds: new Set([chatId]),
|
|
512
|
+
})
|
|
282
513
|
}
|
|
283
514
|
|
|
284
515
|
function broadcastError(message: string) {
|
|
@@ -456,24 +687,28 @@ export function createWsRouter({
|
|
|
456
687
|
case "chat.create": {
|
|
457
688
|
const chat = await store.createChat(command.projectId)
|
|
458
689
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { chatId: chat.id } })
|
|
459
|
-
|
|
690
|
+
await broadcastChatAndSidebar(chat.id)
|
|
691
|
+
return
|
|
460
692
|
}
|
|
461
693
|
case "chat.rename": {
|
|
462
694
|
await store.renameChat(command.chatId, command.title)
|
|
463
695
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
464
|
-
|
|
696
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
697
|
+
return
|
|
465
698
|
}
|
|
466
699
|
case "chat.delete": {
|
|
467
700
|
await agent.cancel(command.chatId)
|
|
468
701
|
await agent.closeChat(command.chatId)
|
|
469
702
|
await store.deleteChat(command.chatId)
|
|
470
703
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
471
|
-
|
|
704
|
+
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
705
|
+
return
|
|
472
706
|
}
|
|
473
707
|
case "chat.markRead": {
|
|
474
708
|
await store.setChatReadState(command.chatId, false)
|
|
475
709
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
476
|
-
|
|
710
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
711
|
+
return
|
|
477
712
|
}
|
|
478
713
|
case "chat.setDraftProtection": {
|
|
479
714
|
ws.data.protectedDraftChatIds = new Set(command.chatIds)
|
|
@@ -493,7 +728,7 @@ export function createWsRouter({
|
|
|
493
728
|
chatId: result.chatId ?? null,
|
|
494
729
|
payloadBytes,
|
|
495
730
|
})
|
|
496
|
-
|
|
731
|
+
return
|
|
497
732
|
}
|
|
498
733
|
case "chat.refreshDiffs": {
|
|
499
734
|
const { project } = resolveChatProject(command.chatId)
|
|
@@ -673,12 +908,12 @@ export function createWsRouter({
|
|
|
673
908
|
case "chat.cancel": {
|
|
674
909
|
await agent.cancel(command.chatId)
|
|
675
910
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
676
|
-
|
|
911
|
+
return
|
|
677
912
|
}
|
|
678
913
|
case "chat.stopDraining": {
|
|
679
914
|
await agent.stopDraining(command.chatId)
|
|
680
915
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
681
|
-
|
|
916
|
+
return
|
|
682
917
|
}
|
|
683
918
|
case "chat.loadHistory": {
|
|
684
919
|
const chat = store.getChat(command.chatId)
|
|
@@ -690,7 +925,7 @@ export function createWsRouter({
|
|
|
690
925
|
case "chat.respondTool": {
|
|
691
926
|
await agent.respondTool(command)
|
|
692
927
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
693
|
-
|
|
928
|
+
return
|
|
694
929
|
}
|
|
695
930
|
case "terminal.create": {
|
|
696
931
|
const project = store.getProject(command.projectId)
|
|
@@ -745,6 +980,9 @@ export function createWsRouter({
|
|
|
745
980
|
sockets.delete(ws)
|
|
746
981
|
},
|
|
747
982
|
broadcastSnapshots,
|
|
983
|
+
scheduleBroadcast,
|
|
984
|
+
scheduleChatStateBroadcast,
|
|
985
|
+
pruneStaleEmptyChats: () => maybePruneStaleEmptyChats(),
|
|
748
986
|
async handleMessage(ws: ServerWebSocket<ClientState>, raw: string | Buffer | ArrayBuffer | Uint8Array) {
|
|
749
987
|
let parsed: unknown
|
|
750
988
|
try {
|
|
@@ -766,12 +1004,12 @@ export function createWsRouter({
|
|
|
766
1004
|
if (parsed.topic.type === "local-projects") {
|
|
767
1005
|
void refreshDiscovery().then(() => {
|
|
768
1006
|
if (ws.data.subscriptions.has(parsed.id)) {
|
|
769
|
-
void pushSnapshots(ws)
|
|
1007
|
+
void pushSnapshots(ws, { skipPrune: true })
|
|
770
1008
|
}
|
|
771
1009
|
})
|
|
772
1010
|
return
|
|
773
1011
|
}
|
|
774
|
-
await pushSnapshots(ws)
|
|
1012
|
+
await pushSnapshots(ws, { skipPrune: true })
|
|
775
1013
|
return
|
|
776
1014
|
}
|
|
777
1015
|
|
|
@@ -786,6 +1024,9 @@ export function createWsRouter({
|
|
|
786
1024
|
await handleCommand(ws, parsed)
|
|
787
1025
|
},
|
|
788
1026
|
dispose() {
|
|
1027
|
+
if (pendingBroadcastTimer) {
|
|
1028
|
+
clearTimeout(pendingBroadcastTimer)
|
|
1029
|
+
}
|
|
789
1030
|
agent.setBackgroundErrorReporter?.(null)
|
|
790
1031
|
disposeTerminalEvents()
|
|
791
1032
|
disposeKeybindingEvents()
|