@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,250 @@
1
+ /**
2
+ * ws-router-diff.ts
3
+ *
4
+ * WS command handlers for all diff/git operations (chat.refreshDiffs,
5
+ * chat.initGit, chat.getGitHubPublishInfo, chat.checkGitHubRepoAvailability,
6
+ * chat.publishToGitHub, chat.listBranches, chat.previewMergeBranch,
7
+ * chat.mergeBranch, chat.checkoutBranch, chat.syncBranch, chat.createBranch,
8
+ * chat.generateCommitMessage, chat.commitDiffs, chat.discardDiffFile,
9
+ * chat.ignoreDiffFile) extracted from ws-router.ts.
10
+ *
11
+ * All 15 handlers delegate exclusively to the injected DiffStoreDep and follow
12
+ * the same pattern — look up the project, call the store, ack, broadcast if
13
+ * the snapshot changed. No closure dependencies on createWsRouter locals.
14
+ */
15
+ import { PROTOCOL_VERSION } from "../shared/types"
16
+ import type { ClientCommand, ServerEnvelope } from "../shared/protocol"
17
+ import type { DiffStore } from "./diff-store"
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Dep interface (duck-typed; avoids circular imports with ws-router.ts)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /** The subset of DiffStore methods consumed by diff/git WS commands. */
24
+ export type DiffStoreDep = Pick<
25
+ DiffStore,
26
+ | "refreshSnapshot"
27
+ | "initializeGit"
28
+ | "getGitHubPublishInfo"
29
+ | "checkGitHubRepoAvailability"
30
+ | "publishToGitHub"
31
+ | "listBranches"
32
+ | "previewMergeBranch"
33
+ | "mergeBranch"
34
+ | "checkoutBranch"
35
+ | "syncBranch"
36
+ | "createBranch"
37
+ | "generateCommitMessage"
38
+ | "commitFiles"
39
+ | "discardFile"
40
+ | "ignoreFile"
41
+ >
42
+
43
+ export interface DiffCommandDeps {
44
+ /** Resolved DiffStore (or its no-op fallback). */
45
+ resolvedDiffStore: DiffStoreDep
46
+ /** Resolve the project for a given chatId — throws if not found. */
47
+ resolveChatProject: (chatId: string) => { project: { id: string; localPath: string } }
48
+ /** Pre-bound to the current WebSocket; called to send an ack envelope. */
49
+ send: (envelope: ServerEnvelope) => void
50
+ /** Called after any operation that may have changed the diff snapshot. */
51
+ broadcastSnapshots: () => void
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Command dispatcher
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Handle one diff/git WS command.
60
+ *
61
+ * Returns `true` when the command was handled (caller should `return`).
62
+ * Returns `false` when the command type is outside this module's scope.
63
+ */
64
+ export async function handleDiffCommand(
65
+ deps: DiffCommandDeps,
66
+ command: ClientCommand,
67
+ id: string,
68
+ ): Promise<boolean> {
69
+ const { resolvedDiffStore, resolveChatProject, send, broadcastSnapshots } = deps
70
+
71
+ switch (command.type) {
72
+ case "chat.refreshDiffs": {
73
+ const { project } = resolveChatProject(command.chatId)
74
+ const changed = await resolvedDiffStore.refreshSnapshot(project.id, project.localPath)
75
+ send({ v: PROTOCOL_VERSION, type: "ack", id })
76
+ if (changed) {
77
+ void broadcastSnapshots()
78
+ }
79
+ return true
80
+ }
81
+ case "chat.initGit": {
82
+ const { project } = resolveChatProject(command.chatId)
83
+ const result = await resolvedDiffStore.initializeGit({
84
+ projectId: project.id,
85
+ projectPath: project.localPath,
86
+ })
87
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
88
+ if (result.snapshotChanged) {
89
+ void broadcastSnapshots()
90
+ }
91
+ return true
92
+ }
93
+ case "chat.getGitHubPublishInfo": {
94
+ const { project } = resolveChatProject(command.chatId)
95
+ const result = await resolvedDiffStore.getGitHubPublishInfo({
96
+ projectPath: project.localPath,
97
+ })
98
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
99
+ return true
100
+ }
101
+ case "chat.checkGitHubRepoAvailability": {
102
+ const result = await resolvedDiffStore.checkGitHubRepoAvailability({
103
+ owner: command.owner,
104
+ name: command.name,
105
+ })
106
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
107
+ return true
108
+ }
109
+ case "chat.publishToGitHub": {
110
+ const { project } = resolveChatProject(command.chatId)
111
+ const result = await resolvedDiffStore.publishToGitHub({
112
+ projectId: project.id,
113
+ projectPath: project.localPath,
114
+ owner: command.owner,
115
+ name: command.name,
116
+ visibility: command.visibility,
117
+ description: command.description,
118
+ })
119
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
120
+ if (result.snapshotChanged) {
121
+ void broadcastSnapshots()
122
+ }
123
+ return true
124
+ }
125
+ case "chat.listBranches": {
126
+ const { project } = resolveChatProject(command.chatId)
127
+ const result = await resolvedDiffStore.listBranches({
128
+ projectPath: project.localPath,
129
+ })
130
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
131
+ return true
132
+ }
133
+ case "chat.previewMergeBranch": {
134
+ const { project } = resolveChatProject(command.chatId)
135
+ const result = await resolvedDiffStore.previewMergeBranch({
136
+ projectPath: project.localPath,
137
+ branch: command.branch,
138
+ })
139
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
140
+ return true
141
+ }
142
+ case "chat.mergeBranch": {
143
+ const { project } = resolveChatProject(command.chatId)
144
+ const result = await resolvedDiffStore.mergeBranch({
145
+ projectId: project.id,
146
+ projectPath: project.localPath,
147
+ branch: command.branch,
148
+ })
149
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
150
+ if (result.snapshotChanged) {
151
+ void broadcastSnapshots()
152
+ }
153
+ return true
154
+ }
155
+ case "chat.checkoutBranch": {
156
+ const { project } = resolveChatProject(command.chatId)
157
+ const result = await resolvedDiffStore.checkoutBranch({
158
+ projectId: project.id,
159
+ projectPath: project.localPath,
160
+ branch: command.branch,
161
+ bringChanges: command.bringChanges,
162
+ })
163
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
164
+ if (result.snapshotChanged) {
165
+ void broadcastSnapshots()
166
+ }
167
+ return true
168
+ }
169
+ case "chat.syncBranch": {
170
+ const { project } = resolveChatProject(command.chatId)
171
+ const result = await resolvedDiffStore.syncBranch({
172
+ projectId: project.id,
173
+ projectPath: project.localPath,
174
+ action: command.action,
175
+ })
176
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
177
+ if (result.snapshotChanged) {
178
+ void broadcastSnapshots()
179
+ }
180
+ return true
181
+ }
182
+ case "chat.createBranch": {
183
+ const { project } = resolveChatProject(command.chatId)
184
+ const result = await resolvedDiffStore.createBranch({
185
+ projectId: project.id,
186
+ projectPath: project.localPath,
187
+ name: command.name,
188
+ baseBranchName: command.baseBranchName,
189
+ })
190
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
191
+ if (result.snapshotChanged) {
192
+ void broadcastSnapshots()
193
+ }
194
+ return true
195
+ }
196
+ case "chat.generateCommitMessage": {
197
+ const { project } = resolveChatProject(command.chatId)
198
+ const result = await resolvedDiffStore.generateCommitMessage({
199
+ projectPath: project.localPath,
200
+ paths: command.paths,
201
+ })
202
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
203
+ return true
204
+ }
205
+ case "chat.commitDiffs": {
206
+ const { project } = resolveChatProject(command.chatId)
207
+ const result = await resolvedDiffStore.commitFiles({
208
+ projectId: project.id,
209
+ projectPath: project.localPath,
210
+ paths: command.paths,
211
+ summary: command.summary,
212
+ description: command.description,
213
+ mode: command.mode,
214
+ })
215
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
216
+ if (result.snapshotChanged) {
217
+ void broadcastSnapshots()
218
+ }
219
+ return true
220
+ }
221
+ case "chat.discardDiffFile": {
222
+ const { project } = resolveChatProject(command.chatId)
223
+ const result = await resolvedDiffStore.discardFile({
224
+ projectId: project.id,
225
+ projectPath: project.localPath,
226
+ path: command.path,
227
+ })
228
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
229
+ if (result.snapshotChanged) {
230
+ void broadcastSnapshots()
231
+ }
232
+ return true
233
+ }
234
+ case "chat.ignoreDiffFile": {
235
+ const { project } = resolveChatProject(command.chatId)
236
+ const result = await resolvedDiffStore.ignoreFile({
237
+ projectId: project.id,
238
+ projectPath: project.localPath,
239
+ path: command.path,
240
+ })
241
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
242
+ if (result.snapshotChanged) {
243
+ void broadcastSnapshots()
244
+ }
245
+ return true
246
+ }
247
+ default:
248
+ return false
249
+ }
250
+ }
@@ -0,0 +1,360 @@
1
+ /**
2
+ * ws-router-envelope.ts
3
+ *
4
+ * Snapshot-envelope computation for the WebSocket router.
5
+ * Extracted from ws-router.ts to reduce its size.
6
+ *
7
+ * `createEnvelopeBuilder(deps)` returns `{ createEnvelope, getSidebarSnapshotCacheEntry }`
8
+ * whose closures capture the stable dep objects — no shared mutable state lives here.
9
+ */
10
+ import os from "node:os"
11
+ import { log } from "../shared/log"
12
+ import { PROTOCOL_VERSION } from "../shared/types"
13
+ import type { ChatSnapshot } from "../shared/types"
14
+ import type { ServerEnvelope, SubscriptionTopic } from "../shared/protocol"
15
+ import type { ServerWebSocket } from "bun"
16
+ import { deriveChatSnapshot, deriveLocalProjectsSnapshot, deriveSidebarData } from "./read-models"
17
+ import { toOrchRunSummary } from "./orchestration-input"
18
+ import type { EventStore } from "./event-store"
19
+ import type { AgentCoordinator } from "./agent"
20
+ import type { TerminalManager } from "./terminal-manager"
21
+ import type { KeybindingsManager } from "./keybindings"
22
+ import type { PtyInstanceRegistry } from "./claude-pty/pty-instance-registry"
23
+ import type { WorkflowRegistry } from "./workflow-registry"
24
+ import type { UpdateManager } from "./update-manager"
25
+ import type { PushManager } from "./push/push-manager"
26
+ import type { DiffStore } from "./diff-store"
27
+ import type { DiscoveredProject } from "./discovery.adapter"
28
+ import {
29
+ getSidebarProjectOrder,
30
+ isSendToStartingProfilingEnabled,
31
+ } from "./ws-router-utils"
32
+ import type { ClientState, SnapshotComputationCache } from "./ws-router-utils"
33
+ import type { ResolvedAppSettings } from "./ws-router-defaults"
34
+
35
+ const DEFAULT_CHAT_RECENT_LIMIT = 200
36
+
37
+ // ── Deps type ─────────────────────────────────────────────────────────────────
38
+
39
+ export interface EnvelopeDeps {
40
+ store: EventStore
41
+ agent: AgentCoordinator
42
+ resolvedAppSettings: ResolvedAppSettings
43
+ keybindings: KeybindingsManager
44
+ resolvedDiffStore: Pick<DiffStore,
45
+ "getProjectSnapshot" | "refreshSnapshot" | "initializeGit" | "getGitHubPublishInfo" |
46
+ "checkGitHubRepoAvailability" | "publishToGitHub" | "listBranches" | "previewMergeBranch" |
47
+ "mergeBranch" | "syncBranch" | "checkoutBranch" | "createBranch" | "generateCommitMessage" |
48
+ "commitFiles" | "discardFile" | "ignoreFile" | "readPatch">
49
+ ptyInstances?: PtyInstanceRegistry
50
+ workflowRegistry?: WorkflowRegistry
51
+ machineDisplayName: string
52
+ updateManager: UpdateManager | null
53
+ getDiscoveredProjects: () => DiscoveredProject[]
54
+ terminals: TerminalManager
55
+ pushManager: PushManager
56
+ }
57
+
58
+ // ── Internal: sidebar cache entry ─────────────────────────────────────────────
59
+
60
+ function buildSidebarSnapshotCacheEntry(
61
+ deps: EnvelopeDeps,
62
+ cache?: SnapshotComputationCache
63
+ ): NonNullable<SnapshotComputationCache["sidebar"]> {
64
+ if (cache?.sidebar) {
65
+ return cache.sidebar
66
+ }
67
+
68
+ const { store, agent, pushManager } = deps
69
+ const startedAt = performance.now()
70
+ const data = deriveSidebarData(store.state, agent.getActiveStatuses(), {
71
+ sidebarProjectOrder: getSidebarProjectOrder(store),
72
+ drainingChatIds: agent.getDrainingChatIds(),
73
+ claudeSessionStates: agent.getClaudeSessionStates?.(),
74
+ })
75
+ const observed = data.projectGroups.flatMap((group) =>
76
+ group.chats.map((chat) => ({
77
+ chatId: chat.chatId,
78
+ projectLocalPath: group.localPath,
79
+ projectTitle: group.localPath.split("/").filter(Boolean).pop() ?? group.localPath,
80
+ chatTitle: chat.title,
81
+ status: chat.status,
82
+ }))
83
+ )
84
+ void pushManager.observeStatuses(observed).catch((error) => {
85
+ log.warn("[kanna/push] observeStatuses failed", { error })
86
+ })
87
+ if (isSendToStartingProfilingEnabled()) {
88
+ const totalChats = data.projectGroups.reduce((count, group) => count + group.chats.length, 0)
89
+ log.info("[kanna/send->starting][server]", JSON.stringify({
90
+ stage: "ws.sidebar_snapshot_built",
91
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
92
+ projectGroupCount: data.projectGroups.length,
93
+ chatCount: totalChats,
94
+ totalChatCount: store.state.chatsById.size,
95
+ totalProjectCount: store.state.projectsById.size,
96
+ }))
97
+ }
98
+
99
+ const sidebar = {
100
+ data,
101
+ signature: JSON.stringify({
102
+ type: "sidebar" as const,
103
+ data,
104
+ }),
105
+ }
106
+
107
+ if (cache) {
108
+ cache.sidebar = sidebar
109
+ }
110
+
111
+ return sidebar
112
+ }
113
+
114
+ // ── Exported factory ──────────────────────────────────────────────────────────
115
+
116
+ export interface EnvelopeBuilder {
117
+ /**
118
+ * Returns a cached sidebar entry (computes it once per cache object).
119
+ * Exposed so callers can read the pre-computed signature for dedup.
120
+ */
121
+ getSidebarSnapshotCacheEntry(cache?: SnapshotComputationCache): NonNullable<SnapshotComputationCache["sidebar"]>
122
+ /**
123
+ * Build the full ServerEnvelope for a given subscription topic.
124
+ */
125
+ createEnvelope(
126
+ id: string,
127
+ topic: SubscriptionTopic,
128
+ cache?: SnapshotComputationCache,
129
+ connection?: ServerWebSocket<ClientState>,
130
+ ): ServerEnvelope
131
+ /**
132
+ * Light chat snapshot (recentLimit 0 — no transcript window; `messages`
133
+ * carry only the synthetic pending_tool_request rows). Feeds the
134
+ * meta-diff of the `chat.ops` broadcast path.
135
+ */
136
+ deriveChatMeta(chatId: string): ChatSnapshot | null
137
+ }
138
+
139
+ export function createEnvelopeBuilder(deps: EnvelopeDeps): EnvelopeBuilder {
140
+ const {
141
+ store,
142
+ agent,
143
+ resolvedAppSettings,
144
+ keybindings,
145
+ resolvedDiffStore,
146
+ ptyInstances,
147
+ workflowRegistry,
148
+ machineDisplayName,
149
+ updateManager,
150
+ getDiscoveredProjects,
151
+ terminals,
152
+ pushManager,
153
+ } = deps
154
+
155
+ function getSidebarSnapshotCacheEntry(cache?: SnapshotComputationCache) {
156
+ return buildSidebarSnapshotCacheEntry(deps, cache)
157
+ }
158
+
159
+ function deriveChatMeta(chatId: string): ChatSnapshot | null {
160
+ return deriveChatSnapshot(
161
+ store.state,
162
+ agent.getActiveStatuses(),
163
+ agent.getDrainingChatIds(),
164
+ agent.getSlashCommandsLoadingChatIds(),
165
+ chatId,
166
+ (id) => store.getRecentChatHistory(id, 0),
167
+ (id) => store.getTunnelEvents(id),
168
+ agent.getWaitStartedAtByChatId(),
169
+ Date.now(),
170
+ agent.getClaudeSessionStates?.() ?? new Map(),
171
+ resolvedAppSettings.getSnapshot().customModels ?? [],
172
+ )
173
+ }
174
+
175
+ function createEnvelope(
176
+ id: string,
177
+ topic: SubscriptionTopic,
178
+ cache?: SnapshotComputationCache,
179
+ connection?: ServerWebSocket<ClientState>,
180
+ ): ServerEnvelope {
181
+ if (topic.type === "sidebar") {
182
+ const sidebar = getSidebarSnapshotCacheEntry(cache)
183
+ return {
184
+ v: PROTOCOL_VERSION,
185
+ type: "snapshot",
186
+ id,
187
+ snapshot: {
188
+ type: "sidebar",
189
+ data: sidebar.data,
190
+ },
191
+ }
192
+ }
193
+
194
+ if (topic.type === "local-projects") {
195
+ const discoveredProjects = getDiscoveredProjects()
196
+ const data = deriveLocalProjectsSnapshot(store.state, discoveredProjects, machineDisplayName, os.homedir())
197
+
198
+ return {
199
+ v: PROTOCOL_VERSION,
200
+ type: "snapshot",
201
+ id,
202
+ snapshot: {
203
+ type: "local-projects",
204
+ data,
205
+ },
206
+ }
207
+ }
208
+
209
+ if (topic.type === "keybindings") {
210
+ return {
211
+ v: PROTOCOL_VERSION,
212
+ type: "snapshot",
213
+ id,
214
+ snapshot: {
215
+ type: "keybindings",
216
+ data: keybindings.getSnapshot(),
217
+ },
218
+ }
219
+ }
220
+
221
+ if (topic.type === "app-settings") {
222
+ return {
223
+ v: PROTOCOL_VERSION,
224
+ type: "snapshot",
225
+ id,
226
+ snapshot: {
227
+ type: "app-settings",
228
+ data: resolvedAppSettings.getSnapshot(),
229
+ },
230
+ }
231
+ }
232
+
233
+ if (topic.type === "push-config") {
234
+ return {
235
+ v: PROTOCOL_VERSION,
236
+ type: "snapshot",
237
+ id,
238
+ snapshot: {
239
+ type: "push-config",
240
+ data: pushManager.getConfigSnapshot(connection?.data.pushDeviceId ?? null),
241
+ },
242
+ }
243
+ }
244
+
245
+ if (topic.type === "update") {
246
+ return {
247
+ v: PROTOCOL_VERSION,
248
+ type: "snapshot",
249
+ id,
250
+ snapshot: {
251
+ type: "update",
252
+ data: updateManager?.getSnapshot() ?? {
253
+ currentVersion: "unknown",
254
+ latestVersion: null,
255
+ status: "idle",
256
+ updateAvailable: false,
257
+ lastCheckedAt: null,
258
+ error: null,
259
+ installAction: "restart",
260
+ reloadRequestedAt: null,
261
+ },
262
+ },
263
+ }
264
+ }
265
+
266
+ if (topic.type === "terminal") {
267
+ return {
268
+ v: PROTOCOL_VERSION,
269
+ type: "snapshot",
270
+ id,
271
+ snapshot: {
272
+ type: "terminal",
273
+ data: terminals.getSnapshot(topic.terminalId),
274
+ },
275
+ }
276
+ }
277
+
278
+ if (topic.type === "project-git") {
279
+ return {
280
+ v: PROTOCOL_VERSION,
281
+ type: "snapshot",
282
+ id,
283
+ snapshot: {
284
+ type: "project-git",
285
+ data: store.getProject(topic.projectId)
286
+ ? resolvedDiffStore.getProjectSnapshot(topic.projectId)
287
+ : null,
288
+ },
289
+ }
290
+ }
291
+
292
+ if (topic.type === "pty-instances") {
293
+ return {
294
+ v: PROTOCOL_VERSION,
295
+ type: "snapshot",
296
+ id,
297
+ snapshot: {
298
+ type: "pty-instances",
299
+ data: { instances: ptyInstances?.snapshot() ?? [] },
300
+ },
301
+ }
302
+ }
303
+
304
+ if (topic.type === "workflows") {
305
+ return {
306
+ v: PROTOCOL_VERSION,
307
+ type: "snapshot",
308
+ id,
309
+ snapshot: {
310
+ type: "workflows",
311
+ data: { chatId: topic.chatId, runs: workflowRegistry?.snapshot(topic.chatId) ?? [] },
312
+ },
313
+ }
314
+ }
315
+
316
+ if (topic.type === "orch-runs") {
317
+ return {
318
+ v: PROTOCOL_VERSION,
319
+ type: "snapshot",
320
+ id,
321
+ snapshot: {
322
+ type: "orch-runs",
323
+ data: { runs: store.getOrchRuns().map(toOrchRunSummary) },
324
+ },
325
+ }
326
+ }
327
+
328
+ // Capture seq BEFORE deriving: ops recorded mid-derive then overlap the
329
+ // snapshot, and the client reducer's upsert-by-_id makes that idempotent.
330
+ // Optional-chained like subscribeOrchRuns: partial store fakes in tests
331
+ // may not implement chatOps; the real EventStore always does.
332
+ const seq = typeof store.chatOps?.currentSeq === "function"
333
+ ? store.chatOps.currentSeq(topic.chatId)
334
+ : undefined
335
+ const data = deriveChatSnapshot(
336
+ store.state,
337
+ agent.getActiveStatuses(),
338
+ agent.getDrainingChatIds(),
339
+ agent.getSlashCommandsLoadingChatIds(),
340
+ topic.chatId,
341
+ (chatId) => store.getRecentChatHistory(chatId, topic.recentLimit ?? DEFAULT_CHAT_RECENT_LIMIT),
342
+ (chatId) => store.getTunnelEvents(chatId),
343
+ agent.getWaitStartedAtByChatId(),
344
+ Date.now(),
345
+ agent.getClaudeSessionStates?.() ?? new Map(),
346
+ resolvedAppSettings.getSnapshot().customModels ?? [],
347
+ )
348
+ return {
349
+ v: PROTOCOL_VERSION,
350
+ type: "snapshot",
351
+ id,
352
+ snapshot: {
353
+ type: "chat",
354
+ data: data && seq !== undefined ? { ...data, seq } : data,
355
+ },
356
+ }
357
+ }
358
+
359
+ return { getSidebarSnapshotCacheEntry, createEnvelope, deriveChatMeta }
360
+ }