kanna-code 0.21.0 → 0.22.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/dist/client/assets/{index-DrUC3AGz.js → index-1BGyFTrZ.js} +177 -171
- package/dist/client/assets/index-DKD9EePK.css +32 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/diff-store.test.ts +1 -0
- package/src/server/diff-store.ts +95 -3
- package/src/server/event-store.test.ts +30 -0
- package/src/server/event-store.ts +88 -2
- package/src/server/read-models.test.ts +57 -1
- package/src/server/read-models.ts +11 -4
- package/src/server/ws-router.test.ts +86 -0
- package/src/server/ws-router.ts +22 -6
- package/src/shared/protocol.ts +6 -3
- package/src/shared/types.ts +37 -0
- package/dist/client/assets/index-B-8fHyaU.css +0 -32
|
@@ -54,8 +54,23 @@ describe("read models", () => {
|
|
|
54
54
|
lastTurnOutcome: null,
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
const chat = deriveChatSnapshot(
|
|
57
|
+
const chat = deriveChatSnapshot(
|
|
58
|
+
state,
|
|
59
|
+
new Map(),
|
|
60
|
+
new Set(),
|
|
61
|
+
"chat-1",
|
|
62
|
+
() => ({
|
|
63
|
+
messages: [],
|
|
64
|
+
history: {
|
|
65
|
+
hasOlder: false,
|
|
66
|
+
olderCursor: null,
|
|
67
|
+
recentLimit: 200,
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
() => ({ status: "unknown", files: [] })
|
|
71
|
+
)
|
|
58
72
|
expect(chat?.runtime.provider).toBe("claude")
|
|
73
|
+
expect(chat?.history.recentLimit).toBe(200)
|
|
59
74
|
expect(chat?.availableProviders.length).toBeGreaterThan(1)
|
|
60
75
|
expect(chat?.availableProviders.find((provider) => provider.id === "codex")?.models.map((model) => model.id)).toEqual([
|
|
61
76
|
"gpt-5.4",
|
|
@@ -106,4 +121,45 @@ describe("read models", () => {
|
|
|
106
121
|
},
|
|
107
122
|
])
|
|
108
123
|
})
|
|
124
|
+
|
|
125
|
+
test("orders sidebar chats by user-visible activity instead of internal updatedAt churn", () => {
|
|
126
|
+
const state = createEmptyState()
|
|
127
|
+
state.projectsById.set("project-1", {
|
|
128
|
+
id: "project-1",
|
|
129
|
+
localPath: "/tmp/project",
|
|
130
|
+
title: "Project",
|
|
131
|
+
createdAt: 1,
|
|
132
|
+
updatedAt: 1,
|
|
133
|
+
})
|
|
134
|
+
state.projectIdsByPath.set("/tmp/project", "project-1")
|
|
135
|
+
state.chatsById.set("chat-old", {
|
|
136
|
+
id: "chat-old",
|
|
137
|
+
projectId: "project-1",
|
|
138
|
+
title: "Older user activity",
|
|
139
|
+
createdAt: 10,
|
|
140
|
+
updatedAt: 500,
|
|
141
|
+
unread: false,
|
|
142
|
+
provider: "claude",
|
|
143
|
+
planMode: false,
|
|
144
|
+
sessionToken: null,
|
|
145
|
+
lastMessageAt: 100,
|
|
146
|
+
lastTurnOutcome: null,
|
|
147
|
+
})
|
|
148
|
+
state.chatsById.set("chat-new", {
|
|
149
|
+
id: "chat-new",
|
|
150
|
+
projectId: "project-1",
|
|
151
|
+
title: "Newer user activity",
|
|
152
|
+
createdAt: 20,
|
|
153
|
+
updatedAt: 50,
|
|
154
|
+
unread: false,
|
|
155
|
+
provider: "claude",
|
|
156
|
+
planMode: false,
|
|
157
|
+
sessionToken: null,
|
|
158
|
+
lastMessageAt: 200,
|
|
159
|
+
lastTurnOutcome: null,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const sidebar = deriveSidebarData(state, new Map())
|
|
163
|
+
expect(sidebar.projectGroups[0]?.chats.map((chat) => chat.chatId)).toEqual(["chat-new", "chat-old"])
|
|
164
|
+
})
|
|
109
165
|
})
|
|
@@ -18,6 +18,10 @@ export function deriveStatus(chat: ChatRecord, activeStatus?: KannaStatus): Kann
|
|
|
18
18
|
return "idle"
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function getSidebarChatSortTimestamp(chat: ChatRecord) {
|
|
22
|
+
return chat.lastMessageAt ?? chat.createdAt
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
export function deriveSidebarData(
|
|
22
26
|
state: StoreState,
|
|
23
27
|
activeStatuses: Map<string, KannaStatus>
|
|
@@ -29,7 +33,7 @@ export function deriveSidebarData(
|
|
|
29
33
|
const projectGroups: SidebarProjectGroup[] = projects.map((project) => {
|
|
30
34
|
const chats: SidebarChatRow[] = [...state.chatsById.values()]
|
|
31
35
|
.filter((chat) => chat.projectId === project.id && !chat.deletedAt)
|
|
32
|
-
.sort((a, b) => (b
|
|
36
|
+
.sort((a, b) => getSidebarChatSortTimestamp(b) - getSidebarChatSortTimestamp(a))
|
|
33
37
|
.map((chat) => ({
|
|
34
38
|
_id: chat.id,
|
|
35
39
|
_creationTime: chat.createdAt,
|
|
@@ -74,7 +78,7 @@ export function deriveLocalProjectsSnapshot(
|
|
|
74
78
|
for (const project of [...state.projectsById.values()].filter((entry) => !entry.deletedAt)) {
|
|
75
79
|
const chats = [...state.chatsById.values()].filter((chat) => chat.projectId === project.id && !chat.deletedAt)
|
|
76
80
|
const lastOpenedAt = chats.reduce(
|
|
77
|
-
(latest, chat) => Math.max(latest, chat
|
|
81
|
+
(latest, chat) => Math.max(latest, getSidebarChatSortTimestamp(chat)),
|
|
78
82
|
project.updatedAt
|
|
79
83
|
)
|
|
80
84
|
|
|
@@ -101,7 +105,7 @@ export function deriveChatSnapshot(
|
|
|
101
105
|
activeStatuses: Map<string, KannaStatus>,
|
|
102
106
|
drainingChatIds: Set<string>,
|
|
103
107
|
chatId: string,
|
|
104
|
-
getMessages: (chatId: string) => ChatSnapshot
|
|
108
|
+
getMessages: (chatId: string) => Pick<ChatSnapshot, "messages" | "history">,
|
|
105
109
|
getDiffs: (chatId: string) => ChatDiffSnapshot
|
|
106
110
|
): ChatSnapshot | null {
|
|
107
111
|
const chat = state.chatsById.get(chatId)
|
|
@@ -121,9 +125,12 @@ export function deriveChatSnapshot(
|
|
|
121
125
|
sessionToken: chat.sessionToken,
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
const transcript = getMessages(chat.id)
|
|
129
|
+
|
|
124
130
|
return {
|
|
125
131
|
runtime,
|
|
126
|
-
messages:
|
|
132
|
+
messages: transcript.messages,
|
|
133
|
+
history: transcript.history,
|
|
127
134
|
diffs: getDiffs(chat.id),
|
|
128
135
|
availableProviders: [...SERVER_PROVIDERS],
|
|
129
136
|
}
|
|
@@ -176,6 +176,92 @@ describe("ws-router", () => {
|
|
|
176
176
|
})
|
|
177
177
|
})
|
|
178
178
|
|
|
179
|
+
test("loads older chat history pages", () => {
|
|
180
|
+
const state = createEmptyState()
|
|
181
|
+
state.projectsById.set("project-1", {
|
|
182
|
+
id: "project-1",
|
|
183
|
+
localPath: "/tmp/project",
|
|
184
|
+
title: "Project",
|
|
185
|
+
createdAt: 1,
|
|
186
|
+
updatedAt: 1,
|
|
187
|
+
})
|
|
188
|
+
state.projectIdsByPath.set("/tmp/project", "project-1")
|
|
189
|
+
state.chatsById.set("chat-1", {
|
|
190
|
+
id: "chat-1",
|
|
191
|
+
projectId: "project-1",
|
|
192
|
+
title: "Chat",
|
|
193
|
+
createdAt: 1,
|
|
194
|
+
updatedAt: 1,
|
|
195
|
+
unread: false,
|
|
196
|
+
provider: null,
|
|
197
|
+
planMode: false,
|
|
198
|
+
sessionToken: null,
|
|
199
|
+
lastTurnOutcome: null,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const router = createWsRouter({
|
|
203
|
+
store: {
|
|
204
|
+
state,
|
|
205
|
+
getMessagesPageBefore: () => ({
|
|
206
|
+
messages: [{
|
|
207
|
+
_id: "msg-1",
|
|
208
|
+
kind: "assistant_text",
|
|
209
|
+
createdAt: 1,
|
|
210
|
+
text: "older message",
|
|
211
|
+
}],
|
|
212
|
+
hasOlder: false,
|
|
213
|
+
olderCursor: null,
|
|
214
|
+
}),
|
|
215
|
+
getChat: () => state.chatsById.get("chat-1") ?? null,
|
|
216
|
+
} as never,
|
|
217
|
+
agent: { getActiveStatuses: () => new Map(), getDrainingChatIds: () => new Set() } as never,
|
|
218
|
+
terminals: {
|
|
219
|
+
getSnapshot: () => null,
|
|
220
|
+
onEvent: () => () => {},
|
|
221
|
+
} as never,
|
|
222
|
+
keybindings: {
|
|
223
|
+
getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
|
|
224
|
+
onChange: () => () => {},
|
|
225
|
+
} as never,
|
|
226
|
+
refreshDiscovery: async () => [],
|
|
227
|
+
getDiscoveredProjects: () => [],
|
|
228
|
+
machineDisplayName: "Local Machine",
|
|
229
|
+
updateManager: null,
|
|
230
|
+
})
|
|
231
|
+
const ws = new FakeWebSocket()
|
|
232
|
+
|
|
233
|
+
router.handleMessage(
|
|
234
|
+
ws as never,
|
|
235
|
+
JSON.stringify({
|
|
236
|
+
v: 1,
|
|
237
|
+
type: "command",
|
|
238
|
+
id: "history-1",
|
|
239
|
+
command: {
|
|
240
|
+
type: "chat.loadHistory",
|
|
241
|
+
chatId: "chat-1",
|
|
242
|
+
beforeCursor: "idx:100",
|
|
243
|
+
limit: 100,
|
|
244
|
+
},
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
expect(ws.sent[0]).toEqual({
|
|
249
|
+
v: PROTOCOL_VERSION,
|
|
250
|
+
type: "ack",
|
|
251
|
+
id: "history-1",
|
|
252
|
+
result: {
|
|
253
|
+
messages: [{
|
|
254
|
+
_id: "msg-1",
|
|
255
|
+
kind: "assistant_text",
|
|
256
|
+
createdAt: 1,
|
|
257
|
+
text: "older message",
|
|
258
|
+
}],
|
|
259
|
+
hasOlder: false,
|
|
260
|
+
olderCursor: null,
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
179
265
|
test("marks chats read and rebroadcasts sidebar snapshots", async () => {
|
|
180
266
|
const state = createEmptyState()
|
|
181
267
|
state.projectsById.set("project-1", {
|
package/src/server/ws-router.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { TerminalManager } from "./terminal-manager"
|
|
|
13
13
|
import type { UpdateManager } from "./update-manager"
|
|
14
14
|
import { deriveChatSnapshot, deriveLocalProjectsSnapshot, deriveSidebarData } from "./read-models"
|
|
15
15
|
|
|
16
|
+
const DEFAULT_CHAT_RECENT_LIMIT = 200
|
|
17
|
+
|
|
16
18
|
export interface ClientState {
|
|
17
19
|
subscriptions: Map<string, SubscriptionTopic>
|
|
18
20
|
snapshotSignatures: Map<string, string>
|
|
@@ -55,10 +57,10 @@ export function createWsRouter({
|
|
|
55
57
|
}: CreateWsRouterArgs) {
|
|
56
58
|
const sockets = new Set<ServerWebSocket<ClientState>>()
|
|
57
59
|
const resolvedDiffStore = diffStore ?? {
|
|
58
|
-
getSnapshot: () => ({ status: "unknown", branchName: undefined, files: [] as const }),
|
|
60
|
+
getSnapshot: () => ({ status: "unknown", branchName: undefined, hasUpstream: undefined, files: [] as const }),
|
|
59
61
|
refreshSnapshot: async () => false,
|
|
60
62
|
generateCommitMessage: async () => ({ subject: "Update selected files", body: "", usedFallback: true, failureMessage: null }),
|
|
61
|
-
commitFiles: async () => false,
|
|
63
|
+
commitFiles: async () => ({ ok: true, mode: "commit_only" as const, branchName: undefined, pushed: false, snapshotChanged: false }),
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
function createEnvelope(id: string, topic: SubscriptionTopic): ServerEnvelope {
|
|
@@ -144,7 +146,7 @@ export function createWsRouter({
|
|
|
144
146
|
agent.getActiveStatuses(),
|
|
145
147
|
agent.getDrainingChatIds(),
|
|
146
148
|
topic.chatId,
|
|
147
|
-
(chatId) => store.
|
|
149
|
+
(chatId) => store.getRecentChatHistory(chatId, topic.recentLimit ?? DEFAULT_CHAT_RECENT_LIMIT),
|
|
148
150
|
(chatId) => resolvedDiffStore.getSnapshot(chatId)
|
|
149
151
|
),
|
|
150
152
|
},
|
|
@@ -155,6 +157,7 @@ export function createWsRouter({
|
|
|
155
157
|
const snapshotSignatures = ensureSnapshotSignatures(ws)
|
|
156
158
|
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
157
159
|
const envelope = createEnvelope(id, topic)
|
|
160
|
+
if (envelope.type !== "snapshot") continue
|
|
158
161
|
const signature = JSON.stringify(envelope.snapshot)
|
|
159
162
|
if (snapshotSignatures.get(id) === signature) {
|
|
160
163
|
continue
|
|
@@ -186,6 +189,7 @@ export function createWsRouter({
|
|
|
186
189
|
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
187
190
|
if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
|
|
188
191
|
const envelope = createEnvelope(id, topic)
|
|
192
|
+
if (envelope.type !== "snapshot") continue
|
|
189
193
|
const signature = JSON.stringify(envelope.snapshot)
|
|
190
194
|
if (snapshotSignatures.get(id) === signature) continue
|
|
191
195
|
snapshotSignatures.set(id, signature)
|
|
@@ -218,6 +222,7 @@ export function createWsRouter({
|
|
|
218
222
|
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
219
223
|
if (topic.type !== "keybindings") continue
|
|
220
224
|
const envelope = createEnvelope(id, topic)
|
|
225
|
+
if (envelope.type !== "snapshot") continue
|
|
221
226
|
const signature = JSON.stringify(envelope.snapshot)
|
|
222
227
|
if (snapshotSignatures.get(id) === signature) continue
|
|
223
228
|
snapshotSignatures.set(id, signature)
|
|
@@ -232,6 +237,7 @@ export function createWsRouter({
|
|
|
232
237
|
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
233
238
|
if (topic.type !== "update") continue
|
|
234
239
|
const envelope = createEnvelope(id, topic)
|
|
240
|
+
if (envelope.type !== "snapshot") continue
|
|
235
241
|
const signature = JSON.stringify(envelope.snapshot)
|
|
236
242
|
if (snapshotSignatures.get(id) === signature) continue
|
|
237
243
|
snapshotSignatures.set(id, signature)
|
|
@@ -387,15 +393,16 @@ export function createWsRouter({
|
|
|
387
393
|
if (!project) {
|
|
388
394
|
throw new Error("Project not found")
|
|
389
395
|
}
|
|
390
|
-
const
|
|
396
|
+
const result = await resolvedDiffStore.commitFiles({
|
|
391
397
|
chatId: command.chatId,
|
|
392
398
|
projectPath: project.localPath,
|
|
393
399
|
paths: command.paths,
|
|
394
400
|
summary: command.summary,
|
|
395
401
|
description: command.description,
|
|
402
|
+
mode: command.mode,
|
|
396
403
|
})
|
|
397
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
398
|
-
if (
|
|
404
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
405
|
+
if (result.snapshotChanged) {
|
|
399
406
|
broadcastSnapshots()
|
|
400
407
|
}
|
|
401
408
|
return
|
|
@@ -410,6 +417,15 @@ export function createWsRouter({
|
|
|
410
417
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
411
418
|
break
|
|
412
419
|
}
|
|
420
|
+
case "chat.loadHistory": {
|
|
421
|
+
const chat = store.getChat(command.chatId)
|
|
422
|
+
if (!chat) {
|
|
423
|
+
throw new Error("Chat not found")
|
|
424
|
+
}
|
|
425
|
+
const page = store.getMessagesPageBefore(command.chatId, command.beforeCursor, command.limit)
|
|
426
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: page })
|
|
427
|
+
return
|
|
428
|
+
}
|
|
413
429
|
case "chat.respondTool": {
|
|
414
430
|
await agent.respondTool(command)
|
|
415
431
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
package/src/shared/protocol.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentProvider,
|
|
3
3
|
ChatAttachment,
|
|
4
|
+
ChatHistoryPage,
|
|
4
5
|
ChatSnapshot,
|
|
6
|
+
DiffCommitMode,
|
|
5
7
|
KeybindingsSnapshot,
|
|
6
8
|
LocalProjectsSnapshot,
|
|
7
9
|
ModelOptions,
|
|
@@ -21,7 +23,7 @@ export type SubscriptionTopic =
|
|
|
21
23
|
| { type: "local-projects" }
|
|
22
24
|
| { type: "update" }
|
|
23
25
|
| { type: "keybindings" }
|
|
24
|
-
| { type: "chat"; chatId: string }
|
|
26
|
+
| { type: "chat"; chatId: string; recentLimit?: number }
|
|
25
27
|
| { type: "terminal"; terminalId: string }
|
|
26
28
|
|
|
27
29
|
export interface TerminalSnapshot {
|
|
@@ -77,9 +79,10 @@ export type ClientCommand =
|
|
|
77
79
|
}
|
|
78
80
|
| { type: "chat.refreshDiffs"; chatId: string }
|
|
79
81
|
| { type: "chat.generateCommitMessage"; chatId: string; paths: string[] }
|
|
80
|
-
| { type: "chat.commitDiffs"; chatId: string; paths: string[]; summary: string; description?: string }
|
|
82
|
+
| { type: "chat.commitDiffs"; chatId: string; paths: string[]; summary: string; description?: string; mode: DiffCommitMode }
|
|
81
83
|
| { type: "chat.cancel"; chatId: string }
|
|
82
84
|
| { type: "chat.stopDraining"; chatId: string }
|
|
85
|
+
| { type: "chat.loadHistory"; chatId: string; beforeCursor: string; limit: number }
|
|
83
86
|
| { type: "chat.respondTool"; chatId: string; toolUseId: string; result: unknown }
|
|
84
87
|
| { type: "terminal.create"; projectId: string; terminalId: string; cols: number; rows: number; scrollback: number }
|
|
85
88
|
| { type: "terminal.input"; terminalId: string; data: string }
|
|
@@ -102,7 +105,7 @@ export type ServerSnapshot =
|
|
|
102
105
|
export type ServerEnvelope =
|
|
103
106
|
| { v: 1; type: "snapshot"; id: string; snapshot: ServerSnapshot }
|
|
104
107
|
| { v: 1; type: "event"; id: string; event: TerminalEvent }
|
|
105
|
-
| { v: 1; type: "ack"; id: string; result?: unknown }
|
|
108
|
+
| { v: 1; type: "ack"; id: string; result?: unknown | ChatHistoryPage }
|
|
106
109
|
| { v: 1; type: "error"; id?: string; message: string }
|
|
107
110
|
|
|
108
111
|
export function isClientEnvelope(value: unknown): value is ClientEnvelope {
|
package/src/shared/types.ts
CHANGED
|
@@ -479,9 +479,33 @@ export interface ChatDiffFile {
|
|
|
479
479
|
export interface ChatDiffSnapshot {
|
|
480
480
|
status: "unknown" | "ready" | "no_repo"
|
|
481
481
|
branchName?: string
|
|
482
|
+
hasUpstream?: boolean
|
|
482
483
|
files: ChatDiffFile[]
|
|
483
484
|
}
|
|
484
485
|
|
|
486
|
+
export type DiffCommitMode = "commit_and_push" | "commit_only"
|
|
487
|
+
|
|
488
|
+
export interface DiffCommitSuccess {
|
|
489
|
+
ok: true
|
|
490
|
+
mode: DiffCommitMode
|
|
491
|
+
branchName?: string
|
|
492
|
+
pushed: boolean
|
|
493
|
+
snapshotChanged: boolean
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export interface DiffCommitFailure {
|
|
497
|
+
ok: false
|
|
498
|
+
mode: DiffCommitMode
|
|
499
|
+
phase: "commit" | "push"
|
|
500
|
+
title: string
|
|
501
|
+
message: string
|
|
502
|
+
detail?: string
|
|
503
|
+
localCommitCreated?: boolean
|
|
504
|
+
snapshotChanged?: boolean
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export type DiffCommitResult = DiffCommitSuccess | DiffCommitFailure
|
|
508
|
+
|
|
485
509
|
export interface ContextWindowUpdatedEntry extends TranscriptEntryBase {
|
|
486
510
|
kind: "context_window_updated"
|
|
487
511
|
usage: ContextWindowUsageSnapshot
|
|
@@ -647,13 +671,26 @@ export interface ChatRuntime {
|
|
|
647
671
|
sessionToken: string | null
|
|
648
672
|
}
|
|
649
673
|
|
|
674
|
+
export interface ChatHistorySnapshot {
|
|
675
|
+
hasOlder: boolean
|
|
676
|
+
olderCursor: string | null
|
|
677
|
+
recentLimit: number
|
|
678
|
+
}
|
|
679
|
+
|
|
650
680
|
export interface ChatSnapshot {
|
|
651
681
|
runtime: ChatRuntime
|
|
652
682
|
messages: TranscriptEntry[]
|
|
683
|
+
history: ChatHistorySnapshot
|
|
653
684
|
diffs: ChatDiffSnapshot
|
|
654
685
|
availableProviders: ProviderCatalogEntry[]
|
|
655
686
|
}
|
|
656
687
|
|
|
688
|
+
export interface ChatHistoryPage {
|
|
689
|
+
messages: TranscriptEntry[]
|
|
690
|
+
hasOlder: boolean
|
|
691
|
+
olderCursor: string | null
|
|
692
|
+
}
|
|
693
|
+
|
|
657
694
|
export interface KannaSnapshot {
|
|
658
695
|
sidebar: SidebarData
|
|
659
696
|
chat?: ChatSnapshot | null
|