@stonerzju/opencode 1.2.17 → 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.
- package/bin/opencode +29 -157
- package/package.json +29 -29
- package/src/acp/agent.ts +4 -4
- package/src/acp/session.ts +1 -1
- package/src/agent/agent.ts +3 -3
- package/src/bun/index.ts +2 -2
- package/src/cli/cmd/acp.ts +3 -3
- package/src/cli/cmd/debug/file.ts +1 -1
- package/src/cli/cmd/github.ts +2 -2
- package/src/cli/cmd/pr.ts +1 -1
- package/src/cli/cmd/tui/app.tsx +24 -24
- package/src/cli/cmd/tui/attach.ts +3 -3
- package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
- package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
- package/src/cli/cmd/tui/component/logo.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
- package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
- package/src/cli/cmd/tui/component/tips.tsx +1 -1
- package/src/cli/cmd/tui/context/directory.ts +1 -1
- package/src/cli/cmd/tui/context/exit.tsx +1 -1
- package/src/cli/cmd/tui/context/keybind.tsx +2 -2
- package/src/cli/cmd/tui/context/kv.tsx +2 -2
- package/src/cli/cmd/tui/context/local.tsx +6 -6
- package/src/cli/cmd/tui/context/sync.tsx +4 -4
- package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
- package/src/cli/cmd/tui/context/theme.tsx +2 -2
- package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
- package/src/cli/cmd/tui/event.ts +2 -2
- package/src/cli/cmd/tui/routes/home.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
- package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
- package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
- package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
- package/src/cli/cmd/tui/thread.ts +9 -9
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
- package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
- package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
- package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
- package/src/cli/cmd/tui/ui/toast.tsx +1 -1
- package/src/cli/cmd/tui/util/editor.ts +3 -3
- package/src/cli/cmd/tui/util/transcript.ts +1 -1
- package/src/cli/cmd/tui/worker.ts +10 -10
- package/src/cli/error.ts +1 -1
- package/src/cli/ui.ts +1 -1
- package/src/cli/upgrade.ts +4 -4
- package/src/command/index.ts +1 -1
- package/src/config/config.ts +10 -10
- package/src/config/markdown.ts +1 -1
- package/src/config/migrate-tui-config.ts +5 -5
- package/src/config/paths.ts +4 -4
- package/src/config/tui.ts +4 -4
- package/src/control/control.sql.ts +1 -1
- package/src/control/index.ts +1 -1
- package/src/control-plane/adaptors/worktree.ts +1 -1
- package/src/control-plane/session-proxy-middleware.ts +1 -1
- package/src/control-plane/workspace.sql.ts +1 -1
- package/src/control-plane/workspace.ts +7 -7
- package/src/file/index.ts +1 -1
- package/src/file/ripgrep.ts +2 -2
- package/src/file/watcher.ts +5 -5
- package/src/format/formatter.ts +1 -1
- package/src/ide/index.ts +3 -3
- package/src/index.ts +1 -1
- package/src/installation/index.ts +3 -3
- package/src/lsp/client.ts +3 -3
- package/src/lsp/index.ts +3 -3
- package/src/mcp/index.ts +4 -4
- package/src/permission/index.ts +2 -2
- package/src/permission/next.ts +10 -10
- package/src/plugin/codex.ts +1 -1
- package/src/plugin/copilot.ts +2 -2
- package/src/plugin/index.ts +1 -1
- package/src/project/bootstrap.ts +2 -2
- package/src/project/instance.ts +4 -4
- package/src/project/project.sql.ts +1 -1
- package/src/project/project.ts +5 -5
- package/src/project/state.ts +1 -1
- package/src/project/vcs.ts +4 -4
- package/src/provider/auth.ts +4 -4
- package/src/provider/error.ts +1 -1
- package/src/provider/models-snapshot.ts +2 -0
- package/src/provider/models.ts +1 -1
- package/src/provider/provider.ts +2 -2
- package/src/provider/transform.ts +2 -2
- package/src/pty/index.ts +5 -5
- package/src/question/index.ts +5 -5
- package/src/server/event.ts +1 -1
- package/src/server/mdns.ts +1 -1
- package/src/server/routes/global.ts +3 -3
- package/src/server/routes/permission.ts +1 -1
- package/src/server/routes/pty.ts +1 -1
- package/src/server/routes/session.ts +4 -4
- package/src/server/routes/tui.ts +1 -1
- package/src/server/server.ts +3 -3
- package/src/session/compaction.ts +7 -7
- package/src/session/index.ts +10 -10
- package/src/session/instruction.ts +1 -1
- package/src/session/llm.ts +11 -11
- package/src/session/message-v2.ts +10 -10
- package/src/session/message.ts +1 -1
- package/src/session/processor.ts +10 -10
- package/src/session/prompt.ts +8 -8
- package/src/session/retry.ts +2 -2
- package/src/session/revert.ts +1 -1
- package/src/session/session.sql.ts +3 -3
- package/src/session/status.ts +3 -3
- package/src/session/summary.ts +5 -5
- package/src/session/system.ts +1 -1
- package/src/session/todo.ts +2 -2
- package/src/share/share-next.ts +7 -7
- package/src/share/share.sql.ts +1 -1
- package/src/shell/shell.ts +3 -3
- package/src/skill/skill.ts +6 -6
- package/src/storage/db.ts +1 -1
- package/src/storage/storage.ts +1 -1
- package/src/tool/bash.ts +6 -6
- package/src/tool/edit.ts +1 -1
- package/src/tool/registry.ts +2 -2
- package/src/tool/skill.ts +1 -1
- package/src/tool/task.ts +3 -3
- package/src/util/array.ts +10 -0
- package/src/util/binary.ts +41 -0
- package/src/util/encode.ts +51 -0
- package/src/util/error.ts +54 -0
- package/src/util/identifier.ts +48 -0
- package/src/util/lazy.ts +4 -16
- package/src/util/path.ts +37 -0
- package/src/util/retry.ts +41 -0
- package/src/util/slug.ts +74 -0
- package/src/worktree/index.ts +3 -3
- package/AGENTS.md +0 -10
- package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
- package/Dockerfile +0 -18
- package/README.md +0 -15
- package/bunfig.toml +0 -7
- package/drizzle.config.ts +0 -10
- package/script/build.ts +0 -224
- package/script/check-migrations.ts +0 -16
- package/script/postinstall.mjs +0 -131
- package/script/publish.ts +0 -181
- package/script/schema.ts +0 -63
- package/script/seed-e2e.ts +0 -50
- package/sst-env.d.ts +0 -10
- package/test/AGENTS.md +0 -81
- package/test/acp/agent-interface.test.ts +0 -51
- package/test/acp/event-subscription.test.ts +0 -683
- package/test/agent/agent.test.ts +0 -689
- package/test/bun.test.ts +0 -53
- package/test/cli/github-action.test.ts +0 -197
- package/test/cli/github-remote.test.ts +0 -80
- package/test/cli/import.test.ts +0 -38
- package/test/cli/plugin-auth-picker.test.ts +0 -120
- package/test/cli/tui/transcript.test.ts +0 -322
- package/test/config/agent-color.test.ts +0 -71
- package/test/config/config.test.ts +0 -1886
- package/test/config/fixtures/empty-frontmatter.md +0 -4
- package/test/config/fixtures/frontmatter.md +0 -28
- package/test/config/fixtures/markdown-header.md +0 -11
- package/test/config/fixtures/no-frontmatter.md +0 -1
- package/test/config/fixtures/weird-model-id.md +0 -13
- package/test/config/markdown.test.ts +0 -228
- package/test/config/tui.test.ts +0 -510
- package/test/control-plane/session-proxy-middleware.test.ts +0 -147
- package/test/control-plane/sse.test.ts +0 -56
- package/test/control-plane/workspace-server-sse.test.ts +0 -65
- package/test/control-plane/workspace-sync.test.ts +0 -97
- package/test/file/ignore.test.ts +0 -10
- package/test/file/index.test.ts +0 -394
- package/test/file/path-traversal.test.ts +0 -198
- package/test/file/ripgrep.test.ts +0 -39
- package/test/file/time.test.ts +0 -361
- package/test/fixture/db.ts +0 -11
- package/test/fixture/fixture.ts +0 -45
- package/test/fixture/lsp/fake-lsp-server.js +0 -77
- package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
- package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
- package/test/fixture/skills/cloudflare/SKILL.md +0 -211
- package/test/fixture/skills/index.json +0 -6
- package/test/ide/ide.test.ts +0 -82
- package/test/keybind.test.ts +0 -421
- package/test/lsp/client.test.ts +0 -95
- package/test/mcp/headers.test.ts +0 -153
- package/test/mcp/oauth-browser.test.ts +0 -249
- package/test/memory/abort-leak.test.ts +0 -136
- package/test/patch/patch.test.ts +0 -348
- package/test/permission/arity.test.ts +0 -33
- package/test/permission/next.test.ts +0 -689
- package/test/permission-task.test.ts +0 -319
- package/test/plugin/auth-override.test.ts +0 -44
- package/test/plugin/codex.test.ts +0 -123
- package/test/preload.ts +0 -80
- package/test/project/project.test.ts +0 -348
- package/test/project/worktree-remove.test.ts +0 -65
- package/test/provider/amazon-bedrock.test.ts +0 -446
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
- package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
- package/test/provider/gitlab-duo.test.ts +0 -262
- package/test/provider/provider.test.ts +0 -2220
- package/test/provider/transform.test.ts +0 -2353
- package/test/pty/pty-output-isolation.test.ts +0 -140
- package/test/question/question.test.ts +0 -300
- package/test/scheduler.test.ts +0 -73
- package/test/server/global-session-list.test.ts +0 -89
- package/test/server/session-list.test.ts +0 -90
- package/test/server/session-select.test.ts +0 -78
- package/test/session/compaction.test.ts +0 -423
- package/test/session/instruction.test.ts +0 -170
- package/test/session/llm.test.ts +0 -667
- package/test/session/message-v2.test.ts +0 -924
- package/test/session/prompt.test.ts +0 -211
- package/test/session/retry.test.ts +0 -188
- package/test/session/revert-compact.test.ts +0 -285
- package/test/session/session.test.ts +0 -71
- package/test/session/structured-output-integration.test.ts +0 -233
- package/test/session/structured-output.test.ts +0 -385
- package/test/skill/discovery.test.ts +0 -110
- package/test/skill/skill.test.ts +0 -388
- package/test/snapshot/snapshot.test.ts +0 -1180
- package/test/storage/json-migration.test.ts +0 -846
- package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
- package/test/tool/apply_patch.test.ts +0 -566
- package/test/tool/bash.test.ts +0 -402
- package/test/tool/edit.test.ts +0 -496
- package/test/tool/external-directory.test.ts +0 -127
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +0 -38413
- package/test/tool/grep.test.ts +0 -110
- package/test/tool/question.test.ts +0 -107
- package/test/tool/read.test.ts +0 -504
- package/test/tool/registry.test.ts +0 -122
- package/test/tool/skill.test.ts +0 -112
- package/test/tool/truncation.test.ts +0 -160
- package/test/tool/webfetch.test.ts +0 -100
- package/test/tool/write.test.ts +0 -348
- package/test/util/filesystem.test.ts +0 -443
- package/test/util/format.test.ts +0 -59
- package/test/util/glob.test.ts +0 -164
- package/test/util/iife.test.ts +0 -36
- package/test/util/lazy.test.ts +0 -50
- package/test/util/lock.test.ts +0 -72
- package/test/util/process.test.ts +0 -59
- package/test/util/timeout.test.ts +0 -21
- package/test/util/wildcard.test.ts +0 -90
- 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
|
-
})
|