@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,321 @@
1
+ /**
2
+ * Tests for claude-tool-respond.ts — the extracted respondTool handler.
3
+ */
4
+ import { describe, it, expect, mock } from "bun:test"
5
+ import {
6
+ respondTool,
7
+ type RespondToolCommand,
8
+ type ToolRespondDeps,
9
+ } from "./claude-tool-respond"
10
+ import type { PendingToolRequest, ActiveTurn } from "./claude-session-state"
11
+ import type { AnyValue } from "../shared/errors"
12
+ import type { AskUserQuestionToolCall, ExitPlanModeToolCall } from "../shared/types"
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Minimal tool call stubs that satisfy the PendingToolRequest.tool type
16
+ // ---------------------------------------------------------------------------
17
+
18
+ function askUserQuestionTool(toolId: string): AskUserQuestionToolCall {
19
+ return {
20
+ kind: "tool",
21
+ toolKind: "ask_user_question",
22
+ toolName: "ask_user_question",
23
+ toolId,
24
+ input: { questions: [] },
25
+ }
26
+ }
27
+
28
+ function exitPlanModeTool(toolId: string): ExitPlanModeToolCall {
29
+ return {
30
+ kind: "tool",
31
+ toolKind: "exit_plan_mode",
32
+ toolName: "exit_plan_mode",
33
+ toolId,
34
+ input: {},
35
+ }
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Helpers
40
+ // ---------------------------------------------------------------------------
41
+
42
+ function makeActiveTurn(overrides: Partial<ActiveTurn> = {}): ActiveTurn {
43
+ return {
44
+ chatId: "chat-1",
45
+ provider: "claude",
46
+ turn: {} as ActiveTurn["turn"],
47
+ model: "claude-opus-4-5",
48
+ planMode: true,
49
+ status: "waiting_for_user",
50
+ pendingTool: null,
51
+ postToolFollowUp: null,
52
+ hasFinalResult: false,
53
+ cancelRequested: false,
54
+ cancelRecorded: false,
55
+ waitStartedAt: Date.now(),
56
+ userMessageId: null,
57
+ ...overrides,
58
+ }
59
+ }
60
+
61
+ type AppendMessageFn = (chatId: string, entry: { kind: string }) => Promise<void>
62
+ type SetSessionTokenFn = (
63
+ chatId: string,
64
+ provider: string,
65
+ token: string | null,
66
+ ) => Promise<void>
67
+
68
+ function makeDeps(
69
+ activeTurns: Map<string, ActiveTurn>,
70
+ appendMessage = mock(async (_chatId: string, _entry: unknown) => {}),
71
+ setSessionTokenForProvider = mock(
72
+ async (_chatId: string, _provider: string, _token: string | null) => {},
73
+ ),
74
+ emitStateChange = mock((_chatId: string) => {}),
75
+ ): ToolRespondDeps {
76
+ return {
77
+ activeTurns,
78
+ store: {
79
+ appendMessage: appendMessage as unknown as AppendMessageFn,
80
+ setSessionTokenForProvider: setSessionTokenForProvider as unknown as SetSessionTokenFn,
81
+ },
82
+ emitStateChange,
83
+ }
84
+ }
85
+
86
+ function makeCommand(
87
+ overrides: Partial<RespondToolCommand> = {},
88
+ ): RespondToolCommand {
89
+ return {
90
+ type: "chat.respondTool",
91
+ chatId: "chat-1",
92
+ toolUseId: "tool-abc",
93
+ result: { confirmed: true } as AnyValue,
94
+ ...overrides,
95
+ }
96
+ }
97
+
98
+ function makePendingTool(
99
+ toolUseId: string,
100
+ tool: PendingToolRequest["tool"],
101
+ resolve: (v: AnyValue) => void,
102
+ ): PendingToolRequest {
103
+ return { toolUseId, tool, resolve }
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Tests
108
+ // ---------------------------------------------------------------------------
109
+
110
+ describe("respondTool", () => {
111
+ it("throws when there is no active turn for the chat", async () => {
112
+ const deps = makeDeps(new Map())
113
+ await expect(respondTool(deps, makeCommand())).rejects.toThrow(
114
+ "No pending tool request",
115
+ )
116
+ })
117
+
118
+ it("throws when the active turn has no pending tool", async () => {
119
+ const active = makeActiveTurn({ pendingTool: null })
120
+ const turns = new Map([["chat-1", active]])
121
+ const deps = makeDeps(turns)
122
+ await expect(respondTool(deps, makeCommand())).rejects.toThrow(
123
+ "No pending tool request",
124
+ )
125
+ })
126
+
127
+ it("throws when toolUseId does not match the pending request", async () => {
128
+ const resolve = mock((_v: AnyValue) => {})
129
+ const pending = makePendingTool(
130
+ "tool-xyz",
131
+ askUserQuestionTool("tool-xyz"),
132
+ resolve,
133
+ )
134
+ const active = makeActiveTurn({ pendingTool: pending })
135
+ const turns = new Map([["chat-1", active]])
136
+ const deps = makeDeps(turns)
137
+
138
+ await expect(
139
+ respondTool(deps, makeCommand({ toolUseId: "tool-DIFFERENT" })),
140
+ ).rejects.toThrow("Tool response does not match active request")
141
+ })
142
+
143
+ it("resolves an ask_user_question tool and updates active turn state", async () => {
144
+ const resolve = mock((_v: AnyValue) => {})
145
+ const appendMessage = mock(async (_chatId: string, _entry: unknown) => {})
146
+ const emitStateChange = mock((_chatId: string) => {})
147
+ const result: AnyValue = { answer: "yes" }
148
+
149
+ const pending = makePendingTool(
150
+ "tool-abc",
151
+ askUserQuestionTool("tool-abc"),
152
+ resolve,
153
+ )
154
+ const active = makeActiveTurn({
155
+ status: "waiting_for_user",
156
+ waitStartedAt: 12345,
157
+ pendingTool: pending,
158
+ })
159
+ const turns = new Map([["chat-1", active]])
160
+ const deps = makeDeps(
161
+ turns,
162
+ appendMessage,
163
+ mock(async (_chatId: string, _provider: string, _token: string | null) => {}),
164
+ emitStateChange,
165
+ )
166
+
167
+ await respondTool(deps, makeCommand({ result }))
168
+
169
+ // tool_result appended
170
+ expect(appendMessage).toHaveBeenCalledTimes(1)
171
+ const callArgs = appendMessage.mock.calls[0] as unknown as [
172
+ string,
173
+ { kind: string },
174
+ ]
175
+ expect(callArgs[1].kind).toBe("tool_result")
176
+
177
+ // active turn state updated
178
+ expect(active.pendingTool).toBeNull()
179
+ expect(active.status).toBe("running")
180
+ expect(active.waitStartedAt).toBeNull()
181
+
182
+ // promise resolved
183
+ expect(resolve).toHaveBeenCalledWith(result)
184
+
185
+ // state change emitted
186
+ expect(emitStateChange).toHaveBeenCalledWith("chat-1")
187
+ })
188
+
189
+ it("clears session token and appends context_cleared when exit_plan_mode confirmed+clearContext", async () => {
190
+ const resolve = mock((_v: AnyValue) => {})
191
+ const appendMessage = mock(async (_chatId: string, _entry: unknown) => {})
192
+ const setSessionTokenForProvider = mock(
193
+ async (_chatId: string, _provider: string, _token: string | null) => {},
194
+ )
195
+ const emitStateChange = mock((_chatId: string) => {})
196
+
197
+ const pending = makePendingTool(
198
+ "tool-abc",
199
+ exitPlanModeTool("tool-abc"),
200
+ resolve,
201
+ )
202
+ const active = makeActiveTurn({ provider: "claude", pendingTool: pending })
203
+ const turns = new Map([["chat-1", active]])
204
+ const deps = makeDeps(turns, appendMessage, setSessionTokenForProvider, emitStateChange)
205
+
206
+ await respondTool(
207
+ deps,
208
+ makeCommand({
209
+ result: { confirmed: true, clearContext: true, message: "" } as AnyValue,
210
+ }),
211
+ )
212
+
213
+ expect(setSessionTokenForProvider).toHaveBeenCalledWith("chat-1", "claude", null)
214
+
215
+ // appendMessage: tool_result + context_cleared
216
+ expect(appendMessage).toHaveBeenCalledTimes(2)
217
+ const call2 = appendMessage.mock.calls[1] as unknown as [string, { kind: string }]
218
+ expect(call2[1].kind).toBe("context_cleared")
219
+ })
220
+
221
+ it("does NOT clear context when confirmed=false even if clearContext=true", async () => {
222
+ const resolve = mock((_v: AnyValue) => {})
223
+ const appendMessage = mock(async (_chatId: string, _entry: unknown) => {})
224
+ const setSessionTokenForProvider = mock(
225
+ async (_chatId: string, _provider: string, _token: string | null) => {},
226
+ )
227
+
228
+ const pending = makePendingTool(
229
+ "tool-abc",
230
+ exitPlanModeTool("tool-abc"),
231
+ resolve,
232
+ )
233
+ const active = makeActiveTurn({ provider: "claude", pendingTool: pending })
234
+ const turns = new Map([["chat-1", active]])
235
+ const deps = makeDeps(turns, appendMessage, setSessionTokenForProvider)
236
+
237
+ await respondTool(
238
+ deps,
239
+ makeCommand({ result: { confirmed: false, clearContext: true } as AnyValue }),
240
+ )
241
+
242
+ expect(setSessionTokenForProvider).not.toHaveBeenCalled()
243
+ // only tool_result appended (no context_cleared)
244
+ expect(appendMessage).toHaveBeenCalledTimes(1)
245
+ })
246
+
247
+ it("sets postToolFollowUp on codex provider when exit_plan_mode confirmed", async () => {
248
+ const resolve = mock((_v: AnyValue) => {})
249
+
250
+ const pending = makePendingTool(
251
+ "tool-abc",
252
+ exitPlanModeTool("tool-abc"),
253
+ resolve,
254
+ )
255
+ const active = makeActiveTurn({ provider: "codex", pendingTool: pending })
256
+ const turns = new Map([["chat-1", active]])
257
+ const deps = makeDeps(turns)
258
+
259
+ await respondTool(
260
+ deps,
261
+ makeCommand({
262
+ result: { confirmed: true, clearContext: false, message: "great plan" } as AnyValue,
263
+ }),
264
+ )
265
+
266
+ expect(active.postToolFollowUp).toEqual({
267
+ content: "Proceed with the approved plan. Additional guidance: great plan",
268
+ planMode: false,
269
+ })
270
+ })
271
+
272
+ it("sets postToolFollowUp on codex provider when exit_plan_mode rejected", async () => {
273
+ const resolve = mock((_v: AnyValue) => {})
274
+
275
+ const pending = makePendingTool(
276
+ "tool-abc",
277
+ exitPlanModeTool("tool-abc"),
278
+ resolve,
279
+ )
280
+ const active = makeActiveTurn({ provider: "codex", pendingTool: pending })
281
+ const turns = new Map([["chat-1", active]])
282
+ const deps = makeDeps(turns)
283
+
284
+ await respondTool(
285
+ deps,
286
+ makeCommand({
287
+ result: { confirmed: false, clearContext: false, message: "needs work" } as AnyValue,
288
+ }),
289
+ )
290
+
291
+ expect(active.postToolFollowUp).toEqual({
292
+ content: "Revise the plan using this feedback: needs work",
293
+ planMode: true,
294
+ })
295
+ })
296
+
297
+ it("uses default messages when message field is empty", async () => {
298
+ const resolve = mock((_v: AnyValue) => {})
299
+
300
+ const pending = makePendingTool(
301
+ "tool-abc",
302
+ exitPlanModeTool("tool-abc"),
303
+ resolve,
304
+ )
305
+ const active = makeActiveTurn({ provider: "codex", pendingTool: pending })
306
+ const turns = new Map([["chat-1", active]])
307
+ const deps = makeDeps(turns)
308
+
309
+ await respondTool(
310
+ deps,
311
+ makeCommand({
312
+ result: { confirmed: true, clearContext: false, message: "" } as AnyValue,
313
+ }),
314
+ )
315
+
316
+ expect(active.postToolFollowUp).toEqual({
317
+ content: "Proceed with the approved plan.",
318
+ planMode: false,
319
+ })
320
+ })
321
+ })
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Standalone respondTool handler for AgentCoordinator.
3
+ *
4
+ * Extracted from agent.ts so the pending-tool resolution logic lives in its
5
+ * own testable module. The coordinator delegates to `respondTool` by passing
6
+ * an object literal that satisfies `ToolRespondDeps`.
7
+ *
8
+ * Side-effect seal: this module contains NO direct IO (no node:fs, no HTTP
9
+ * calls, no Bun primitives). Every effectful operation is injected through
10
+ * the deps interface.
11
+ */
12
+
13
+ import type { AgentProvider, TranscriptEntry } from "../shared/types"
14
+ import type { AnyValue } from "../shared/errors"
15
+ import { isRecord } from "../shared/errors"
16
+ import type { ActiveTurn } from "./claude-session-state"
17
+ import { timestamped, normalizeToolContent } from "./claude-message-normalizer"
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Structural sub-interfaces — only the slices this module calls.
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /** Subset of EventStore required by the tool-respond handler. */
24
+ interface ToolRespondStore {
25
+ appendMessage(chatId: string, entry: TranscriptEntry): Promise<void>
26
+ setSessionTokenForProvider(
27
+ chatId: string,
28
+ provider: AgentProvider,
29
+ sessionToken: string | null,
30
+ ): Promise<void>
31
+ }
32
+
33
+ /** Subset of the activeTurns map used by the tool-respond handler. */
34
+ interface ToolRespondActiveTurnsMap {
35
+ get(chatId: string): ActiveTurn | undefined
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Command shape (inlined to avoid protocol import cycle)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /** The slice of ClientCommand that this handler processes. */
43
+ export interface RespondToolCommand {
44
+ type: "chat.respondTool"
45
+ chatId: string
46
+ toolUseId: string
47
+ result: AnyValue
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Dependency bundle injected by AgentCoordinator
52
+ // ---------------------------------------------------------------------------
53
+
54
+ export interface ToolRespondDeps {
55
+ /** The shared in-memory active-turns map owned by AgentCoordinator. */
56
+ activeTurns: ToolRespondActiveTurnsMap
57
+
58
+ /** Persists transcript entries and session tokens. */
59
+ store: ToolRespondStore
60
+
61
+ /** Notify subscribers that a chat's observable state has changed. */
62
+ emitStateChange(chatId: string): void
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Exported handler
67
+ // ---------------------------------------------------------------------------
68
+
69
+ /**
70
+ * Resolve a pending tool request (AskUserQuestion / ExitPlanMode) coming back
71
+ * from the UI.
72
+ *
73
+ * @throws if there is no pending tool for the chat or the toolUseId does not
74
+ * match.
75
+ */
76
+ export async function respondTool(
77
+ deps: ToolRespondDeps,
78
+ command: RespondToolCommand,
79
+ ): Promise<void> {
80
+ const { activeTurns, store, emitStateChange } = deps
81
+
82
+ const active = activeTurns.get(command.chatId)
83
+ if (!active || !active.pendingTool) {
84
+ throw new Error("No pending tool request")
85
+ }
86
+
87
+ const pending = active.pendingTool
88
+ if (pending.toolUseId !== command.toolUseId) {
89
+ throw new Error("Tool response does not match active request")
90
+ }
91
+
92
+ await store.appendMessage(
93
+ command.chatId,
94
+ timestamped({
95
+ kind: "tool_result",
96
+ toolId: command.toolUseId,
97
+ content: normalizeToolContent(command.result),
98
+ }),
99
+ )
100
+
101
+ active.pendingTool = null
102
+ active.status = "running"
103
+ active.waitStartedAt = null
104
+
105
+ if (pending.tool.toolKind === "exit_plan_mode") {
106
+ const resultRec: Record<string, unknown> = isRecord(command.result)
107
+ ? command.result
108
+ : {}
109
+ const confirmed = Boolean(resultRec.confirmed)
110
+ const clearContext = Boolean(resultRec.clearContext)
111
+ const message =
112
+ typeof resultRec.message === "string" ? resultRec.message : ""
113
+
114
+ if (confirmed && clearContext) {
115
+ await store.setSessionTokenForProvider(command.chatId, active.provider, null)
116
+ await store.appendMessage(
117
+ command.chatId,
118
+ timestamped({ kind: "context_cleared" }),
119
+ )
120
+ }
121
+
122
+ if (active.provider === "codex") {
123
+ active.postToolFollowUp = confirmed
124
+ ? {
125
+ content: message
126
+ ? `Proceed with the approved plan. Additional guidance: ${message}`
127
+ : "Proceed with the approved plan.",
128
+ planMode: false,
129
+ }
130
+ : {
131
+ content: message
132
+ ? `Revise the plan using this feedback: ${message}`
133
+ : "Revise the plan using this feedback.",
134
+ planMode: true,
135
+ }
136
+ }
137
+ }
138
+
139
+ pending.resolve(command.result)
140
+
141
+ emitStateChange(command.chatId)
142
+ }