kanna-code 0.36.1 → 0.37.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-pUhvjSZr.css +32 -0
- package/dist/client/assets/index-yvyssqF_.js +2618 -0
- package/dist/client/index.html +2 -2
- package/dist/export-viewer/assets/index-CY-dlB67.css +1 -0
- package/dist/export-viewer/assets/{index-Cghqxose.js → index-Dw6GRRjm.js} +89 -89
- package/dist/export-viewer/index.html +2 -2
- package/package.json +1 -1
- package/src/server/event-store.test.ts +38 -0
- package/src/server/event-store.ts +43 -3
- package/src/server/events.ts +13 -0
- package/src/server/read-models.test.ts +45 -2
- package/src/server/read-models.ts +14 -7
- package/src/server/ws-router.test.ts +0 -2
- package/src/server/ws-router.ts +12 -12
- package/src/shared/protocol.ts +2 -0
- package/src/shared/types.ts +1 -0
- package/dist/client/assets/index-BIJ8Xw2U.css +0 -32
- package/dist/client/assets/index-j8t9Uev7.js +0 -2613
- package/dist/export-viewer/assets/index-D3nRTJ38.css +0 -1
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
6
|
<meta name="theme-color" content="#ffffff" />
|
|
7
7
|
<title>Kanna Transcript</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-Dw6GRRjm.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CY-dlB67.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -537,4 +537,42 @@ describe("EventStore", () => {
|
|
|
537
537
|
expect(forked.lastMessageAt).toBeUndefined()
|
|
538
538
|
expect(store.getMessages(forked.id)).toEqual(store.getMessages(source.id))
|
|
539
539
|
})
|
|
540
|
+
|
|
541
|
+
test("reopening a removed project restores its existing chats", async () => {
|
|
542
|
+
const dataDir = await createTempDataDir()
|
|
543
|
+
const store = new EventStore(dataDir)
|
|
544
|
+
await store.initialize()
|
|
545
|
+
|
|
546
|
+
const project = await store.openProject("/tmp/project")
|
|
547
|
+
const chat = await store.createChat(project.id)
|
|
548
|
+
|
|
549
|
+
await store.removeProject(project.id)
|
|
550
|
+
expect(store.getProject(project.id)).toBeNull()
|
|
551
|
+
|
|
552
|
+
const reopened = await store.openProject("/tmp/project")
|
|
553
|
+
|
|
554
|
+
expect(reopened.id).toBe(project.id)
|
|
555
|
+
expect(store.listChatsByProject(reopened.id).map((entry) => entry.id)).toEqual([chat.id])
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
test("archives chats without deleting their transcript", async () => {
|
|
559
|
+
const dataDir = await createTempDataDir()
|
|
560
|
+
const store = new EventStore(dataDir)
|
|
561
|
+
await store.initialize()
|
|
562
|
+
|
|
563
|
+
const project = await store.openProject("/tmp/project")
|
|
564
|
+
const chat = await store.createChat(project.id)
|
|
565
|
+
await store.appendMessage(chat.id, entry("user_prompt", chat.createdAt + 1, { content: "keep this" }))
|
|
566
|
+
|
|
567
|
+
await store.archiveChat(chat.id)
|
|
568
|
+
|
|
569
|
+
expect(store.getChat(chat.id)?.archivedAt).toBeNumber()
|
|
570
|
+
expect(store.listChatsByProject(project.id)).toEqual([])
|
|
571
|
+
expect(store.getMessages(chat.id).map((message) => message.kind)).toEqual(["user_prompt"])
|
|
572
|
+
|
|
573
|
+
await store.unarchiveChat(chat.id)
|
|
574
|
+
|
|
575
|
+
expect(store.getChat(chat.id)?.archivedAt).toBeUndefined()
|
|
576
|
+
expect(store.listChatsByProject(project.id).map((entry) => entry.id)).toEqual([chat.id])
|
|
577
|
+
})
|
|
540
578
|
})
|
|
@@ -104,6 +104,8 @@ function getReplayEventPriority(event: StoreEvent) {
|
|
|
104
104
|
case "chat_read_state_set":
|
|
105
105
|
return 9
|
|
106
106
|
case "chat_deleted":
|
|
107
|
+
case "chat_archived":
|
|
108
|
+
case "chat_unarchived":
|
|
107
109
|
return 10
|
|
108
110
|
}
|
|
109
111
|
}
|
|
@@ -474,6 +476,20 @@ export class EventStore {
|
|
|
474
476
|
this.state.queuedMessagesByChatId.delete(event.chatId)
|
|
475
477
|
break
|
|
476
478
|
}
|
|
479
|
+
case "chat_archived": {
|
|
480
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
481
|
+
if (!chat) break
|
|
482
|
+
chat.archivedAt = event.timestamp
|
|
483
|
+
chat.updatedAt = event.timestamp
|
|
484
|
+
break
|
|
485
|
+
}
|
|
486
|
+
case "chat_unarchived": {
|
|
487
|
+
const chat = this.state.chatsById.get(event.chatId)
|
|
488
|
+
if (!chat) break
|
|
489
|
+
delete chat.archivedAt
|
|
490
|
+
chat.updatedAt = event.timestamp
|
|
491
|
+
break
|
|
492
|
+
}
|
|
477
493
|
case "chat_provider_set": {
|
|
478
494
|
const chat = this.state.chatsById.get(event.chatId)
|
|
479
495
|
if (!chat) break
|
|
@@ -626,7 +642,9 @@ export class EventStore {
|
|
|
626
642
|
}
|
|
627
643
|
}
|
|
628
644
|
|
|
629
|
-
const
|
|
645
|
+
const hiddenProject = [...this.state.projectsById.values()]
|
|
646
|
+
.find((project) => project.localPath === normalized && project.deletedAt)
|
|
647
|
+
const projectId = hiddenProject?.id ?? crypto.randomUUID()
|
|
630
648
|
const event: ProjectEvent = {
|
|
631
649
|
v: STORE_VERSION,
|
|
632
650
|
type: "project_opened",
|
|
@@ -764,6 +782,28 @@ export class EventStore {
|
|
|
764
782
|
await this.append(this.chatsLogPath, event)
|
|
765
783
|
}
|
|
766
784
|
|
|
785
|
+
async archiveChat(chatId: string) {
|
|
786
|
+
this.requireChat(chatId)
|
|
787
|
+
const event: ChatEvent = {
|
|
788
|
+
v: STORE_VERSION,
|
|
789
|
+
type: "chat_archived",
|
|
790
|
+
timestamp: Date.now(),
|
|
791
|
+
chatId,
|
|
792
|
+
}
|
|
793
|
+
await this.append(this.chatsLogPath, event)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async unarchiveChat(chatId: string) {
|
|
797
|
+
this.requireChat(chatId)
|
|
798
|
+
const event: ChatEvent = {
|
|
799
|
+
v: STORE_VERSION,
|
|
800
|
+
type: "chat_unarchived",
|
|
801
|
+
timestamp: Date.now(),
|
|
802
|
+
chatId,
|
|
803
|
+
}
|
|
804
|
+
await this.append(this.chatsLogPath, event)
|
|
805
|
+
}
|
|
806
|
+
|
|
767
807
|
async pruneStaleEmptyChats(args?: {
|
|
768
808
|
now?: number
|
|
769
809
|
maxAgeMs?: number
|
|
@@ -779,7 +819,7 @@ export class EventStore {
|
|
|
779
819
|
const prunedChatIds: string[] = []
|
|
780
820
|
|
|
781
821
|
for (const chat of this.state.chatsById.values()) {
|
|
782
|
-
if (chat.deletedAt || protectedChatIds.has(chat.id)) continue
|
|
822
|
+
if (chat.deletedAt || chat.archivedAt || protectedChatIds.has(chat.id)) continue
|
|
783
823
|
if (now - chat.createdAt < maxAgeMs) continue
|
|
784
824
|
if (chat.hasMessages) continue
|
|
785
825
|
if (this.getMessages(chat.id).length > 0) {
|
|
@@ -1100,7 +1140,7 @@ export class EventStore {
|
|
|
1100
1140
|
|
|
1101
1141
|
listChatsByProject(projectId: string) {
|
|
1102
1142
|
return [...this.state.chatsById.values()]
|
|
1103
|
-
.filter((chat) => chat.projectId === projectId && !chat.deletedAt)
|
|
1143
|
+
.filter((chat) => chat.projectId === projectId && !chat.deletedAt && !chat.archivedAt)
|
|
1104
1144
|
.sort((a, b) => (b.lastMessageAt ?? b.updatedAt) - (a.lastMessageAt ?? a.updatedAt))
|
|
1105
1145
|
}
|
|
1106
1146
|
|
package/src/server/events.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface ChatRecord {
|
|
|
11
11
|
createdAt: number
|
|
12
12
|
updatedAt: number
|
|
13
13
|
deletedAt?: number
|
|
14
|
+
archivedAt?: number
|
|
14
15
|
unread: boolean
|
|
15
16
|
provider: AgentProvider | null
|
|
16
17
|
planMode: boolean
|
|
@@ -74,6 +75,18 @@ export type ChatEvent =
|
|
|
74
75
|
timestamp: number
|
|
75
76
|
chatId: string
|
|
76
77
|
}
|
|
78
|
+
| {
|
|
79
|
+
v: 2
|
|
80
|
+
type: "chat_archived"
|
|
81
|
+
timestamp: number
|
|
82
|
+
chatId: string
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
v: 2
|
|
86
|
+
type: "chat_unarchived"
|
|
87
|
+
timestamp: number
|
|
88
|
+
chatId: string
|
|
89
|
+
}
|
|
77
90
|
| {
|
|
78
91
|
v: 2
|
|
79
92
|
type: "chat_provider_set"
|
|
@@ -35,6 +35,48 @@ describe("read models", () => {
|
|
|
35
35
|
expect(sidebar.projectGroups[0]?.defaultCollapsed).toBe(false)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
+
test("keeps archived chats out of the main sidebar rows", () => {
|
|
39
|
+
const state = createEmptyState()
|
|
40
|
+
state.projectsById.set("project-1", {
|
|
41
|
+
id: "project-1",
|
|
42
|
+
localPath: "/tmp/project",
|
|
43
|
+
title: "Project",
|
|
44
|
+
createdAt: 1,
|
|
45
|
+
updatedAt: 1,
|
|
46
|
+
})
|
|
47
|
+
state.projectIdsByPath.set("/tmp/project", "project-1")
|
|
48
|
+
state.chatsById.set("chat-active", {
|
|
49
|
+
id: "chat-active",
|
|
50
|
+
projectId: "project-1",
|
|
51
|
+
title: "Active",
|
|
52
|
+
createdAt: 1,
|
|
53
|
+
updatedAt: 1,
|
|
54
|
+
unread: false,
|
|
55
|
+
provider: null,
|
|
56
|
+
planMode: false,
|
|
57
|
+
sessionToken: null,
|
|
58
|
+
lastTurnOutcome: null,
|
|
59
|
+
})
|
|
60
|
+
state.chatsById.set("chat-archived", {
|
|
61
|
+
id: "chat-archived",
|
|
62
|
+
projectId: "project-1",
|
|
63
|
+
title: "Archived",
|
|
64
|
+
createdAt: 2,
|
|
65
|
+
updatedAt: 3,
|
|
66
|
+
archivedAt: 3,
|
|
67
|
+
unread: false,
|
|
68
|
+
provider: null,
|
|
69
|
+
planMode: false,
|
|
70
|
+
sessionToken: null,
|
|
71
|
+
lastTurnOutcome: null,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const sidebar = deriveSidebarData(state, new Map(), { nowMs: 1_000_000 })
|
|
75
|
+
|
|
76
|
+
expect(sidebar.projectGroups[0]?.chats.map((chat) => chat.chatId)).toEqual(["chat-active"])
|
|
77
|
+
expect(sidebar.projectGroups[0]?.archivedChats?.map((chat) => chat.chatId)).toEqual(["chat-archived"])
|
|
78
|
+
})
|
|
79
|
+
|
|
38
80
|
test("includes available providers in chat snapshots", () => {
|
|
39
81
|
const state = createEmptyState()
|
|
40
82
|
state.projectsById.set("project-1", {
|
|
@@ -249,7 +291,7 @@ describe("read models", () => {
|
|
|
249
291
|
expect(sidebar.projectGroups[0]?.defaultCollapsed).toBe(false)
|
|
250
292
|
})
|
|
251
293
|
|
|
252
|
-
test("
|
|
294
|
+
test("shows all recent chats in the preview before folding older chats", () => {
|
|
253
295
|
const state = createEmptyState()
|
|
254
296
|
state.projectsById.set("project-1", {
|
|
255
297
|
id: "project-1",
|
|
@@ -285,8 +327,9 @@ describe("read models", () => {
|
|
|
285
327
|
"chat-3",
|
|
286
328
|
"chat-4",
|
|
287
329
|
"chat-5",
|
|
330
|
+
"chat-6",
|
|
288
331
|
])
|
|
289
|
-
expect(sidebar.projectGroups[0]?.olderChats.map((chat) => chat.chatId)).toEqual([
|
|
332
|
+
expect(sidebar.projectGroups[0]?.olderChats.map((chat) => chat.chatId)).toEqual([])
|
|
290
333
|
})
|
|
291
334
|
|
|
292
335
|
test("disables forking for active and draining chats, but allows pending fork chats", () => {
|
|
@@ -13,7 +13,6 @@ import { resolveLocalPath } from "./paths"
|
|
|
13
13
|
import { SERVER_PROVIDERS } from "./provider-catalog"
|
|
14
14
|
|
|
15
15
|
const SIDEBAR_RECENT_WINDOW_MS = 24 * 60 * 60 * 1_000
|
|
16
|
-
const SIDEBAR_RECENT_PREVIEW_LIMIT = 5
|
|
17
16
|
const SIDEBAR_FALLBACK_PREVIEW_LIMIT = 5
|
|
18
17
|
|
|
19
18
|
export function deriveStatus(chat: ChatRecord, activeStatus?: KannaStatus): KannaStatus {
|
|
@@ -49,7 +48,7 @@ function isSidebarChatRecent(chat: Pick<SidebarChatRow, "lastMessageAt" | "_crea
|
|
|
49
48
|
function getSidebarChatBuckets(chats: SidebarChatRow[], nowMs: number) {
|
|
50
49
|
const recentChats = chats.filter((chat) => isSidebarChatRecent(chat, nowMs))
|
|
51
50
|
const previewChats = recentChats.length > 0
|
|
52
|
-
? recentChats
|
|
51
|
+
? recentChats
|
|
53
52
|
: chats.slice(0, Math.min(SIDEBAR_FALLBACK_PREVIEW_LIMIT, chats.length))
|
|
54
53
|
const previewChatIds = new Set(previewChats.map((chat) => chat.chatId))
|
|
55
54
|
|
|
@@ -71,14 +70,16 @@ export function deriveSidebarData(
|
|
|
71
70
|
const nowMs = options?.nowMs ?? Date.now()
|
|
72
71
|
const drainingChatIds = options?.drainingChatIds ?? new Set<string>()
|
|
73
72
|
const chatsByProjectId = new Map<string, ChatRecord[]>()
|
|
73
|
+
const archivedChatsByProjectId = new Map<string, ChatRecord[]>()
|
|
74
74
|
for (const chat of state.chatsById.values()) {
|
|
75
75
|
if (chat.deletedAt) continue
|
|
76
|
-
const
|
|
76
|
+
const targetMap = chat.archivedAt ? archivedChatsByProjectId : chatsByProjectId
|
|
77
|
+
const projectChats = targetMap.get(chat.projectId)
|
|
77
78
|
if (projectChats) {
|
|
78
79
|
projectChats.push(chat)
|
|
79
80
|
continue
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
+
targetMap.set(chat.projectId, [chat])
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
const allProjects = [...state.projectsById.values()]
|
|
@@ -95,8 +96,8 @@ export function deriveSidebarData(
|
|
|
95
96
|
...unorderedProjects.filter((project) => !orderedProjectIds.has(project.id)),
|
|
96
97
|
]
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
function toSidebarChatRows(project: NonNullable<typeof projects[number]>, projectChats: ChatRecord[]) {
|
|
100
|
+
return projectChats
|
|
100
101
|
.sort((a, b) => getSidebarChatSortTimestamp(b) - getSidebarChatSortTimestamp(a))
|
|
101
102
|
.map((chat) => ({
|
|
102
103
|
_id: chat.id,
|
|
@@ -111,6 +112,11 @@ export function deriveSidebarData(
|
|
|
111
112
|
hasAutomation: false,
|
|
112
113
|
canFork: canForkChat(chat, activeStatuses, drainingChatIds) || undefined,
|
|
113
114
|
}))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const projectGroups: SidebarProjectGroup[] = projects.map((project) => {
|
|
118
|
+
const chats = toSidebarChatRows(project, chatsByProjectId.get(project.id) ?? [])
|
|
119
|
+
const archivedChats = toSidebarChatRows(project, archivedChatsByProjectId.get(project.id) ?? [])
|
|
114
120
|
const { previewChats, olderChats } = getSidebarChatBuckets(chats, nowMs)
|
|
115
121
|
|
|
116
122
|
return {
|
|
@@ -119,6 +125,7 @@ export function deriveSidebarData(
|
|
|
119
125
|
chats,
|
|
120
126
|
previewChats,
|
|
121
127
|
olderChats,
|
|
128
|
+
...(archivedChats.length ? { archivedChats } : {}),
|
|
122
129
|
defaultCollapsed: chats.every((chat) => !isSidebarChatRecent(chat, nowMs)),
|
|
123
130
|
}
|
|
124
131
|
})
|
|
@@ -145,7 +152,7 @@ export function deriveLocalProjectsSnapshot(
|
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
for (const project of [...state.projectsById.values()].filter((entry) => !entry.deletedAt)) {
|
|
148
|
-
const chats = [...state.chatsById.values()].filter((chat) => chat.projectId === project.id && !chat.deletedAt)
|
|
155
|
+
const chats = [...state.chatsById.values()].filter((chat) => chat.projectId === project.id && !chat.deletedAt && !chat.archivedAt)
|
|
149
156
|
const lastOpenedAt = chats.reduce(
|
|
150
157
|
(latest, chat) => Math.max(latest, getSidebarChatSortTimestamp(chat)),
|
|
151
158
|
project.updatedAt
|
package/src/server/ws-router.ts
CHANGED
|
@@ -939,21 +939,9 @@ export function createWsRouter({
|
|
|
939
939
|
break
|
|
940
940
|
}
|
|
941
941
|
case "project.remove": {
|
|
942
|
-
const project = store.getProject(command.projectId)
|
|
943
|
-
const chats = store.listChatsByProject(command.projectId)
|
|
944
|
-
for (const chat of chats) {
|
|
945
|
-
await agent.cancel(chat.id)
|
|
946
|
-
await agent.closeChat(chat.id)
|
|
947
|
-
}
|
|
948
|
-
if (project) {
|
|
949
|
-
terminals.closeByCwd(project.localPath)
|
|
950
|
-
}
|
|
951
942
|
await store.removeProject(command.projectId)
|
|
952
943
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
953
944
|
resolvedAnalytics.track("project_removed")
|
|
954
|
-
for (const _chat of chats) {
|
|
955
|
-
resolvedAnalytics.track("chat_deleted")
|
|
956
|
-
}
|
|
957
945
|
break
|
|
958
946
|
}
|
|
959
947
|
case "sidebar.reorderProjectGroups": {
|
|
@@ -998,6 +986,18 @@ export function createWsRouter({
|
|
|
998
986
|
await broadcastChatAndSidebar(command.chatId)
|
|
999
987
|
return
|
|
1000
988
|
}
|
|
989
|
+
case "chat.archive": {
|
|
990
|
+
await store.archiveChat(command.chatId)
|
|
991
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
992
|
+
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
993
|
+
return
|
|
994
|
+
}
|
|
995
|
+
case "chat.unarchive": {
|
|
996
|
+
await store.unarchiveChat(command.chatId)
|
|
997
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
998
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
999
|
+
return
|
|
1000
|
+
}
|
|
1001
1001
|
case "chat.delete": {
|
|
1002
1002
|
await agent.cancel(command.chatId)
|
|
1003
1003
|
await agent.closeChat(command.chatId)
|
package/src/shared/protocol.ts
CHANGED
|
@@ -93,6 +93,8 @@ export type ClientCommand =
|
|
|
93
93
|
| { type: "chat.create"; projectId: string }
|
|
94
94
|
| { type: "chat.fork"; chatId: string }
|
|
95
95
|
| { type: "chat.rename"; chatId: string; title: string }
|
|
96
|
+
| { type: "chat.archive"; chatId: string }
|
|
97
|
+
| { type: "chat.unarchive"; chatId: string }
|
|
96
98
|
| { type: "chat.delete"; chatId: string }
|
|
97
99
|
| { type: "chat.setDraftProtection"; chatIds: string[] }
|
|
98
100
|
| { type: "chat.markRead"; chatId: string }
|