@stonerzju/opencode 1.2.16-offline.1 → 1.2.18

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 (262) 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/package.json.bak +0 -140
  155. package/script/build.ts +0 -224
  156. package/script/check-migrations.ts +0 -16
  157. package/script/postinstall.mjs +0 -131
  158. package/script/publish.ts +0 -181
  159. package/script/schema.ts +0 -63
  160. package/script/seed-e2e.ts +0 -50
  161. package/sst-env.d.ts +0 -10
  162. package/test/AGENTS.md +0 -81
  163. package/test/acp/agent-interface.test.ts +0 -51
  164. package/test/acp/event-subscription.test.ts +0 -683
  165. package/test/agent/agent.test.ts +0 -689
  166. package/test/bun.test.ts +0 -53
  167. package/test/cli/github-action.test.ts +0 -197
  168. package/test/cli/github-remote.test.ts +0 -80
  169. package/test/cli/import.test.ts +0 -38
  170. package/test/cli/plugin-auth-picker.test.ts +0 -120
  171. package/test/cli/tui/transcript.test.ts +0 -322
  172. package/test/config/agent-color.test.ts +0 -71
  173. package/test/config/config.test.ts +0 -1886
  174. package/test/config/fixtures/empty-frontmatter.md +0 -4
  175. package/test/config/fixtures/frontmatter.md +0 -28
  176. package/test/config/fixtures/markdown-header.md +0 -11
  177. package/test/config/fixtures/no-frontmatter.md +0 -1
  178. package/test/config/fixtures/weird-model-id.md +0 -13
  179. package/test/config/markdown.test.ts +0 -228
  180. package/test/config/tui.test.ts +0 -510
  181. package/test/control-plane/session-proxy-middleware.test.ts +0 -147
  182. package/test/control-plane/sse.test.ts +0 -56
  183. package/test/control-plane/workspace-server-sse.test.ts +0 -65
  184. package/test/control-plane/workspace-sync.test.ts +0 -97
  185. package/test/file/ignore.test.ts +0 -10
  186. package/test/file/index.test.ts +0 -394
  187. package/test/file/path-traversal.test.ts +0 -198
  188. package/test/file/ripgrep.test.ts +0 -39
  189. package/test/file/time.test.ts +0 -361
  190. package/test/fixture/db.ts +0 -11
  191. package/test/fixture/fixture.ts +0 -45
  192. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  193. package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
  194. package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
  195. package/test/fixture/skills/cloudflare/SKILL.md +0 -211
  196. package/test/fixture/skills/index.json +0 -6
  197. package/test/ide/ide.test.ts +0 -82
  198. package/test/keybind.test.ts +0 -421
  199. package/test/lsp/client.test.ts +0 -95
  200. package/test/mcp/headers.test.ts +0 -153
  201. package/test/mcp/oauth-browser.test.ts +0 -249
  202. package/test/memory/abort-leak.test.ts +0 -136
  203. package/test/patch/patch.test.ts +0 -348
  204. package/test/permission/arity.test.ts +0 -33
  205. package/test/permission/next.test.ts +0 -689
  206. package/test/permission-task.test.ts +0 -319
  207. package/test/plugin/auth-override.test.ts +0 -44
  208. package/test/plugin/codex.test.ts +0 -123
  209. package/test/preload.ts +0 -80
  210. package/test/project/project.test.ts +0 -348
  211. package/test/project/worktree-remove.test.ts +0 -65
  212. package/test/provider/amazon-bedrock.test.ts +0 -446
  213. package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
  214. package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
  215. package/test/provider/gitlab-duo.test.ts +0 -262
  216. package/test/provider/provider.test.ts +0 -2220
  217. package/test/provider/transform.test.ts +0 -2353
  218. package/test/pty/pty-output-isolation.test.ts +0 -140
  219. package/test/question/question.test.ts +0 -300
  220. package/test/scheduler.test.ts +0 -73
  221. package/test/server/global-session-list.test.ts +0 -89
  222. package/test/server/session-list.test.ts +0 -90
  223. package/test/server/session-select.test.ts +0 -78
  224. package/test/session/compaction.test.ts +0 -423
  225. package/test/session/instruction.test.ts +0 -170
  226. package/test/session/llm.test.ts +0 -667
  227. package/test/session/message-v2.test.ts +0 -924
  228. package/test/session/prompt.test.ts +0 -211
  229. package/test/session/retry.test.ts +0 -188
  230. package/test/session/revert-compact.test.ts +0 -285
  231. package/test/session/session.test.ts +0 -71
  232. package/test/session/structured-output-integration.test.ts +0 -233
  233. package/test/session/structured-output.test.ts +0 -385
  234. package/test/skill/discovery.test.ts +0 -110
  235. package/test/skill/skill.test.ts +0 -388
  236. package/test/snapshot/snapshot.test.ts +0 -1180
  237. package/test/storage/json-migration.test.ts +0 -846
  238. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  239. package/test/tool/apply_patch.test.ts +0 -566
  240. package/test/tool/bash.test.ts +0 -402
  241. package/test/tool/edit.test.ts +0 -496
  242. package/test/tool/external-directory.test.ts +0 -127
  243. package/test/tool/fixtures/large-image.png +0 -0
  244. package/test/tool/fixtures/models-api.json +0 -38413
  245. package/test/tool/grep.test.ts +0 -110
  246. package/test/tool/question.test.ts +0 -107
  247. package/test/tool/read.test.ts +0 -504
  248. package/test/tool/registry.test.ts +0 -122
  249. package/test/tool/skill.test.ts +0 -112
  250. package/test/tool/truncation.test.ts +0 -160
  251. package/test/tool/webfetch.test.ts +0 -100
  252. package/test/tool/write.test.ts +0 -348
  253. package/test/util/filesystem.test.ts +0 -443
  254. package/test/util/format.test.ts +0 -59
  255. package/test/util/glob.test.ts +0 -164
  256. package/test/util/iife.test.ts +0 -36
  257. package/test/util/lazy.test.ts +0 -50
  258. package/test/util/lock.test.ts +0 -72
  259. package/test/util/process.test.ts +0 -59
  260. package/test/util/timeout.test.ts +0 -21
  261. package/test/util/wildcard.test.ts +0 -90
  262. 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
- })