kanna-code 0.26.9 → 0.26.11
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-C5RBxQW4.css → index-Dx_Ruzm5.css} +1 -1
- package/dist/client/assets/{index-C6vlAZYX.js → index-nIQ7yQW3.js} +192 -192
- 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 +12 -3
- package/src/server/ws-router.test.ts +10 -2
- package/src/server/ws-router.ts +153 -14
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-nIQ7yQW3.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Dx_Ruzm5.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/server/agent.ts
CHANGED
|
@@ -91,7 +91,7 @@ interface ClaudeSessionState {
|
|
|
91
91
|
|
|
92
92
|
interface AgentCoordinatorArgs {
|
|
93
93
|
store: EventStore
|
|
94
|
-
onStateChange: () => void
|
|
94
|
+
onStateChange: (chatId?: string, options?: { immediate?: boolean }) => void
|
|
95
95
|
codexManager?: CodexAppServerManager
|
|
96
96
|
generateTitle?: (messageContent: string, cwd: string) => Promise<GenerateChatTitleResult>
|
|
97
97
|
startClaudeSession?: (args: {
|
|
@@ -629,7 +629,7 @@ async function startClaudeSession(args: {
|
|
|
629
629
|
|
|
630
630
|
export class AgentCoordinator {
|
|
631
631
|
private readonly store: EventStore
|
|
632
|
-
private readonly onStateChange: () => void
|
|
632
|
+
private readonly onStateChange: (chatId?: string, options?: { immediate?: boolean }) => void
|
|
633
633
|
private readonly codexManager: CodexAppServerManager
|
|
634
634
|
private readonly generateTitle: (messageContent: string, cwd: string) => Promise<GenerateChatTitleResult>
|
|
635
635
|
private readonly startClaudeSessionFn: NonNullable<AgentCoordinatorArgs["startClaudeSession"]>
|
|
@@ -668,6 +668,10 @@ export class AgentCoordinator {
|
|
|
668
668
|
return new Set(this.drainingStreams.keys())
|
|
669
669
|
}
|
|
670
670
|
|
|
671
|
+
private emitStateChange(chatId?: string, options?: { immediate?: boolean }) {
|
|
672
|
+
this.onStateChange(chatId, options)
|
|
673
|
+
}
|
|
674
|
+
|
|
671
675
|
getActiveTurnProfile(chatId: string): SendToStartingProfile | null {
|
|
672
676
|
const active = this.activeTurns.get(chatId)
|
|
673
677
|
if (!active?.clientTraceId || active.profilingStartedAt === undefined) {
|
|
@@ -685,7 +689,7 @@ export class AgentCoordinator {
|
|
|
685
689
|
if (!draining) return
|
|
686
690
|
draining.turn.close()
|
|
687
691
|
this.drainingStreams.delete(chatId)
|
|
688
|
-
this.
|
|
692
|
+
this.emitStateChange(chatId)
|
|
689
693
|
}
|
|
690
694
|
|
|
691
695
|
async closeChat(chatId: string) {
|
|
@@ -695,7 +699,7 @@ export class AgentCoordinator {
|
|
|
695
699
|
claudeSession.session.close()
|
|
696
700
|
this.claudeSessions.delete(chatId)
|
|
697
701
|
}
|
|
698
|
-
this.
|
|
702
|
+
this.emitStateChange(chatId)
|
|
699
703
|
}
|
|
700
704
|
|
|
701
705
|
private resolveProvider(command: Extract<ClientCommand, { type: "chat.send" }>, currentProvider: AgentProvider | null) {
|
|
@@ -813,7 +817,7 @@ export class AgentCoordinator {
|
|
|
813
817
|
}
|
|
814
818
|
|
|
815
819
|
active.status = "waiting_for_user"
|
|
816
|
-
this.
|
|
820
|
+
this.emitStateChange(args.chatId)
|
|
817
821
|
|
|
818
822
|
return await new Promise<unknown>((resolve) => {
|
|
819
823
|
active.pendingTool = {
|
|
@@ -901,7 +905,7 @@ export class AgentCoordinator {
|
|
|
901
905
|
chatId: args.chatId,
|
|
902
906
|
status: active.status,
|
|
903
907
|
})
|
|
904
|
-
this.
|
|
908
|
+
this.emitStateChange(args.chatId, { immediate: active.status === "starting" })
|
|
905
909
|
logSendToStartingProfile(args.profile, "start_turn.state_change_emitted", {
|
|
906
910
|
chatId: args.chatId,
|
|
907
911
|
status: active.status,
|
|
@@ -921,7 +925,7 @@ export class AgentCoordinator {
|
|
|
921
925
|
}
|
|
922
926
|
}
|
|
923
927
|
await this.store.appendMessage(args.chatId, timestamped({ kind: "account_info", accountInfo }))
|
|
924
|
-
this.
|
|
928
|
+
this.emitStateChange(args.chatId)
|
|
925
929
|
})
|
|
926
930
|
.catch(() => undefined)
|
|
927
931
|
}
|
|
@@ -1055,7 +1059,7 @@ export class AgentCoordinator {
|
|
|
1055
1059
|
if (event.type === "session_token" && event.sessionToken) {
|
|
1056
1060
|
session.sessionToken = event.sessionToken
|
|
1057
1061
|
await this.store.setSessionToken(session.chatId, event.sessionToken)
|
|
1058
|
-
this.
|
|
1062
|
+
this.emitStateChange(session.chatId)
|
|
1059
1063
|
continue
|
|
1060
1064
|
}
|
|
1061
1065
|
|
|
@@ -1077,7 +1081,7 @@ export class AgentCoordinator {
|
|
|
1077
1081
|
this.activeTurns.delete(session.chatId)
|
|
1078
1082
|
}
|
|
1079
1083
|
|
|
1080
|
-
this.
|
|
1084
|
+
this.emitStateChange(session.chatId)
|
|
1081
1085
|
}
|
|
1082
1086
|
} catch (error) {
|
|
1083
1087
|
const active = this.activeTurns.get(session.chatId)
|
|
@@ -1105,7 +1109,7 @@ export class AgentCoordinator {
|
|
|
1105
1109
|
this.activeTurns.delete(session.chatId)
|
|
1106
1110
|
}
|
|
1107
1111
|
session.session.close()
|
|
1108
|
-
this.
|
|
1112
|
+
this.emitStateChange(session.chatId)
|
|
1109
1113
|
}
|
|
1110
1114
|
}
|
|
1111
1115
|
|
|
@@ -1123,7 +1127,7 @@ export class AgentCoordinator {
|
|
|
1123
1127
|
if (chat.title !== expectedCurrentTitle) return
|
|
1124
1128
|
|
|
1125
1129
|
await this.store.renameChat(chatId, result.title)
|
|
1126
|
-
this.
|
|
1130
|
+
this.emitStateChange(chatId)
|
|
1127
1131
|
} catch (error) {
|
|
1128
1132
|
const message = error instanceof Error ? error.message : String(error)
|
|
1129
1133
|
this.reportBackgroundError?.(
|
|
@@ -1141,7 +1145,7 @@ export class AgentCoordinator {
|
|
|
1141
1145
|
|
|
1142
1146
|
if (event.type === "session_token" && event.sessionToken) {
|
|
1143
1147
|
await this.store.setSessionToken(active.chatId, event.sessionToken)
|
|
1144
|
-
this.
|
|
1148
|
+
this.emitStateChange(active.chatId)
|
|
1145
1149
|
continue
|
|
1146
1150
|
}
|
|
1147
1151
|
|
|
@@ -1169,7 +1173,7 @@ export class AgentCoordinator {
|
|
|
1169
1173
|
this.drainingStreams.set(active.chatId, { turn: active.turn })
|
|
1170
1174
|
}
|
|
1171
1175
|
|
|
1172
|
-
this.
|
|
1176
|
+
this.emitStateChange(active.chatId)
|
|
1173
1177
|
}
|
|
1174
1178
|
} catch (error) {
|
|
1175
1179
|
if (!active.cancelRequested) {
|
|
@@ -1199,7 +1203,7 @@ export class AgentCoordinator {
|
|
|
1199
1203
|
}
|
|
1200
1204
|
// Stream has fully ended — no longer draining.
|
|
1201
1205
|
this.drainingStreams.delete(active.chatId)
|
|
1202
|
-
this.
|
|
1206
|
+
this.emitStateChange(active.chatId)
|
|
1203
1207
|
|
|
1204
1208
|
if (active.postToolFollowUp && !active.cancelRequested) {
|
|
1205
1209
|
try {
|
|
@@ -1227,7 +1231,7 @@ export class AgentCoordinator {
|
|
|
1227
1231
|
})
|
|
1228
1232
|
)
|
|
1229
1233
|
await this.store.recordTurnFailed(active.chatId, message)
|
|
1230
|
-
this.
|
|
1234
|
+
this.emitStateChange(active.chatId)
|
|
1231
1235
|
}
|
|
1232
1236
|
}
|
|
1233
1237
|
}
|
|
@@ -1274,7 +1278,7 @@ export class AgentCoordinator {
|
|
|
1274
1278
|
// Remove from activeTurns immediately so the UI reflects the cancellation
|
|
1275
1279
|
// right away, rather than waiting for interrupt() which may hang.
|
|
1276
1280
|
this.activeTurns.delete(chatId)
|
|
1277
|
-
this.
|
|
1281
|
+
this.emitStateChange(chatId)
|
|
1278
1282
|
|
|
1279
1283
|
// Now attempt to interrupt/close the underlying stream in the background.
|
|
1280
1284
|
// This is best-effort — the turn is already removed from active state above,
|
|
@@ -1343,6 +1347,6 @@ export class AgentCoordinator {
|
|
|
1343
1347
|
|
|
1344
1348
|
pending.resolve(command.result)
|
|
1345
1349
|
|
|
1346
|
-
this.
|
|
1350
|
+
this.emitStateChange(command.chatId)
|
|
1347
1351
|
}
|
|
1348
1352
|
}
|
|
@@ -305,7 +305,7 @@ export class EventStore {
|
|
|
305
305
|
break
|
|
306
306
|
}
|
|
307
307
|
case "chat_created": {
|
|
308
|
-
|
|
308
|
+
const chat = {
|
|
309
309
|
id: event.chatId,
|
|
310
310
|
projectId: event.projectId,
|
|
311
311
|
title: event.title,
|
|
@@ -315,6 +315,7 @@ export class EventStore {
|
|
|
315
315
|
provider: null,
|
|
316
316
|
planMode: false,
|
|
317
317
|
sessionToken: null,
|
|
318
|
+
hasMessages: false,
|
|
318
319
|
lastTurnOutcome: null,
|
|
319
320
|
}
|
|
320
321
|
this.state.chatsById.set(chat.id, chat)
|
|
@@ -404,6 +405,7 @@ export class EventStore {
|
|
|
404
405
|
private applyMessageMetadata(chatId: string, entry: TranscriptEntry) {
|
|
405
406
|
const chat = this.state.chatsById.get(chatId)
|
|
406
407
|
if (!chat) return
|
|
408
|
+
chat.hasMessages = true
|
|
407
409
|
if (entry.kind === "user_prompt") {
|
|
408
410
|
chat.lastMessageAt = entry.createdAt
|
|
409
411
|
}
|
|
@@ -540,7 +542,11 @@ export class EventStore {
|
|
|
540
542
|
for (const chat of this.state.chatsById.values()) {
|
|
541
543
|
if (chat.deletedAt || protectedChatIds.has(chat.id)) continue
|
|
542
544
|
if (now - chat.createdAt < maxAgeMs) continue
|
|
543
|
-
if (
|
|
545
|
+
if (chat.hasMessages) continue
|
|
546
|
+
if (this.getMessages(chat.id).length > 0) {
|
|
547
|
+
chat.hasMessages = true
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
544
550
|
|
|
545
551
|
const event: ChatEvent = {
|
|
546
552
|
v: STORE_VERSION,
|
package/src/server/events.ts
CHANGED
package/src/server/server.ts
CHANGED
|
@@ -99,8 +99,16 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
99
99
|
: null
|
|
100
100
|
const agent = new AgentCoordinator({
|
|
101
101
|
store,
|
|
102
|
-
onStateChange: () => {
|
|
103
|
-
|
|
102
|
+
onStateChange: (chatId?: string, options?: { immediate?: boolean }) => {
|
|
103
|
+
if (chatId) {
|
|
104
|
+
if (options?.immediate) {
|
|
105
|
+
void router.broadcastChatStateImmediately(chatId)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
router.scheduleChatStateBroadcast(chatId)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
router.scheduleBroadcast()
|
|
104
112
|
},
|
|
105
113
|
})
|
|
106
114
|
router = createWsRouter({
|
|
@@ -115,7 +123,8 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
115
123
|
updateManager,
|
|
116
124
|
})
|
|
117
125
|
const staleEmptyChatPruneInterval = setInterval(() => {
|
|
118
|
-
void router.
|
|
126
|
+
void router.pruneStaleEmptyChats()
|
|
127
|
+
.then(() => router.broadcastSnapshots())
|
|
119
128
|
}, STALE_EMPTY_CHAT_PRUNE_INTERVAL_MS)
|
|
120
129
|
|
|
121
130
|
const distDir = path.join(import.meta.dir, "..", "..", "dist", "client")
|
|
@@ -60,6 +60,7 @@ describe("ws-router", () => {
|
|
|
60
60
|
updateManager: null,
|
|
61
61
|
})
|
|
62
62
|
const ws = new FakeWebSocket()
|
|
63
|
+
router.handleOpen(ws as never)
|
|
63
64
|
|
|
64
65
|
ws.data.subscriptions.set("sub-1", { type: "sidebar" })
|
|
65
66
|
await router.handleMessage(
|
|
@@ -143,6 +144,7 @@ describe("ws-router", () => {
|
|
|
143
144
|
updateManager: null,
|
|
144
145
|
})
|
|
145
146
|
const ws = new FakeWebSocket()
|
|
147
|
+
router.handleOpen(ws as never)
|
|
146
148
|
|
|
147
149
|
await router.handleMessage(
|
|
148
150
|
ws as never,
|
|
@@ -230,6 +232,7 @@ describe("ws-router", () => {
|
|
|
230
232
|
updateManager: null,
|
|
231
233
|
})
|
|
232
234
|
const ws = new FakeWebSocket()
|
|
235
|
+
router.handleOpen(ws as never)
|
|
233
236
|
|
|
234
237
|
await router.handleMessage(
|
|
235
238
|
ws as never,
|
|
@@ -302,6 +305,7 @@ describe("ws-router", () => {
|
|
|
302
305
|
updateManager: null,
|
|
303
306
|
})
|
|
304
307
|
const ws = new FakeWebSocket()
|
|
308
|
+
router.handleOpen(ws as never)
|
|
305
309
|
|
|
306
310
|
await router.handleMessage(
|
|
307
311
|
ws as never,
|
|
@@ -383,6 +387,7 @@ describe("ws-router", () => {
|
|
|
383
387
|
updateManager: null,
|
|
384
388
|
})
|
|
385
389
|
const ws = new FakeWebSocket()
|
|
390
|
+
router.handleOpen(ws as never)
|
|
386
391
|
|
|
387
392
|
await router.handleMessage(
|
|
388
393
|
ws as never,
|
|
@@ -666,7 +671,7 @@ describe("ws-router", () => {
|
|
|
666
671
|
})
|
|
667
672
|
})
|
|
668
673
|
|
|
669
|
-
test("prunes stale empty chats
|
|
674
|
+
test("prunes stale empty chats during explicit maintenance runs", async () => {
|
|
670
675
|
const state = createEmptyState()
|
|
671
676
|
state.projectsById.set("project-1", {
|
|
672
677
|
id: "project-1",
|
|
@@ -715,6 +720,7 @@ describe("ws-router", () => {
|
|
|
715
720
|
})
|
|
716
721
|
const ws = new FakeWebSocket()
|
|
717
722
|
|
|
723
|
+
await router.pruneStaleEmptyChats()
|
|
718
724
|
await router.handleMessage(
|
|
719
725
|
ws as never,
|
|
720
726
|
JSON.stringify({
|
|
@@ -743,7 +749,7 @@ describe("ws-router", () => {
|
|
|
743
749
|
})
|
|
744
750
|
})
|
|
745
751
|
|
|
746
|
-
test("protects draft-bearing chats
|
|
752
|
+
test("protects draft-bearing chats during explicit maintenance runs", async () => {
|
|
747
753
|
const state = createEmptyState()
|
|
748
754
|
state.projectsById.set("project-1", {
|
|
749
755
|
id: "project-1",
|
|
@@ -790,6 +796,7 @@ describe("ws-router", () => {
|
|
|
790
796
|
updateManager: null,
|
|
791
797
|
})
|
|
792
798
|
const ws = new FakeWebSocket()
|
|
799
|
+
router.handleOpen(ws as never)
|
|
793
800
|
|
|
794
801
|
await router.handleMessage(
|
|
795
802
|
ws as never,
|
|
@@ -804,6 +811,7 @@ describe("ws-router", () => {
|
|
|
804
811
|
})
|
|
805
812
|
)
|
|
806
813
|
|
|
814
|
+
await router.pruneStaleEmptyChats()
|
|
807
815
|
await router.handleMessage(
|
|
808
816
|
ws as never,
|
|
809
817
|
JSON.stringify({
|
package/src/server/ws-router.ts
CHANGED
|
@@ -102,6 +102,16 @@ interface CreateWsRouterArgs {
|
|
|
102
102
|
updateManager: UpdateManager | null
|
|
103
103
|
}
|
|
104
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
|
+
|
|
105
115
|
function send(ws: ServerWebSocket<ClientState>, message: ServerEnvelope) {
|
|
106
116
|
const payload = JSON.stringify(message)
|
|
107
117
|
ws.send(payload)
|
|
@@ -128,6 +138,9 @@ export function createWsRouter({
|
|
|
128
138
|
updateManager,
|
|
129
139
|
}: CreateWsRouterArgs) {
|
|
130
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>()
|
|
131
144
|
const resolvedDiffStore = diffStore ?? {
|
|
132
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 } }),
|
|
133
146
|
refreshSnapshot: async () => false,
|
|
@@ -198,6 +211,36 @@ export function createWsRouter({
|
|
|
198
211
|
}
|
|
199
212
|
}
|
|
200
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
|
|
242
|
+
}
|
|
243
|
+
|
|
201
244
|
function createEnvelope(id: string, topic: SubscriptionTopic): ServerEnvelope {
|
|
202
245
|
if (topic.type === "sidebar") {
|
|
203
246
|
const startedAt = performance.now()
|
|
@@ -314,7 +357,10 @@ export function createWsRouter({
|
|
|
314
357
|
}
|
|
315
358
|
}
|
|
316
359
|
|
|
317
|
-
async function pushSnapshots(
|
|
360
|
+
async function pushSnapshots(
|
|
361
|
+
ws: ServerWebSocket<ClientState>,
|
|
362
|
+
options?: { skipPrune?: boolean; filter?: SnapshotBroadcastFilter }
|
|
363
|
+
) {
|
|
318
364
|
const pushStartedAt = performance.now()
|
|
319
365
|
if (!options?.skipPrune) {
|
|
320
366
|
await maybePruneStaleEmptyChats([ws])
|
|
@@ -323,6 +369,9 @@ export function createWsRouter({
|
|
|
323
369
|
let sentCount = 0
|
|
324
370
|
let skippedCount = 0
|
|
325
371
|
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
372
|
+
if (!shouldIncludeTopic(topic, options?.filter)) {
|
|
373
|
+
continue
|
|
374
|
+
}
|
|
326
375
|
const envelopeStartedAt = performance.now()
|
|
327
376
|
const envelope = createEnvelope(id, topic)
|
|
328
377
|
const createdAt = performance.now()
|
|
@@ -369,8 +418,6 @@ export function createWsRouter({
|
|
|
369
418
|
|
|
370
419
|
async function broadcastSnapshots() {
|
|
371
420
|
const startedAt = performance.now()
|
|
372
|
-
await maybePruneStaleEmptyChats()
|
|
373
|
-
const afterPruneAt = performance.now()
|
|
374
421
|
let socketCount = 0
|
|
375
422
|
for (const ws of sockets) {
|
|
376
423
|
socketCount += 1
|
|
@@ -380,7 +427,7 @@ export function createWsRouter({
|
|
|
380
427
|
console.log("[kanna/send->starting][server]", JSON.stringify({
|
|
381
428
|
stage: "ws.broadcast_snapshots_completed",
|
|
382
429
|
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
|
|
383
|
-
pruneMs:
|
|
430
|
+
pruneMs: 0,
|
|
384
431
|
socketCount,
|
|
385
432
|
totalChatCount: store.state.chatsById.size,
|
|
386
433
|
totalProjectCount: store.state.projectsById.size,
|
|
@@ -388,6 +435,87 @@ export function createWsRouter({
|
|
|
388
435
|
}
|
|
389
436
|
}
|
|
390
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
|
+
})
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function broadcastChatStateImmediately(chatId: string) {
|
|
516
|
+
await broadcastChatAndSidebar(chatId)
|
|
517
|
+
}
|
|
518
|
+
|
|
391
519
|
function broadcastError(message: string) {
|
|
392
520
|
for (const ws of sockets) {
|
|
393
521
|
send(ws, {
|
|
@@ -563,24 +691,28 @@ export function createWsRouter({
|
|
|
563
691
|
case "chat.create": {
|
|
564
692
|
const chat = await store.createChat(command.projectId)
|
|
565
693
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { chatId: chat.id } })
|
|
566
|
-
|
|
694
|
+
await broadcastChatAndSidebar(chat.id)
|
|
695
|
+
return
|
|
567
696
|
}
|
|
568
697
|
case "chat.rename": {
|
|
569
698
|
await store.renameChat(command.chatId, command.title)
|
|
570
699
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
571
|
-
|
|
700
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
701
|
+
return
|
|
572
702
|
}
|
|
573
703
|
case "chat.delete": {
|
|
574
704
|
await agent.cancel(command.chatId)
|
|
575
705
|
await agent.closeChat(command.chatId)
|
|
576
706
|
await store.deleteChat(command.chatId)
|
|
577
707
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
578
|
-
|
|
708
|
+
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
709
|
+
return
|
|
579
710
|
}
|
|
580
711
|
case "chat.markRead": {
|
|
581
712
|
await store.setChatReadState(command.chatId, false)
|
|
582
713
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
583
|
-
|
|
714
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
715
|
+
return
|
|
584
716
|
}
|
|
585
717
|
case "chat.setDraftProtection": {
|
|
586
718
|
ws.data.protectedDraftChatIds = new Set(command.chatIds)
|
|
@@ -600,7 +732,7 @@ export function createWsRouter({
|
|
|
600
732
|
chatId: result.chatId ?? null,
|
|
601
733
|
payloadBytes,
|
|
602
734
|
})
|
|
603
|
-
|
|
735
|
+
return
|
|
604
736
|
}
|
|
605
737
|
case "chat.refreshDiffs": {
|
|
606
738
|
const { project } = resolveChatProject(command.chatId)
|
|
@@ -780,12 +912,12 @@ export function createWsRouter({
|
|
|
780
912
|
case "chat.cancel": {
|
|
781
913
|
await agent.cancel(command.chatId)
|
|
782
914
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
783
|
-
|
|
915
|
+
return
|
|
784
916
|
}
|
|
785
917
|
case "chat.stopDraining": {
|
|
786
918
|
await agent.stopDraining(command.chatId)
|
|
787
919
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
788
|
-
|
|
920
|
+
return
|
|
789
921
|
}
|
|
790
922
|
case "chat.loadHistory": {
|
|
791
923
|
const chat = store.getChat(command.chatId)
|
|
@@ -797,7 +929,7 @@ export function createWsRouter({
|
|
|
797
929
|
case "chat.respondTool": {
|
|
798
930
|
await agent.respondTool(command)
|
|
799
931
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
800
|
-
|
|
932
|
+
return
|
|
801
933
|
}
|
|
802
934
|
case "terminal.create": {
|
|
803
935
|
const project = store.getProject(command.projectId)
|
|
@@ -852,6 +984,10 @@ export function createWsRouter({
|
|
|
852
984
|
sockets.delete(ws)
|
|
853
985
|
},
|
|
854
986
|
broadcastSnapshots,
|
|
987
|
+
broadcastChatStateImmediately,
|
|
988
|
+
scheduleBroadcast,
|
|
989
|
+
scheduleChatStateBroadcast,
|
|
990
|
+
pruneStaleEmptyChats: () => maybePruneStaleEmptyChats(),
|
|
855
991
|
async handleMessage(ws: ServerWebSocket<ClientState>, raw: string | Buffer | ArrayBuffer | Uint8Array) {
|
|
856
992
|
let parsed: unknown
|
|
857
993
|
try {
|
|
@@ -873,12 +1009,12 @@ export function createWsRouter({
|
|
|
873
1009
|
if (parsed.topic.type === "local-projects") {
|
|
874
1010
|
void refreshDiscovery().then(() => {
|
|
875
1011
|
if (ws.data.subscriptions.has(parsed.id)) {
|
|
876
|
-
void pushSnapshots(ws)
|
|
1012
|
+
void pushSnapshots(ws, { skipPrune: true })
|
|
877
1013
|
}
|
|
878
1014
|
})
|
|
879
1015
|
return
|
|
880
1016
|
}
|
|
881
|
-
await pushSnapshots(ws)
|
|
1017
|
+
await pushSnapshots(ws, { skipPrune: true })
|
|
882
1018
|
return
|
|
883
1019
|
}
|
|
884
1020
|
|
|
@@ -893,6 +1029,9 @@ export function createWsRouter({
|
|
|
893
1029
|
await handleCommand(ws, parsed)
|
|
894
1030
|
},
|
|
895
1031
|
dispose() {
|
|
1032
|
+
if (pendingBroadcastTimer) {
|
|
1033
|
+
clearTimeout(pendingBroadcastTimer)
|
|
1034
|
+
}
|
|
896
1035
|
agent.setBackgroundErrorReporter?.(null)
|
|
897
1036
|
disposeTerminalEvents()
|
|
898
1037
|
disposeKeybindingEvents()
|