@cuongtran001/kanna 1.3.0 → 1.4.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.
Files changed (200) hide show
  1. package/dist/client/assets/{arc-CAysfmX5.js → arc-BHj370W7.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-1G88q1PY.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-D0L3Co5v.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-CQtERl3H.js} +1 -1
  5. package/dist/client/assets/channel-BP9IMEvd.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-DxOwt09f.js} +1 -1
  7. package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-Wry66y9R.js} +1 -1
  8. package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-D6p-Hi4x.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-CRQZt23N.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-BzgTKOM9.js} +1 -1
  11. package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CKfD-91C.js} +1 -1
  12. package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-a71B27wc.js} +1 -1
  13. package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-vv9gJwx0.js} +1 -1
  14. package/dist/client/assets/classDiagram-4FO5ZUOK--W6Wt7FF.js +1 -0
  15. package/dist/client/assets/classDiagram-v2-Q7XG4LA2--W6Wt7FF.js +1 -0
  16. package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-Cu-dCCaI.js} +1 -1
  17. package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-D8coOWLK.js} +1 -1
  18. package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-NIwZ65jE.js} +1 -1
  19. package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-d46aE_le.js} +1 -1
  20. package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-xdm2UxWc.js} +1 -1
  21. package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-hDbLwC5K.js} +1 -1
  22. package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-MHUGG7ki.js} +1 -1
  23. package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-Ro6n9X70.js} +1 -1
  24. package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-DOZqvRMu.js} +1 -1
  25. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-Bn2NXYIy.js} +1 -1
  26. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DK44bfbC.js} +1 -1
  27. package/dist/client/assets/{index-C0U2kwuB.js → index-CWJpHyQy.js} +115 -115
  28. package/dist/client/assets/{index-CLsOWXff.css → index-Cyzd4zZx.css} +1 -1
  29. package/dist/client/assets/{index-CbSY9HfS.js → index-DU56d4oC.js} +1 -1
  30. package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-C2tW_Dwj.js} +1 -1
  31. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-C__4Z1wz.js} +1 -1
  32. package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-Bbn5EawR.js} +1 -1
  33. package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-RgS5IRWl.js} +1 -1
  34. package/dist/client/assets/{linear-DLR6miy3.js → linear-DfI0qBO_.js} +1 -1
  35. package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-BiN10VHc.js} +4 -4
  36. package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-BQhibhh3.js} +1 -1
  37. package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-BEfLRn7a.js} +1 -1
  38. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-ba641Y2x.js} +1 -1
  39. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-BGtl-tVO.js} +1 -1
  40. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-DyziaSFr.js} +1 -1
  41. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BfYBAYga.js} +1 -1
  42. package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-BfUfmLYA.js} +1 -1
  43. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CdGcgYEq.js +1 -0
  44. package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-T6z7dEFl.js} +1 -1
  45. package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-BAwQbFFd.js} +1 -1
  46. package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-DF1cRKsP.js} +1 -1
  47. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BRtAZ8za.js} +1 -1
  48. package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-Brb-q76h.js} +1 -1
  49. package/dist/client/index.html +2 -2
  50. package/package.json +1 -1
  51. package/src/server/agent-coordinator-types.ts +158 -0
  52. package/src/server/agent-coordinator.ts +919 -0
  53. package/src/server/agent-deps-builders.ts +425 -0
  54. package/src/server/agent.test.ts +22 -6
  55. package/src/server/agent.ts +35 -4987
  56. package/src/server/chat-op-log.test.ts +63 -0
  57. package/src/server/chat-op-log.ts +71 -0
  58. package/src/server/chat-ops-diff.test.ts +120 -0
  59. package/src/server/chat-ops-diff.ts +97 -0
  60. package/src/server/chat-ops-parity.test.ts +140 -0
  61. package/src/server/claude-autocontinue-commands.test.ts +494 -0
  62. package/src/server/claude-autocontinue-commands.ts +256 -0
  63. package/src/server/claude-cancel-handler.test.ts +472 -0
  64. package/src/server/claude-cancel-handler.ts +241 -0
  65. package/src/server/claude-chat-management.test.ts +395 -0
  66. package/src/server/claude-chat-management.ts +251 -0
  67. package/src/server/claude-harness-stream.test.ts +273 -0
  68. package/src/server/claude-harness-stream.ts +158 -0
  69. package/src/server/claude-loop-orch-commands.test.ts +579 -0
  70. package/src/server/claude-loop-orch-commands.ts +466 -0
  71. package/src/server/claude-message-normalizer.test.ts +213 -0
  72. package/src/server/claude-message-normalizer.ts +436 -0
  73. package/src/server/claude-prompt-helpers.test.ts +248 -0
  74. package/src/server/claude-prompt-helpers.ts +139 -0
  75. package/src/server/claude-sdk-queue.test.ts +169 -0
  76. package/src/server/claude-sdk-queue.ts +82 -0
  77. package/src/server/claude-send-command.test.ts +414 -0
  78. package/src/server/claude-send-command.ts +420 -0
  79. package/src/server/claude-session-config-helpers.test.ts +279 -0
  80. package/src/server/claude-session-config-helpers.ts +151 -0
  81. package/src/server/claude-session-config.test.ts +250 -0
  82. package/src/server/claude-session-config.ts +159 -0
  83. package/src/server/claude-session-error-handler.test.ts +498 -0
  84. package/src/server/claude-session-error-handler.ts +361 -0
  85. package/src/server/claude-session-lifecycle.test.ts +669 -0
  86. package/src/server/claude-session-lifecycle.ts +283 -0
  87. package/src/server/claude-session-rebuild.test.ts +224 -0
  88. package/src/server/claude-session-rebuild.ts +101 -0
  89. package/src/server/claude-session-runner.test.ts +523 -0
  90. package/src/server/claude-session-runner.ts +521 -0
  91. package/src/server/claude-session-spawner.test.ts +401 -0
  92. package/src/server/claude-session-spawner.ts +401 -0
  93. package/src/server/claude-session-start.test.ts +205 -0
  94. package/src/server/claude-session-start.ts +277 -0
  95. package/src/server/claude-session-state-queries.test.ts +344 -0
  96. package/src/server/claude-session-state-queries.ts +160 -0
  97. package/src/server/claude-session-state.ts +104 -0
  98. package/src/server/claude-slash-commands.test.ts +277 -0
  99. package/src/server/claude-slash-commands.ts +234 -0
  100. package/src/server/claude-spawn-helpers.test.ts +221 -0
  101. package/src/server/claude-spawn-helpers.ts +179 -0
  102. package/src/server/claude-steer-log.test.ts +243 -0
  103. package/src/server/claude-steer-log.ts +58 -0
  104. package/src/server/claude-subagent-tool-response.test.ts +213 -0
  105. package/src/server/claude-subagent-tool-response.ts +165 -0
  106. package/src/server/claude-subagent-wiring.test.ts +263 -0
  107. package/src/server/claude-subagent-wiring.ts +324 -0
  108. package/src/server/claude-tool-respond.test.ts +321 -0
  109. package/src/server/claude-tool-respond.ts +142 -0
  110. package/src/server/claude-turn-runner.test.ts +310 -0
  111. package/src/server/claude-turn-runner.ts +228 -0
  112. package/src/server/claude-turn-starter.test.ts +266 -0
  113. package/src/server/claude-turn-starter.ts +566 -0
  114. package/src/server/claude-usage-math.test.ts +169 -0
  115. package/src/server/claude-usage-math.ts +132 -0
  116. package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
  117. package/src/server/diff-store-branch-ops.adapter.ts +602 -0
  118. package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
  119. package/src/server/diff-store-commit-ops.adapter.ts +258 -0
  120. package/src/server/diff-store-errors.ts +85 -0
  121. package/src/server/diff-store-file-ops.adapter.ts +212 -0
  122. package/src/server/diff-store-git-branch.adapter.ts +686 -0
  123. package/src/server/diff-store-io.adapter.ts +27 -0
  124. package/src/server/diff-store-parse.test.ts +212 -0
  125. package/src/server/diff-store-parse.ts +122 -0
  126. package/src/server/diff-store-state.ts +82 -0
  127. package/src/server/diff-store.ts +76 -1854
  128. package/src/server/event-store-apply.test.ts +280 -0
  129. package/src/server/event-store-apply.ts +132 -0
  130. package/src/server/event-store-chat-lifecycle.test.ts +408 -0
  131. package/src/server/event-store-chat-lifecycle.ts +441 -0
  132. package/src/server/event-store-entity-write.ts +356 -0
  133. package/src/server/event-store-helpers.test.ts +230 -0
  134. package/src/server/event-store-helpers.ts +235 -0
  135. package/src/server/event-store-init.ts +276 -0
  136. package/src/server/event-store-messages.adapter.test.ts +532 -0
  137. package/src/server/event-store-messages.adapter.ts +455 -0
  138. package/src/server/event-store-orch-subscription.ts +124 -0
  139. package/src/server/event-store-orch.test.ts +462 -0
  140. package/src/server/event-store-orch.ts +312 -0
  141. package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
  142. package/src/server/event-store-peripheral-events.adapter.ts +105 -0
  143. package/src/server/event-store-snapshot.test.ts +597 -0
  144. package/src/server/event-store-snapshot.ts +658 -0
  145. package/src/server/event-store-subagent.test.ts +408 -0
  146. package/src/server/event-store-subagent.ts +198 -0
  147. package/src/server/event-store-tool-requests.test.ts +210 -0
  148. package/src/server/event-store-tool-requests.ts +131 -0
  149. package/src/server/event-store-transcript-write.adapter.ts +291 -0
  150. package/src/server/event-store-write-ops.test.ts +554 -0
  151. package/src/server/event-store-write-ops.ts +529 -0
  152. package/src/server/event-store.test.ts +75 -0
  153. package/src/server/event-store.ts +257 -2285
  154. package/src/server/events.ts +1 -1
  155. package/src/server/harness-types.ts +15 -1
  156. package/src/server/oauth-errors.ts +19 -0
  157. package/src/server/storage/backend.ts +7 -0
  158. package/src/server/storage/fs-storage.adapter.ts +21 -1
  159. package/src/server/storage/in-memory-storage.ts +12 -0
  160. package/src/server/ws-router-agent-ctrl.test.ts +187 -0
  161. package/src/server/ws-router-agent-ctrl.ts +137 -0
  162. package/src/server/ws-router-broadcast.ts +560 -0
  163. package/src/server/ws-router-chat.test.ts +461 -0
  164. package/src/server/ws-router-chat.ts +279 -0
  165. package/src/server/ws-router-defaults.test.ts +283 -0
  166. package/src/server/ws-router-defaults.ts +434 -0
  167. package/src/server/ws-router-diff.test.ts +167 -0
  168. package/src/server/ws-router-diff.ts +250 -0
  169. package/src/server/ws-router-envelope.ts +360 -0
  170. package/src/server/ws-router-misc.test.ts +422 -0
  171. package/src/server/ws-router-misc.ts +265 -0
  172. package/src/server/ws-router-orch.test.ts +190 -0
  173. package/src/server/ws-router-orch.ts +93 -0
  174. package/src/server/ws-router-project.test.ts +403 -0
  175. package/src/server/ws-router-project.ts +256 -0
  176. package/src/server/ws-router-push.test.ts +193 -0
  177. package/src/server/ws-router-push.ts +120 -0
  178. package/src/server/ws-router-settings.test.ts +173 -0
  179. package/src/server/ws-router-settings.ts +416 -0
  180. package/src/server/ws-router-skills.test.ts +194 -0
  181. package/src/server/ws-router-skills.ts +204 -0
  182. package/src/server/ws-router-utils.ts +238 -0
  183. package/src/server/ws-router.test.ts +90 -0
  184. package/src/server/ws-router.ts +340 -2254
  185. package/src/shared/app-settings-types.ts +365 -0
  186. package/src/shared/chat-ops.test.ts +138 -0
  187. package/src/shared/chat-ops.ts +71 -0
  188. package/src/shared/core-types.ts +27 -0
  189. package/src/shared/git-diff-types.ts +165 -0
  190. package/src/shared/mcp-types.ts +105 -0
  191. package/src/shared/protocol.ts +3 -2
  192. package/src/shared/provider-model-types.ts +437 -0
  193. package/src/shared/subagent-types.ts +198 -0
  194. package/src/shared/tool-call-types.ts +314 -0
  195. package/src/shared/transcript-types.ts +270 -0
  196. package/src/shared/types.ts +31 -1711
  197. package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
  198. package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
  199. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
  200. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
