kanna-code 0.23.0 → 0.24.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.
@@ -11,6 +11,7 @@ describe("generateCommitMessageDetailed", () => {
11
11
  files: [{
12
12
  path: "app.ts",
13
13
  changeType: "modified",
14
+ isUntracked: false,
14
15
  patch: "diff --git a/app.ts b/app.ts\n--- a/app.ts\n+++ b/app.ts\n@@\n-old\n+new\n",
15
16
  }],
16
17
  },
@@ -38,6 +39,7 @@ describe("generateCommitMessageDetailed", () => {
38
39
  files: [{
39
40
  path: "src/feature.ts",
40
41
  changeType: "modified",
42
+ isUntracked: false,
41
43
  patch: "diff --git a/src/feature.ts b/src/feature.ts\n",
42
44
  }],
43
45
  },
@@ -62,11 +62,11 @@ function buildCommitMessagePrompt(args: {
62
62
  const combinedPatch = args.files.map((file) => file.patch).join("\n\n")
63
63
 
64
64
  return [
65
- "Generate a concise git commit message for the selected changes.",
65
+ "Generate a git commit message for the selected changes.",
66
66
  "Return JSON with keys: subject, body.",
67
67
  "Rules:",
68
68
  "- subject must be imperative, under 72 chars, and have no trailing period",
69
- "- body may be an empty string or short bullet points",
69
+ "- body may be an empty string or 3-5 bullet points",
70
70
  "- capture the primary user-visible or developer-visible change",
71
71
  "",
72
72
  `Branch: ${args.branchName ?? "current branch"}`,
@@ -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,188 @@ 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: [], defaultBranchName: "main", originRepoSlug: "acme/repo", aheadCount: 0, behindCount: 0, lastFetchedAt: undefined }),
681
+ refreshSnapshot: async () => false,
682
+ syncBranch: async () => ({ ok: true as const, action: "fetch" as const, snapshotChanged: false }),
683
+ generateCommitMessage: async () => ({ subject: "", body: "" }),
684
+ commitFiles: async () => ({ ok: true as const, mode: "commit_only" as const, pushed: false, snapshotChanged: false }),
685
+ discardFile: async (args: { chatId: string; projectPath: string; path: string }) => {
686
+ discardCalls.push(args)
687
+ return { snapshotChanged: true }
688
+ },
689
+ ignoreFile: async () => ({ snapshotChanged: false }),
690
+ }
691
+
692
+ const router = createWsRouter({
693
+ store: {
694
+ state,
695
+ getChat: (chatId: string) => state.chatsById.get(chatId) ?? null,
696
+ getProject: (projectId: string) => state.projectsById.get(projectId) ?? null,
697
+ getRecentChatHistory: () => ({ entries: [], hasOlder: false, olderCursor: null }),
698
+ } as never,
699
+ diffStore: diffStore as never,
700
+ agent: { getActiveStatuses: () => new Map(), getDrainingChatIds: () => new Set() } as never,
701
+ terminals: {
702
+ getSnapshot: () => null,
703
+ onEvent: () => () => {},
704
+ } as never,
705
+ keybindings: {
706
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
707
+ onChange: () => () => {},
708
+ } as never,
709
+ refreshDiscovery: async () => [],
710
+ getDiscoveredProjects: () => [],
711
+ machineDisplayName: "Local Machine",
712
+ updateManager: null,
713
+ })
714
+ const ws = new FakeWebSocket()
715
+
716
+ router.handleOpen(ws as never)
717
+ router.handleMessage(
718
+ ws as never,
719
+ JSON.stringify({
720
+ v: 1,
721
+ type: "subscribe",
722
+ id: "chat-sub",
723
+ topic: { type: "chat", chatId: "chat-1" },
724
+ })
725
+ )
726
+
727
+ await router.handleMessage(
728
+ ws as never,
729
+ JSON.stringify({
730
+ v: 1,
731
+ type: "command",
732
+ id: "discard-1",
733
+ command: {
734
+ type: "chat.discardDiffFile",
735
+ chatId: "chat-1",
736
+ path: "app.txt",
737
+ },
738
+ })
739
+ )
740
+
741
+ expect(discardCalls).toEqual([{
742
+ chatId: "chat-1",
743
+ projectPath: "/tmp/project",
744
+ path: "app.txt",
745
+ }])
746
+ expect(ws.sent).toContainEqual({
747
+ v: PROTOCOL_VERSION,
748
+ type: "ack",
749
+ id: "discard-1",
750
+ result: { snapshotChanged: true },
751
+ })
752
+ })
753
+
754
+ test("routes ignore diff file commands through the diff store", async () => {
755
+ const state = createEmptyState()
756
+ state.projectsById.set("project-1", {
757
+ id: "project-1",
758
+ localPath: "/tmp/project",
759
+ title: "Project",
760
+ createdAt: 1,
761
+ updatedAt: 1,
762
+ })
763
+ state.projectIdsByPath.set("/tmp/project", "project-1")
764
+ state.chatsById.set("chat-1", {
765
+ id: "chat-1",
766
+ projectId: "project-1",
767
+ title: "Chat",
768
+ createdAt: 1,
769
+ updatedAt: 1,
770
+ unread: false,
771
+ provider: null,
772
+ planMode: false,
773
+ sessionToken: null,
774
+ lastTurnOutcome: null,
775
+ })
776
+
777
+ const ignoreCalls: Array<{ chatId: string; projectPath: string; path: string }> = []
778
+ const router = createWsRouter({
779
+ store: {
780
+ state,
781
+ getChat: (chatId: string) => state.chatsById.get(chatId) ?? null,
782
+ getProject: (projectId: string) => state.projectsById.get(projectId) ?? null,
783
+ } as never,
784
+ diffStore: {
785
+ getSnapshot: () => ({ status: "ready" as const, files: [], defaultBranchName: "main", originRepoSlug: "acme/repo", aheadCount: 0, behindCount: 0, lastFetchedAt: undefined }),
786
+ refreshSnapshot: async () => false,
787
+ syncBranch: async () => ({ ok: true as const, action: "fetch" as const, snapshotChanged: false }),
788
+ generateCommitMessage: async () => ({ subject: "", body: "" }),
789
+ commitFiles: async () => ({ ok: true as const, mode: "commit_only" as const, pushed: false, snapshotChanged: false }),
790
+ discardFile: async () => ({ snapshotChanged: false }),
791
+ ignoreFile: async (args: { chatId: string; projectPath: string; path: string }) => {
792
+ ignoreCalls.push(args)
793
+ return { snapshotChanged: false }
794
+ },
795
+ } as never,
796
+ agent: { getActiveStatuses: () => new Map(), getDrainingChatIds: () => new Set() } as never,
797
+ terminals: {
798
+ getSnapshot: () => null,
799
+ onEvent: () => () => {},
800
+ } as never,
801
+ keybindings: {
802
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
803
+ onChange: () => () => {},
804
+ } as never,
805
+ refreshDiscovery: async () => [],
806
+ getDiscoveredProjects: () => [],
807
+ machineDisplayName: "Local Machine",
808
+ updateManager: null,
809
+ })
810
+ const ws = new FakeWebSocket()
811
+
812
+ await router.handleMessage(
813
+ ws as never,
814
+ JSON.stringify({
815
+ v: 1,
816
+ type: "command",
817
+ id: "ignore-1",
818
+ command: {
819
+ type: "chat.ignoreDiffFile",
820
+ chatId: "chat-1",
821
+ path: "scratch.log",
822
+ },
823
+ })
824
+ )
825
+
826
+ expect(ignoreCalls).toEqual([{
827
+ chatId: "chat-1",
828
+ projectPath: "/tmp/project",
829
+ path: "scratch.log",
830
+ }])
831
+ expect(ws.sent).toContainEqual({
832
+ v: PROTOCOL_VERSION,
833
+ type: "ack",
834
+ id: "ignore-1",
835
+ result: { snapshotChanged: false },
836
+ })
837
+ })
654
838
  })
