@cuongtran001/kanna 1.3.0 → 1.3.1

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 (186) hide show
  1. package/dist/client/assets/{arc-CAysfmX5.js → arc-DXtN3EWJ.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
  5. package/dist/client/assets/channel-BEYTrUAm.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
  7. package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
  8. package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
  11. package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
  12. package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
  13. package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
  14. package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
  15. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
  16. package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
  17. package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
  18. package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
  19. package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
  20. package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
  21. package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
  22. package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
  23. package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
  24. package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
  25. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
  26. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
  27. package/dist/client/assets/{index-CbSY9HfS.js → index-BqUY1Bm4.js} +1 -1
  28. package/dist/client/assets/{index-C0U2kwuB.js → index-TU1k6ov6.js} +5 -5
  29. package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
  30. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
  31. package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
  32. package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
  33. package/dist/client/assets/{linear-DLR6miy3.js → linear-ByKHtsIx.js} +1 -1
  34. package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-Dl_f2KLc.js} +4 -4
  35. package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
  36. package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
  37. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
  38. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
  39. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
  40. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
  41. package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
  42. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
  43. package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
  44. package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
  45. package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
  46. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
  47. package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
  48. package/dist/client/index.html +1 -1
  49. package/package.json +1 -1
  50. package/src/server/agent-coordinator-types.ts +158 -0
  51. package/src/server/agent-coordinator.ts +919 -0
  52. package/src/server/agent-deps-builders.ts +425 -0
  53. package/src/server/agent.test.ts +22 -6
  54. package/src/server/agent.ts +35 -4987
  55. package/src/server/claude-autocontinue-commands.test.ts +494 -0
  56. package/src/server/claude-autocontinue-commands.ts +256 -0
  57. package/src/server/claude-cancel-handler.test.ts +472 -0
  58. package/src/server/claude-cancel-handler.ts +241 -0
  59. package/src/server/claude-chat-management.test.ts +395 -0
  60. package/src/server/claude-chat-management.ts +251 -0
  61. package/src/server/claude-harness-stream.test.ts +273 -0
  62. package/src/server/claude-harness-stream.ts +158 -0
  63. package/src/server/claude-loop-orch-commands.test.ts +579 -0
  64. package/src/server/claude-loop-orch-commands.ts +466 -0
  65. package/src/server/claude-message-normalizer.test.ts +213 -0
  66. package/src/server/claude-message-normalizer.ts +436 -0
  67. package/src/server/claude-prompt-helpers.test.ts +248 -0
  68. package/src/server/claude-prompt-helpers.ts +139 -0
  69. package/src/server/claude-sdk-queue.test.ts +169 -0
  70. package/src/server/claude-sdk-queue.ts +82 -0
  71. package/src/server/claude-send-command.test.ts +414 -0
  72. package/src/server/claude-send-command.ts +420 -0
  73. package/src/server/claude-session-config-helpers.test.ts +279 -0
  74. package/src/server/claude-session-config-helpers.ts +151 -0
  75. package/src/server/claude-session-config.test.ts +250 -0
  76. package/src/server/claude-session-config.ts +159 -0
  77. package/src/server/claude-session-error-handler.test.ts +498 -0
  78. package/src/server/claude-session-error-handler.ts +361 -0
  79. package/src/server/claude-session-lifecycle.test.ts +669 -0
  80. package/src/server/claude-session-lifecycle.ts +283 -0
  81. package/src/server/claude-session-rebuild.test.ts +224 -0
  82. package/src/server/claude-session-rebuild.ts +101 -0
  83. package/src/server/claude-session-runner.test.ts +523 -0
  84. package/src/server/claude-session-runner.ts +521 -0
  85. package/src/server/claude-session-spawner.test.ts +401 -0
  86. package/src/server/claude-session-spawner.ts +401 -0
  87. package/src/server/claude-session-start.test.ts +205 -0
  88. package/src/server/claude-session-start.ts +277 -0
  89. package/src/server/claude-session-state-queries.test.ts +344 -0
  90. package/src/server/claude-session-state-queries.ts +160 -0
  91. package/src/server/claude-session-state.ts +104 -0
  92. package/src/server/claude-slash-commands.test.ts +277 -0
  93. package/src/server/claude-slash-commands.ts +234 -0
  94. package/src/server/claude-spawn-helpers.test.ts +221 -0
  95. package/src/server/claude-spawn-helpers.ts +179 -0
  96. package/src/server/claude-steer-log.test.ts +243 -0
  97. package/src/server/claude-steer-log.ts +58 -0
  98. package/src/server/claude-subagent-tool-response.test.ts +213 -0
  99. package/src/server/claude-subagent-tool-response.ts +165 -0
  100. package/src/server/claude-subagent-wiring.test.ts +263 -0
  101. package/src/server/claude-subagent-wiring.ts +324 -0
  102. package/src/server/claude-tool-respond.test.ts +321 -0
  103. package/src/server/claude-tool-respond.ts +142 -0
  104. package/src/server/claude-turn-runner.test.ts +310 -0
  105. package/src/server/claude-turn-runner.ts +228 -0
  106. package/src/server/claude-turn-starter.test.ts +266 -0
  107. package/src/server/claude-turn-starter.ts +566 -0
  108. package/src/server/claude-usage-math.test.ts +169 -0
  109. package/src/server/claude-usage-math.ts +132 -0
  110. package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
  111. package/src/server/diff-store-branch-ops.adapter.ts +602 -0
  112. package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
  113. package/src/server/diff-store-commit-ops.adapter.ts +258 -0
  114. package/src/server/diff-store-errors.ts +85 -0
  115. package/src/server/diff-store-file-ops.adapter.ts +212 -0
  116. package/src/server/diff-store-git-branch.adapter.ts +686 -0
  117. package/src/server/diff-store-io.adapter.ts +27 -0
  118. package/src/server/diff-store-parse.test.ts +212 -0
  119. package/src/server/diff-store-parse.ts +122 -0
  120. package/src/server/diff-store-state.ts +82 -0
  121. package/src/server/diff-store.ts +76 -1854
  122. package/src/server/event-store-apply.test.ts +280 -0
  123. package/src/server/event-store-apply.ts +132 -0
  124. package/src/server/event-store-chat-lifecycle.test.ts +408 -0
  125. package/src/server/event-store-chat-lifecycle.ts +441 -0
  126. package/src/server/event-store-entity-write.ts +356 -0
  127. package/src/server/event-store-helpers.test.ts +230 -0
  128. package/src/server/event-store-helpers.ts +235 -0
  129. package/src/server/event-store-init.ts +276 -0
  130. package/src/server/event-store-messages.adapter.test.ts +365 -0
  131. package/src/server/event-store-messages.adapter.ts +213 -0
  132. package/src/server/event-store-orch-subscription.ts +124 -0
  133. package/src/server/event-store-orch.test.ts +462 -0
  134. package/src/server/event-store-orch.ts +312 -0
  135. package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
  136. package/src/server/event-store-peripheral-events.adapter.ts +105 -0
  137. package/src/server/event-store-snapshot.test.ts +597 -0
  138. package/src/server/event-store-snapshot.ts +658 -0
  139. package/src/server/event-store-subagent.test.ts +408 -0
  140. package/src/server/event-store-subagent.ts +198 -0
  141. package/src/server/event-store-tool-requests.test.ts +210 -0
  142. package/src/server/event-store-tool-requests.ts +131 -0
  143. package/src/server/event-store-transcript-write.adapter.ts +285 -0
  144. package/src/server/event-store-write-ops.test.ts +554 -0
  145. package/src/server/event-store-write-ops.ts +529 -0
  146. package/src/server/event-store.test.ts +35 -0
  147. package/src/server/event-store.ts +251 -2285
  148. package/src/server/harness-types.ts +15 -1
  149. package/src/server/oauth-errors.ts +19 -0
  150. package/src/server/ws-router-agent-ctrl.test.ts +187 -0
  151. package/src/server/ws-router-agent-ctrl.ts +137 -0
  152. package/src/server/ws-router-broadcast.ts +488 -0
  153. package/src/server/ws-router-chat.test.ts +461 -0
  154. package/src/server/ws-router-chat.ts +279 -0
  155. package/src/server/ws-router-defaults.test.ts +283 -0
  156. package/src/server/ws-router-defaults.ts +434 -0
  157. package/src/server/ws-router-diff.test.ts +167 -0
  158. package/src/server/ws-router-diff.ts +250 -0
  159. package/src/server/ws-router-envelope.ts +329 -0
  160. package/src/server/ws-router-misc.test.ts +422 -0
  161. package/src/server/ws-router-misc.ts +265 -0
  162. package/src/server/ws-router-orch.test.ts +190 -0
  163. package/src/server/ws-router-orch.ts +93 -0
  164. package/src/server/ws-router-project.test.ts +403 -0
  165. package/src/server/ws-router-project.ts +256 -0
  166. package/src/server/ws-router-push.test.ts +193 -0
  167. package/src/server/ws-router-push.ts +120 -0
  168. package/src/server/ws-router-settings.test.ts +173 -0
  169. package/src/server/ws-router-settings.ts +416 -0
  170. package/src/server/ws-router-skills.test.ts +194 -0
  171. package/src/server/ws-router-skills.ts +204 -0
  172. package/src/server/ws-router-utils.ts +224 -0
  173. package/src/server/ws-router.ts +339 -2254
  174. package/src/shared/app-settings-types.ts +365 -0
  175. package/src/shared/core-types.ts +27 -0
  176. package/src/shared/git-diff-types.ts +165 -0
  177. package/src/shared/mcp-types.ts +105 -0
  178. package/src/shared/provider-model-types.ts +437 -0
  179. package/src/shared/subagent-types.ts +198 -0
  180. package/src/shared/tool-call-types.ts +314 -0
  181. package/src/shared/transcript-types.ts +270 -0
  182. package/src/shared/types.ts +25 -1711
  183. package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
  184. package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
  185. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
  186. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
