kanna-code 0.22.0 → 0.23.1

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.
@@ -38,7 +38,7 @@ const DEFAULT_UPDATE_SNAPSHOT: UpdateSnapshot = {
38
38
  }
39
39
 
40
40
  describe("ws-router", () => {
41
- test("acks system.ping without broadcasting snapshots", () => {
41
+ test("acks system.ping without broadcasting snapshots", async () => {
42
42
  const router = createWsRouter({
43
43
  store: { state: createEmptyState() } as never,
44
44
  agent: { getActiveStatuses: () => new Map(), getDrainingChatIds: () => new Set() } as never,
@@ -58,7 +58,7 @@ describe("ws-router", () => {
58
58
  const ws = new FakeWebSocket()
59
59
 
60
60
  ws.data.subscriptions.set("sub-1", { type: "sidebar" })
61
- router.handleMessage(
61
+ await router.handleMessage(
62
62
  ws as never,
63
63
  JSON.stringify({
64
64
  v: 1,
@@ -77,7 +77,7 @@ describe("ws-router", () => {
77
77
  ])
78
78
  })
79
79
 
80
- test("acks terminal.input without rebroadcasting terminal snapshots", () => {
80
+ test("acks terminal.input without rebroadcasting terminal snapshots", async () => {
81
81
  const router = createWsRouter({
82
82
  store: { state: createEmptyState() } as never,
83
83
  agent: { getActiveStatuses: () => new Map(), getDrainingChatIds: () => new Set() } as never,
@@ -98,7 +98,7 @@ describe("ws-router", () => {
98
98
  const ws = new FakeWebSocket()
99
99
 
100
100
  ws.data.subscriptions.set("sub-terminal", { type: "terminal", terminalId: "terminal-1" })
101
- router.handleMessage(
101
+ await router.handleMessage(
102
102
  ws as never,
103
103
  JSON.stringify({
104
104
  v: 1,
@@ -121,7 +121,7 @@ describe("ws-router", () => {
121
121
  ])
122
122
  })
123
123
 
124
- test("subscribes and unsubscribes chat topics", () => {
124
+ test("subscribes and unsubscribes chat topics", async () => {
125
125
  const router = createWsRouter({
126
126
  store: { state: createEmptyState() } as never,
127
127
  agent: { getActiveStatuses: () => new Map(), getDrainingChatIds: () => new Set() } as never,
@@ -140,7 +140,7 @@ describe("ws-router", () => {
140
140
  })
141
141
  const ws = new FakeWebSocket()
142
142
 
143
- router.handleMessage(
143
+ await router.handleMessage(
144
144
  ws as never,
145
145
  JSON.stringify({
146
146
  v: 1,
@@ -160,7 +160,7 @@ describe("ws-router", () => {
160
160
  },
161
161
  })
162
162
 
163
- router.handleMessage(
163
+ await router.handleMessage(
164
164
  ws as never,
165
165
  JSON.stringify({
166
166
  v: 1,
@@ -176,7 +176,7 @@ describe("ws-router", () => {
176
176
  })
177
177
  })
178
178
 
179
- test("loads older chat history pages", () => {
179
+ test("loads older chat history pages", async () => {
180
180
  const state = createEmptyState()
181
181
  state.projectsById.set("project-1", {
182
182
  id: "project-1",
@@ -230,7 +230,7 @@ describe("ws-router", () => {
230
230
  })
231
231
  const ws = new FakeWebSocket()
232
232
 
233
- router.handleMessage(
233
+ await router.handleMessage(
234
234
  ws as never,
235
235
  JSON.stringify({
236
236
  v: 1,
@@ -472,7 +472,7 @@ describe("ws-router", () => {
472
472
  })
473
473
  const ws = new FakeWebSocket()
474
474
 
475
- router.handleMessage(
475
+ await router.handleMessage(
476
476
  ws as never,
477
477
  JSON.stringify({
478
478
  v: 1,
@@ -492,7 +492,7 @@ describe("ws-router", () => {
492
492
  },
493
493
  })
494
494
 
495
- router.handleMessage(
495
+ await router.handleMessage(
496
496
  ws as never,
497
497
  JSON.stringify({
498
498
  v: 1,
@@ -576,7 +576,7 @@ describe("ws-router", () => {
576
576
  })
577
577
  const ws = new FakeWebSocket()
578
578
 
579
- router.handleMessage(
579
+ await router.handleMessage(
580
580
  ws as never,
581
581
  JSON.stringify({
582
582
  v: 1,
@@ -596,7 +596,7 @@ describe("ws-router", () => {
596
596
  },
597
597
  })
598
598
 
599
- router.handleMessage(
599
+ await router.handleMessage(
600
600
  ws as never,
601
601
  JSON.stringify({
602
602
  v: 1,
@@ -625,7 +625,7 @@ describe("ws-router", () => {
625
625
  },
626
626
  })
627
627
 
628
- router.handleMessage(
628
+ await router.handleMessage(
629
629
  ws as never,
630
630
  JSON.stringify({
631
631
  v: 1,
@@ -651,4 +651,186 @@ describe("ws-router", () => {
651
651
  },
652
652
  })
653
653
  })
654
+
655
+ test("routes discard diff file commands through the diff store and rebroadcasts chat snapshots", async () => {
656
+ const state = createEmptyState()
657
+ state.projectsById.set("project-1", {
658
+ id: "project-1",
659
+ localPath: "/tmp/project",
660
+ title: "Project",
661
+ createdAt: 1,
662
+ updatedAt: 1,
663
+ })
664
+ state.projectIdsByPath.set("/tmp/project", "project-1")
665
+ state.chatsById.set("chat-1", {
666
+ id: "chat-1",
667
+ projectId: "project-1",
668
+ title: "Chat",
669
+ createdAt: 1,
670
+ updatedAt: 1,
671
+ unread: false,
672
+ provider: null,
673
+ planMode: false,
674
+ sessionToken: null,
675
+ lastTurnOutcome: null,
676
+ })
677
+
678
+ const discardCalls: Array<{ chatId: string; projectPath: string; path: string }> = []
679
+ const diffStore = {
680
+ getSnapshot: () => ({ status: "ready" as const, files: [] }),
681
+ refreshSnapshot: async () => false,
682
+ generateCommitMessage: async () => ({ subject: "", body: "" }),
683
+ commitFiles: async () => ({ ok: true as const, mode: "commit_only" as const, pushed: false, snapshotChanged: false }),
684
+ discardFile: async (args: { chatId: string; projectPath: string; path: string }) => {
685
+ discardCalls.push(args)
686
+ return { snapshotChanged: true }
687
+ },
688
+ ignoreFile: async () => ({ snapshotChanged: false }),
689
+ }
690
+
691
+ const router = createWsRouter({
692
+ store: {
693
+ state,
694
+ getChat: (chatId: string) => state.chatsById.get(chatId) ?? null,
695
+ getProject: (projectId: string) => state.projectsById.get(projectId) ?? null,
696
+ getRecentChatHistory: () => ({ entries: [], hasOlder: false, olderCursor: null }),
697
+ } as never,
698
+ diffStore: diffStore as never,
699
+ agent: { getActiveStatuses: () => new Map(), getDrainingChatIds: () => new Set() } as never,
700
+ terminals: {
701
+ getSnapshot: () => null,
702
+ onEvent: () => () => {},
703
+ } as never,
704
+ keybindings: {
705
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
706
+ onChange: () => () => {},
707
+ } as never,
708
+ refreshDiscovery: async () => [],
709
+ getDiscoveredProjects: () => [],
710
+ machineDisplayName: "Local Machine",
711
+ updateManager: null,
712
+ })
713
+ const ws = new FakeWebSocket()
714
+
715
+ router.handleOpen(ws as never)
716
+ router.handleMessage(
717
+ ws as never,
718
+ JSON.stringify({
719
+ v: 1,
720
+ type: "subscribe",
721
+ id: "chat-sub",
722
+ topic: { type: "chat", chatId: "chat-1" },
723
+ })
724
+ )
725
+
726
+ await router.handleMessage(
727
+ ws as never,
728
+ JSON.stringify({
729
+ v: 1,
730
+ type: "command",
731
+ id: "discard-1",
732
+ command: {
733
+ type: "chat.discardDiffFile",
734
+ chatId: "chat-1",
735
+ path: "app.txt",
736
+ },
737
+ })
738
+ )
739
+
740
+ expect(discardCalls).toEqual([{
741
+ chatId: "chat-1",
742
+ projectPath: "/tmp/project",
743
+ path: "app.txt",
744
+ }])
745
+ expect(ws.sent).toContainEqual({
746
+ v: PROTOCOL_VERSION,
747
+ type: "ack",
748
+ id: "discard-1",
749
+ result: { snapshotChanged: true },
750
+ })
751
+ })
752
+
753
+ test("routes ignore diff file commands through the diff store", async () => {
754
+ const state = createEmptyState()
755
+ state.projectsById.set("project-1", {
756
+ id: "project-1",
757
+ localPath: "/tmp/project",
758
+ title: "Project",
759
+ createdAt: 1,
760
+ updatedAt: 1,
761
+ })
762
+ state.projectIdsByPath.set("/tmp/project", "project-1")
763
+ state.chatsById.set("chat-1", {
764
+ id: "chat-1",
765
+ projectId: "project-1",
766
+ title: "Chat",
767
+ createdAt: 1,
768
+ updatedAt: 1,
769
+ unread: false,
770
+ provider: null,
771
+ planMode: false,
772
+ sessionToken: null,
773
+ lastTurnOutcome: null,
774
+ })
775
+
776
+ const ignoreCalls: Array<{ chatId: string; projectPath: string; path: string }> = []
777
+ const router = createWsRouter({
778
+ store: {
779
+ state,
780
+ getChat: (chatId: string) => state.chatsById.get(chatId) ?? null,
781
+ getProject: (projectId: string) => state.projectsById.get(projectId) ?? null,
782
+ } as never,
783
+ diffStore: {
784
+ getSnapshot: () => ({ status: "ready" as const, files: [] }),
785
+ refreshSnapshot: async () => false,
786
+ generateCommitMessage: async () => ({ subject: "", body: "" }),
787
+ commitFiles: async () => ({ ok: true as const, mode: "commit_only" as const, pushed: false, snapshotChanged: false }),
788
+ discardFile: async () => ({ snapshotChanged: false }),
789
+ ignoreFile: async (args: { chatId: string; projectPath: string; path: string }) => {
790
+ ignoreCalls.push(args)
791
+ return { snapshotChanged: false }
792
+ },
793
+ } as never,
794
+ agent: { getActiveStatuses: () => new Map(), getDrainingChatIds: () => new Set() } as never,
795
+ terminals: {
796
+ getSnapshot: () => null,
797
+ onEvent: () => () => {},
798
+ } as never,
799
+ keybindings: {
800
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
801
+ onChange: () => () => {},
802
+ } as never,
803
+ refreshDiscovery: async () => [],
804
+ getDiscoveredProjects: () => [],
805
+ machineDisplayName: "Local Machine",
806
+ updateManager: null,
807
+ })
808
+ const ws = new FakeWebSocket()
809
+
810
+ await router.handleMessage(
811
+ ws as never,
812
+ JSON.stringify({
813
+ v: 1,
814
+ type: "command",
815
+ id: "ignore-1",
816
+ command: {
817
+ type: "chat.ignoreDiffFile",
818
+ chatId: "chat-1",
819
+ path: "scratch.log",
820
+ },
821
+ })
822
+ )
823
+
824
+ expect(ignoreCalls).toEqual([{
825
+ chatId: "chat-1",
826
+ projectPath: "/tmp/project",
827
+ path: "scratch.log",
828
+ }])
829
+ expect(ws.sent).toContainEqual({
830
+ v: PROTOCOL_VERSION,
831
+ type: "ack",
832
+ id: "ignore-1",
833
+ result: { snapshotChanged: false },
834
+ })
835
+ })
654
836
  })
@@ -22,7 +22,7 @@ export interface ClientState {
22
22
 
23
23
  interface CreateWsRouterArgs {
24
24
  store: EventStore
25
- diffStore?: Pick<DiffStore, "getSnapshot" | "refreshSnapshot" | "generateCommitMessage" | "commitFiles">
25
+ diffStore?: Pick<DiffStore, "getSnapshot" | "refreshSnapshot" | "generateCommitMessage" | "commitFiles" | "discardFile" | "ignoreFile">
26
26
  agent: AgentCoordinator
27
27
  terminals: TerminalManager
28
28
  keybindings: KeybindingsManager
@@ -61,6 +61,8 @@ export function createWsRouter({
61
61
  refreshSnapshot: async () => false,
62
62
  generateCommitMessage: async () => ({ subject: "Update selected files", body: "", usedFallback: true, failureMessage: null }),
63
63
  commitFiles: async () => ({ ok: true, mode: "commit_only" as const, branchName: undefined, pushed: false, snapshotChanged: false }),
64
+ discardFile: async () => ({ snapshotChanged: false }),
65
+ ignoreFile: async () => ({ snapshotChanged: false }),
64
66
  }
65
67
 
66
68
  function createEnvelope(id: string, topic: SubscriptionTopic): ServerEnvelope {
@@ -407,6 +409,46 @@ export function createWsRouter({
407
409
  }
408
410
  return
409
411
  }
412
+ case "chat.discardDiffFile": {
413
+ const chat = store.getChat(command.chatId)
414
+ if (!chat) {
415
+ throw new Error("Chat not found")
416
+ }
417
+ const project = store.getProject(chat.projectId)
418
+ if (!project) {
419
+ throw new Error("Project not found")
420
+ }
421
+ const result = await resolvedDiffStore.discardFile({
422
+ chatId: command.chatId,
423
+ projectPath: project.localPath,
424
+ path: command.path,
425
+ })
426
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
427
+ if (result.snapshotChanged) {
428
+ broadcastSnapshots()
429
+ }
430
+ return
431
+ }
432
+ case "chat.ignoreDiffFile": {
433
+ const chat = store.getChat(command.chatId)
434
+ if (!chat) {
435
+ throw new Error("Chat not found")
436
+ }
437
+ const project = store.getProject(chat.projectId)
438
+ if (!project) {
439
+ throw new Error("Project not found")
440
+ }
441
+ const result = await resolvedDiffStore.ignoreFile({
442
+ chatId: command.chatId,
443
+ projectPath: project.localPath,
444
+ path: command.path,
445
+ })
446
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
447
+ if (result.snapshotChanged) {
448
+ broadcastSnapshots()
449
+ }
450
+ return
451
+ }
410
452
  case "chat.cancel": {
411
453
  await agent.cancel(command.chatId)
412
454
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
@@ -80,6 +80,8 @@ export type ClientCommand =
80
80
  | { type: "chat.refreshDiffs"; chatId: string }
81
81
  | { type: "chat.generateCommitMessage"; chatId: string; paths: string[] }
82
82
  | { type: "chat.commitDiffs"; chatId: string; paths: string[]; summary: string; description?: string; mode: DiffCommitMode }
83
+ | { type: "chat.discardDiffFile"; chatId: string; path: string }
84
+ | { type: "chat.ignoreDiffFile"; chatId: string; path: string }
83
85
  | { type: "chat.cancel"; chatId: string }
84
86
  | { type: "chat.stopDraining"; chatId: string }
85
87
  | { type: "chat.loadHistory"; chatId: string; beforeCursor: string; limit: number }
@@ -471,6 +471,7 @@ export interface ContextWindowUsageSnapshot {
471
471
  export interface ChatDiffFile {
472
472
  path: string
473
473
  changeType: "added" | "deleted" | "modified" | "renamed"
474
+ isUntracked: boolean
474
475
  patch: string
475
476
  mimeType?: string
476
477
  size?: number