kanna-code 0.23.1 → 0.25.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.
@@ -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" | "discardFile" | "ignoreFile">
25
+ diffStore?: Pick<DiffStore, "getProjectSnapshot" | "refreshSnapshot" | "listBranches" | "previewMergeBranch" | "mergeBranch" | "syncBranch" | "checkoutBranch" | "createBranch" | "generateCommitMessage" | "commitFiles" | "discardFile" | "ignoreFile" | "readPatch">
26
26
  agent: AgentCoordinator
27
27
  terminals: TerminalManager
28
28
  keybindings: KeybindingsManager
@@ -57,12 +57,19 @@ 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
+ getProjectSnapshot: () => ({ 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
+ previewMergeBranch: async () => ({ currentBranchName: undefined, targetBranchName: "", targetDisplayName: "", status: "error" as const, commitCount: 0, hasConflicts: false, message: "Merge preview unavailable." }),
64
+ mergeBranch: async () => ({ ok: false as const, title: "Merge failed", message: "Merge unavailable.", snapshotChanged: false }),
65
+ syncBranch: async () => ({ ok: true, action: "fetch" as const, branchName: undefined, snapshotChanged: false }),
66
+ checkoutBranch: async () => ({ ok: true, branchName: undefined, snapshotChanged: false }),
67
+ createBranch: async () => ({ ok: true, branchName: "main", snapshotChanged: false }),
62
68
  generateCommitMessage: async () => ({ subject: "Update selected files", body: "", usedFallback: true, failureMessage: null }),
63
69
  commitFiles: async () => ({ ok: true, mode: "commit_only" as const, branchName: undefined, pushed: false, snapshotChanged: false }),
64
70
  discardFile: async () => ({ snapshotChanged: false }),
65
71
  ignoreFile: async () => ({ snapshotChanged: false }),
72
+ readPatch: async () => ({ patch: "" }),
66
73
  }
67
74
 
68
75
  function createEnvelope(id: string, topic: SubscriptionTopic): ServerEnvelope {
@@ -137,6 +144,20 @@ export function createWsRouter({
137
144
  }
138
145
  }
139
146
 
147
+ if (topic.type === "project-git") {
148
+ return {
149
+ v: PROTOCOL_VERSION,
150
+ type: "snapshot",
151
+ id,
152
+ snapshot: {
153
+ type: "project-git",
154
+ data: store.getProject(topic.projectId)
155
+ ? resolvedDiffStore.getProjectSnapshot(topic.projectId)
156
+ : null,
157
+ },
158
+ }
159
+ }
160
+
140
161
  return {
141
162
  v: PROTOCOL_VERSION,
142
163
  type: "snapshot",
@@ -148,8 +169,7 @@ export function createWsRouter({
148
169
  agent.getActiveStatuses(),
149
170
  agent.getDrainingChatIds(),
150
171
  topic.chatId,
151
- (chatId) => store.getRecentChatHistory(chatId, topic.recentLimit ?? DEFAULT_CHAT_RECENT_LIMIT),
152
- (chatId) => resolvedDiffStore.getSnapshot(chatId)
172
+ (chatId) => store.getRecentChatHistory(chatId, topic.recentLimit ?? DEFAULT_CHAT_RECENT_LIMIT)
153
173
  ),
154
174
  },
155
175
  }
@@ -250,6 +270,14 @@ export function createWsRouter({
250
270
 
251
271
  agent.setBackgroundErrorReporter?.(broadcastError)
252
272
 
273
+ function resolveChatProject(chatId: string) {
274
+ const chat = store.getChat(chatId)
275
+ if (!chat) throw new Error("Chat not found")
276
+ const project = store.getProject(chat.projectId)
277
+ if (!project) throw new Error("Project not found")
278
+ return { chat, project }
279
+ }
280
+
253
281
  async function handleCommand(ws: ServerWebSocket<ClientState>, message: Extract<ClientEnvelope, { type: "command" }>) {
254
282
  const { command, id } = message
255
283
  try {
@@ -322,6 +350,18 @@ export function createWsRouter({
322
350
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
323
351
  break
324
352
  }
353
+ case "project.readDiffPatch": {
354
+ const project = store.getProject(command.projectId)
355
+ if (!project) {
356
+ throw new Error("Project not found")
357
+ }
358
+ const result = await resolvedDiffStore.readPatch({
359
+ projectPath: project.localPath,
360
+ path: command.path,
361
+ })
362
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
363
+ return
364
+ }
325
365
  case "system.openExternal": {
326
366
  await openExternal(command)
327
367
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
@@ -355,30 +395,87 @@ export function createWsRouter({
355
395
  break
356
396
  }
357
397
  case "chat.refreshDiffs": {
358
- const chat = store.getChat(command.chatId)
359
- if (!chat) {
360
- throw new Error("Chat not found")
361
- }
362
- const project = store.getProject(chat.projectId)
363
- if (!project) {
364
- throw new Error("Project not found")
365
- }
366
- const changed = await resolvedDiffStore.refreshSnapshot(command.chatId, project.localPath)
398
+ const { project } = resolveChatProject(command.chatId)
399
+ const changed = await resolvedDiffStore.refreshSnapshot(project.id, project.localPath)
367
400
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
368
401
  if (changed) {
369
402
  broadcastSnapshots()
370
403
  }
371
404
  return
372
405
  }
373
- case "chat.generateCommitMessage": {
374
- const chat = store.getChat(command.chatId)
375
- if (!chat) {
376
- throw new Error("Chat not found")
406
+ case "chat.listBranches": {
407
+ const { project } = resolveChatProject(command.chatId)
408
+ const result = await resolvedDiffStore.listBranches({
409
+ projectPath: project.localPath,
410
+ })
411
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
412
+ return
413
+ }
414
+ case "chat.previewMergeBranch": {
415
+ const { project } = resolveChatProject(command.chatId)
416
+ const result = await resolvedDiffStore.previewMergeBranch({
417
+ projectPath: project.localPath,
418
+ branch: command.branch,
419
+ })
420
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
421
+ return
422
+ }
423
+ case "chat.mergeBranch": {
424
+ const { project } = resolveChatProject(command.chatId)
425
+ const result = await resolvedDiffStore.mergeBranch({
426
+ projectId: project.id,
427
+ projectPath: project.localPath,
428
+ branch: command.branch,
429
+ })
430
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
431
+ if (result.snapshotChanged) {
432
+ broadcastSnapshots()
377
433
  }
378
- const project = store.getProject(chat.projectId)
379
- if (!project) {
380
- throw new Error("Project not found")
434
+ return
435
+ }
436
+ case "chat.checkoutBranch": {
437
+ const { project } = resolveChatProject(command.chatId)
438
+ const result = await resolvedDiffStore.checkoutBranch({
439
+ projectId: project.id,
440
+ projectPath: project.localPath,
441
+ branch: command.branch,
442
+ bringChanges: command.bringChanges,
443
+ })
444
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
445
+ if (result.snapshotChanged) {
446
+ broadcastSnapshots()
447
+ }
448
+ return
449
+ }
450
+ case "chat.syncBranch": {
451
+ const { project } = resolveChatProject(command.chatId)
452
+ const result = await resolvedDiffStore.syncBranch({
453
+ projectId: project.id,
454
+ projectPath: project.localPath,
455
+ action: command.action,
456
+ })
457
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
458
+ if (result.snapshotChanged) {
459
+ broadcastSnapshots()
381
460
  }
461
+ return
462
+ }
463
+ case "chat.createBranch": {
464
+ const { project } = resolveChatProject(command.chatId)
465
+ const result = await resolvedDiffStore.createBranch({
466
+ projectId: project.id,
467
+ projectPath: project.localPath,
468
+ name: command.name,
469
+ baseBranchName: command.baseBranchName,
470
+ })
471
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
472
+ if (result.snapshotChanged) {
473
+ broadcastSnapshots()
474
+ }
475
+ return
476
+ }
477
+ case "chat.generateCommitMessage": {
478
+ const { project } = resolveChatProject(command.chatId)
382
479
  const result = await resolvedDiffStore.generateCommitMessage({
383
480
  projectPath: project.localPath,
384
481
  paths: command.paths,
@@ -387,16 +484,9 @@ export function createWsRouter({
387
484
  return
388
485
  }
389
486
  case "chat.commitDiffs": {
390
- const chat = store.getChat(command.chatId)
391
- if (!chat) {
392
- throw new Error("Chat not found")
393
- }
394
- const project = store.getProject(chat.projectId)
395
- if (!project) {
396
- throw new Error("Project not found")
397
- }
487
+ const { project } = resolveChatProject(command.chatId)
398
488
  const result = await resolvedDiffStore.commitFiles({
399
- chatId: command.chatId,
489
+ projectId: project.id,
400
490
  projectPath: project.localPath,
401
491
  paths: command.paths,
402
492
  summary: command.summary,
@@ -410,16 +500,9 @@ export function createWsRouter({
410
500
  return
411
501
  }
412
502
  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
- }
503
+ const { project } = resolveChatProject(command.chatId)
421
504
  const result = await resolvedDiffStore.discardFile({
422
- chatId: command.chatId,
505
+ projectId: project.id,
423
506
  projectPath: project.localPath,
424
507
  path: command.path,
425
508
  })
@@ -430,16 +513,9 @@ export function createWsRouter({
430
513
  return
431
514
  }
432
515
  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
- }
516
+ const { project } = resolveChatProject(command.chatId)
441
517
  const result = await resolvedDiffStore.ignoreFile({
442
- chatId: command.chatId,
518
+ projectId: project.id,
443
519
  projectPath: project.localPath,
444
520
  path: command.path,
445
521
  })
@@ -461,9 +537,7 @@ export function createWsRouter({
461
537
  }
462
538
  case "chat.loadHistory": {
463
539
  const chat = store.getChat(command.chatId)
464
- if (!chat) {
465
- throw new Error("Chat not found")
466
- }
540
+ if (!chat) throw new Error("Chat not found")
467
541
  const page = store.getMessagesPageBefore(command.chatId, command.beforeCursor, command.limit)
468
542
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: page })
469
543
  return
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AgentProvider,
3
3
  ChatAttachment,
4
+ ChatDiffSnapshot,
4
5
  ChatHistoryPage,
5
6
  ChatSnapshot,
6
7
  DiffCommitMode,
@@ -24,6 +25,7 @@ export type SubscriptionTopic =
24
25
  | { type: "update" }
25
26
  | { type: "keybindings" }
26
27
  | { type: "chat"; chatId: string; recentLimit?: number }
28
+ | { type: "project-git"; projectId: string }
27
29
  | { type: "terminal"; terminalId: string }
28
30
 
29
31
  export interface TerminalSnapshot {
@@ -48,6 +50,7 @@ export type ClientCommand =
48
50
  | { type: "project.open"; localPath: string }
49
51
  | { type: "project.create"; localPath: string; title: string }
50
52
  | { type: "project.remove"; projectId: string }
53
+ | { type: "project.readDiffPatch"; projectId: string; path: string }
51
54
  | { type: "system.ping" }
52
55
  | { type: "update.check"; force?: boolean }
53
56
  | { type: "update.install" }
@@ -78,6 +81,58 @@ export type ClientCommand =
78
81
  planMode?: boolean
79
82
  }
80
83
  | { type: "chat.refreshDiffs"; chatId: string }
84
+ | { type: "chat.listBranches"; chatId: string }
85
+ | {
86
+ type: "chat.previewMergeBranch"
87
+ chatId: string
88
+ branch:
89
+ | { kind: "local"; name: string }
90
+ | { kind: "remote"; name: string; remoteRef: string }
91
+ | {
92
+ kind: "pull_request"
93
+ name: string
94
+ prNumber: number
95
+ headRefName: string
96
+ headRepoCloneUrl?: string
97
+ isCrossRepository?: boolean
98
+ remoteRef?: string
99
+ }
100
+ }
101
+ | {
102
+ type: "chat.mergeBranch"
103
+ chatId: string
104
+ branch:
105
+ | { kind: "local"; name: string }
106
+ | { kind: "remote"; name: string; remoteRef: string }
107
+ | {
108
+ kind: "pull_request"
109
+ name: string
110
+ prNumber: number
111
+ headRefName: string
112
+ headRepoCloneUrl?: string
113
+ isCrossRepository?: boolean
114
+ remoteRef?: string
115
+ }
116
+ }
117
+ | { type: "chat.syncBranch"; chatId: string; action: "fetch" | "pull" | "push" | "publish" }
118
+ | {
119
+ type: "chat.checkoutBranch"
120
+ chatId: string
121
+ branch:
122
+ | { kind: "local"; name: string }
123
+ | { kind: "remote"; name: string; remoteRef: string }
124
+ | {
125
+ kind: "pull_request"
126
+ name: string
127
+ prNumber: number
128
+ headRefName: string
129
+ headRepoCloneUrl?: string
130
+ isCrossRepository?: boolean
131
+ remoteRef?: string
132
+ }
133
+ bringChanges?: boolean
134
+ }
135
+ | { type: "chat.createBranch"; chatId: string; name: string; baseBranchName?: string }
81
136
  | { type: "chat.generateCommitMessage"; chatId: string; paths: string[] }
82
137
  | { type: "chat.commitDiffs"; chatId: string; paths: string[]; summary: string; description?: string; mode: DiffCommitMode }
83
138
  | { type: "chat.discardDiffFile"; chatId: string; path: string }
@@ -102,6 +157,7 @@ export type ServerSnapshot =
102
157
  | { type: "update"; data: UpdateSnapshot }
103
158
  | { type: "keybindings"; data: KeybindingsSnapshot }
104
159
  | { type: "chat"; data: ChatSnapshot | null }
160
+ | { type: "project-git"; data: ChatDiffSnapshot | null }
105
161
  | { type: "terminal"; data: TerminalSnapshot | null }
106
162
 
107
163
  export type ServerEnvelope =
@@ -273,6 +273,9 @@ export type KeybindingAction =
273
273
  | "openInFinder"
274
274
  | "openInEditor"
275
275
  | "addSplitTerminal"
276
+ | "jumpToSidebarChat"
277
+ | "createChatInCurrentProject"
278
+ | "openAddProject"
276
279
 
277
280
  export const DEFAULT_KEYBINDINGS: Record<KeybindingAction, string[]> = {
278
281
  toggleEmbeddedTerminal: ["cmd+j", "ctrl+`"],
@@ -280,6 +283,9 @@ export const DEFAULT_KEYBINDINGS: Record<KeybindingAction, string[]> = {
280
283
  openInFinder: ["cmd+alt+f", "ctrl+alt+f"],
281
284
  openInEditor: ["cmd+shift+o", "ctrl+shift+o"],
282
285
  addSplitTerminal: ["cmd+/", "ctrl+/"],
286
+ jumpToSidebarChat: ["cmd+alt"],
287
+ createChatInCurrentProject: ["cmd+alt+n"],
288
+ openAddProject: ["cmd+alt+o"],
283
289
  }
284
290
 
285
291
  export interface KeybindingsSnapshot {
@@ -472,39 +478,140 @@ export interface ChatDiffFile {
472
478
  path: string
473
479
  changeType: "added" | "deleted" | "modified" | "renamed"
474
480
  isUntracked: boolean
475
- patch: string
481
+ additions: number
482
+ deletions: number
483
+ patchDigest: string
476
484
  mimeType?: string
477
485
  size?: number
478
486
  }
479
487
 
480
- export interface ChatDiffSnapshot {
481
- status: "unknown" | "ready" | "no_repo"
488
+ export interface ChatBranchHistoryEntry {
489
+ sha: string
490
+ summary: string
491
+ description: string
492
+ authorName?: string
493
+ authoredAt: string
494
+ tags: string[]
495
+ githubUrl?: string
496
+ }
497
+
498
+ export interface ChatBranchHistorySnapshot {
499
+ entries: ChatBranchHistoryEntry[]
500
+ }
501
+
502
+ export type ChatBranchListEntryKind = "local" | "remote" | "pull_request"
503
+
504
+ export interface ChatBranchListEntry {
505
+ id: string
506
+ kind: ChatBranchListEntryKind
507
+ name: string
508
+ displayName: string
509
+ updatedAt?: string
510
+ description?: string
511
+ remoteRef?: string
512
+ prNumber?: number
513
+ prTitle?: string
514
+ headRefName?: string
515
+ headLabel?: string
516
+ headRepoCloneUrl?: string
517
+ isCrossRepository?: boolean
518
+ }
519
+
520
+ export interface ChatBranchListResult {
521
+ currentBranchName?: string
522
+ defaultBranchName?: string
523
+ recent: ChatBranchListEntry[]
524
+ local: ChatBranchListEntry[]
525
+ remote: ChatBranchListEntry[]
526
+ pullRequests: ChatBranchListEntry[]
527
+ pullRequestsStatus: "available" | "unavailable" | "error"
528
+ pullRequestsError?: string
529
+ }
530
+
531
+ export interface BranchMetadata {
482
532
  branchName?: string
533
+ defaultBranchName?: string
534
+ originRepoSlug?: string
483
535
  hasUpstream?: boolean
484
- files: ChatDiffFile[]
485
536
  }
486
537
 
487
- export type DiffCommitMode = "commit_and_push" | "commit_only"
538
+ export interface UpstreamStatus {
539
+ aheadCount?: number
540
+ behindCount?: number
541
+ lastFetchedAt?: string
542
+ }
543
+
544
+ export interface ChatDiffSnapshot extends BranchMetadata, UpstreamStatus {
545
+ status: "unknown" | "ready" | "no_repo"
546
+ files: ChatDiffFile[]
547
+ branchHistory?: ChatBranchHistorySnapshot
548
+ }
488
549
 
489
- export interface DiffCommitSuccess {
550
+ export interface BranchActionSuccess {
490
551
  ok: true
491
- mode: DiffCommitMode
492
552
  branchName?: string
493
- pushed: boolean
494
553
  snapshotChanged: boolean
495
554
  }
496
555
 
497
- export interface DiffCommitFailure {
556
+ export interface BranchActionFailure {
498
557
  ok: false
499
- mode: DiffCommitMode
500
- phase: "commit" | "push"
501
558
  title: string
502
559
  message: string
503
560
  detail?: string
504
- localCommitCreated?: boolean
561
+ cancelled?: boolean
505
562
  snapshotChanged?: boolean
506
563
  }
507
564
 
565
+ export type ChatSyncSuccess = BranchActionSuccess & {
566
+ action: "fetch" | "pull" | "push" | "publish"
567
+ aheadCount?: number
568
+ behindCount?: number
569
+ }
570
+
571
+ export type ChatSyncFailure = BranchActionFailure & {
572
+ action: "fetch" | "pull" | "push" | "publish"
573
+ }
574
+
575
+ export type ChatSyncResult = ChatSyncSuccess | ChatSyncFailure
576
+
577
+ export type DiffCommitMode = "commit_and_push" | "commit_only"
578
+
579
+ export type ChatCheckoutBranchSuccess = BranchActionSuccess
580
+ export type ChatCheckoutBranchFailure = BranchActionFailure
581
+ export type ChatCheckoutBranchResult = ChatCheckoutBranchSuccess | ChatCheckoutBranchFailure
582
+
583
+ export type ChatCreateBranchSuccess = BranchActionSuccess & { branchName: string }
584
+ export type ChatCreateBranchFailure = BranchActionFailure
585
+ export type ChatCreateBranchResult = ChatCreateBranchSuccess | ChatCreateBranchFailure
586
+
587
+ export type ChatMergePreviewStatus = "up_to_date" | "mergeable" | "conflicts" | "error"
588
+
589
+ export interface ChatMergePreviewResult {
590
+ currentBranchName?: string
591
+ targetBranchName: string
592
+ targetDisplayName: string
593
+ status: ChatMergePreviewStatus
594
+ commitCount: number
595
+ hasConflicts: boolean
596
+ message: string
597
+ detail?: string
598
+ }
599
+
600
+ export type ChatMergeBranchSuccess = BranchActionSuccess
601
+ export type ChatMergeBranchFailure = BranchActionFailure
602
+ export type ChatMergeBranchResult = ChatMergeBranchSuccess | ChatMergeBranchFailure
603
+
604
+ export type DiffCommitSuccess = BranchActionSuccess & {
605
+ mode: DiffCommitMode
606
+ pushed: boolean
607
+ }
608
+
609
+ export type DiffCommitFailure = BranchActionFailure & {
610
+ mode: DiffCommitMode
611
+ phase: "commit" | "push"
612
+ localCommitCreated?: boolean
613
+ }
614
+
508
615
  export type DiffCommitResult = DiffCommitSuccess | DiffCommitFailure
509
616
 
510
617
  export interface ContextWindowUpdatedEntry extends TranscriptEntryBase {
@@ -682,7 +789,6 @@ export interface ChatSnapshot {
682
789
  runtime: ChatRuntime
683
790
  messages: TranscriptEntry[]
684
791
  history: ChatHistorySnapshot
685
- diffs: ChatDiffSnapshot
686
792
  availableProviders: ProviderCatalogEntry[]
687
793
  }
688
794