@@ -0,0 +1,408 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { AgentProvider } from "../shared/types"
3
+ import type { ChatRecord, ChatTimingState, ProjectRecord, StackRecord, StoreState } from "./events"
4
+ import type { AutoContinueEvent } from "./auto-continue/events"
5
+ import {
6
+ applyChatLifecycleEvent,
7
+ applyChatMessageMetadata,
8
+ applyAutoContinueToState,
9
+ applyProjectEvent,
10
+ applyStackEvent,
11
+ updateChatTiming,
12
+ } from "./event-store-chat-lifecycle"
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const TS = 1_700_000_000_000
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function makeProjectState(): Pick<StoreState, "projectsById" | "projectIdsByPath" | "sidebarProjectOrder"> {
25
+ return {
26
+ projectsById: new Map(),
27
+ projectIdsByPath: new Map(),
28
+ sidebarProjectOrder: [],
29
+ }
30
+ }
31
+
32
+ function makeStacksById(): Map<string, StackRecord> {
33
+ return new Map()
34
+ }
35
+
36
+ function makeTimings(): Map<string, ChatTimingState> {
37
+ return new Map()
38
+ }
39
+
40
+ function makeChatLifecycleState(): Pick<
41
+ StoreState,
42
+ "chatsById" | "queuedMessagesByChatId" | "autoContinueEventsByChatId" | "chatTimingsByChatId" | "subagentRunsByChatId"
43
+ > {
44
+ return {
45
+ chatsById: new Map(),
46
+ queuedMessagesByChatId: new Map(),
47
+ autoContinueEventsByChatId: new Map(),
48
+ chatTimingsByChatId: new Map(),
49
+ subagentRunsByChatId: new Map(),
50
+ }
51
+ }
52
+
53
+ function makeReplayChatProvider(): Map<string, AgentProvider | null> {
54
+ return new Map()
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // applyProjectEvent
59
+ // ---------------------------------------------------------------------------
60
+
61
+ describe("applyProjectEvent", () => {
62
+ test("project_opened creates project and maps localPath", () => {
63
+ const state = makeProjectState()
64
+ applyProjectEvent(state, {
65
+ v: 3,
66
+ type: "project_opened",
67
+ timestamp: TS,
68
+ projectId: "proj-1",
69
+ localPath: "/home/user/repo",
70
+ title: "My Repo",
71
+ })
72
+ const project = state.projectsById.get("proj-1")
73
+ expect(project).toBeDefined()
74
+ expect(project!.title).toBe("My Repo")
75
+ expect(state.projectIdsByPath.get(project!.localPath)).toBe("proj-1")
76
+ })
77
+
78
+ test("project_removed soft-deletes and removes path index", () => {
79
+ const state = makeProjectState()
80
+ const project: ProjectRecord = { id: "proj-1", localPath: "/tmp/repo", title: "R", createdAt: TS, updatedAt: TS }
81
+ state.projectsById.set("proj-1", project)
82
+ state.projectIdsByPath.set("/tmp/repo", "proj-1")
83
+
84
+ applyProjectEvent(state, { v: 3, type: "project_removed", timestamp: TS + 1, projectId: "proj-1" })
85
+
86
+ expect(project.deletedAt).toBe(TS + 1)
87
+ expect(state.projectIdsByPath.has("/tmp/repo")).toBe(false)
88
+ })
89
+
90
+ test("project_removed is a no-op for unknown project", () => {
91
+ const state = makeProjectState()
92
+ // Should not throw
93
+ applyProjectEvent(state, { v: 3, type: "project_removed", timestamp: TS, projectId: "nope" })
94
+ expect(state.projectsById.size).toBe(0)
95
+ })
96
+
97
+ test("sidebar_project_order_set replaces sidebarProjectOrder", () => {
98
+ const state = makeProjectState()
99
+ state.sidebarProjectOrder = ["old-1"]
100
+ applyProjectEvent(state, { v: 3, type: "sidebar_project_order_set", timestamp: TS, projectIds: ["p1", "p2"] })
101
+ expect(state.sidebarProjectOrder).toEqual(["p1", "p2"])
102
+ })
103
+
104
+ test("project_star_set sets and clears starredAt", () => {
105
+ const state = makeProjectState()
106
+ const project: ProjectRecord = { id: "proj-1", localPath: "/tmp", title: "R", createdAt: TS, updatedAt: TS }
107
+ state.projectsById.set("proj-1", project)
108
+
109
+ applyProjectEvent(state, { v: 3, type: "project_star_set", timestamp: TS + 1, projectId: "proj-1", starredAt: TS + 1 })
110
+ expect(project.starredAt).toBe(TS + 1)
111
+
112
+ applyProjectEvent(state, { v: 3, type: "project_star_set", timestamp: TS + 2, projectId: "proj-1", starredAt: null })
113
+ expect(project.starredAt).toBeUndefined()
114
+ })
115
+ })
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // applyStackEvent
119
+ // ---------------------------------------------------------------------------
120
+
121
+ describe("applyStackEvent", () => {
122
+ test("stack_added creates stack", () => {
123
+ const m = makeStacksById()
124
+ applyStackEvent(m, { v: 3, type: "stack_added", timestamp: TS, stackId: "s1", title: "S", projectIds: ["p1", "p2"] })
125
+ const s = m.get("s1")
126
+ expect(s).toBeDefined()
127
+ expect(s!.projectIds).toEqual(["p1", "p2"])
128
+ })
129
+
130
+ test("stack_removed soft-deletes", () => {
131
+ const m = makeStacksById()
132
+ const s: StackRecord = { id: "s1", title: "S", projectIds: ["p1"], createdAt: TS, updatedAt: TS }
133
+ m.set("s1", s)
134
+ applyStackEvent(m, { v: 3, type: "stack_removed", timestamp: TS + 1, stackId: "s1" })
135
+ expect(s.deletedAt).toBe(TS + 1)
136
+ })
137
+
138
+ test("stack_project_added appends without duplicates", () => {
139
+ const m = makeStacksById()
140
+ const s: StackRecord = { id: "s1", title: "S", projectIds: ["p1"], createdAt: TS, updatedAt: TS }
141
+ m.set("s1", s)
142
+ applyStackEvent(m, { v: 3, type: "stack_project_added", timestamp: TS + 1, stackId: "s1", projectId: "p2" })
143
+ applyStackEvent(m, { v: 3, type: "stack_project_added", timestamp: TS + 2, stackId: "s1", projectId: "p2" })
144
+ expect(s.projectIds).toEqual(["p1", "p2"])
145
+ })
146
+
147
+ test("stack_project_removed removes project", () => {
148
+ const m = makeStacksById()
149
+ const s: StackRecord = { id: "s1", title: "S", projectIds: ["p1", "p2"], createdAt: TS, updatedAt: TS }
150
+ m.set("s1", s)
151
+ applyStackEvent(m, { v: 3, type: "stack_project_removed", timestamp: TS + 1, stackId: "s1", projectId: "p1" })
152
+ expect(s.projectIds).toEqual(["p2"])
153
+ })
154
+
155
+ test("stack_renamed updates title", () => {
156
+ const m = makeStacksById()
157
+ const s: StackRecord = { id: "s1", title: "Old", projectIds: [], createdAt: TS, updatedAt: TS }
158
+ m.set("s1", s)
159
+ applyStackEvent(m, { v: 3, type: "stack_renamed", timestamp: TS + 1, stackId: "s1", title: "New" })
160
+ expect(s.title).toBe("New")
161
+ })
162
+
163
+ test("ops on deleted stack are ignored", () => {
164
+ const m = makeStacksById()
165
+ const s: StackRecord = { id: "s1", title: "S", projectIds: ["p1"], createdAt: TS, updatedAt: TS, deletedAt: TS }
166
+ m.set("s1", s)
167
+ applyStackEvent(m, { v: 3, type: "stack_project_added", timestamp: TS + 1, stackId: "s1", projectId: "p2" })
168
+ expect(s.projectIds).toEqual(["p1"]) // no change
169
+ })
170
+ })
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // updateChatTiming
174
+ // ---------------------------------------------------------------------------
175
+
176
+ describe("updateChatTiming", () => {
177
+ test("seeds initial timing on first call (chat_created path)", () => {
178
+ const m = makeTimings()
179
+ updateChatTiming(m, "chat-1", TS, "idle")
180
+ const t = m.get("chat-1")!
181
+ expect(t.status).toBe("idle")
182
+ expect(t.activeSessionStartedAt).toBe(TS)
183
+ expect(t.cumulativeMs).toEqual({ idle: 0, starting: 0, running: 0, failed: 0 })
184
+ })
185
+
186
+ test("accumulates segment time on transition", () => {
187
+ const m = makeTimings()
188
+ updateChatTiming(m, "chat-1", TS, "idle")
189
+ updateChatTiming(m, "chat-1", TS + 1000, "running", true)
190
+ const t = m.get("chat-1")!
191
+ expect(t.cumulativeMs.idle).toBe(1000)
192
+ expect(t.status).toBe("running")
193
+ expect(t.lastTurnStartedAt).toBe(TS + 1000)
194
+ })
195
+
196
+ test("records lastTurnDurationMs on turn finish", () => {
197
+ const m = makeTimings()
198
+ updateChatTiming(m, "chat-1", TS, "idle")
199
+ updateChatTiming(m, "chat-1", TS + 500, "running", true)
200
+ updateChatTiming(m, "chat-1", TS + 2500, "idle", false, true)
201
+ const t = m.get("chat-1")!
202
+ expect(t.lastTurnDurationMs).toBe(2000)
203
+ })
204
+ })
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // applyChatLifecycleEvent
208
+ // ---------------------------------------------------------------------------
209
+
210
+ describe("applyChatLifecycleEvent — chat events", () => {
211
+ test("chat_created sets initial fields and seeds maps", () => {
212
+ const state = makeChatLifecycleState()
213
+ const replay = makeReplayChatProvider()
214
+ applyChatLifecycleEvent(state, replay, {
215
+ v: 3, type: "chat_created", timestamp: TS, chatId: "c1", projectId: "p1", title: "Chat",
216
+ })
217
+ const chat = state.chatsById.get("c1")!
218
+ expect(chat.projectId).toBe("p1")
219
+ expect(chat.unread).toBe(false)
220
+ expect(chat.provider).toBeNull()
221
+ expect(chat.lastTurnOutcome).toBeNull()
222
+ expect(replay.get("c1")).toBeNull()
223
+ expect(state.subagentRunsByChatId.has("c1")).toBe(true)
224
+ expect(state.chatTimingsByChatId.has("c1")).toBe(true)
225
+ })
226
+
227
+ test("chat_deleted soft-deletes and cleans sub-maps", () => {
228
+ const state = makeChatLifecycleState()
229
+ const replay = makeReplayChatProvider()
230
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_created", timestamp: TS, chatId: "c1", projectId: "p1", title: "C" })
231
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_deleted", timestamp: TS + 1, chatId: "c1" })
232
+ expect(state.chatsById.get("c1")!.deletedAt).toBe(TS + 1)
233
+ expect(state.queuedMessagesByChatId.has("c1")).toBe(false)
234
+ expect(state.chatTimingsByChatId.has("c1")).toBe(false)
235
+ })
236
+
237
+ test("chat_provider_set updates provider and replay map", () => {
238
+ const state = makeChatLifecycleState()
239
+ const replay = makeReplayChatProvider()
240
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_created", timestamp: TS, chatId: "c1", projectId: "p1", title: "C" })
241
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_provider_set", timestamp: TS + 1, chatId: "c1", provider: "claude" })
242
+ expect(state.chatsById.get("c1")!.provider).toBe("claude")
243
+ expect(replay.get("c1")).toBe("claude")
244
+ })
245
+
246
+ test("session_token_set uses explicit provider", () => {
247
+ const state = makeChatLifecycleState()
248
+ const replay = makeReplayChatProvider()
249
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_created", timestamp: TS, chatId: "c1", projectId: "p1", title: "C" })
250
+ applyChatLifecycleEvent(state, replay, {
251
+ v: 3, type: "session_token_set", timestamp: TS + 1, chatId: "c1", sessionToken: "tok", provider: "claude",
252
+ })
253
+ expect(state.chatsById.get("c1")!.sessionTokensByProvider.claude).toBe("tok")
254
+ })
255
+
256
+ test("session_token_set falls back to replayChatProvider", () => {
257
+ const state = makeChatLifecycleState()
258
+ const replay = makeReplayChatProvider()
259
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_created", timestamp: TS, chatId: "c1", projectId: "p1", title: "C" })
260
+ replay.set("c1", "codex")
261
+ applyChatLifecycleEvent(state, replay, {
262
+ v: 3, type: "session_token_set", timestamp: TS + 1, chatId: "c1", sessionToken: "tok2",
263
+ })
264
+ expect(state.chatsById.get("c1")!.sessionTokensByProvider.codex).toBe("tok2")
265
+ })
266
+ })
267
+
268
+ describe("applyChatLifecycleEvent — turn events", () => {
269
+ test("turn_started transitions timing to running and updates chat", () => {
270
+ const state = makeChatLifecycleState()
271
+ const replay = makeReplayChatProvider()
272
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_created", timestamp: TS, chatId: "c1", projectId: "p1", title: "C" })
273
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "turn_started", timestamp: TS + 100, chatId: "c1" })
274
+ expect(state.chatsById.get("c1")!.updatedAt).toBe(TS + 100)
275
+ expect(state.chatTimingsByChatId.get("c1")!.status).toBe("running")
276
+ })
277
+
278
+ test("turn_finished sets unread and lastTurnOutcome", () => {
279
+ const state = makeChatLifecycleState()
280
+ const replay = makeReplayChatProvider()
281
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_created", timestamp: TS, chatId: "c1", projectId: "p1", title: "C" })
282
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "turn_started", timestamp: TS + 100, chatId: "c1" })
283
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "turn_finished", timestamp: TS + 200, chatId: "c1" })
284
+ const chat = state.chatsById.get("c1")!
285
+ expect(chat.unread).toBe(true)
286
+ expect(chat.lastTurnOutcome).toBe("success")
287
+ expect(state.chatTimingsByChatId.get("c1")!.status).toBe("idle")
288
+ })
289
+
290
+ test("turn_failed sets failed outcome and timing", () => {
291
+ const state = makeChatLifecycleState()
292
+ const replay = makeReplayChatProvider()
293
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_created", timestamp: TS, chatId: "c1", projectId: "p1", title: "C" })
294
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "turn_failed", timestamp: TS + 200, chatId: "c1", error: "oops" })
295
+ expect(state.chatsById.get("c1")!.lastTurnOutcome).toBe("failed")
296
+ expect(state.chatTimingsByChatId.get("c1")!.status).toBe("failed")
297
+ })
298
+ })
299
+
300
+ describe("applyChatLifecycleEvent — queued message events", () => {
301
+ test("queued_message_enqueued appends and updates chat", () => {
302
+ const state = makeChatLifecycleState()
303
+ const replay = makeReplayChatProvider()
304
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_created", timestamp: TS, chatId: "c1", projectId: "p1", title: "C" })
305
+ applyChatLifecycleEvent(state, replay, {
306
+ v: 3,
307
+ type: "queued_message_enqueued",
308
+ timestamp: TS + 10,
309
+ chatId: "c1",
310
+ message: { id: "qm1", createdAt: TS + 10, content: "hello", attachments: [] },
311
+ })
312
+ const queued = state.queuedMessagesByChatId.get("c1")
313
+ expect(queued?.length).toBe(1)
314
+ expect(queued![0].id).toBe("qm1")
315
+ })
316
+
317
+ test("queued_message_removed removes message and deletes key when empty", () => {
318
+ const state = makeChatLifecycleState()
319
+ const replay = makeReplayChatProvider()
320
+ applyChatLifecycleEvent(state, replay, { v: 3, type: "chat_created", timestamp: TS, chatId: "c1", projectId: "p1", title: "C" })
321
+ applyChatLifecycleEvent(state, replay, {
322
+ v: 3,
323
+ type: "queued_message_enqueued",
324
+ timestamp: TS + 10,
325
+ chatId: "c1",
326
+ message: { id: "qm1", createdAt: TS + 10, content: "hello", attachments: [] },
327
+ })
328
+ applyChatLifecycleEvent(state, replay, {
329
+ v: 3, type: "queued_message_removed", timestamp: TS + 20, chatId: "c1", queuedMessageId: "qm1",
330
+ })
331
+ expect(state.queuedMessagesByChatId.has("c1")).toBe(false)
332
+ })
333
+ })
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // applyAutoContinueToState
337
+ // ---------------------------------------------------------------------------
338
+
339
+ describe("applyAutoContinueToState", () => {
340
+ test("appends event to list", () => {
341
+ const m = new Map<string, AutoContinueEvent[]>()
342
+ const ev: AutoContinueEvent = {
343
+ v: 3,
344
+ kind: "auto_continue_accepted",
345
+ chatId: "c1",
346
+ scheduleId: "sched-1",
347
+ timestamp: TS,
348
+ scheduledAt: TS - 100,
349
+ tz: "UTC",
350
+ source: "auto_setting",
351
+ resetAt: TS + 1000,
352
+ detectedAt: TS - 200,
353
+ prompt: "continue",
354
+ }
355
+ applyAutoContinueToState(m, ev)
356
+ applyAutoContinueToState(m, { ...ev, timestamp: TS + 1 })
357
+ expect(m.get("c1")?.length).toBe(2)
358
+ })
359
+ })
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // applyChatMessageMetadata
363
+ // ---------------------------------------------------------------------------
364
+
365
+ describe("applyChatMessageMetadata", () => {
366
+ test("sets hasMessages and lastMessageAt for user_prompt", () => {
367
+ const chatsById = new Map<string, ChatRecord>()
368
+ chatsById.set("c1", {
369
+ id: "c1", projectId: "p1", title: "C", createdAt: TS, updatedAt: TS,
370
+ unread: false, provider: null, planMode: false,
371
+ sessionTokensByProvider: {}, sourceHash: null, pendingForkSessionToken: null,
372
+ hasMessages: false, lastTurnOutcome: null,
373
+ })
374
+ applyChatMessageMetadata(chatsById, "c1", {
375
+ _id: "msg-1", kind: "user_prompt", createdAt: TS + 500,
376
+ content: "hi", attachments: [],
377
+ })
378
+ const chat = chatsById.get("c1")!
379
+ expect(chat.hasMessages).toBe(true)
380
+ expect(chat.lastMessageAt).toBe(TS + 500)
381
+ expect(chat.updatedAt).toBe(TS + 500)
382
+ })
383
+
384
+ test("does not set lastMessageAt for non-user_prompt entries", () => {
385
+ const chatsById = new Map<string, ChatRecord>()
386
+ chatsById.set("c1", {
387
+ id: "c1", projectId: "p1", title: "C", createdAt: TS, updatedAt: TS,
388
+ unread: false, provider: null, planMode: false,
389
+ sessionTokensByProvider: {}, sourceHash: null, pendingForkSessionToken: null,
390
+ hasMessages: false, lastTurnOutcome: null,
391
+ })
392
+ applyChatMessageMetadata(chatsById, "c1", {
393
+ _id: "msg-2", kind: "context_cleared", createdAt: TS + 100,
394
+ })
395
+ const chat = chatsById.get("c1")!
396
+ expect(chat.hasMessages).toBe(true)
397
+ expect(chat.lastMessageAt).toBeUndefined()
398
+ })
399
+
400
+ test("is a no-op for unknown chatId", () => {
401
+ const chatsById = new Map<string, ChatRecord>()
402
+ // should not throw
403
+ applyChatMessageMetadata(chatsById, "ghost", {
404
+ _id: "msg-3", kind: "user_prompt", createdAt: TS, content: "hi", attachments: [],
405
+ })
406
+ expect(chatsById.size).toBe(0)
407
+ })
408
+ })