@stonerzju/opencode 1.2.17 → 1.2.19

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 (261) hide show
  1. package/bin/opencode +29 -157
  2. package/package.json +29 -29
  3. package/src/acp/agent.ts +4 -4
  4. package/src/acp/session.ts +1 -1
  5. package/src/agent/agent.ts +3 -3
  6. package/src/bun/index.ts +2 -2
  7. package/src/cli/cmd/acp.ts +3 -3
  8. package/src/cli/cmd/debug/file.ts +1 -1
  9. package/src/cli/cmd/github.ts +2 -2
  10. package/src/cli/cmd/pr.ts +1 -1
  11. package/src/cli/cmd/tui/app.tsx +24 -24
  12. package/src/cli/cmd/tui/attach.ts +3 -3
  13. package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
  14. package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
  15. package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
  16. package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
  17. package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
  18. package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
  19. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
  20. package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
  21. package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
  22. package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
  23. package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
  24. package/src/cli/cmd/tui/component/logo.tsx +2 -2
  25. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
  26. package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
  27. package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
  28. package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
  29. package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
  30. package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
  31. package/src/cli/cmd/tui/component/tips.tsx +1 -1
  32. package/src/cli/cmd/tui/context/directory.ts +1 -1
  33. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  34. package/src/cli/cmd/tui/context/keybind.tsx +2 -2
  35. package/src/cli/cmd/tui/context/kv.tsx +2 -2
  36. package/src/cli/cmd/tui/context/local.tsx +6 -6
  37. package/src/cli/cmd/tui/context/sync.tsx +4 -4
  38. package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
  39. package/src/cli/cmd/tui/context/theme.tsx +2 -2
  40. package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
  41. package/src/cli/cmd/tui/event.ts +2 -2
  42. package/src/cli/cmd/tui/routes/home.tsx +6 -6
  43. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
  44. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
  45. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
  46. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
  47. package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
  48. package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
  49. package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
  50. package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
  51. package/src/cli/cmd/tui/thread.ts +9 -9
  52. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
  53. package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
  54. package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
  55. package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
  56. package/src/cli/cmd/tui/ui/toast.tsx +1 -1
  57. package/src/cli/cmd/tui/util/editor.ts +3 -3
  58. package/src/cli/cmd/tui/util/transcript.ts +1 -1
  59. package/src/cli/cmd/tui/worker.ts +10 -10
  60. package/src/cli/error.ts +1 -1
  61. package/src/cli/ui.ts +1 -1
  62. package/src/cli/upgrade.ts +4 -4
  63. package/src/command/index.ts +1 -1
  64. package/src/config/config.ts +10 -10
  65. package/src/config/markdown.ts +1 -1
  66. package/src/config/migrate-tui-config.ts +5 -5
  67. package/src/config/paths.ts +4 -4
  68. package/src/config/tui.ts +4 -4
  69. package/src/control/control.sql.ts +1 -1
  70. package/src/control/index.ts +1 -1
  71. package/src/control-plane/adaptors/worktree.ts +1 -1
  72. package/src/control-plane/session-proxy-middleware.ts +1 -1
  73. package/src/control-plane/workspace.sql.ts +1 -1
  74. package/src/control-plane/workspace.ts +7 -7
  75. package/src/file/index.ts +1 -1
  76. package/src/file/ripgrep.ts +2 -2
  77. package/src/file/watcher.ts +5 -5
  78. package/src/format/formatter.ts +1 -1
  79. package/src/ide/index.ts +3 -3
  80. package/src/index.ts +1 -1
  81. package/src/installation/index.ts +3 -3
  82. package/src/lsp/client.ts +3 -3
  83. package/src/lsp/index.ts +3 -3
  84. package/src/mcp/index.ts +4 -4
  85. package/src/permission/index.ts +2 -2
  86. package/src/permission/next.ts +10 -10
  87. package/src/plugin/codex.ts +1 -1
  88. package/src/plugin/copilot.ts +2 -2
  89. package/src/plugin/index.ts +1 -1
  90. package/src/project/bootstrap.ts +2 -2
  91. package/src/project/instance.ts +4 -4
  92. package/src/project/project.sql.ts +1 -1
  93. package/src/project/project.ts +5 -5
  94. package/src/project/state.ts +1 -1
  95. package/src/project/vcs.ts +4 -4
  96. package/src/provider/auth.ts +4 -4
  97. package/src/provider/error.ts +1 -1
  98. package/src/provider/models-snapshot.ts +2 -0
  99. package/src/provider/models.ts +1 -1
  100. package/src/provider/provider.ts +2 -2
  101. package/src/provider/transform.ts +2 -2
  102. package/src/pty/index.ts +5 -5
  103. package/src/question/index.ts +5 -5
  104. package/src/server/event.ts +1 -1
  105. package/src/server/mdns.ts +1 -1
  106. package/src/server/routes/global.ts +3 -3
  107. package/src/server/routes/permission.ts +1 -1
  108. package/src/server/routes/pty.ts +1 -1
  109. package/src/server/routes/session.ts +4 -4
  110. package/src/server/routes/tui.ts +1 -1
  111. package/src/server/server.ts +3 -3
  112. package/src/session/compaction.ts +7 -7
  113. package/src/session/index.ts +10 -10
  114. package/src/session/instruction.ts +1 -1
  115. package/src/session/llm.ts +11 -11
  116. package/src/session/message-v2.ts +10 -10
  117. package/src/session/message.ts +1 -1
  118. package/src/session/processor.ts +10 -10
  119. package/src/session/prompt.ts +8 -8
  120. package/src/session/retry.ts +2 -2
  121. package/src/session/revert.ts +1 -1
  122. package/src/session/session.sql.ts +3 -3
  123. package/src/session/status.ts +3 -3
  124. package/src/session/summary.ts +5 -5
  125. package/src/session/system.ts +1 -1
  126. package/src/session/todo.ts +2 -2
  127. package/src/share/share-next.ts +7 -7
  128. package/src/share/share.sql.ts +1 -1
  129. package/src/shell/shell.ts +3 -3
  130. package/src/skill/skill.ts +6 -6
  131. package/src/storage/db.ts +1 -1
  132. package/src/storage/storage.ts +1 -1
  133. package/src/tool/bash.ts +6 -6
  134. package/src/tool/edit.ts +1 -1
  135. package/src/tool/registry.ts +2 -2
  136. package/src/tool/skill.ts +1 -1
  137. package/src/tool/task.ts +3 -3
  138. package/src/util/array.ts +10 -0
  139. package/src/util/binary.ts +41 -0
  140. package/src/util/encode.ts +51 -0
  141. package/src/util/error.ts +54 -0
  142. package/src/util/identifier.ts +48 -0
  143. package/src/util/lazy.ts +4 -16
  144. package/src/util/path.ts +37 -0
  145. package/src/util/retry.ts +41 -0
  146. package/src/util/slug.ts +74 -0
  147. package/src/worktree/index.ts +3 -3
  148. package/AGENTS.md +0 -10
  149. package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
  150. package/Dockerfile +0 -18
  151. package/README.md +0 -15
  152. package/bunfig.toml +0 -7
  153. package/drizzle.config.ts +0 -10
  154. package/script/build.ts +0 -224
  155. package/script/check-migrations.ts +0 -16
  156. package/script/postinstall.mjs +0 -131
  157. package/script/publish.ts +0 -181
  158. package/script/schema.ts +0 -63
  159. package/script/seed-e2e.ts +0 -50
  160. package/sst-env.d.ts +0 -10
  161. package/test/AGENTS.md +0 -81
  162. package/test/acp/agent-interface.test.ts +0 -51
  163. package/test/acp/event-subscription.test.ts +0 -683
  164. package/test/agent/agent.test.ts +0 -689
  165. package/test/bun.test.ts +0 -53
  166. package/test/cli/github-action.test.ts +0 -197
  167. package/test/cli/github-remote.test.ts +0 -80
  168. package/test/cli/import.test.ts +0 -38
  169. package/test/cli/plugin-auth-picker.test.ts +0 -120
  170. package/test/cli/tui/transcript.test.ts +0 -322
  171. package/test/config/agent-color.test.ts +0 -71
  172. package/test/config/config.test.ts +0 -1886
  173. package/test/config/fixtures/empty-frontmatter.md +0 -4
  174. package/test/config/fixtures/frontmatter.md +0 -28
  175. package/test/config/fixtures/markdown-header.md +0 -11
  176. package/test/config/fixtures/no-frontmatter.md +0 -1
  177. package/test/config/fixtures/weird-model-id.md +0 -13
  178. package/test/config/markdown.test.ts +0 -228
  179. package/test/config/tui.test.ts +0 -510
  180. package/test/control-plane/session-proxy-middleware.test.ts +0 -147
  181. package/test/control-plane/sse.test.ts +0 -56
  182. package/test/control-plane/workspace-server-sse.test.ts +0 -65
  183. package/test/control-plane/workspace-sync.test.ts +0 -97
  184. package/test/file/ignore.test.ts +0 -10
  185. package/test/file/index.test.ts +0 -394
  186. package/test/file/path-traversal.test.ts +0 -198
  187. package/test/file/ripgrep.test.ts +0 -39
  188. package/test/file/time.test.ts +0 -361
  189. package/test/fixture/db.ts +0 -11
  190. package/test/fixture/fixture.ts +0 -45
  191. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  192. package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
  193. package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
  194. package/test/fixture/skills/cloudflare/SKILL.md +0 -211
  195. package/test/fixture/skills/index.json +0 -6
  196. package/test/ide/ide.test.ts +0 -82
  197. package/test/keybind.test.ts +0 -421
  198. package/test/lsp/client.test.ts +0 -95
  199. package/test/mcp/headers.test.ts +0 -153
  200. package/test/mcp/oauth-browser.test.ts +0 -249
  201. package/test/memory/abort-leak.test.ts +0 -136
  202. package/test/patch/patch.test.ts +0 -348
  203. package/test/permission/arity.test.ts +0 -33
  204. package/test/permission/next.test.ts +0 -689
  205. package/test/permission-task.test.ts +0 -319
  206. package/test/plugin/auth-override.test.ts +0 -44
  207. package/test/plugin/codex.test.ts +0 -123
  208. package/test/preload.ts +0 -80
  209. package/test/project/project.test.ts +0 -348
  210. package/test/project/worktree-remove.test.ts +0 -65
  211. package/test/provider/amazon-bedrock.test.ts +0 -446
  212. package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
  213. package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
  214. package/test/provider/gitlab-duo.test.ts +0 -262
  215. package/test/provider/provider.test.ts +0 -2220
  216. package/test/provider/transform.test.ts +0 -2353
  217. package/test/pty/pty-output-isolation.test.ts +0 -140
  218. package/test/question/question.test.ts +0 -300
  219. package/test/scheduler.test.ts +0 -73
  220. package/test/server/global-session-list.test.ts +0 -89
  221. package/test/server/session-list.test.ts +0 -90
  222. package/test/server/session-select.test.ts +0 -78
  223. package/test/session/compaction.test.ts +0 -423
  224. package/test/session/instruction.test.ts +0 -170
  225. package/test/session/llm.test.ts +0 -667
  226. package/test/session/message-v2.test.ts +0 -924
  227. package/test/session/prompt.test.ts +0 -211
  228. package/test/session/retry.test.ts +0 -188
  229. package/test/session/revert-compact.test.ts +0 -285
  230. package/test/session/session.test.ts +0 -71
  231. package/test/session/structured-output-integration.test.ts +0 -233
  232. package/test/session/structured-output.test.ts +0 -385
  233. package/test/skill/discovery.test.ts +0 -110
  234. package/test/skill/skill.test.ts +0 -388
  235. package/test/snapshot/snapshot.test.ts +0 -1180
  236. package/test/storage/json-migration.test.ts +0 -846
  237. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  238. package/test/tool/apply_patch.test.ts +0 -566
  239. package/test/tool/bash.test.ts +0 -402
  240. package/test/tool/edit.test.ts +0 -496
  241. package/test/tool/external-directory.test.ts +0 -127
  242. package/test/tool/fixtures/large-image.png +0 -0
  243. package/test/tool/fixtures/models-api.json +0 -38413
  244. package/test/tool/grep.test.ts +0 -110
  245. package/test/tool/question.test.ts +0 -107
  246. package/test/tool/read.test.ts +0 -504
  247. package/test/tool/registry.test.ts +0 -122
  248. package/test/tool/skill.test.ts +0 -112
  249. package/test/tool/truncation.test.ts +0 -160
  250. package/test/tool/webfetch.test.ts +0 -100
  251. package/test/tool/write.test.ts +0 -348
  252. package/test/util/filesystem.test.ts +0 -443
  253. package/test/util/format.test.ts +0 -59
  254. package/test/util/glob.test.ts +0 -164
  255. package/test/util/iife.test.ts +0 -36
  256. package/test/util/lazy.test.ts +0 -50
  257. package/test/util/lock.test.ts +0 -72
  258. package/test/util/process.test.ts +0 -59
  259. package/test/util/timeout.test.ts +0 -21
  260. package/test/util/wildcard.test.ts +0 -90
  261. package/tsconfig.json +0 -16