@@ -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" | "listBranches" | "syncBranch" | "checkoutBranch" | "createBranch" | "generateCommitMessage" | "commitFiles" | "discardFile" | "ignoreFile">
26
26
  agent: AgentCoordinator
27
27
  terminals: TerminalManager
28
28
  keybindings: KeybindingsManager
@@ -57,10 +57,16 @@ export function createWsRouter({
57
57
  }: CreateWsRouterArgs) {
58
58
  const sockets = new Set<ServerWebSocket<ClientState>>()
59
59
  const resolvedDiffStore = diffStore ?? {
60
- getSnapshot: () => ({ status: "unknown", branchName: undefined, hasUpstream: undefined, files: [] as const }),
60
+ getSnapshot: () => ({ status: "unknown", branchName: undefined, defaultBranchName: undefined, originRepoSlug: undefined, hasUpstream: undefined, aheadCount: undefined, behindCount: undefined, lastFetchedAt: undefined, files: [] as const, branchHistory: { entries: [] as const } }),
61
61
  refreshSnapshot: async () => false,
62
+ listBranches: async () => ({ recent: [], local: [], remote: [], pullRequests: [], pullRequestsStatus: "unavailable" as const }),
63
+ syncBranch: async () => ({ ok: true, action: "fetch" as const, branchName: undefined, snapshotChanged: false }),
64
+ checkoutBranch: async () => ({ ok: true, branchName: undefined, snapshotChanged: false }),
65
+ createBranch: async () => ({ ok: true, branchName: "main", snapshotChanged: false }),
62
66
  generateCommitMessage: async () => ({ subject: "Update selected files", body: "", usedFallback: true, failureMessage: null }),
63
67
  commitFiles: async () => ({ ok: true, mode: "commit_only" as const, branchName: undefined, pushed: false, snapshotChanged: false }),
68
+ discardFile: async () => ({ snapshotChanged: false }),
69
+ ignoreFile: async () => ({ snapshotChanged: false }),
64
70
  }
65
71
 
66
72
  function createEnvelope(id: string, topic: SubscriptionTopic): ServerEnvelope {
@@ -368,6 +374,83 @@ export function createWsRouter({
368
374
  }
369
375
  return
370
376
  }
377
+ case "chat.listBranches": {
378
+ const chat = store.getChat(command.chatId)
379
+ if (!chat) {
380
+ throw new Error("Chat not found")
381
+ }
382
+ const project = store.getProject(chat.projectId)
383
+ if (!project) {
384
+ throw new Error("Project not found")
385
+ }
386
+ const result = await resolvedDiffStore.listBranches({
387
+ projectPath: project.localPath,
388
+ })
389
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
390
+ return
391
+ }
392
+ case "chat.checkoutBranch": {
393
+ const chat = store.getChat(command.chatId)
394
+ if (!chat) {
395
+ throw new Error("Chat not found")
396
+ }
397
+ const project = store.getProject(chat.projectId)
398
+ if (!project) {
399
+ throw new Error("Project not found")
400
+ }
401
+ const result = await resolvedDiffStore.checkoutBranch({
402
+ chatId: command.chatId,
403
+ projectPath: project.localPath,
404
+ branch: command.branch,
405
+ bringChanges: command.bringChanges,
406
+ })
407
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
408
+ if (result.snapshotChanged) {
409
+ broadcastSnapshots()
410
+ }
411
+ return
412
+ }
413
+ case "chat.syncBranch": {
414
+ const chat = store.getChat(command.chatId)
415
+ if (!chat) {
416
+ throw new Error("Chat not found")
417
+ }
418
+ const project = store.getProject(chat.projectId)
419
+ if (!project) {
420
+ throw new Error("Project not found")
421
+ }
422
+ const result = await resolvedDiffStore.syncBranch({
423
+ chatId: command.chatId,
424
+ projectPath: project.localPath,
425
+ action: command.action,
426
+ })
427
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
428
+ if (result.snapshotChanged) {
429
+ broadcastSnapshots()
430
+ }
431
+ return
432
+ }
433
+ case "chat.createBranch": {
434
+ const chat = store.getChat(command.chatId)
435
+ if (!chat) {
436
+ throw new Error("Chat not found")
437
+ }
438
+ const project = store.getProject(chat.projectId)
439
+ if (!project) {
440
+ throw new Error("Project not found")
441
+ }
442
+ const result = await resolvedDiffStore.createBranch({
443
+ chatId: command.chatId,
444
+ projectPath: project.localPath,
445
+ name: command.name,
446
+ baseBranchName: command.baseBranchName,
447
+ })
448
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
449
+ if (result.snapshotChanged) {
450
+ broadcastSnapshots()
451
+ }
452
+ return
453
+ }
371
454
  case "chat.generateCommitMessage": {
372
455
  const chat = store.getChat(command.chatId)
373
456
  if (!chat) {
@@ -407,6 +490,46 @@ export function createWsRouter({
407
490
  }
408
491
  return
409
492
  }
493
+ case "chat.discardDiffFile": {
494
+ const chat = store.getChat(command.chatId)
495
+ if (!chat) {
496
+ throw new Error("Chat not found")
497
+ }
498
+ const project = store.getProject(chat.projectId)
499
+ if (!project) {
500
+ throw new Error("Project not found")
501
+ }
502
+ const result = await resolvedDiffStore.discardFile({
503
+ chatId: command.chatId,
504
+ projectPath: project.localPath,
505
+ path: command.path,
506
+ })
507
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
508
+ if (result.snapshotChanged) {
509
+ broadcastSnapshots()
510
+ }
511
+ return
512
+ }
513
+ case "chat.ignoreDiffFile": {
514
+ const chat = store.getChat(command.chatId)
515
+ if (!chat) {
516
+ throw new Error("Chat not found")
517
+ }
518
+ const project = store.getProject(chat.projectId)
519
+ if (!project) {
520
+ throw new Error("Project not found")
521
+ }
522
+ const result = await resolvedDiffStore.ignoreFile({
523
+ chatId: command.chatId,
524
+ projectPath: project.localPath,
525
+ path: command.path,
526
+ })
527
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
528
+ if (result.snapshotChanged) {
529
+ broadcastSnapshots()
530
+ }
531
+ return
532
+ }
410
533
  case "chat.cancel": {
411
534
  await agent.cancel(command.chatId)
412
535
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
@@ -78,8 +78,30 @@ export type ClientCommand =
78
78
  planMode?: boolean
79
79
  }
80
80
  | { type: "chat.refreshDiffs"; chatId: string }
81
+ | { type: "chat.listBranches"; chatId: string }
82
+ | { type: "chat.syncBranch"; chatId: string; action: "fetch" | "pull" | "publish" }
83
+ | {
84
+ type: "chat.checkoutBranch"
85
+ chatId: string
86
+ branch:
87
+ | { kind: "local"; name: string }
88
+ | { kind: "remote"; name: string; remoteRef: string }
89
+ | {
90
+ kind: "pull_request"
91
+ name: string
92
+ prNumber: number
93
+ headRefName: string
94
+ headRepoCloneUrl?: string
95
+ isCrossRepository?: boolean
96
+ remoteRef?: string
97
+ }
98
+ bringChanges?: boolean
99
+ }
100
+ | { type: "chat.createBranch"; chatId: string; name: string; baseBranchName?: string }
81
101
  | { type: "chat.generateCommitMessage"; chatId: string; paths: string[] }
82
102
  | { type: "chat.commitDiffs"; chatId: string; paths: string[]; summary: string; description?: string; mode: DiffCommitMode }
103
+ | { type: "chat.discardDiffFile"; chatId: string; path: string }
104
+ | { type: "chat.ignoreDiffFile"; chatId: string; path: string }
83
105
  | { type: "chat.cancel"; chatId: string }
84
106
  | { type: "chat.stopDraining"; chatId: string }
85
107
  | { type: "chat.loadHistory"; chatId: string; beforeCursor: string; limit: number }
@@ -471,20 +471,123 @@ 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
477
478
  }
478
479
 
480
+ export interface ChatBranchHistoryEntry {
481
+ sha: string
482
+ summary: string
483
+ description: string
484
+ authorName?: string
485
+ authoredAt: string
486
+ tags: string[]
487
+ githubUrl?: string
488
+ }
489
+
490
+ export interface ChatBranchHistorySnapshot {
491
+ entries: ChatBranchHistoryEntry[]
492
+ }
493
+
494
+ export type ChatBranchListEntryKind = "local" | "remote" | "pull_request"
495
+
496
+ export interface ChatBranchListEntry {
497
+ id: string
498
+ kind: ChatBranchListEntryKind
499
+ name: string
500
+ displayName: string
501
+ updatedAt?: string
502
+ description?: string
503
+ remoteRef?: string
504
+ prNumber?: number
505
+ prTitle?: string
506
+ headRefName?: string
507
+ headLabel?: string
508
+ headRepoCloneUrl?: string
509
+ isCrossRepository?: boolean
510
+ }
511
+
512
+ export interface ChatBranchListResult {
513
+ currentBranchName?: string
514
+ defaultBranchName?: string
515
+ recent: ChatBranchListEntry[]
516
+ local: ChatBranchListEntry[]
517
+ remote: ChatBranchListEntry[]
518
+ pullRequests: ChatBranchListEntry[]
519
+ pullRequestsStatus: "available" | "unavailable" | "error"
520
+ pullRequestsError?: string
521
+ }
522
+
479
523
  export interface ChatDiffSnapshot {
480
524
  status: "unknown" | "ready" | "no_repo"
481
525
  branchName?: string
526
+ defaultBranchName?: string
527
+ originRepoSlug?: string
482
528
  hasUpstream?: boolean
529
+ aheadCount?: number
530
+ behindCount?: number
531
+ lastFetchedAt?: string
483
532
  files: ChatDiffFile[]
533
+ branchHistory?: ChatBranchHistorySnapshot
484
534
  }
485
535
 
536
+ export interface ChatSyncSuccess {
537
+ ok: true
538
+ action: "fetch" | "pull" | "publish"
539
+ branchName?: string
540
+ aheadCount?: number
541
+ behindCount?: number
542
+ snapshotChanged: boolean
543
+ }
544
+
545
+ export interface ChatSyncFailure {
546
+ ok: false
547
+ action: "fetch" | "pull" | "publish"
548
+ title: string
549
+ message: string
550
+ detail?: string
551
+ snapshotChanged?: boolean
552
+ }
553
+
554
+ export type ChatSyncResult = ChatSyncSuccess | ChatSyncFailure
555
+
486
556
  export type DiffCommitMode = "commit_and_push" | "commit_only"
487
557
 
558
+ export interface ChatCheckoutBranchSuccess {
559
+ ok: true
560
+ branchName?: string
561
+ snapshotChanged: boolean
562
+ }
563
+
564
+ export interface ChatCheckoutBranchFailure {
565
+ ok: false
566
+ cancelled?: boolean
567
+ title: string
568
+ message: string
569
+ detail?: string
570
+ snapshotChanged?: boolean
571
+ }
572
+
573
+ export type ChatCheckoutBranchResult = ChatCheckoutBranchSuccess | ChatCheckoutBranchFailure
574
+
575
+ export interface ChatCreateBranchSuccess {
576
+ ok: true
577
+ branchName: string
578
+ snapshotChanged: boolean
579
+ }
580
+
581
+ export interface ChatCreateBranchFailure {
582
+ ok: false
583
+ title: string
584
+ message: string
585
+ detail?: string
586
+ snapshotChanged?: boolean
587
+ }
588
+
589
+ export type ChatCreateBranchResult = ChatCreateBranchSuccess | ChatCreateBranchFailure
590
+
488
591
  export interface DiffCommitSuccess {
489
592
  ok: true
490
593
  mode: DiffCommitMode