@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,434 @@
1
+ /**
2
+ * ws-router-defaults.ts
3
+ *
4
+ * Pure helpers and fallback-factory functions used by createWsRouter when
5
+ * optional dependencies (diffStore, llmProvider, appSettings) are absent.
6
+ *
7
+ * All exports are side-effect-free (no IO, no process.env reads).
8
+ * Extracted from ws-router.ts.
9
+ */
10
+ import { randomUUID } from "node:crypto"
11
+ import {
12
+ AUTH_DEFAULTS,
13
+ CLAUDE_AUTH_DEFAULTS,
14
+ CLAUDE_DRIVER_DEFAULTS,
15
+ CLAUDE_PTY_LIFECYCLE_DEFAULTS,
16
+ CLOUDFLARE_TUNNEL_DEFAULTS,
17
+ DEFAULT_OPENROUTER_SDK_MODEL,
18
+ UPLOAD_DEFAULTS,
19
+ } from "../shared/types"
20
+ import type {
21
+ AppSettingsPatch,
22
+ AppSettingsSnapshot,
23
+ LlmProviderSnapshot,
24
+ LlmProviderValidationResult,
25
+ Subagent,
26
+ } from "../shared/types"
27
+ import type { AppSettingsManager } from "./app-settings"
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // mergeAppSettingsPatch — pure transform
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Apply an `AppSettingsPatch` onto an existing `AppSettingsSnapshot`.
35
+ * Pure function — does not mutate the input snapshot.
36
+ */
37
+ export function mergeAppSettingsPatch(
38
+ snapshot: AppSettingsSnapshot,
39
+ patch: AppSettingsPatch,
40
+ ): AppSettingsSnapshot {
41
+ let subagents = snapshot.subagents
42
+ if (patch.subagents?.create) {
43
+ const now = Date.now()
44
+ subagents = [...subagents, {
45
+ id: randomUUID(),
46
+ ...patch.subagents.create,
47
+ name: patch.subagents.create.name.trim(),
48
+ triggerMode: patch.subagents.create.triggerMode ?? "auto",
49
+ createdAt: now,
50
+ updatedAt: now,
51
+ }]
52
+ } else if (patch.subagents?.update) {
53
+ subagents = subagents.map((subagent): Subagent => subagent.id === patch.subagents?.update?.id
54
+ ? {
55
+ ...subagent,
56
+ ...patch.subagents.update.patch,
57
+ name: patch.subagents.update.patch.name?.trim() ?? subagent.name,
58
+ description: patch.subagents.update.patch.description === null
59
+ ? undefined
60
+ : patch.subagents.update.patch.description ?? subagent.description,
61
+ modelOptions: <Subagent["modelOptions"]>{ ...subagent.modelOptions, ...(patch.subagents.update.patch.modelOptions ?? {}) },
62
+ workingDir: patch.subagents.update.patch.workingDir === null
63
+ ? undefined
64
+ : patch.subagents.update.patch.workingDir ?? subagent.workingDir,
65
+ allowedPaths: patch.subagents.update.patch.allowedPaths === null
66
+ ? undefined
67
+ : patch.subagents.update.patch.allowedPaths ?? subagent.allowedPaths,
68
+ maxTurns: patch.subagents.update.patch.maxTurns === null
69
+ ? undefined
70
+ : patch.subagents.update.patch.maxTurns ?? subagent.maxTurns,
71
+ updatedAt: Date.now(),
72
+ }
73
+ : subagent)
74
+ } else if (patch.subagents?.delete) {
75
+ subagents = subagents.filter((subagent) => subagent.id !== patch.subagents?.delete?.id)
76
+ }
77
+
78
+ return {
79
+ ...snapshot,
80
+ ...patch,
81
+ terminal: {
82
+ ...snapshot.terminal,
83
+ ...patch.terminal,
84
+ },
85
+ editor: {
86
+ ...snapshot.editor,
87
+ ...patch.editor,
88
+ },
89
+ providerDefaults: {
90
+ claude: {
91
+ ...snapshot.providerDefaults.claude,
92
+ ...patch.providerDefaults?.claude,
93
+ modelOptions: {
94
+ ...snapshot.providerDefaults.claude.modelOptions,
95
+ ...patch.providerDefaults?.claude?.modelOptions,
96
+ },
97
+ },
98
+ codex: {
99
+ ...snapshot.providerDefaults.codex,
100
+ ...patch.providerDefaults?.codex,
101
+ modelOptions: {
102
+ ...snapshot.providerDefaults.codex.modelOptions,
103
+ ...patch.providerDefaults?.codex?.modelOptions,
104
+ },
105
+ },
106
+ openrouter: {
107
+ ...snapshot.providerDefaults.openrouter,
108
+ ...patch.providerDefaults?.openrouter,
109
+ modelOptions: {},
110
+ },
111
+ },
112
+ cloudflareTunnel: {
113
+ ...snapshot.cloudflareTunnel,
114
+ ...patch.cloudflareTunnel,
115
+ },
116
+ auth: {
117
+ ...snapshot.auth,
118
+ ...patch.auth,
119
+ },
120
+ claudeAuth: {
121
+ tokens: patch.claudeAuth?.tokens ?? snapshot.claudeAuth.tokens,
122
+ concurrencyDefault: patch.claudeAuth?.concurrencyDefault ?? snapshot.claudeAuth.concurrencyDefault,
123
+ },
124
+ uploads: {
125
+ ...snapshot.uploads,
126
+ ...patch.uploads,
127
+ },
128
+ subagents,
129
+ customMcpServers: snapshot.customMcpServers,
130
+ customModels: snapshot.customModels,
131
+ textSnippets: snapshot.textSnippets,
132
+ claudeDriver: {
133
+ preference: patch.claudeDriver?.preference ?? snapshot.claudeDriver.preference,
134
+ lifecycle: {
135
+ ...snapshot.claudeDriver.lifecycle,
136
+ ...patch.claudeDriver?.lifecycle,
137
+ },
138
+ },
139
+ subagentRuntime: {
140
+ runTimeoutMs: patch.subagentRuntime?.runTimeoutMs ?? snapshot.subagentRuntime.runTimeoutMs,
141
+ defaultLoopSubagentId: patch.subagentRuntime?.defaultLoopSubagentId !== undefined
142
+ ? patch.subagentRuntime.defaultLoopSubagentId
143
+ : snapshot.subagentRuntime.defaultLoopSubagentId,
144
+ },
145
+ }
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // buildInitialAppSettingsSnapshot — the out-of-the-box fallback snapshot
150
+ // ---------------------------------------------------------------------------
151
+
152
+ export function buildInitialAppSettingsSnapshot(): AppSettingsSnapshot {
153
+ return {
154
+ analyticsEnabled: true,
155
+ browserSettingsMigrated: false,
156
+ theme: "system",
157
+ chatSoundPreference: "always",
158
+ chatSoundId: "funk",
159
+ terminal: {
160
+ scrollbackLines: 1_000,
161
+ minColumnWidth: 450,
162
+ },
163
+ editor: {
164
+ preset: "cursor",
165
+ commandTemplate: "cursor {path}",
166
+ },
167
+ defaultProvider: "last_used",
168
+ providerDefaults: {
169
+ claude: {
170
+ model: "claude-opus-4-7",
171
+ modelOptions: {
172
+ reasoningEffort: "high",
173
+ contextWindow: "200k",
174
+ },
175
+ planMode: false,
176
+ },
177
+ codex: {
178
+ model: "gpt-5.5",
179
+ modelOptions: {
180
+ reasoningEffort: "high",
181
+ fastMode: false,
182
+ },
183
+ planMode: false,
184
+ },
185
+ openrouter: {
186
+ model: DEFAULT_OPENROUTER_SDK_MODEL,
187
+ modelOptions: {},
188
+ planMode: false,
189
+ },
190
+ },
191
+ warning: null,
192
+ filePathDisplay: "~/.kanna/data/settings.json",
193
+ cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
194
+ auth: AUTH_DEFAULTS,
195
+ claudeAuth: CLAUDE_AUTH_DEFAULTS,
196
+ uploads: UPLOAD_DEFAULTS,
197
+ subagents: [],
198
+ customMcpServers: [],
199
+ customModels: [],
200
+ textSnippets: [],
201
+ claudeDriver: { ...CLAUDE_DRIVER_DEFAULTS, lifecycle: { ...CLAUDE_PTY_LIFECYCLE_DEFAULTS } },
202
+ globalPromptAppend: "",
203
+ shareDefaultTtlHours: 24,
204
+ subagentRuntime: { runTimeoutMs: 600_000, defaultLoopSubagentId: null },
205
+ }
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // buildFallbackDiffStore — noop DiffStore for when diffStore arg is absent
210
+ // ---------------------------------------------------------------------------
211
+
212
+ export function buildFallbackDiffStore() {
213
+ return {
214
+ getProjectSnapshot: (_projectId: string) => ({
215
+ status: "unknown" as const,
216
+ branchName: undefined,
217
+ defaultBranchName: undefined,
218
+ hasOriginRemote: undefined,
219
+ originRepoSlug: undefined,
220
+ hasUpstream: undefined,
221
+ aheadCount: undefined,
222
+ behindCount: undefined,
223
+ lastFetchedAt: undefined,
224
+ files: [],
225
+ branchHistory: { entries: [] },
226
+ }),
227
+ refreshSnapshot: async (_projectId: string) => false as const,
228
+ initializeGit: async () => ({ ok: true as const, branchName: undefined, snapshotChanged: false }),
229
+ getGitHubPublishInfo: async () => ({
230
+ ghInstalled: false,
231
+ authenticated: false,
232
+ activeAccountLogin: undefined,
233
+ owners: [],
234
+ suggestedRepoName: "my-repo",
235
+ }),
236
+ checkGitHubRepoAvailability: async () => ({ available: false, message: "Unavailable" }),
237
+ publishToGitHub: async () => ({
238
+ ok: false,
239
+ title: "Publish failed",
240
+ message: "Unavailable",
241
+ snapshotChanged: false,
242
+ }),
243
+ listBranches: async () => ({
244
+ recent: [],
245
+ local: [],
246
+ remote: [],
247
+ pullRequests: [],
248
+ pullRequestsStatus: "unavailable" as const,
249
+ }),
250
+ previewMergeBranch: async () => ({
251
+ currentBranchName: undefined,
252
+ targetBranchName: "",
253
+ targetDisplayName: "",
254
+ status: "error" as const,
255
+ commitCount: 0,
256
+ hasConflicts: false,
257
+ message: "Merge preview unavailable.",
258
+ }),
259
+ mergeBranch: async () => ({
260
+ ok: false as const,
261
+ title: "Merge failed",
262
+ message: "Merge unavailable.",
263
+ snapshotChanged: false,
264
+ }),
265
+ syncBranch: async () => ({
266
+ ok: true as const,
267
+ action: "fetch" as const,
268
+ branchName: undefined,
269
+ snapshotChanged: false,
270
+ }),
271
+ checkoutBranch: async () => ({ ok: true as const, branchName: undefined, snapshotChanged: false }),
272
+ createBranch: async () => ({ ok: true as const, branchName: "main", snapshotChanged: false }),
273
+ generateCommitMessage: async () => ({
274
+ subject: "Update selected files",
275
+ body: "",
276
+ usedFallback: true,
277
+ failureMessage: null,
278
+ }),
279
+ commitFiles: async () => ({
280
+ ok: true as const,
281
+ mode: "commit_only" as const,
282
+ branchName: undefined,
283
+ pushed: false,
284
+ snapshotChanged: false,
285
+ }),
286
+ discardFile: async () => ({ snapshotChanged: false }),
287
+ ignoreFile: async () => ({ snapshotChanged: false }),
288
+ readPatch: async () => ({ patch: "" }),
289
+ }
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // buildFallbackLlmProvider — noop LlmProvider for when llmProvider arg is absent
294
+ // ---------------------------------------------------------------------------
295
+
296
+ export function buildFallbackLlmProvider() {
297
+ return {
298
+ read: async (): Promise<LlmProviderSnapshot> => ({
299
+ provider: "openai" as const,
300
+ apiKey: "",
301
+ model: "gpt-5.4-mini",
302
+ baseUrl: "",
303
+ resolvedBaseUrl: "https://api.openai.com/v1",
304
+ enabled: false,
305
+ warning: null,
306
+ filePathDisplay: "~/.kanna/llm-provider.json",
307
+ }),
308
+ write: async ({
309
+ provider,
310
+ apiKey,
311
+ model,
312
+ baseUrl,
313
+ }: Pick<LlmProviderSnapshot, "provider" | "apiKey" | "model" | "baseUrl">): Promise<LlmProviderSnapshot> => {
314
+ let resolvedBaseUrl: string
315
+ if (provider === "openrouter") {
316
+ resolvedBaseUrl = "https://openrouter.ai/api/v1"
317
+ } else if (provider === "custom") {
318
+ resolvedBaseUrl = baseUrl
319
+ } else {
320
+ resolvedBaseUrl = "https://api.openai.com/v1"
321
+ }
322
+ return {
323
+ provider,
324
+ apiKey,
325
+ model,
326
+ baseUrl,
327
+ resolvedBaseUrl,
328
+ enabled: false,
329
+ warning: null,
330
+ filePathDisplay: "~/.kanna/llm-provider.json",
331
+ }
332
+ },
333
+ validate: async (
334
+ _value: Pick<LlmProviderSnapshot, "provider" | "apiKey" | "model" | "baseUrl">,
335
+ ): Promise<LlmProviderValidationResult> => ({
336
+ ok: false,
337
+ error: {
338
+ type: "config_error",
339
+ message: "LLM provider validation unavailable.",
340
+ },
341
+ }),
342
+ }
343
+ }
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // buildResolvedAppSettings — wraps optional AppSettingsManager with in-memory
347
+ // fallback, enabling all callers to treat settings as always-present.
348
+ // ---------------------------------------------------------------------------
349
+
350
+ type AppSettingsManagerSubset = Pick<AppSettingsManager,
351
+ "getSnapshot" | "write"
352
+ > & Partial<Pick<AppSettingsManager,
353
+ "setCloudflareTunnel" | "setClaudeAuth" | "writePatch" | "onChange" |
354
+ "createSubagent" | "updateSubagent" | "deleteSubagent"
355
+ >>
356
+
357
+ export function buildResolvedAppSettings(
358
+ appSettings: AppSettingsManagerSubset | null | undefined,
359
+ ) {
360
+ let fallbackSnapshot: AppSettingsSnapshot = buildInitialAppSettingsSnapshot()
361
+
362
+ const self = {
363
+ getSnapshot: (): AppSettingsSnapshot =>
364
+ appSettings?.getSnapshot() ?? fallbackSnapshot,
365
+
366
+ write: async (value: { analyticsEnabled: boolean }): Promise<AppSettingsSnapshot> => {
367
+ if (appSettings) return await appSettings.write(value)
368
+ fallbackSnapshot = { ...fallbackSnapshot, analyticsEnabled: value.analyticsEnabled }
369
+ return fallbackSnapshot
370
+ },
371
+
372
+ writePatch: async (patch: AppSettingsPatch): Promise<AppSettingsSnapshot> => {
373
+ if (appSettings?.writePatch) return await appSettings.writePatch(patch)
374
+ if (appSettings && patch.analyticsEnabled !== undefined && Object.keys(patch).length === 1) {
375
+ return await appSettings.write({ analyticsEnabled: patch.analyticsEnabled })
376
+ }
377
+ fallbackSnapshot = mergeAppSettingsPatch(appSettings?.getSnapshot() ?? fallbackSnapshot, patch)
378
+ return fallbackSnapshot
379
+ },
380
+
381
+ setCloudflareTunnel: async (
382
+ patch: Partial<AppSettingsSnapshot["cloudflareTunnel"]>,
383
+ ): Promise<AppSettingsSnapshot> => {
384
+ if (appSettings?.setCloudflareTunnel) return await appSettings.setCloudflareTunnel(patch)
385
+ fallbackSnapshot = mergeAppSettingsPatch(appSettings?.getSnapshot() ?? fallbackSnapshot, { cloudflareTunnel: patch })
386
+ return fallbackSnapshot
387
+ },
388
+
389
+ setClaudeAuth: async (
390
+ patch: Partial<AppSettingsSnapshot["claudeAuth"]>,
391
+ ): Promise<AppSettingsSnapshot> => {
392
+ if (appSettings?.setClaudeAuth) return await appSettings.setClaudeAuth(patch)
393
+ fallbackSnapshot = mergeAppSettingsPatch(
394
+ appSettings?.getSnapshot() ?? fallbackSnapshot,
395
+ { claudeAuth: patch },
396
+ )
397
+ return fallbackSnapshot
398
+ },
399
+
400
+ createSubagent: async (
401
+ input: Parameters<AppSettingsManager["createSubagent"]>[0],
402
+ ): ReturnType<AppSettingsManager["createSubagent"]> => {
403
+ if (appSettings?.createSubagent) return await appSettings.createSubagent(input)
404
+ const snapshot = await self.writePatch({ subagents: { create: input } })
405
+ return snapshot.subagents[snapshot.subagents.length - 1]
406
+ ?? { code: "NOT_FOUND" as const, message: "Created subagent not found" }
407
+ },
408
+
409
+ updateSubagent: async (
410
+ id: string,
411
+ patch: Parameters<AppSettingsManager["updateSubagent"]>[1],
412
+ ): ReturnType<AppSettingsManager["updateSubagent"]> => {
413
+ if (appSettings?.updateSubagent) return await appSettings.updateSubagent(id, patch)
414
+ const snapshot = await self.writePatch({ subagents: { update: { id, patch } } })
415
+ return snapshot.subagents.find((subagent) => subagent.id === id)
416
+ ?? { code: "NOT_FOUND" as const, message: `Subagent ${id} not found` }
417
+ },
418
+
419
+ deleteSubagent: async (id: string): ReturnType<AppSettingsManager["deleteSubagent"]> => {
420
+ if (appSettings?.deleteSubagent) return await appSettings.deleteSubagent(id)
421
+ await self.writePatch({ subagents: { delete: { id } } })
422
+ },
423
+
424
+ onChange: (
425
+ listener: (snapshot: AppSettingsSnapshot) => void,
426
+ ): (() => void) =>
427
+ appSettings?.onChange?.(listener) ?? (() => {}),
428
+ }
429
+
430
+ return self
431
+ }
432
+
433
+ /** The inferred return type of buildResolvedAppSettings. */
434
+ export type ResolvedAppSettings = ReturnType<typeof buildResolvedAppSettings>
@@ -0,0 +1,167 @@
1
+ import { describe, expect, mock, test } from "bun:test"
2
+ import type { DiffCommandDeps, DiffStoreDep } from "./ws-router-diff"
3
+ import { handleDiffCommand } from "./ws-router-diff"
4
+ import type { ClientCommand } from "../shared/protocol"
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function makeStore(overrides: Partial<DiffStoreDep> = {}): DiffStoreDep {
11
+ return {
12
+ refreshSnapshot: mock(async () => false),
13
+ initializeGit: mock(async () => ({ ok: true as const, branchName: "main", snapshotChanged: false })),
14
+ getGitHubPublishInfo: mock(async () => ({
15
+ ghInstalled: false,
16
+ authenticated: false,
17
+ activeAccountLogin: undefined,
18
+ owners: [],
19
+ suggestedRepoName: "my-repo",
20
+ })),
21
+ checkGitHubRepoAvailability: mock(async () => ({ available: true, message: "" })),
22
+ publishToGitHub: mock(async () => ({ ok: false, title: "", message: "", snapshotChanged: false })),
23
+ listBranches: mock(async () => ({ recent: [], local: [], remote: [], pullRequests: [], pullRequestsStatus: "unavailable" as const })),
24
+ previewMergeBranch: mock(async () => ({
25
+ currentBranchName: "main",
26
+ targetBranchName: "feat",
27
+ targetDisplayName: "feat",
28
+ status: "mergeable" as const,
29
+ commitCount: 1,
30
+ hasConflicts: false,
31
+ message: "",
32
+ })),
33
+ mergeBranch: mock(async () => ({ ok: true as const, title: "", message: "", snapshotChanged: false })),
34
+ checkoutBranch: mock(async () => ({ ok: true as const, branchName: "main", snapshotChanged: false })),
35
+ syncBranch: mock(async () => ({ ok: true as const, action: "fetch" as const, branchName: "main", snapshotChanged: false })),
36
+ createBranch: mock(async () => ({ ok: true as const, branchName: "new-branch", snapshotChanged: false })),
37
+ generateCommitMessage: mock(async () => ({ subject: "Update", body: "", usedFallback: false, failureMessage: null })),
38
+ commitFiles: mock(async () => ({ ok: true as const, mode: "commit_only" as const, branchName: "main", pushed: false, snapshotChanged: false })),
39
+ discardFile: mock(async () => ({ snapshotChanged: false })),
40
+ ignoreFile: mock(async () => ({ snapshotChanged: false })),
41
+ ...overrides,
42
+ }
43
+ }
44
+
45
+ function makeDeps(storeOverrides?: Partial<DiffStoreDep>): DiffCommandDeps & { sent: unknown[]; broadcastCount: number } {
46
+ const sent: unknown[] = []
47
+ let broadcastCount = 0
48
+ return {
49
+ resolvedDiffStore: makeStore(storeOverrides),
50
+ resolveChatProject: (chatId) => ({ project: { id: `proj-${chatId}`, localPath: `/tmp/${chatId}` } }),
51
+ send: (envelope) => { sent.push(envelope) },
52
+ broadcastSnapshots: () => { broadcastCount++ },
53
+ sent,
54
+ get broadcastCount() { return broadcastCount },
55
+ }
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Unrecognized command
60
+ // ---------------------------------------------------------------------------
61
+
62
+ describe("handleDiffCommand", () => {
63
+ test("returns false for a non-diff command", async () => {
64
+ const deps = makeDeps()
65
+ const handled = await handleDiffCommand(deps, { type: "settings.readAppSettings" } as unknown as ClientCommand, "r0")
66
+ expect(handled).toBe(false)
67
+ expect(deps.sent).toHaveLength(0)
68
+ })
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // chat.refreshDiffs
72
+ // ---------------------------------------------------------------------------
73
+
74
+ test("chat.refreshDiffs — acks and does NOT broadcast when unchanged", async () => {
75
+ const deps = makeDeps({ refreshSnapshot: mock(async () => false) })
76
+ const handled = await handleDiffCommand(deps, { type: "chat.refreshDiffs", chatId: "c1" }, "r1")
77
+ expect(handled).toBe(true)
78
+ expect(deps.sent).toHaveLength(1)
79
+ expect((deps.sent[0] as { type: string }).type).toBe("ack")
80
+ expect(deps.broadcastCount).toBe(0)
81
+ })
82
+
83
+ test("chat.refreshDiffs — broadcasts when snapshot changed", async () => {
84
+ const deps = makeDeps({ refreshSnapshot: mock(async () => true) })
85
+ await handleDiffCommand(deps, { type: "chat.refreshDiffs", chatId: "c1" }, "r2")
86
+ expect(deps.broadcastCount).toBe(1)
87
+ })
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // chat.initGit
91
+ // ---------------------------------------------------------------------------
92
+
93
+ test("chat.initGit — acks with result", async () => {
94
+ const deps = makeDeps()
95
+ const handled = await handleDiffCommand(deps, { type: "chat.initGit", chatId: "c1" }, "r3")
96
+ expect(handled).toBe(true)
97
+ const ack = deps.sent[0] as { result: { ok: boolean } }
98
+ expect(ack.result.ok).toBe(true)
99
+ })
100
+
101
+ test("chat.initGit — broadcasts when snapshotChanged", async () => {
102
+ const deps = makeDeps({ initializeGit: mock(async () => ({ ok: true as const, branchName: "main", snapshotChanged: true })) })
103
+ await handleDiffCommand(deps, { type: "chat.initGit", chatId: "c1" }, "r4")
104
+ expect(deps.broadcastCount).toBe(1)
105
+ })
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // chat.getGitHubPublishInfo — no broadcast ever
109
+ // ---------------------------------------------------------------------------
110
+
111
+ test("chat.getGitHubPublishInfo — acks and never broadcasts", async () => {
112
+ const deps = makeDeps()
113
+ const handled = await handleDiffCommand(deps, { type: "chat.getGitHubPublishInfo", chatId: "c1" }, "r5")
114
+ expect(handled).toBe(true)
115
+ expect(deps.broadcastCount).toBe(0)
116
+ })
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // chat.checkGitHubRepoAvailability — does not need chatId
120
+ // ---------------------------------------------------------------------------
121
+
122
+ test("chat.checkGitHubRepoAvailability — acks without project lookup", async () => {
123
+ const resolveSpy = mock((chatId: string) => ({ project: { id: `proj-${chatId}`, localPath: `/tmp/${chatId}` } }))
124
+ const deps = { ...makeDeps(), resolveChatProject: resolveSpy }
125
+ const handled = await handleDiffCommand(
126
+ deps,
127
+ { type: "chat.checkGitHubRepoAvailability", chatId: "c1", owner: "my-org", name: "my-repo" },
128
+ "r6",
129
+ )
130
+ expect(handled).toBe(true)
131
+ // resolveChatProject should NOT have been called — this command doesn't need a chatId
132
+ expect(resolveSpy).not.toHaveBeenCalled()
133
+ })
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // chat.commitDiffs — broadcasts on snapshotChanged
137
+ // ---------------------------------------------------------------------------
138
+
139
+ test("chat.commitDiffs — broadcasts when snapshot changed", async () => {
140
+ const deps = makeDeps({
141
+ commitFiles: mock(async () => ({ ok: true as const, mode: "commit_only" as const, branchName: "main", pushed: false, snapshotChanged: true })),
142
+ })
143
+ await handleDiffCommand(
144
+ deps,
145
+ { type: "chat.commitDiffs", chatId: "c1", paths: ["src/foo.ts"], summary: "Fix", description: "", mode: "commit_only" },
146
+ "r7",
147
+ )
148
+ expect(deps.broadcastCount).toBe(1)
149
+ })
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // chat.discardDiffFile / chat.ignoreDiffFile — no broadcast when unchanged
153
+ // ---------------------------------------------------------------------------
154
+
155
+ test("chat.discardDiffFile — does not broadcast when snapshotChanged is false", async () => {
156
+ const deps = makeDeps({ discardFile: mock(async () => ({ snapshotChanged: false })) })
157
+ const handled = await handleDiffCommand(deps, { type: "chat.discardDiffFile", chatId: "c1", path: "src/foo.ts" }, "r8")
158
+ expect(handled).toBe(true)
159
+ expect(deps.broadcastCount).toBe(0)
160
+ })
161
+
162
+ test("chat.ignoreDiffFile — broadcasts when snapshotChanged is true", async () => {
163
+ const deps = makeDeps({ ignoreFile: mock(async () => ({ snapshotChanged: true })) })
164
+ await handleDiffCommand(deps, { type: "chat.ignoreDiffFile", chatId: "c1", path: "src/foo.ts" }, "r9")
165
+ expect(deps.broadcastCount).toBe(1)
166
+ })
167
+ })