@@ -0,0 +1,602 @@
1
+ import {
2
+ formatGitFailure,
3
+ runGit,
4
+ summarizeGitFailure,
5
+ } from "./diff-store-io.adapter"
6
+ import {
7
+ createBranchActionFailure,
8
+ createMergeActionFailure,
9
+ extractGitHubRepoSlug,
10
+ fetchGitHubPullRequests,
11
+ getBranchName,
12
+ getBranchUpdatedAtMap,
13
+ getGitHubRemoteSlugs,
14
+ getLocalBranchNames,
15
+ getMergeCommitCount,
16
+ getOriginRemoteUrl,
17
+ getRecentBranchNames,
18
+ getRemoteBranchNames,
19
+ getUpstreamStatusCounts,
20
+ hasUpstreamBranch,
21
+ predictMergeConflicts,
22
+ resolveDefaultBranchName,
23
+ resolveRepo,
24
+ resolveSelectedBranchRef,
25
+ } from "./diff-store-git-branch.adapter"
26
+ import type { SelectedBranch } from "./diff-store-git-branch.adapter"
27
+ import { createSyncPushFailure } from "./diff-store-errors"
28
+ import { listDirtyPaths } from "./diff-store-file-ops.adapter"
29
+ import type {
30
+ ChatBranchListEntry,
31
+ ChatBranchListResult,
32
+ ChatCheckoutBranchResult,
33
+ ChatCreateBranchResult,
34
+ ChatMergeBranchResult,
35
+ ChatMergePreviewResult,
36
+ ChatSyncResult,
37
+ } from "../shared/types"
38
+
39
+ export interface DiffStoreBranchOpsDeps {
40
+ readonly refreshSnapshot: (projectId: string, projectPath: string) => Promise<boolean>
41
+ }
42
+
43
+ export async function listBranches(
44
+ _deps: DiffStoreBranchOpsDeps,
45
+ args: { projectPath: string }
46
+ ): Promise<ChatBranchListResult> {
47
+ const repo = await resolveRepo(args.projectPath)
48
+ if (!repo) {
49
+ throw new Error("Project is not in a git repository")
50
+ }
51
+
52
+ const [currentBranchName, defaultBranchName, localBranchNames, remoteBranchNames, recentBranchNames, localUpdatedAtMap, remoteUpdatedAtMap] = await Promise.all([
53
+ getBranchName(repo.repoRoot),
54
+ resolveDefaultBranchName(repo.repoRoot),
55
+ getLocalBranchNames(repo.repoRoot),
56
+ getRemoteBranchNames(repo.repoRoot),
57
+ getRecentBranchNames(repo.repoRoot),
58
+ getBranchUpdatedAtMap(repo.repoRoot, "refs/heads"),
59
+ getBranchUpdatedAtMap(repo.repoRoot, "refs/remotes"),
60
+ ])
61
+
62
+ const local = localBranchNames.map((name) => ({
63
+ id: `local:${name}`,
64
+ kind: "local",
65
+ name,
66
+ displayName: name,
67
+ updatedAt: localUpdatedAtMap.get(name),
68
+ } satisfies ChatBranchListEntry))
69
+
70
+ const remote = remoteBranchNames.map((remoteRef) => ({
71
+ id: `remote:${remoteRef}`,
72
+ kind: "remote",
73
+ name: remoteRef.replace(/^[^/]+\//u, ""),
74
+ displayName: remoteRef,
75
+ updatedAt: remoteUpdatedAtMap.get(remoteRef),
76
+ remoteRef,
77
+ } satisfies ChatBranchListEntry))
78
+
79
+ const localBranchSet = new Set(localBranchNames)
80
+ const remoteByName = new Map(remote.map((entry) => [entry.name, entry]))
81
+ const remoteEntriesByName = new Map<string, ChatBranchListEntry[]>()
82
+ for (const entry of remote) {
83
+ const entries = remoteEntriesByName.get(entry.name) ?? []
84
+ entries.push(entry)
85
+ remoteEntriesByName.set(entry.name, entries)
86
+ }
87
+ const recent: ChatBranchListEntry[] = recentBranchNames.flatMap<ChatBranchListEntry>((branchName) => {
88
+ if (localBranchSet.has(branchName)) {
89
+ return {
90
+ id: `recent:local:${branchName}`,
91
+ kind: "local",
92
+ name: branchName,
93
+ displayName: branchName,
94
+ updatedAt: localUpdatedAtMap.get(branchName),
95
+ } satisfies ChatBranchListEntry
96
+ }
97
+ const remoteEntry = remoteByName.get(branchName)
98
+ return remoteEntry
99
+ ? {
100
+ ...remoteEntry,
101
+ id: `recent:${remoteEntry.id}`,
102
+ } satisfies ChatBranchListEntry
103
+ : []
104
+ })
105
+
106
+ const [remoteUrl, githubRemoteSlugs] = await Promise.all([
107
+ getOriginRemoteUrl(repo.repoRoot),
108
+ getGitHubRemoteSlugs(repo.repoRoot),
109
+ ])
110
+ const repoSlug = extractGitHubRepoSlug(remoteUrl)
111
+ let pullRequests: ChatBranchListEntry[] = []
112
+ const pullRequestRemoteRefs = new Set<string>()
113
+ const pullRequestHeadNames = new Set<string>()
114
+ let pullRequestsStatus: ChatBranchListResult["pullRequestsStatus"] = "unavailable"
115
+ let pullRequestsError: string | undefined
116
+
117
+ if (repoSlug) {
118
+ try {
119
+ const prs = await fetchGitHubPullRequests(repoSlug)
120
+ pullRequests = prs.flatMap<ChatBranchListEntry>((pr) => {
121
+ const headRefName = pr.head?.ref?.trim()
122
+ if (!headRefName) return []
123
+ pullRequestHeadNames.add(headRefName)
124
+ const cloneUrl = pr.head?.repo?.clone_url?.trim() || undefined
125
+ const fullName = pr.head?.repo?.full_name?.trim() || undefined
126
+ const headRepoSlug = fullName?.toLowerCase()
127
+ const matchingRemoteEntries = (remoteEntriesByName.get(headRefName) ?? []).filter((entry) => {
128
+ const remoteName = entry.remoteRef?.split("/")[0]
129
+ if (!remoteName) return false
130
+ const remoteSlug = githubRemoteSlugs.get(remoteName)
131
+ if (!remoteSlug) return false
132
+ if (headRepoSlug) {
133
+ return remoteSlug === headRepoSlug
134
+ }
135
+ return remoteName === "origin"
136
+ })
137
+ for (const entry of matchingRemoteEntries) {
138
+ if (entry.remoteRef) {
139
+ pullRequestRemoteRefs.add(entry.remoteRef)
140
+ }
141
+ }
142
+ const preferredRemoteEntry = matchingRemoteEntries[0] ?? remoteByName.get(headRefName)
143
+ const remoteRef = preferredRemoteEntry?.remoteRef ?? `origin/${headRefName}`
144
+ return {
145
+ id: `pr:${pr.number}`,
146
+ kind: "pull_request",
147
+ name: headRefName,
148
+ displayName: `PR #${pr.number}`,
149
+ updatedAt: (remoteRef ? remoteUpdatedAtMap.get(remoteRef) : undefined) ?? localUpdatedAtMap.get(headRefName),
150
+ description: pr.title,
151
+ remoteRef,
152
+ prNumber: pr.number,
153
+ prTitle: pr.title,
154
+ headRefName,
155
+ headLabel: pr.head?.label?.trim() || fullName,
156
+ headRepoCloneUrl: cloneUrl,
157
+ isCrossRepository: Boolean(fullName && fullName.toLowerCase() !== repoSlug.toLowerCase()),
158
+ } satisfies ChatBranchListEntry
159
+ })
160
+ pullRequestsStatus = "available"
161
+ } catch (error) {
162
+ pullRequestsStatus = "error"
163
+ pullRequestsError = error instanceof Error ? error.message : String(error)
164
+ }
165
+ }
166
+
167
+ const visibleRemote = remote.filter((entry) => {
168
+ if (pullRequestHeadNames.has(entry.name)) {
169
+ return false
170
+ }
171
+ return !entry.remoteRef || !pullRequestRemoteRefs.has(entry.remoteRef)
172
+ })
173
+ const visibleRemoteByName = new Map(visibleRemote.map((entry) => [entry.name, entry]))
174
+ const visibleRecent = recent.filter((entry) => entry.kind !== "remote" || !entry.remoteRef || visibleRemoteByName.has(entry.name))
175
+
176
+ return {
177
+ currentBranchName,
178
+ defaultBranchName,
179
+ recent: visibleRecent,
180
+ local,
181
+ remote: visibleRemote,
182
+ pullRequests,
183
+ pullRequestsStatus,
184
+ pullRequestsError,
185
+ }
186
+ }
187
+
188
+ export async function previewMergeBranch(
189
+ _deps: DiffStoreBranchOpsDeps,
190
+ args: { projectPath: string; branch: SelectedBranch }
191
+ ): Promise<ChatMergePreviewResult> {
192
+ const repo = await resolveRepo(args.projectPath)
193
+ if (!repo) {
194
+ throw new Error("Project is not in a git repository")
195
+ }
196
+
197
+ const currentBranchName = await getBranchName(repo.repoRoot)
198
+ const resolvedBranch = await resolveSelectedBranchRef(repo.repoRoot, args.branch)
199
+
200
+ if (currentBranchName && resolvedBranch.branchName === currentBranchName) {
201
+ return {
202
+ currentBranchName,
203
+ targetBranchName: resolvedBranch.branchName,
204
+ targetDisplayName: resolvedBranch.displayName,
205
+ status: "up_to_date",
206
+ commitCount: 0,
207
+ hasConflicts: false,
208
+ message: `${currentBranchName} is already up to date with ${resolvedBranch.displayName}.`,
209
+ }
210
+ }
211
+
212
+ try {
213
+ const commitCount = await getMergeCommitCount(repo.repoRoot, resolvedBranch.ref)
214
+ if (commitCount === 0) {
215
+ return {
216
+ currentBranchName,
217
+ targetBranchName: resolvedBranch.branchName,
218
+ targetDisplayName: resolvedBranch.displayName,
219
+ status: "up_to_date",
220
+ commitCount,
221
+ hasConflicts: false,
222
+ message: `${currentBranchName ?? "Current branch"} is already up to date with ${resolvedBranch.displayName}.`,
223
+ }
224
+ }
225
+
226
+ const conflictPrediction = await predictMergeConflicts(repo.repoRoot, resolvedBranch.ref)
227
+ if (conflictPrediction.hasConflicts) {
228
+ return {
229
+ currentBranchName,
230
+ targetBranchName: resolvedBranch.branchName,
231
+ targetDisplayName: resolvedBranch.displayName,
232
+ status: "conflicts",
233
+ commitCount,
234
+ hasConflicts: true,
235
+ message: `${commitCount} ${commitCount === 1 ? "commit" : "commits"} from ${resolvedBranch.displayName} would merge into ${currentBranchName ?? "the current branch"}, but conflicts are expected.`,
236
+ detail: conflictPrediction.detail,
237
+ }
238
+ }
239
+
240
+ return {
241
+ currentBranchName,
242
+ targetBranchName: resolvedBranch.branchName,
243
+ targetDisplayName: resolvedBranch.displayName,
244
+ status: "mergeable",
245
+ commitCount,
246
+ hasConflicts: false,
247
+ message: `${commitCount} ${commitCount === 1 ? "commit" : "commits"} from ${resolvedBranch.displayName} will merge into ${currentBranchName ?? "the current branch"}.`,
248
+ }
249
+ } catch (error) {
250
+ const message = error instanceof Error ? error.message : String(error)
251
+ return {
252
+ currentBranchName,
253
+ targetBranchName: resolvedBranch.branchName,
254
+ targetDisplayName: resolvedBranch.displayName,
255
+ status: "error",
256
+ commitCount: 0,
257
+ hasConflicts: false,
258
+ message: "Could not preview this merge.",
259
+ detail: message,
260
+ }
261
+ }
262
+ }
263
+
264
+ export async function mergeBranch(
265
+ deps: DiffStoreBranchOpsDeps,
266
+ args: { projectId: string; projectPath: string; branch: SelectedBranch }
267
+ ): Promise<ChatMergeBranchResult> {
268
+ const repo = await resolveRepo(args.projectPath)
269
+ if (!repo) {
270
+ throw new Error("Project is not in a git repository")
271
+ }
272
+
273
+ const currentDirtyPaths = await listDirtyPaths(repo.repoRoot)
274
+ if (currentDirtyPaths.length > 0) {
275
+ return {
276
+ ok: false,
277
+ title: "Merge blocked",
278
+ message: "Commit, discard, or stash your local changes before merging.",
279
+ snapshotChanged: false,
280
+ }
281
+ }
282
+
283
+ const resolvedBranch = await resolveSelectedBranchRef(repo.repoRoot, args.branch)
284
+ const commitCount = await getMergeCommitCount(repo.repoRoot, resolvedBranch.ref)
285
+ if (commitCount === 0) {
286
+ return {
287
+ ok: false,
288
+ title: "Already up to date",
289
+ message: `${resolvedBranch.displayName} is already merged into ${await getBranchName(repo.repoRoot) ?? "the current branch"}.`,
290
+ snapshotChanged: false,
291
+ }
292
+ }
293
+
294
+ const mergeResult = await runGit(["merge", "--no-edit", resolvedBranch.ref], repo.repoRoot)
295
+ const detail = formatGitFailure(mergeResult)
296
+
297
+ if (mergeResult.exitCode !== 0) {
298
+ const snapshotChanged = await deps.refreshSnapshot(args.projectId, args.projectPath)
299
+ const normalized = detail.toLowerCase()
300
+ const title = normalized.includes("conflict")
301
+ ? "Merge conflicts need resolution"
302
+ : "Merge failed"
303
+ const fallback = normalized.includes("conflict")
304
+ ? "Git reported merge conflicts while merging this branch."
305
+ : "Git could not merge this branch."
306
+ return createMergeActionFailure({
307
+ title,
308
+ detail,
309
+ fallback,
310
+ snapshotChanged,
311
+ })
312
+ }
313
+
314
+ const snapshotChanged = await deps.refreshSnapshot(args.projectId, args.projectPath)
315
+ return {
316
+ ok: true,
317
+ branchName: await getBranchName(repo.repoRoot),
318
+ snapshotChanged,
319
+ }
320
+ }
321
+
322
+ export async function checkoutBranch(
323
+ deps: DiffStoreBranchOpsDeps,
324
+ args: {
325
+ projectId: string
326
+ projectPath: string
327
+ branch: SelectedBranch
328
+ bringChanges?: boolean
329
+ }
330
+ ): Promise<ChatCheckoutBranchResult> {
331
+ const repo = await resolveRepo(args.projectPath)
332
+ if (!repo) {
333
+ throw new Error("Project is not in a git repository")
334
+ }
335
+
336
+ const currentDirtyPaths = await listDirtyPaths(repo.repoRoot)
337
+ if (currentDirtyPaths.length > 0 && !args.bringChanges) {
338
+ return {
339
+ ok: false,
340
+ cancelled: true,
341
+ title: "Branch switch cancelled",
342
+ message: "Your current changes were kept on the current branch.",
343
+ snapshotChanged: false,
344
+ }
345
+ }
346
+
347
+ let switchResult: Awaited<ReturnType<typeof runGit>>
348
+ if (args.branch.kind === "local") {
349
+ switchResult = await runGit(["switch", args.branch.name], repo.repoRoot)
350
+ } else if (args.branch.kind === "remote") {
351
+ const localBranchNames = await getLocalBranchNames(repo.repoRoot)
352
+ if (localBranchNames.includes(args.branch.name)) {
353
+ switchResult = await runGit(["switch", args.branch.name], repo.repoRoot)
354
+ } else {
355
+ switchResult = await runGit(["switch", "--track", "--no-guess", args.branch.remoteRef], repo.repoRoot)
356
+ }
357
+ } else {
358
+ const localBranchNames = await getLocalBranchNames(repo.repoRoot)
359
+ let localBranchName = args.branch.name
360
+
361
+ if (localBranchNames.includes(localBranchName) && args.branch.isCrossRepository) {
362
+ localBranchName = `${args.branch.name}-pr-${args.branch.prNumber}`
363
+ }
364
+
365
+ if (localBranchNames.includes(localBranchName)) {
366
+ switchResult = await runGit(["switch", localBranchName], repo.repoRoot)
367
+ } else if (args.branch.isCrossRepository && args.branch.headRepoCloneUrl) {
368
+ const fetchResult = await runGit(
369
+ [
370
+ "fetch",
371
+ "--no-tags",
372
+ args.branch.headRepoCloneUrl,
373
+ `refs/heads/${args.branch.headRefName}:refs/heads/${localBranchName}`,
374
+ ],
375
+ repo.repoRoot
376
+ )
377
+ if (fetchResult.exitCode !== 0) {
378
+ return createBranchActionFailure("Checkout failed", formatGitFailure(fetchResult), "Git could not fetch the pull request branch.")
379
+ }
380
+ switchResult = await runGit(["switch", localBranchName], repo.repoRoot)
381
+ } else {
382
+ const remoteRef = args.branch.remoteRef ?? `origin/${args.branch.headRefName}`
383
+ const remoteBranchNames = await getRemoteBranchNames(repo.repoRoot)
384
+ if (!remoteBranchNames.includes(remoteRef)) {
385
+ const fetchResult = await runGit(
386
+ ["fetch", "--no-tags", "origin", `refs/heads/${args.branch.headRefName}:refs/remotes/${remoteRef}`],
387
+ repo.repoRoot
388
+ )
389
+ if (fetchResult.exitCode !== 0) {
390
+ return createBranchActionFailure("Checkout failed", formatGitFailure(fetchResult), "Git could not fetch the pull request branch.")
391
+ }
392
+ }
393
+ switchResult = await runGit(["switch", "--track", "--no-guess", remoteRef], repo.repoRoot)
394
+ }
395
+ }
396
+
397
+ if (switchResult.exitCode !== 0) {
398
+ return createBranchActionFailure("Checkout failed", formatGitFailure(switchResult), "Git could not switch branches.")
399
+ }
400
+
401
+ const snapshotChanged = await deps.refreshSnapshot(args.projectId, args.projectPath)
402
+ return {
403
+ ok: true,
404
+ branchName: await getBranchName(repo.repoRoot),
405
+ snapshotChanged,
406
+ }
407
+ }
408
+
409
+ export async function createBranch(
410
+ deps: DiffStoreBranchOpsDeps,
411
+ args: {
412
+ projectId: string
413
+ projectPath: string
414
+ name: string
415
+ baseBranchName?: string
416
+ }
417
+ ): Promise<ChatCreateBranchResult> {
418
+ const repo = await resolveRepo(args.projectPath)
419
+ if (!repo) {
420
+ throw new Error("Project is not in a git repository")
421
+ }
422
+
423
+ const branchName = args.name.trim()
424
+ if (!branchName) {
425
+ throw new Error("Branch name is required")
426
+ }
427
+
428
+ const refValidation = await runGit(["check-ref-format", "--branch", branchName], repo.repoRoot)
429
+ if (refValidation.exitCode !== 0) {
430
+ return createBranchActionFailure("Create branch failed", formatGitFailure(refValidation), "Branch name is not valid.")
431
+ }
432
+
433
+ const localBranchNames = await getLocalBranchNames(repo.repoRoot)
434
+ if (localBranchNames.includes(branchName)) {
435
+ return {
436
+ ok: false,
437
+ title: "Create branch failed",
438
+ message: `A local branch named "${branchName}" already exists.`,
439
+ snapshotChanged: false,
440
+ }
441
+ }
442
+
443
+ const baseBranchName = args.baseBranchName?.trim() || await resolveDefaultBranchName(repo.repoRoot) || await getBranchName(repo.repoRoot)
444
+ if (!baseBranchName) {
445
+ throw new Error("Could not determine a base branch")
446
+ }
447
+
448
+ const switchResult = await runGit(["switch", "-c", branchName, baseBranchName], repo.repoRoot)
449
+ if (switchResult.exitCode !== 0) {
450
+ return createBranchActionFailure("Create branch failed", formatGitFailure(switchResult), "Git could not create the branch.")
451
+ }
452
+
453
+ const snapshotChanged = await deps.refreshSnapshot(args.projectId, args.projectPath)
454
+ return {
455
+ ok: true,
456
+ branchName,
457
+ snapshotChanged,
458
+ }
459
+ }
460
+
461
+ export async function syncBranch(
462
+ deps: DiffStoreBranchOpsDeps,
463
+ args: {
464
+ projectId: string
465
+ projectPath: string
466
+ action: "fetch" | "pull" | "push" | "publish"
467
+ }
468
+ ): Promise<ChatSyncResult> {
469
+ const repo = await resolveRepo(args.projectPath)
470
+ if (!repo) {
471
+ throw new Error("Project is not in a git repository")
472
+ }
473
+
474
+ const hasUpstream = await hasUpstreamBranch(repo.repoRoot)
475
+ if (args.action === "publish") {
476
+ const publishResult = await runGit(["push", "-u", "origin", "HEAD"], repo.repoRoot)
477
+ if (publishResult.exitCode !== 0) {
478
+ const detail = formatGitFailure(publishResult)
479
+ const normalized = detail.toLowerCase()
480
+ let title = "Publish branch failed"
481
+ let message = summarizeGitFailure(detail, "Git could not publish this branch.")
482
+
483
+ if (normalized.includes("could not read from remote repository") || normalized.includes("authentication failed") || normalized.includes("permission denied")) {
484
+ title = "Remote authentication failed"
485
+ message = "Git could not authenticate with the remote repository."
486
+ }
487
+
488
+ return {
489
+ ok: false,
490
+ action: args.action,
491
+ title,
492
+ message,
493
+ detail,
494
+ snapshotChanged: false,
495
+ }
496
+ }
497
+
498
+ const snapshotChanged = await deps.refreshSnapshot(args.projectId, args.projectPath)
499
+ const branchName = await getBranchName(repo.repoRoot)
500
+ const nextHasUpstream = await hasUpstreamBranch(repo.repoRoot)
501
+ const { aheadCount, behindCount } = nextHasUpstream
502
+ ? await getUpstreamStatusCounts(repo.repoRoot)
503
+ : { aheadCount: undefined, behindCount: undefined }
504
+
505
+ return {
506
+ ok: true,
507
+ action: args.action,
508
+ branchName,
509
+ aheadCount,
510
+ behindCount,
511
+ snapshotChanged,
512
+ }
513
+ }
514
+
515
+ if (args.action === "push") {
516
+ if (!hasUpstream) {
517
+ return {
518
+ ok: false,
519
+ action: args.action,
520
+ title: "Push failed",
521
+ message: "This branch does not have an upstream remote branch configured yet.",
522
+ snapshotChanged: false,
523
+ }
524
+ }
525
+
526
+ const pushResult = await runGit(["push"], repo.repoRoot)
527
+ if (pushResult.exitCode !== 0) {
528
+ const detail = formatGitFailure(pushResult)
529
+ return createSyncPushFailure(detail, false)
530
+ }
531
+
532
+ const snapshotChanged = await deps.refreshSnapshot(args.projectId, args.projectPath)
533
+ const branchName = await getBranchName(repo.repoRoot)
534
+ const nextHasUpstream = await hasUpstreamBranch(repo.repoRoot)
535
+ const { aheadCount, behindCount } = nextHasUpstream
536
+ ? await getUpstreamStatusCounts(repo.repoRoot)
537
+ : { aheadCount: undefined, behindCount: undefined }
538
+
539
+ return {
540
+ ok: true,
541
+ action: args.action,
542
+ branchName,
543
+ aheadCount,
544
+ behindCount,
545
+ snapshotChanged,
546
+ }
547
+ }
548
+
549
+ if (args.action === "pull" && !hasUpstream) {
550
+ return {
551
+ ok: false,
552
+ action: args.action,
553
+ title: "Pull failed",
554
+ message: "This branch does not have an upstream remote branch configured yet.",
555
+ snapshotChanged: false,
556
+ }
557
+ }
558
+
559
+ const syncResult = args.action === "pull"
560
+ ? await runGit(["pull", "--ff-only"], repo.repoRoot)
561
+ : await runGit(["fetch", "--all", "--prune"], repo.repoRoot)
562
+
563
+ if (syncResult.exitCode !== 0) {
564
+ const detail = formatGitFailure(syncResult)
565
+ const normalized = detail.toLowerCase()
566
+ let title = args.action === "pull" ? "Pull failed" : "Fetch failed"
567
+ let message = summarizeGitFailure(detail, args.action === "pull" ? "Git could not pull the latest changes." : "Git could not fetch the latest changes.")
568
+
569
+ if (args.action === "pull" && normalized.includes("not possible to fast-forward")) {
570
+ title = "Pull requires merge or rebase"
571
+ message = "Your branch cannot be fast-forwarded. Rebase or merge manually, then try again."
572
+ } else if (normalized.includes("could not read from remote repository") || normalized.includes("authentication failed") || normalized.includes("permission denied")) {
573
+ title = "Remote authentication failed"
574
+ message = "Git could not authenticate with the remote repository."
575
+ }
576
+
577
+ return {
578
+ ok: false,
579
+ action: args.action,
580
+ title,
581
+ message,
582
+ detail,
583
+ snapshotChanged: false,
584
+ }
585
+ }
586
+
587
+ const snapshotChanged = await deps.refreshSnapshot(args.projectId, args.projectPath)
588
+ const branchName = await getBranchName(repo.repoRoot)
589
+ const nextHasUpstream = await hasUpstreamBranch(repo.repoRoot)
590
+ const { aheadCount, behindCount } = nextHasUpstream
591
+ ? await getUpstreamStatusCounts(repo.repoRoot)
592
+ : { aheadCount: undefined, behindCount: undefined }
593
+
594
+ return {
595
+ ok: true,
596
+ action: args.action,
597
+ branchName,
598
+ aheadCount,
599
+ behindCount,
600
+ snapshotChanged,
601
+ }
602
+ }