@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,190 @@
1
+ import { describe, expect, mock, test } from "bun:test"
2
+ import type { OrchCommandDeps, OrchAgentDep } from "./ws-router-orch"
3
+ import { handleOrchCommand } from "./ws-router-orch"
4
+ import type { ClientCommand } from "../shared/protocol"
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function makeAgent(overrides: Partial<OrchAgentDep> = {}): OrchAgentDep {
11
+ return {
12
+ runOrchestration: mock(async () => ({ ok: true as const, runId: "run-1" })),
13
+ cancelOrchRun: mock(async () => {}),
14
+ getOrchRunDetail: mock(() => null),
15
+ ...overrides,
16
+ }
17
+ }
18
+
19
+ function makeDeps(
20
+ agentOverrides?: Partial<OrchAgentDep>,
21
+ wfOverride?: OrchCommandDeps["workflowRegistry"],
22
+ saOverride?: OrchCommandDeps["subagentTranscriptRegistry"],
23
+ ): OrchCommandDeps & { sent: unknown[] } {
24
+ const sent: unknown[] = []
25
+ return {
26
+ agent: makeAgent(agentOverrides),
27
+ workflowRegistry: wfOverride,
28
+ subagentTranscriptRegistry: saOverride,
29
+ send: (envelope) => { sent.push(envelope) },
30
+ sent,
31
+ }
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Unrecognized command
36
+ // ---------------------------------------------------------------------------
37
+
38
+ describe("handleOrchCommand", () => {
39
+ test("returns false for a non-orch command", async () => {
40
+ const deps = makeDeps()
41
+ const handled = await handleOrchCommand(
42
+ deps,
43
+ { type: "settings.readAppSettings" } as unknown as ClientCommand,
44
+ "r0",
45
+ )
46
+ expect(handled).toBe(false)
47
+ expect(deps.sent).toHaveLength(0)
48
+ })
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // workflows.getRun
52
+ // ---------------------------------------------------------------------------
53
+
54
+ test("workflows.getRun — returns run from registry", async () => {
55
+ const run = { runId: "wf-1", taskId: "t-1", workflowName: "test" } as unknown as ReturnType<
56
+ NonNullable<OrchCommandDeps["workflowRegistry"]>["getRun"]
57
+ >
58
+ const wf: NonNullable<OrchCommandDeps["workflowRegistry"]> = {
59
+ getRun: mock(() => run),
60
+ getAgentTranscript: mock(() => []),
61
+ }
62
+ const deps = makeDeps(undefined, wf)
63
+ const handled = await handleOrchCommand(
64
+ deps,
65
+ { type: "workflows.getRun", chatId: "c-1", runId: "wf-1" },
66
+ "r1",
67
+ )
68
+ expect(handled).toBe(true)
69
+ expect(wf.getRun).toHaveBeenCalledWith("c-1", "wf-1")
70
+ expect(deps.sent).toHaveLength(1)
71
+ expect((deps.sent[0] as { result: unknown }).result).toBe(run)
72
+ })
73
+
74
+ test("workflows.getRun — returns null when registry absent", async () => {
75
+ const deps = makeDeps(undefined, undefined)
76
+ const handled = await handleOrchCommand(
77
+ deps,
78
+ { type: "workflows.getRun", chatId: "c-1", runId: "wf-1" },
79
+ "r2",
80
+ )
81
+ expect(handled).toBe(true)
82
+ expect((deps.sent[0] as { result: unknown }).result).toBeNull()
83
+ })
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // workflows.getAgentTranscript
87
+ // ---------------------------------------------------------------------------
88
+
89
+ test("workflows.getAgentTranscript — returns entries", async () => {
90
+ const entries = [{ type: "assistant", content: "hi" }] as unknown as ReturnType<
91
+ NonNullable<OrchCommandDeps["workflowRegistry"]>["getAgentTranscript"]
92
+ >
93
+ const wf: NonNullable<OrchCommandDeps["workflowRegistry"]> = {
94
+ getRun: mock(() => null),
95
+ getAgentTranscript: mock(() => entries),
96
+ }
97
+ const deps = makeDeps(undefined, wf)
98
+ const handled = await handleOrchCommand(
99
+ deps,
100
+ { type: "workflows.getAgentTranscript", chatId: "c-1", runId: "wf-1", agentId: "ag-1" },
101
+ "r3",
102
+ )
103
+ expect(handled).toBe(true)
104
+ expect(wf.getAgentTranscript).toHaveBeenCalledWith("c-1", "wf-1", "ag-1")
105
+ expect((deps.sent[0] as { result: unknown }).result).toBe(entries)
106
+ })
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // subagents.getRun
110
+ // ---------------------------------------------------------------------------
111
+
112
+ test("subagents.getRun — returns entries from transcript registry", async () => {
113
+ const entries = [{ type: "assistant", content: "hello" }] as unknown as ReturnType<
114
+ NonNullable<OrchCommandDeps["subagentTranscriptRegistry"]>["getAgentTranscript"]
115
+ >
116
+ const sa: NonNullable<OrchCommandDeps["subagentTranscriptRegistry"]> = {
117
+ getAgentTranscript: mock(() => entries),
118
+ }
119
+ const deps = makeDeps(undefined, undefined, sa)
120
+ const handled = await handleOrchCommand(
121
+ deps,
122
+ { type: "subagents.getRun", chatId: "c-1", agentId: "ag-2" },
123
+ "r4",
124
+ )
125
+ expect(handled).toBe(true)
126
+ expect(sa.getAgentTranscript).toHaveBeenCalledWith("c-1", "ag-2")
127
+ expect((deps.sent[0] as { result: unknown }).result).toBe(entries)
128
+ })
129
+
130
+ test("subagents.getRun — returns empty array when registry absent", async () => {
131
+ const deps = makeDeps(undefined, undefined, undefined)
132
+ const handled = await handleOrchCommand(
133
+ deps,
134
+ { type: "subagents.getRun", chatId: "c-1", agentId: "ag-2" },
135
+ "r5",
136
+ )
137
+ expect(handled).toBe(true)
138
+ expect((deps.sent[0] as { result: unknown }).result).toEqual([])
139
+ })
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // orch.run
143
+ // ---------------------------------------------------------------------------
144
+
145
+ test("orch.run — delegates to agent.runOrchestration and acks with result", async () => {
146
+ const orchResult = { ok: true as const, runId: "run-42" }
147
+ const deps = makeDeps({ runOrchestration: mock(async () => orchResult) })
148
+ const handled = await handleOrchCommand(
149
+ deps,
150
+ { type: "orch.run", chatId: "c-1", input: { tasks: ["task A"] } },
151
+ "r6",
152
+ )
153
+ expect(handled).toBe(true)
154
+ expect(deps.agent.runOrchestration).toHaveBeenCalledWith("c-1", { tasks: ["task A"] })
155
+ expect((deps.sent[0] as { result: unknown }).result).toEqual(orchResult)
156
+ })
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // orch.cancelRun
160
+ // ---------------------------------------------------------------------------
161
+
162
+ test("orch.cancelRun — delegates to agent.cancelOrchRun and acks ok", async () => {
163
+ const deps = makeDeps()
164
+ const handled = await handleOrchCommand(
165
+ deps,
166
+ { type: "orch.cancelRun", runId: "run-42" },
167
+ "r7",
168
+ )
169
+ expect(handled).toBe(true)
170
+ expect(deps.agent.cancelOrchRun).toHaveBeenCalledWith("run-42")
171
+ expect((deps.sent[0] as { result: unknown }).result).toEqual({ ok: true })
172
+ })
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // orch.getRun
176
+ // ---------------------------------------------------------------------------
177
+
178
+ test("orch.getRun — delegates to agent.getOrchRunDetail and acks with detail", async () => {
179
+ const detail = { runId: "run-42", status: "running" } as unknown as ReturnType<OrchAgentDep["getOrchRunDetail"]>
180
+ const deps = makeDeps({ getOrchRunDetail: mock(() => detail) })
181
+ const handled = await handleOrchCommand(
182
+ deps,
183
+ { type: "orch.getRun", runId: "run-42" },
184
+ "r8",
185
+ )
186
+ expect(handled).toBe(true)
187
+ expect(deps.agent.getOrchRunDetail).toHaveBeenCalledWith("run-42")
188
+ expect((deps.sent[0] as { result: unknown }).result).toBe(detail)
189
+ })
190
+ })
@@ -0,0 +1,93 @@
1
+ /**
2
+ * ws-router-orch.ts
3
+ *
4
+ * WS command handlers for orchestration, workflow observability, and
5
+ * subagent-transcript queries:
6
+ * orch.run, orch.cancelRun, orch.getRun,
7
+ * workflows.getRun, workflows.getAgentTranscript,
8
+ * subagents.getRun
9
+ *
10
+ * Extracted from ws-router.ts.
11
+ */
12
+ import { PROTOCOL_VERSION } from "../shared/types"
13
+ import type { ClientCommand, ServerEnvelope } from "../shared/protocol"
14
+ import type { OrchRunInput, OrchRunDetail } from "../shared/orchestration-types"
15
+ import type { WorkflowRegistry } from "./workflow-registry"
16
+ import type { SubagentTranscriptRegistry } from "./subagent-transcript-registry"
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Dep interface (duck-typed; avoids circular imports with ws-router.ts)
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** The subset of AgentCoordinator methods consumed by orch/workflow WS commands. */
23
+ export interface OrchAgentDep {
24
+ runOrchestration(
25
+ chatId: string,
26
+ input: OrchRunInput,
27
+ ): Promise<{ ok: true; runId: string } | { ok: false; errors: string[] }>
28
+ cancelOrchRun(runId: string): Promise<void>
29
+ getOrchRunDetail(runId: string): OrchRunDetail | null
30
+ }
31
+
32
+ export interface OrchCommandDeps {
33
+ /** Orchestration methods from AgentCoordinator. */
34
+ agent: OrchAgentDep
35
+ /** Optional workflow registry (may be absent if not configured). */
36
+ workflowRegistry: Pick<WorkflowRegistry, "getRun" | "getAgentTranscript"> | undefined
37
+ /** Optional subagent transcript registry. */
38
+ subagentTranscriptRegistry: Pick<SubagentTranscriptRegistry, "getAgentTranscript"> | undefined
39
+ /** Pre-bound to the current WebSocket; called to send an ack envelope. */
40
+ send: (envelope: ServerEnvelope) => void
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Command dispatcher
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Handle one orchestration/workflow/subagent-transcript WS command.
49
+ *
50
+ * Returns `true` when the command was handled (caller should `return`).
51
+ * Returns `false` when the command type is outside this module's scope.
52
+ */
53
+ export async function handleOrchCommand(
54
+ deps: OrchCommandDeps,
55
+ command: ClientCommand,
56
+ id: string,
57
+ ): Promise<boolean> {
58
+ const { agent, workflowRegistry, subagentTranscriptRegistry, send } = deps
59
+
60
+ switch (command.type) {
61
+ case "workflows.getRun": {
62
+ const run = workflowRegistry?.getRun(command.chatId, command.runId) ?? null
63
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result: run })
64
+ return true
65
+ }
66
+ case "workflows.getAgentTranscript": {
67
+ const entries = workflowRegistry?.getAgentTranscript(command.chatId, command.runId, command.agentId) ?? []
68
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result: entries })
69
+ return true
70
+ }
71
+ case "subagents.getRun": {
72
+ const entries = subagentTranscriptRegistry?.getAgentTranscript(command.chatId, command.agentId) ?? []
73
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result: entries })
74
+ return true
75
+ }
76
+ case "orch.run": {
77
+ const result = await agent.runOrchestration(command.chatId, command.input)
78
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result })
79
+ return true
80
+ }
81
+ case "orch.cancelRun": {
82
+ await agent.cancelOrchRun(command.runId)
83
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result: { ok: true } })
84
+ return true
85
+ }
86
+ case "orch.getRun": {
87
+ send({ v: PROTOCOL_VERSION, type: "ack", id, result: agent.getOrchRunDetail(command.runId) })
88
+ return true
89
+ }
90
+ default:
91
+ return false
92
+ }
93
+ }
@@ -0,0 +1,403 @@
1
+ /**
2
+ * ws-router-project.test.ts
3
+ *
4
+ * Unit tests for the project / session / sidebar / system / update WS command handlers.
5
+ */
6
+ import { describe, expect, mock, test } from "bun:test"
7
+ import type {
8
+ ProjectAnalyticsDep,
9
+ ProjectCommandDeps,
10
+ ProjectDiffStoreDep,
11
+ ProjectStoreDep,
12
+ ProjectTerminalsDep,
13
+ ProjectUpdateManagerDep,
14
+ } from "./ws-router-project"
15
+ import { handleProjectCommand } from "./ws-router-project"
16
+ import type { ClientCommand, ServerEnvelope } from "../shared/protocol"
17
+ import type { UpdateInstallResult, UpdateSnapshot } from "../shared/types"
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Helpers
21
+ // ---------------------------------------------------------------------------
22
+
23
+ function makeStore(overrides: Partial<ProjectStoreDep> = {}): ProjectStoreDep {
24
+ return {
25
+ state: { projectIdsByPath: new Map() },
26
+ openProject: mock(async () => ({ id: "proj-1" })),
27
+ removeProject: mock(async () => {}),
28
+ getProject: mock(() => ({ id: "proj-1", localPath: "/tmp/proj" })),
29
+ setProjectStar: mock(async () => {}),
30
+ setSidebarProjectOrder: mock(async () => {}),
31
+ ...overrides,
32
+ }
33
+ }
34
+
35
+ function makeUpdateManager(overrides: Partial<ProjectUpdateManagerDep> = {}): ProjectUpdateManagerDep {
36
+ const base: UpdateSnapshot = {
37
+ currentVersion: "1.0.0",
38
+ latestVersion: "1.1.0",
39
+ status: "idle",
40
+ updateAvailable: true,
41
+ lastCheckedAt: null,
42
+ error: null,
43
+ installAction: "restart",
44
+ reloadRequestedAt: null,
45
+ }
46
+ const installResult: UpdateInstallResult = {
47
+ ok: true,
48
+ action: "restart",
49
+ errorCode: null,
50
+ userTitle: null,
51
+ userMessage: null,
52
+ }
53
+ return {
54
+ checkForUpdates: mock(async () => base),
55
+ installUpdate: mock(async () => installResult),
56
+ forceReload: mock(async () => installResult),
57
+ ...overrides,
58
+ }
59
+ }
60
+
61
+ function makeAnalytics(): ProjectAnalyticsDep & { events: string[] } {
62
+ const events: string[] = []
63
+ return {
64
+ events,
65
+ track: (event: string) => { events.push(event) },
66
+ }
67
+ }
68
+
69
+ function makeDiffStore(): ProjectDiffStoreDep {
70
+ return {
71
+ readPatch: mock(async () => ({ patch: "diff content" })),
72
+ }
73
+ }
74
+
75
+ function makeTerminals(): ProjectTerminalsDep {
76
+ return { closeByCwd: mock(() => {}) }
77
+ }
78
+
79
+ interface TestDeps extends ProjectCommandDeps {
80
+ sent: ServerEnvelope[]
81
+ sidebarBroadcasts: number
82
+ }
83
+
84
+ function makeDeps(
85
+ options: {
86
+ storeOverrides?: Partial<ProjectStoreDep>
87
+ updateManager?: ProjectUpdateManagerDep | null
88
+ importResult?: Partial<{ imported: number; updated: number; skipped: number; failed: number; newProjects: number }>
89
+ openExternalFn?: () => Promise<void>
90
+ } = {},
91
+ ): TestDeps {
92
+ const sent: ServerEnvelope[] = []
93
+ let sidebarBroadcasts = 0
94
+ const analytics = makeAnalytics()
95
+
96
+ return {
97
+ store: makeStore(options.storeOverrides),
98
+ updateManager: options.updateManager !== undefined ? options.updateManager : makeUpdateManager(),
99
+ diffStore: makeDiffStore(),
100
+ analytics,
101
+ terminals: makeTerminals(),
102
+ refreshDiscovery: mock(async () => []),
103
+ ensureProjectDirectory: mock(async () => {}),
104
+ resolveLocalPath: (p) => p,
105
+ importClaudeSessionsFn: mock(async () => ({
106
+ imported: 1,
107
+ updated: 0,
108
+ skipped: 0,
109
+ failed: 0,
110
+ newProjects: options.importResult?.newProjects ?? 0,
111
+ ...options.importResult,
112
+ })),
113
+ openExternalFn: options.openExternalFn ?? mock(async () => {}),
114
+ send: (envelope) => { sent.push(envelope) },
115
+ broadcastSidebar: mock(async () => { sidebarBroadcasts++ }),
116
+ sent,
117
+ get sidebarBroadcasts() { return sidebarBroadcasts },
118
+ }
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Tests
123
+ // ---------------------------------------------------------------------------
124
+
125
+ describe("handleProjectCommand", () => {
126
+ // -------------------------------------------------------------------------
127
+ // Unknown / out-of-scope
128
+ // -------------------------------------------------------------------------
129
+
130
+ test("returns false for a non-project command", async () => {
131
+ const deps = makeDeps()
132
+ const handled = await handleProjectCommand(
133
+ deps,
134
+ { type: "chat.create" } as unknown as ClientCommand,
135
+ "r0",
136
+ )
137
+ expect(handled).toBe(false)
138
+ expect(deps.sent).toHaveLength(0)
139
+ })
140
+
141
+ // -------------------------------------------------------------------------
142
+ // system.ping
143
+ // -------------------------------------------------------------------------
144
+
145
+ test("system.ping — sends ack, returns true, no broadcast", async () => {
146
+ const deps = makeDeps()
147
+ const handled = await handleProjectCommand(deps, { type: "system.ping" }, "r1")
148
+ expect(handled).toBe(true)
149
+ expect(deps.sent).toHaveLength(1)
150
+ expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r1" })
151
+ expect(deps.sidebarBroadcasts).toBe(0)
152
+ })
153
+
154
+ // -------------------------------------------------------------------------
155
+ // update.check
156
+ // -------------------------------------------------------------------------
157
+
158
+ test("update.check — no updateManager — returns fallback snapshot", async () => {
159
+ const deps = makeDeps({ updateManager: null })
160
+ const handled = await handleProjectCommand(
161
+ deps,
162
+ { type: "update.check", force: false },
163
+ "r2",
164
+ )
165
+ expect(handled).toBe(true)
166
+ expect(deps.sent).toHaveLength(1)
167
+ const ack = deps.sent[0] as { type: string; result: { status: string; error: string } }
168
+ expect(ack.result.status).toBe("error")
169
+ expect(ack.result.error).toContain("unavailable")
170
+ })
171
+
172
+ test("update.check — with updateManager — calls checkForUpdates", async () => {
173
+ const um = makeUpdateManager()
174
+ const deps = makeDeps({ updateManager: um })
175
+ const handled = await handleProjectCommand(
176
+ deps,
177
+ { type: "update.check", force: true },
178
+ "r3",
179
+ )
180
+ expect(handled).toBe(true)
181
+ expect(um.checkForUpdates as ReturnType<typeof mock>).toHaveBeenCalledWith({ force: true })
182
+ const ack = deps.sent[0] as { type: string; result: { currentVersion: string } }
183
+ expect(ack.result.currentVersion).toBe("1.0.0")
184
+ })
185
+
186
+ test("update.install — no updateManager — throws", async () => {
187
+ const deps = makeDeps({ updateManager: null })
188
+ await expect(
189
+ handleProjectCommand(deps, { type: "update.install", version: "1.1.0" }, "r4"),
190
+ ).rejects.toThrow("unavailable")
191
+ })
192
+
193
+ test("update.reload — calls forceReload, acks", async () => {
194
+ const um = makeUpdateManager()
195
+ const deps = makeDeps({ updateManager: um })
196
+ const handled = await handleProjectCommand(deps, { type: "update.reload" }, "r5")
197
+ expect(handled).toBe(true)
198
+ expect(um.forceReload as ReturnType<typeof mock>).toHaveBeenCalled()
199
+ expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r5" })
200
+ })
201
+
202
+ // -------------------------------------------------------------------------
203
+ // project.open
204
+ // -------------------------------------------------------------------------
205
+
206
+ test("project.open — new project — tracks analytics, acks with projectId", async () => {
207
+ const deps = makeDeps({
208
+ storeOverrides: {
209
+ state: { projectIdsByPath: new Map() }, // path not in map → new
210
+ openProject: mock(async () => ({ id: "new-proj" })),
211
+ },
212
+ })
213
+ const handled = await handleProjectCommand(
214
+ deps,
215
+ { type: "project.open", localPath: "/tmp/newproj" },
216
+ "r6",
217
+ )
218
+ expect(handled).toBe(true)
219
+ const ack = deps.sent[0] as { type: string; result: { projectId: string } }
220
+ expect(ack.result.projectId).toBe("new-proj")
221
+ expect((deps.analytics as ReturnType<typeof makeAnalytics>).events).toContain("project_opened")
222
+ expect(deps.ensureProjectDirectory as ReturnType<typeof mock>).toHaveBeenCalledWith("/tmp/newproj")
223
+ expect(deps.refreshDiscovery as ReturnType<typeof mock>).toHaveBeenCalled()
224
+ })
225
+
226
+ test("project.open — existing project — skips analytics", async () => {
227
+ const deps = makeDeps({
228
+ storeOverrides: {
229
+ state: { projectIdsByPath: new Map([["/tmp/existing", "old-id"]]) },
230
+ openProject: mock(async () => ({ id: "old-id" })),
231
+ },
232
+ })
233
+ await handleProjectCommand(
234
+ deps,
235
+ { type: "project.open", localPath: "/tmp/existing" },
236
+ "r7",
237
+ )
238
+ expect((deps.analytics as ReturnType<typeof makeAnalytics>).events).not.toContain("project_opened")
239
+ })
240
+
241
+ // -------------------------------------------------------------------------
242
+ // project.create
243
+ // -------------------------------------------------------------------------
244
+
245
+ test("project.create — new project — tracks both analytics events, acks", async () => {
246
+ const deps = makeDeps({
247
+ storeOverrides: {
248
+ state: { projectIdsByPath: new Map() },
249
+ openProject: mock(async () => ({ id: "created-proj" })),
250
+ },
251
+ })
252
+ const handled = await handleProjectCommand(
253
+ deps,
254
+ { type: "project.create", localPath: "/tmp/created", title: "My App" },
255
+ "r8",
256
+ )
257
+ expect(handled).toBe(true)
258
+ const evts = (deps.analytics as ReturnType<typeof makeAnalytics>).events
259
+ expect(evts).toContain("project_opened")
260
+ expect(evts).toContain("project_created")
261
+ const ack = deps.sent[0] as { type: string; result: { projectId: string } }
262
+ expect(ack.result.projectId).toBe("created-proj")
263
+ })
264
+
265
+ // -------------------------------------------------------------------------
266
+ // project.remove
267
+ // -------------------------------------------------------------------------
268
+
269
+ test("project.remove — removes project, closes terminal, tracks analytics", async () => {
270
+ const closeByCwd = mock(() => {})
271
+ const removeProject = mock(async () => {})
272
+ const deps = makeDeps({
273
+ storeOverrides: {
274
+ getProject: mock(() => ({ id: "p1", localPath: "/tmp/proj" })),
275
+ removeProject,
276
+ },
277
+ })
278
+ ;(deps.terminals as ProjectTerminalsDep).closeByCwd = closeByCwd
279
+
280
+ const handled = await handleProjectCommand(
281
+ deps,
282
+ { type: "project.remove", projectId: "p1" },
283
+ "r9",
284
+ )
285
+ expect(handled).toBe(true)
286
+ expect(removeProject as ReturnType<typeof mock>).toHaveBeenCalledWith("p1")
287
+ expect(closeByCwd as ReturnType<typeof mock>).toHaveBeenCalledWith("/tmp/proj")
288
+ expect((deps.analytics as ReturnType<typeof makeAnalytics>).events).toContain("project_removed")
289
+ expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r9" })
290
+ })
291
+
292
+ // -------------------------------------------------------------------------
293
+ // project.setStar
294
+ // -------------------------------------------------------------------------
295
+
296
+ test("project.setStar — sets star, acks, broadcasts sidebar", async () => {
297
+ const setProjectStar = mock(async () => {})
298
+ const deps = makeDeps({ storeOverrides: { setProjectStar } })
299
+ const handled = await handleProjectCommand(
300
+ deps,
301
+ { type: "project.setStar", projectId: "p2", starred: true },
302
+ "r10",
303
+ )
304
+ expect(handled).toBe(true)
305
+ expect(setProjectStar as ReturnType<typeof mock>).toHaveBeenCalledWith("p2", true)
306
+ expect(deps.sidebarBroadcasts).toBe(1)
307
+ expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r10" })
308
+ })
309
+
310
+ // -------------------------------------------------------------------------
311
+ // project.readDiffPatch
312
+ // -------------------------------------------------------------------------
313
+
314
+ test("project.readDiffPatch — returns patch content", async () => {
315
+ const deps = makeDeps({
316
+ storeOverrides: {
317
+ getProject: mock(() => ({ id: "p3", localPath: "/tmp/proj3" })),
318
+ },
319
+ })
320
+ const handled = await handleProjectCommand(
321
+ deps,
322
+ { type: "project.readDiffPatch", projectId: "p3", path: "src/foo.ts" },
323
+ "r11",
324
+ )
325
+ expect(handled).toBe(true)
326
+ const ack = deps.sent[0] as { type: string; result: { patch: string } }
327
+ expect(ack.result.patch).toBe("diff content")
328
+ expect(deps.diffStore.readPatch as ReturnType<typeof mock>).toHaveBeenCalledWith({
329
+ projectPath: "/tmp/proj3",
330
+ path: "src/foo.ts",
331
+ })
332
+ })
333
+
334
+ test("project.readDiffPatch — missing project — throws Project not found", async () => {
335
+ const deps = makeDeps({
336
+ storeOverrides: {
337
+ getProject: mock(() => undefined),
338
+ },
339
+ })
340
+ await expect(
341
+ handleProjectCommand(
342
+ deps,
343
+ { type: "project.readDiffPatch", projectId: "ghost", path: "foo.ts" },
344
+ "r12",
345
+ ),
346
+ ).rejects.toThrow("Project not found")
347
+ })
348
+
349
+ // -------------------------------------------------------------------------
350
+ // sessions.importClaude
351
+ // -------------------------------------------------------------------------
352
+
353
+ test("sessions.importClaude — imports sessions, acks, broadcasts sidebar", async () => {
354
+ const deps = makeDeps({ importResult: { imported: 3, newProjects: 0 } })
355
+ const handled = await handleProjectCommand(deps, { type: "sessions.importClaude" }, "r13")
356
+ expect(handled).toBe(true)
357
+ const ack = deps.sent[0] as { type: string; result: { imported: number } }
358
+ expect(ack.result.imported).toBe(3)
359
+ expect(deps.sidebarBroadcasts).toBe(1)
360
+ })
361
+
362
+ test("sessions.importClaude — newProjects > 0 — calls refreshDiscovery", async () => {
363
+ const deps = makeDeps({ importResult: { imported: 1, newProjects: 2 } })
364
+ await handleProjectCommand(deps, { type: "sessions.importClaude" }, "r14")
365
+ expect(deps.refreshDiscovery as ReturnType<typeof mock>).toHaveBeenCalled()
366
+ })
367
+
368
+ // -------------------------------------------------------------------------
369
+ // sidebar.reorderProjectGroups
370
+ // -------------------------------------------------------------------------
371
+
372
+ test("sidebar.reorderProjectGroups — sets order, acks, broadcasts sidebar", async () => {
373
+ const setSidebarProjectOrder = mock(async () => {})
374
+ const deps = makeDeps({ storeOverrides: { setSidebarProjectOrder } })
375
+ const handled = await handleProjectCommand(
376
+ deps,
377
+ { type: "sidebar.reorderProjectGroups", projectIds: ["p1", "p2"] },
378
+ "r15",
379
+ )
380
+ expect(handled).toBe(true)
381
+ expect(setSidebarProjectOrder as ReturnType<typeof mock>).toHaveBeenCalledWith(["p1", "p2"])
382
+ expect(deps.sidebarBroadcasts).toBe(1)
383
+ expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r15" })
384
+ })
385
+
386
+ // -------------------------------------------------------------------------
387
+ // system.openExternal
388
+ // -------------------------------------------------------------------------
389
+
390
+ test("system.openExternal — calls openExternalFn, acks", async () => {
391
+ const openExternalFn = mock(async () => {})
392
+ const deps = makeDeps({ openExternalFn })
393
+ const cmd: ClientCommand = {
394
+ type: "system.openExternal",
395
+ kind: "external",
396
+ target: "https://example.com",
397
+ } as unknown as ClientCommand
398
+ const handled = await handleProjectCommand(deps, cmd, "r16")
399
+ expect(handled).toBe(true)
400
+ expect(openExternalFn as ReturnType<typeof mock>).toHaveBeenCalledWith(cmd)
401
+ expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r16" })
402
+ })
403
+ })