@@ -1,683 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { ACP } from "../../src/acp/agent"
3
- import type { AgentSideConnection } from "@agentclientprotocol/sdk"
4
- import type { Event, EventMessagePartUpdated, ToolStatePending, ToolStateRunning } from "@opencode-ai/sdk/v2"
5
- import { Instance } from "../../src/project/instance"
6
- import { tmpdir } from "../fixture/fixture"
7
-
8
- type SessionUpdateParams = Parameters<AgentSideConnection["sessionUpdate"]>[0]
9
- type RequestPermissionParams = Parameters<AgentSideConnection["requestPermission"]>[0]
10
- type RequestPermissionResult = Awaited<ReturnType<AgentSideConnection["requestPermission"]>>
11
-
12
- type GlobalEventEnvelope = {
13
- directory?: string
14
- payload?: Event
15
- }
16
-
17
- type EventController = {
18
- push: (event: GlobalEventEnvelope) => void
19
- close: () => void
20
- }
21
-
22
- function inProgressText(update: SessionUpdateParams["update"]) {
23
- if (update.sessionUpdate !== "tool_call_update") return undefined
24
- if (update.status !== "in_progress") return undefined
25
- if (!update.content || !Array.isArray(update.content)) return undefined
26
- const first = update.content[0]
27
- if (!first || first.type !== "content") return undefined
28
- if (first.content.type !== "text") return undefined
29
- return first.content.text
30
- }
31
-
32
- function isToolCallUpdate(
33
- update: SessionUpdateParams["update"],
34
- ): update is Extract<SessionUpdateParams["update"], { sessionUpdate: "tool_call_update" }> {
35
- return update.sessionUpdate === "tool_call_update"
36
- }
37
-
38
- function toolEvent(
39
- sessionId: string,
40
- cwd: string,
41
- opts: {
42
- callID: string
43
- tool: string
44
- input: Record<string, unknown>
45
- } & ({ status: "running"; metadata?: Record<string, unknown> } | { status: "pending"; raw: string }),
46
- ): GlobalEventEnvelope {
47
- const state: ToolStatePending | ToolStateRunning =
48
- opts.status === "running"
49
- ? {
50
- status: "running",
51
- input: opts.input,
52
- ...(opts.metadata && { metadata: opts.metadata }),
53
- time: { start: Date.now() },
54
- }
55
- : {
56
- status: "pending",
57
- input: opts.input,
58
- raw: opts.raw,
59
- }
60
- const payload: EventMessagePartUpdated = {
61
- type: "message.part.updated",
62
- properties: {
63
- part: {
64
- id: `part_${opts.callID}`,
65
- sessionID: sessionId,
66
- messageID: `msg_${opts.callID}`,
67
- type: "tool",
68
- callID: opts.callID,
69
- tool: opts.tool,
70
- state,
71
- },
72
- },
73
- }
74
- return { directory: cwd, payload }
75
- }
76
-
77
- function createEventStream() {
78
- const queue: GlobalEventEnvelope[] = []
79
- const waiters: Array<(value: GlobalEventEnvelope | undefined) => void> = []
80
- const state = { closed: false }
81
-
82
- const push = (event: GlobalEventEnvelope) => {
83
- const waiter = waiters.shift()
84
- if (waiter) {
85
- waiter(event)
86
- return
87
- }
88
- queue.push(event)
89
- }
90
-
91
- const close = () => {
92
- state.closed = true
93
- for (const waiter of waiters.splice(0)) {
94
- waiter(undefined)
95
- }
96
- }
97
-
98
- const stream = async function* (signal?: AbortSignal) {
99
- while (true) {
100
- if (signal?.aborted) return
101
- const next = queue.shift()
102
- if (next) {
103
- yield next
104
- continue
105
- }
106
- if (state.closed) return
107
- const value = await new Promise<GlobalEventEnvelope | undefined>((resolve) => {
108
- waiters.push(resolve)
109
- if (!signal) return
110
- signal.addEventListener("abort", () => resolve(undefined), { once: true })
111
- })
112
- if (!value) return
113
- yield value
114
- }
115
- }
116
-
117
- return { controller: { push, close } satisfies EventController, stream }
118
- }
119
-
120
- function createFakeAgent() {
121
- const updates = new Map<string, string[]>()
122
- const chunks = new Map<string, string>()
123
- const sessionUpdates: SessionUpdateParams[] = []
124
- const record = (sessionId: string, type: string) => {
125
- const list = updates.get(sessionId) ?? []
126
- list.push(type)
127
- updates.set(sessionId, list)
128
- }
129
-
130
- const connection = {
131
- async sessionUpdate(params: SessionUpdateParams) {
132
- sessionUpdates.push(params)
133
- const update = params.update
134
- const type = update?.sessionUpdate ?? "unknown"
135
- record(params.sessionId, type)
136
- if (update?.sessionUpdate === "agent_message_chunk") {
137
- const content = update.content
138
- if (content?.type !== "text") return
139
- if (typeof content.text !== "string") return
140
- chunks.set(params.sessionId, (chunks.get(params.sessionId) ?? "") + content.text)
141
- }
142
- },
143
- async requestPermission(_params: RequestPermissionParams): Promise<RequestPermissionResult> {
144
- return { outcome: { outcome: "selected", optionId: "once" } } as RequestPermissionResult
145
- },
146
- } as unknown as AgentSideConnection
147
-
148
- const { controller, stream } = createEventStream()
149
- const calls = {
150
- eventSubscribe: 0,
151
- sessionCreate: 0,
152
- }
153
-
154
- const sdk = {
155
- global: {
156
- event: async (opts?: { signal?: AbortSignal }) => {
157
- calls.eventSubscribe++
158
- return { stream: stream(opts?.signal) }
159
- },
160
- },
161
- session: {
162
- create: async (_params?: any) => {
163
- calls.sessionCreate++
164
- return {
165
- data: {
166
- id: `ses_${calls.sessionCreate}`,
167
- time: { created: new Date().toISOString() },
168
- },
169
- }
170
- },
171
- get: async (_params?: any) => {
172
- return {
173
- data: {
174
- id: "ses_1",
175
- time: { created: new Date().toISOString() },
176
- },
177
- }
178
- },
179
- messages: async () => {
180
- return { data: [] }
181
- },
182
- message: async (params?: any) => {
183
- // Return a message with parts that can be looked up by partID
184
- return {
185
- data: {
186
- info: {
187
- role: "assistant",
188
- },
189
- parts: [
190
- {
191
- id: params?.messageID ? `${params.messageID}_part` : "part_1",
192
- type: "text",
193
- text: "",
194
- },
195
- ],
196
- },
197
- }
198
- },
199
- },
200
- permission: {
201
- respond: async () => {
202
- return { data: true }
203
- },
204
- },
205
- config: {
206
- providers: async () => {
207
- return {
208
- data: {
209
- providers: [
210
- {
211
- id: "opencode",
212
- name: "opencode",
213
- models: {
214
- "big-pickle": { id: "big-pickle", name: "big-pickle" },
215
- },
216
- },
217
- ],
218
- },
219
- }
220
- },
221
- },
222
- app: {
223
- agents: async () => {
224
- return {
225
- data: [
226
- {
227
- name: "build",
228
- description: "build",
229
- mode: "agent",
230
- },
231
- ],
232
- }
233
- },
234
- },
235
- command: {
236
- list: async () => {
237
- return { data: [] }
238
- },
239
- },
240
- mcp: {
241
- add: async () => {
242
- return { data: true }
243
- },
244
- },
245
- } as any
246
-
247
- const agent = new ACP.Agent(connection, {
248
- sdk,
249
- defaultModel: { providerID: "opencode", modelID: "big-pickle" },
250
- } as any)
251
-
252
- const stop = () => {
253
- controller.close()
254
- ;(agent as any).eventAbort.abort()
255
- }
256
-
257
- return { agent, controller, calls, updates, chunks, sessionUpdates, stop, sdk, connection }
258
- }
259
-
260
- describe("acp.agent event subscription", () => {
261
- test("routes message.part.delta by the event sessionID (no cross-session pollution)", async () => {
262
- await using tmp = await tmpdir()
263
- await Instance.provide({
264
- directory: tmp.path,
265
- fn: async () => {
266
- const { agent, controller, updates, stop } = createFakeAgent()
267
- const cwd = "/tmp/opencode-acp-test"
268
-
269
- const sessionA = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
270
- const sessionB = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
271
-
272
- controller.push({
273
- directory: cwd,
274
- payload: {
275
- type: "message.part.delta",
276
- properties: {
277
- sessionID: sessionB,
278
- messageID: "msg_1",
279
- partID: "msg_1_part",
280
- field: "text",
281
- delta: "hello",
282
- },
283
- },
284
- } as any)
285
-
286
- await new Promise((r) => setTimeout(r, 10))
287
-
288
- expect((updates.get(sessionA) ?? []).includes("agent_message_chunk")).toBe(false)
289
- expect((updates.get(sessionB) ?? []).includes("agent_message_chunk")).toBe(true)
290
-
291
- stop()
292
- },
293
- })
294
- })
295
-
296
- test("keeps concurrent sessions isolated when message.part.delta events are interleaved", async () => {
297
- await using tmp = await tmpdir()
298
- await Instance.provide({
299
- directory: tmp.path,
300
- fn: async () => {
301
- const { agent, controller, chunks, stop } = createFakeAgent()
302
- const cwd = "/tmp/opencode-acp-test"
303
-
304
- const sessionA = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
305
- const sessionB = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
306
-
307
- const tokenA = ["ALPHA_", "111", "_X"]
308
- const tokenB = ["BETA_", "222", "_Y"]
309
-
310
- const push = (sessionId: string, messageID: string, delta: string) => {
311
- controller.push({
312
- directory: cwd,
313
- payload: {
314
- type: "message.part.delta",
315
- properties: {
316
- sessionID: sessionId,
317
- messageID,
318
- partID: `${messageID}_part`,
319
- field: "text",
320
- delta,
321
- },
322
- },
323
- } as any)
324
- }
325
-
326
- push(sessionA, "msg_a", tokenA[0])
327
- push(sessionB, "msg_b", tokenB[0])
328
- push(sessionA, "msg_a", tokenA[1])
329
- push(sessionB, "msg_b", tokenB[1])
330
- push(sessionA, "msg_a", tokenA[2])
331
- push(sessionB, "msg_b", tokenB[2])
332
-
333
- await new Promise((r) => setTimeout(r, 20))
334
-
335
- const a = chunks.get(sessionA) ?? ""
336
- const b = chunks.get(sessionB) ?? ""
337
-
338
- expect(a).toContain(tokenA.join(""))
339
- expect(b).toContain(tokenB.join(""))
340
- for (const part of tokenB) expect(a).not.toContain(part)
341
- for (const part of tokenA) expect(b).not.toContain(part)
342
-
343
- stop()
344
- },
345
- })
346
- })
347
-
348
- test("does not create additional event subscriptions on repeated loadSession()", async () => {
349
- await using tmp = await tmpdir()
350
- await Instance.provide({
351
- directory: tmp.path,
352
- fn: async () => {
353
- const { agent, calls, stop } = createFakeAgent()
354
- const cwd = "/tmp/opencode-acp-test"
355
-
356
- const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
357
-
358
- await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any)
359
- await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any)
360
- await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any)
361
- await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any)
362
-
363
- expect(calls.eventSubscribe).toBe(1)
364
-
365
- stop()
366
- },
367
- })
368
- })
369
-
370
- test("permission.asked events are handled and replied", async () => {
371
- await using tmp = await tmpdir()
372
- await Instance.provide({
373
- directory: tmp.path,
374
- fn: async () => {
375
- const permissionReplies: string[] = []
376
- const { agent, controller, stop, sdk } = createFakeAgent()
377
- sdk.permission.reply = async (params: any) => {
378
- permissionReplies.push(params.requestID)
379
- return { data: true }
380
- }
381
- const cwd = "/tmp/opencode-acp-test"
382
-
383
- const sessionA = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
384
-
385
- controller.push({
386
- directory: cwd,
387
- payload: {
388
- type: "permission.asked",
389
- properties: {
390
- id: "perm_1",
391
- sessionID: sessionA,
392
- permission: "bash",
393
- patterns: ["*"],
394
- metadata: {},
395
- always: [],
396
- },
397
- },
398
- } as any)
399
-
400
- await new Promise((r) => setTimeout(r, 20))
401
-
402
- expect(permissionReplies).toContain("perm_1")
403
-
404
- stop()
405
- },
406
- })
407
- })
408
-
409
- test("permission prompt on session A does not block message updates for session B", async () => {
410
- await using tmp = await tmpdir()
411
- await Instance.provide({
412
- directory: tmp.path,
413
- fn: async () => {
414
- const permissionReplies: string[] = []
415
- let resolvePermissionA: (() => void) | undefined
416
- const permissionABlocking = new Promise<void>((r) => {
417
- resolvePermissionA = r
418
- })
419
-
420
- const { agent, controller, chunks, stop, sdk, connection } = createFakeAgent()
421
-
422
- // Make permission request for session A block until we release it
423
- const originalRequestPermission = connection.requestPermission.bind(connection)
424
- let permissionCalls = 0
425
- connection.requestPermission = async (params: RequestPermissionParams) => {
426
- permissionCalls++
427
- if (params.sessionId.endsWith("1")) {
428
- await permissionABlocking
429
- }
430
- return originalRequestPermission(params)
431
- }
432
-
433
- sdk.permission.reply = async (params: any) => {
434
- permissionReplies.push(params.requestID)
435
- return { data: true }
436
- }
437
-
438
- const cwd = "/tmp/opencode-acp-test"
439
-
440
- const sessionA = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
441
- const sessionB = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
442
-
443
- // Push permission.asked for session A (will block)
444
- controller.push({
445
- directory: cwd,
446
- payload: {
447
- type: "permission.asked",
448
- properties: {
449
- id: "perm_a",
450
- sessionID: sessionA,
451
- permission: "bash",
452
- patterns: ["*"],
453
- metadata: {},
454
- always: [],
455
- },
456
- },
457
- } as any)
458
-
459
- // Give time for permission handling to start
460
- await new Promise((r) => setTimeout(r, 10))
461
-
462
- // Push message for session B while A's permission is pending
463
- controller.push({
464
- directory: cwd,
465
- payload: {
466
- type: "message.part.delta",
467
- properties: {
468
- sessionID: sessionB,
469
- messageID: "msg_b",
470
- partID: "msg_b_part",
471
- field: "text",
472
- delta: "session_b_message",
473
- },
474
- },
475
- } as any)
476
-
477
- // Wait for session B's message to be processed
478
- await new Promise((r) => setTimeout(r, 20))
479
-
480
- // Session B should have received message even though A's permission is still pending
481
- expect(chunks.get(sessionB) ?? "").toContain("session_b_message")
482
- expect(permissionReplies).not.toContain("perm_a")
483
-
484
- // Release session A's permission
485
- resolvePermissionA!()
486
- await new Promise((r) => setTimeout(r, 20))
487
-
488
- // Now session A's permission should be replied
489
- expect(permissionReplies).toContain("perm_a")
490
-
491
- stop()
492
- },
493
- })
494
- })
495
-
496
- test("streams running bash output snapshots and de-dupes identical snapshots", async () => {
497
- await using tmp = await tmpdir()
498
- await Instance.provide({
499
- directory: tmp.path,
500
- fn: async () => {
501
- const { agent, controller, sessionUpdates, stop } = createFakeAgent()
502
- const cwd = "/tmp/opencode-acp-test"
503
- const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
504
- const input = { command: "echo hello", description: "run command" }
505
-
506
- for (const output of ["a", "a", "ab"]) {
507
- controller.push(
508
- toolEvent(sessionId, cwd, {
509
- callID: "call_1",
510
- tool: "bash",
511
- status: "running",
512
- input,
513
- metadata: { output },
514
- }),
515
- )
516
- }
517
- await new Promise((r) => setTimeout(r, 20))
518
-
519
- const snapshots = sessionUpdates
520
- .filter((u) => u.sessionId === sessionId)
521
- .filter((u) => isToolCallUpdate(u.update))
522
- .map((u) => inProgressText(u.update))
523
-
524
- expect(snapshots).toEqual(["a", undefined, "ab"])
525
- stop()
526
- },
527
- })
528
- })
529
-
530
- test("emits synthetic pending before first running update for any tool", async () => {
531
- await using tmp = await tmpdir()
532
- await Instance.provide({
533
- directory: tmp.path,
534
- fn: async () => {
535
- const { agent, controller, sessionUpdates, stop } = createFakeAgent()
536
- const cwd = "/tmp/opencode-acp-test"
537
- const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
538
-
539
- controller.push(
540
- toolEvent(sessionId, cwd, {
541
- callID: "call_bash",
542
- tool: "bash",
543
- status: "running",
544
- input: { command: "echo hi", description: "run command" },
545
- metadata: { output: "hi\n" },
546
- }),
547
- )
548
- controller.push(
549
- toolEvent(sessionId, cwd, {
550
- callID: "call_read",
551
- tool: "read",
552
- status: "running",
553
- input: { filePath: "/tmp/example.txt" },
554
- }),
555
- )
556
- await new Promise((r) => setTimeout(r, 20))
557
-
558
- const types = sessionUpdates
559
- .filter((u) => u.sessionId === sessionId)
560
- .map((u) => u.update.sessionUpdate)
561
- .filter((u) => u === "tool_call" || u === "tool_call_update")
562
- expect(types).toEqual(["tool_call", "tool_call_update", "tool_call", "tool_call_update"])
563
-
564
- const pendings = sessionUpdates.filter(
565
- (u) => u.sessionId === sessionId && u.update.sessionUpdate === "tool_call",
566
- )
567
- expect(pendings.every((p) => p.update.sessionUpdate === "tool_call" && p.update.status === "pending")).toBe(
568
- true,
569
- )
570
- stop()
571
- },
572
- })
573
- })
574
-
575
- test("does not emit duplicate synthetic pending after replayed running tool", async () => {
576
- await using tmp = await tmpdir()
577
- await Instance.provide({
578
- directory: tmp.path,
579
- fn: async () => {
580
- const { agent, controller, sessionUpdates, stop, sdk } = createFakeAgent()
581
- const cwd = "/tmp/opencode-acp-test"
582
- const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
583
- const input = { command: "echo hi", description: "run command" }
584
-
585
- sdk.session.messages = async () => ({
586
- data: [
587
- {
588
- info: {
589
- role: "assistant",
590
- sessionID: sessionId,
591
- },
592
- parts: [
593
- {
594
- type: "tool",
595
- callID: "call_1",
596
- tool: "bash",
597
- state: {
598
- status: "running",
599
- input,
600
- metadata: { output: "hi\n" },
601
- time: { start: Date.now() },
602
- },
603
- },
604
- ],
605
- },
606
- ],
607
- })
608
-
609
- await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any)
610
- controller.push(
611
- toolEvent(sessionId, cwd, {
612
- callID: "call_1",
613
- tool: "bash",
614
- status: "running",
615
- input,
616
- metadata: { output: "hi\nthere\n" },
617
- }),
618
- )
619
- await new Promise((r) => setTimeout(r, 20))
620
-
621
- const types = sessionUpdates
622
- .filter((u) => u.sessionId === sessionId)
623
- .map((u) => u.update)
624
- .filter((u) => "toolCallId" in u && u.toolCallId === "call_1")
625
- .map((u) => u.sessionUpdate)
626
- .filter((u) => u === "tool_call" || u === "tool_call_update")
627
-
628
- expect(types).toEqual(["tool_call", "tool_call_update", "tool_call_update"])
629
- stop()
630
- },
631
- })
632
- })
633
-
634
- test("clears bash snapshot marker on pending state", async () => {
635
- await using tmp = await tmpdir()
636
- await Instance.provide({
637
- directory: tmp.path,
638
- fn: async () => {
639
- const { agent, controller, sessionUpdates, stop } = createFakeAgent()
640
- const cwd = "/tmp/opencode-acp-test"
641
- const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
642
- const input = { command: "echo hello", description: "run command" }
643
-
644
- controller.push(
645
- toolEvent(sessionId, cwd, {
646
- callID: "call_1",
647
- tool: "bash",
648
- status: "running",
649
- input,
650
- metadata: { output: "a" },
651
- }),
652
- )
653
- controller.push(
654
- toolEvent(sessionId, cwd, {
655
- callID: "call_1",
656
- tool: "bash",
657
- status: "pending",
658
- input,
659
- raw: '{"command":"echo hello"}',
660
- }),
661
- )
662
- controller.push(
663
- toolEvent(sessionId, cwd, {
664
- callID: "call_1",
665
- tool: "bash",
666
- status: "running",
667
- input,
668
- metadata: { output: "a" },
669
- }),
670
- )
671
- await new Promise((r) => setTimeout(r, 20))
672
-
673
- const snapshots = sessionUpdates
674
- .filter((u) => u.sessionId === sessionId)
675
- .filter((u) => isToolCallUpdate(u.update))
676
- .map((u) => inProgressText(u.update))
677
-
678
- expect(snapshots).toEqual(["a", "a"])
679
- stop()
680
- },
681
- })
682
- })
683
- })