kanna-code 0.24.0 → 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" | "listBranches" | "syncBranch" | "checkoutBranch" | "createBranch" | "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,9 +57,11 @@ 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, defaultBranchName: undefined, originRepoSlug: undefined, hasUpstream: undefined, aheadCount: undefined, behindCount: undefined, lastFetchedAt: undefined, files: [] as const, branchHistory: { entries: [] 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
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 }),
63
65
  syncBranch: async () => ({ ok: true, action: "fetch" as const, branchName: undefined, snapshotChanged: false }),
64
66
  checkoutBranch: async () => ({ ok: true, branchName: undefined, snapshotChanged: false }),
65
67
  createBranch: async () => ({ ok: true, branchName: "main", snapshotChanged: false }),
@@ -67,6 +69,7 @@ export function createWsRouter({
67
69
  commitFiles: async () => ({ ok: true, mode: "commit_only" as const, branchName: undefined, pushed: false, snapshotChanged: false }),
68
70
  discardFile: async () => ({ snapshotChanged: false }),
69
71
  ignoreFile: async () => ({ snapshotChanged: false }),
72
+ readPatch: async () => ({ patch: "" }),
70
73
  }
71
74
 
72
75
  function createEnvelope(id: string, topic: SubscriptionTopic): ServerEnvelope {
@@ -141,6 +144,20 @@ export function createWsRouter({
141
144
  }
142
145
  }
143
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
+
144
161
  return {
145
162
  v: PROTOCOL_VERSION,
146
163
  type: "snapshot",
@@ -152,8 +169,7 @@ export function createWsRouter({
152
169
  agent.getActiveStatuses(),
153
170
  agent.getDrainingChatIds(),
154
171
  topic.chatId,
155
- (chatId) => store.getRecentChatHistory(chatId, topic.recentLimit ?? DEFAULT_CHAT_RECENT_LIMIT),
156
- (chatId) => resolvedDiffStore.getSnapshot(chatId)
172
+ (chatId) => store.getRecentChatHistory(chatId, topic.recentLimit ?? DEFAULT_CHAT_RECENT_LIMIT)
157
173
  ),
158
174
  },
159
175
  }
@@ -254,6 +270,14 @@ export function createWsRouter({
254
270
 
255
271
  agent.setBackgroundErrorReporter?.(broadcastError)
256
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
+
257
281
  async function handleCommand(ws: ServerWebSocket<ClientState>, message: Extract<ClientEnvelope, { type: "command" }>) {
258
282
  const { command, id } = message
259
283
  try {
@@ -326,6 +350,18 @@ export function createWsRouter({
326
350
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
327
351
  break
328
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
+ }
329
365
  case "system.openExternal": {
330
366
  await openExternal(command)
331
367
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
@@ -359,15 +395,8 @@ export function createWsRouter({
359
395
  break
360
396
  }
361
397
  case "chat.refreshDiffs": {
362
- const chat = store.getChat(command.chatId)
363
- if (!chat) {
364
- throw new Error("Chat not found")
365
- }
366
- const project = store.getProject(chat.projectId)
367
- if (!project) {
368
- throw new Error("Project not found")
369
- }
370
- 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)
371
400
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
372
401
  if (changed) {
373
402
  broadcastSnapshots()
@@ -375,31 +404,39 @@ export function createWsRouter({
375
404
  return
376
405
  }
377
406
  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
- }
407
+ const { project } = resolveChatProject(command.chatId)
386
408
  const result = await resolvedDiffStore.listBranches({
387
409
  projectPath: project.localPath,
388
410
  })
389
411
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
390
412
  return
391
413
  }
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")
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()
400
433
  }
434
+ return
435
+ }
436
+ case "chat.checkoutBranch": {
437
+ const { project } = resolveChatProject(command.chatId)
401
438
  const result = await resolvedDiffStore.checkoutBranch({
402
- chatId: command.chatId,
439
+ projectId: project.id,
403
440
  projectPath: project.localPath,
404
441
  branch: command.branch,
405
442
  bringChanges: command.bringChanges,
@@ -411,16 +448,9 @@ export function createWsRouter({
411
448
  return
412
449
  }
413
450
  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
- }
451
+ const { project } = resolveChatProject(command.chatId)
422
452
  const result = await resolvedDiffStore.syncBranch({
423
- chatId: command.chatId,
453
+ projectId: project.id,
424
454
  projectPath: project.localPath,
425
455
  action: command.action,
426
456
  })
@@ -431,16 +461,9 @@ export function createWsRouter({
431
461
  return
432
462
  }
433
463
  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
- }
464
+ const { project } = resolveChatProject(command.chatId)
442
465
  const result = await resolvedDiffStore.createBranch({
443
- chatId: command.chatId,
466
+ projectId: project.id,
444
467
  projectPath: project.localPath,
445
468
  name: command.name,
446
469
  baseBranchName: command.baseBranchName,
@@ -452,14 +475,7 @@ export function createWsRouter({
452
475
  return
453
476
  }
454
477
  case "chat.generateCommitMessage": {
455
- const chat = store.getChat(command.chatId)
456
- if (!chat) {
457
- throw new Error("Chat not found")
458
- }
459
- const project = store.getProject(chat.projectId)
460
- if (!project) {
461
- throw new Error("Project not found")
462
- }
478
+ const { project } = resolveChatProject(command.chatId)
463
479
  const result = await resolvedDiffStore.generateCommitMessage({
464
480
  projectPath: project.localPath,
465
481
  paths: command.paths,
@@ -468,16 +484,9 @@ export function createWsRouter({
468
484
  return
469
485
  }
470
486
  case "chat.commitDiffs": {
471
- const chat = store.getChat(command.chatId)
472
- if (!chat) {
473
- throw new Error("Chat not found")
474
- }
475
- const project = store.getProject(chat.projectId)
476
- if (!project) {
477
- throw new Error("Project not found")
478
- }
487
+ const { project } = resolveChatProject(command.chatId)
479
488
  const result = await resolvedDiffStore.commitFiles({
480
- chatId: command.chatId,
489
+ projectId: project.id,
481
490
  projectPath: project.localPath,
482
491
  paths: command.paths,
483
492
  summary: command.summary,
@@ -491,16 +500,9 @@ export function createWsRouter({
491
500
  return
492
501
  }
493
502
  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
- }
503
+ const { project } = resolveChatProject(command.chatId)
502
504
  const result = await resolvedDiffStore.discardFile({
503
- chatId: command.chatId,
505
+ projectId: project.id,
504
506
  projectPath: project.localPath,
505
507
  path: command.path,
506
508
  })
@@ -511,16 +513,9 @@ export function createWsRouter({
511
513
  return
512
514
  }
513
515
  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
- }
516
+ const { project } = resolveChatProject(command.chatId)
522
517
  const result = await resolvedDiffStore.ignoreFile({
523
- chatId: command.chatId,
518
+ projectId: project.id,
524
519
  projectPath: project.localPath,
525
520
  path: command.path,
526
521
  })
@@ -542,9 +537,7 @@ export function createWsRouter({
542
537
  }
543
538
  case "chat.loadHistory": {
544
539
  const chat = store.getChat(command.chatId)
545
- if (!chat) {
546
- throw new Error("Chat not found")
547
- }
540
+ if (!chat) throw new Error("Chat not found")
548
541
  const page = store.getMessagesPageBefore(command.chatId, command.beforeCursor, command.limit)
549
542
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: page })
550
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" }
@@ -79,7 +82,39 @@ export type ClientCommand =
79
82
  }
80
83
  | { type: "chat.refreshDiffs"; chatId: string }
81
84
  | { type: "chat.listBranches"; chatId: string }
82
- | { type: "chat.syncBranch"; chatId: string; action: "fetch" | "pull" | "publish" }
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" }
83
118
  | {
84
119
  type: "chat.checkoutBranch"
85
120
  chatId: string
@@ -122,6 +157,7 @@ export type ServerSnapshot =
122
157
  | { type: "update"; data: UpdateSnapshot }
123
158
  | { type: "keybindings"; data: KeybindingsSnapshot }
124
159
  | { type: "chat"; data: ChatSnapshot | null }
160
+ | { type: "project-git"; data: ChatDiffSnapshot | null }
125
161
  | { type: "terminal"; data: TerminalSnapshot | null }
126
162
 
127
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,7 +478,9 @@ 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
  }
@@ -520,91 +528,88 @@ export interface ChatBranchListResult {
520
528
  pullRequestsError?: string
521
529
  }
522
530
 
523
- export interface ChatDiffSnapshot {
524
- status: "unknown" | "ready" | "no_repo"
531
+ export interface BranchMetadata {
525
532
  branchName?: string
526
533
  defaultBranchName?: string
527
534
  originRepoSlug?: string
528
535
  hasUpstream?: boolean
536
+ }
537
+
538
+ export interface UpstreamStatus {
529
539
  aheadCount?: number
530
540
  behindCount?: number
531
541
  lastFetchedAt?: string
542
+ }
543
+
544
+ export interface ChatDiffSnapshot extends BranchMetadata, UpstreamStatus {
545
+ status: "unknown" | "ready" | "no_repo"
532
546
  files: ChatDiffFile[]
533
547
  branchHistory?: ChatBranchHistorySnapshot
534
548
  }
535
549
 
536
- export interface ChatSyncSuccess {
550
+ export interface BranchActionSuccess {
537
551
  ok: true
538
- action: "fetch" | "pull" | "publish"
539
552
  branchName?: string
540
- aheadCount?: number
541
- behindCount?: number
542
553
  snapshotChanged: boolean
543
554
  }
544
555
 
545
- export interface ChatSyncFailure {
556
+ export interface BranchActionFailure {
546
557
  ok: false
547
- action: "fetch" | "pull" | "publish"
548
558
  title: string
549
559
  message: string
550
560
  detail?: string
561
+ cancelled?: boolean
551
562
  snapshotChanged?: boolean
552
563
  }
553
564
 
554
- export type ChatSyncResult = ChatSyncSuccess | ChatSyncFailure
555
-
556
- export type DiffCommitMode = "commit_and_push" | "commit_only"
557
-
558
- export interface ChatCheckoutBranchSuccess {
559
- ok: true
560
- branchName?: string
561
- snapshotChanged: boolean
565
+ export type ChatSyncSuccess = BranchActionSuccess & {
566
+ action: "fetch" | "pull" | "push" | "publish"
567
+ aheadCount?: number
568
+ behindCount?: number
562
569
  }
563
570
 
564
- export interface ChatCheckoutBranchFailure {
565
- ok: false
566
- cancelled?: boolean
567
- title: string
568
- message: string
569
- detail?: string
570
- snapshotChanged?: boolean
571
+ export type ChatSyncFailure = BranchActionFailure & {
572
+ action: "fetch" | "pull" | "push" | "publish"
571
573
  }
572
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
573
581
  export type ChatCheckoutBranchResult = ChatCheckoutBranchSuccess | ChatCheckoutBranchFailure
574
582
 
575
- export interface ChatCreateBranchSuccess {
576
- ok: true
577
- branchName: string
578
- snapshotChanged: boolean
579
- }
583
+ export type ChatCreateBranchSuccess = BranchActionSuccess & { branchName: string }
584
+ export type ChatCreateBranchFailure = BranchActionFailure
585
+ export type ChatCreateBranchResult = ChatCreateBranchSuccess | ChatCreateBranchFailure
580
586
 
581
- export interface ChatCreateBranchFailure {
582
- ok: false
583
- title: string
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
584
596
  message: string
585
597
  detail?: string
586
- snapshotChanged?: boolean
587
598
  }
588
599
 
589
- export type ChatCreateBranchResult = ChatCreateBranchSuccess | ChatCreateBranchFailure
600
+ export type ChatMergeBranchSuccess = BranchActionSuccess
601
+ export type ChatMergeBranchFailure = BranchActionFailure
602
+ export type ChatMergeBranchResult = ChatMergeBranchSuccess | ChatMergeBranchFailure
590
603
 
591
- export interface DiffCommitSuccess {
592
- ok: true
604
+ export type DiffCommitSuccess = BranchActionSuccess & {
593
605
  mode: DiffCommitMode
594
- branchName?: string
595
606
  pushed: boolean
596
- snapshotChanged: boolean
597
607
  }
598
608
 
599
- export interface DiffCommitFailure {
600
- ok: false
609
+ export type DiffCommitFailure = BranchActionFailure & {
601
610
  mode: DiffCommitMode
602
611
  phase: "commit" | "push"
603
- title: string
604
- message: string
605
- detail?: string
606
612
  localCommitCreated?: boolean
607
- snapshotChanged?: boolean
608
613
  }
609
614
 
610
615
  export type DiffCommitResult = DiffCommitSuccess | DiffCommitFailure
@@ -784,7 +789,6 @@ export interface ChatSnapshot {
784
789
  runtime: ChatRuntime
785
790
  messages: TranscriptEntry[]
786
791
  history: ChatHistorySnapshot
787
- diffs: ChatDiffSnapshot
788
792
  availableProviders: ProviderCatalogEntry[]
789
793
  }
790
794