kanna-code 0.26.9 → 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-C6vlAZYX.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 +148 -14
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) => 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) => 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) {
|
|
672
|
+
this.onStateChange(chatId)
|
|
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)
|
|
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,12 @@ 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) => {
|
|
103
|
+
if (chatId) {
|
|
104
|
+
router.scheduleChatStateBroadcast(chatId)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
router.scheduleBroadcast()
|
|
104
108
|
},
|
|
105
109
|
})
|
|
106
110
|
router = createWsRouter({
|
|
@@ -115,7 +119,8 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
115
119
|
updateManager,
|
|
116
120
|
})
|
|
117
121
|
const staleEmptyChatPruneInterval = setInterval(() => {
|
|
118
|
-
void router.
|
|
122
|
+
void router.pruneStaleEmptyChats()
|
|
123
|
+
.then(() => router.broadcastSnapshots())
|
|
119
124
|
}, STALE_EMPTY_CHAT_PRUNE_INTERVAL_MS)
|
|
120
125
|
|
|
121
126
|
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,83 @@ 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
|
+
|
|
391
515
|
function broadcastError(message: string) {
|
|
392
516
|
for (const ws of sockets) {
|
|
393
517
|
send(ws, {
|
|
@@ -563,24 +687,28 @@ export function createWsRouter({
|
|
|
563
687
|
case "chat.create": {
|
|
564
688
|
const chat = await store.createChat(command.projectId)
|
|
565
689
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { chatId: chat.id } })
|
|
566
|
-
|
|
690
|
+
await broadcastChatAndSidebar(chat.id)
|
|
691
|
+
return
|
|
567
692
|
}
|
|
568
693
|
case "chat.rename": {
|
|
569
694
|
await store.renameChat(command.chatId, command.title)
|
|
570
695
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
571
|
-
|
|
696
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
697
|
+
return
|
|
572
698
|
}
|
|
573
699
|
case "chat.delete": {
|
|
574
700
|
await agent.cancel(command.chatId)
|
|
575
701
|
await agent.closeChat(command.chatId)
|
|
576
702
|
await store.deleteChat(command.chatId)
|
|
577
703
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
578
|
-
|
|
704
|
+
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
705
|
+
return
|
|
579
706
|
}
|
|
580
707
|
case "chat.markRead": {
|
|
581
708
|
await store.setChatReadState(command.chatId, false)
|
|
582
709
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
583
|
-
|
|
710
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
711
|
+
return
|
|
584
712
|
}
|
|
585
713
|
case "chat.setDraftProtection": {
|
|
586
714
|
ws.data.protectedDraftChatIds = new Set(command.chatIds)
|
|
@@ -600,7 +728,7 @@ export function createWsRouter({
|
|
|
600
728
|
chatId: result.chatId ?? null,
|
|
601
729
|
payloadBytes,
|
|
602
730
|
})
|
|
603
|
-
|
|
731
|
+
return
|
|
604
732
|
}
|
|
605
733
|
case "chat.refreshDiffs": {
|
|
606
734
|
const { project } = resolveChatProject(command.chatId)
|
|
@@ -780,12 +908,12 @@ export function createWsRouter({
|
|
|
780
908
|
case "chat.cancel": {
|
|
781
909
|
await agent.cancel(command.chatId)
|
|
782
910
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
783
|
-
|
|
911
|
+
return
|
|
784
912
|
}
|
|
785
913
|
case "chat.stopDraining": {
|
|
786
914
|
await agent.stopDraining(command.chatId)
|
|
787
915
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
788
|
-
|
|
916
|
+
return
|
|
789
917
|
}
|
|
790
918
|
case "chat.loadHistory": {
|
|
791
919
|
const chat = store.getChat(command.chatId)
|
|
@@ -797,7 +925,7 @@ export function createWsRouter({
|
|
|
797
925
|
case "chat.respondTool": {
|
|
798
926
|
await agent.respondTool(command)
|
|
799
927
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
800
|
-
|
|
928
|
+
return
|
|
801
929
|
}
|
|
802
930
|
case "terminal.create": {
|
|
803
931
|
const project = store.getProject(command.projectId)
|
|
@@ -852,6 +980,9 @@ export function createWsRouter({
|
|
|
852
980
|
sockets.delete(ws)
|
|
853
981
|
},
|
|
854
982
|
broadcastSnapshots,
|
|
983
|
+
scheduleBroadcast,
|
|
984
|
+
scheduleChatStateBroadcast,
|
|
985
|
+
pruneStaleEmptyChats: () => maybePruneStaleEmptyChats(),
|
|
855
986
|
async handleMessage(ws: ServerWebSocket<ClientState>, raw: string | Buffer | ArrayBuffer | Uint8Array) {
|
|
856
987
|
let parsed: unknown
|
|
857
988
|
try {
|
|
@@ -873,12 +1004,12 @@ export function createWsRouter({
|
|
|
873
1004
|
if (parsed.topic.type === "local-projects") {
|
|
874
1005
|
void refreshDiscovery().then(() => {
|
|
875
1006
|
if (ws.data.subscriptions.has(parsed.id)) {
|
|
876
|
-
void pushSnapshots(ws)
|
|
1007
|
+
void pushSnapshots(ws, { skipPrune: true })
|
|
877
1008
|
}
|
|
878
1009
|
})
|
|
879
1010
|
return
|
|
880
1011
|
}
|
|
881
|
-
await pushSnapshots(ws)
|
|
1012
|
+
await pushSnapshots(ws, { skipPrune: true })
|
|
882
1013
|
return
|
|
883
1014
|
}
|
|
884
1015
|
|
|
@@ -893,6 +1024,9 @@ export function createWsRouter({
|
|
|
893
1024
|
await handleCommand(ws, parsed)
|
|
894
1025
|
},
|
|
895
1026
|
dispose() {
|
|
1027
|
+
if (pendingBroadcastTimer) {
|
|
1028
|
+
clearTimeout(pendingBroadcastTimer)
|
|
1029
|
+
}
|
|
896
1030
|
agent.setBackgroundErrorReporter?.(null)
|
|
897
1031
|
disposeTerminalEvents()
|
|
898
1032
|
disposeKeybindingEvents()
|