soraacode 0.1.0
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/AGENTS.md +131 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/opencode +199 -0
- package/bin/soraacode +199 -0
- package/bunfig.toml +7 -0
- package/git +0 -0
- package/migration/20260511173437_session-metadata/migration.sql +1 -0
- package/migration/20260511173437_session-metadata/snapshot.json +1500 -0
- package/package.json +107 -0
- package/parsers-config.ts +1 -0
- package/script/bench-search.ts +94 -0
- package/script/bench-test-suite.ts +52 -0
- package/script/build-headless.ts +51 -0
- package/script/build.ts +245 -0
- package/script/generate.ts +14 -0
- package/script/httpapi-exercise.ts +1 -0
- package/script/postinstall.mjs +189 -0
- package/script/profile-test-files.ts +42 -0
- package/script/publish.ts +213 -0
- package/script/run-workspace-server +106 -0
- package/script/schema.ts +77 -0
- package/script/time.ts +6 -0
- package/script/trace-imports.ts +153 -0
- package/specs/effect/error-boundaries-plan.md +235 -0
- package/specs/effect/errors.md +207 -0
- package/specs/effect/facades.md +218 -0
- package/specs/effect/guide.md +247 -0
- package/specs/effect/instance-context.md +13 -0
- package/specs/effect/loose-ends.md +30 -0
- package/specs/effect/migration.md +62 -0
- package/specs/effect/routes.md +61 -0
- package/specs/effect/schema.md +88 -0
- package/specs/effect/server-package.md +58 -0
- package/specs/effect/todo.md +241 -0
- package/specs/effect/tools.md +88 -0
- package/specs/openapi-translation-cleanup.md +204 -0
- package/specs/tui-plugins.md +544 -0
- package/specs/v2/api.ts +67 -0
- package/specs/v2/message-shape.md +136 -0
- package/specs/v2/notifications.md +13 -0
- package/specs/v2/tui-command-shim.md +67 -0
- package/src/account/account.ts +481 -0
- package/src/account/repo.ts +171 -0
- package/src/account/schema.ts +99 -0
- package/src/account/url.ts +8 -0
- package/src/acp/agent.ts +95 -0
- package/src/acp/config-option.ts +206 -0
- package/src/acp/content.ts +269 -0
- package/src/acp/directory.ts +212 -0
- package/src/acp/error.ts +97 -0
- package/src/acp/event.ts +421 -0
- package/src/acp/permission.ts +254 -0
- package/src/acp/profile.ts +42 -0
- package/src/acp/service.ts +1226 -0
- package/src/acp/session.ts +232 -0
- package/src/acp/tool.ts +364 -0
- package/src/acp/usage.ts +243 -0
- package/src/agent/agent.ts +450 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/compaction.txt +5 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +11 -0
- package/src/agent/prompt/title.txt +44 -0
- package/src/agent/subagent-permissions.ts +27 -0
- package/src/audio.d.ts +14 -0
- package/src/auth/index.ts +97 -0
- package/src/background/job.ts +37 -0
- package/src/bus/global.ts +22 -0
- package/src/cli/bootstrap.ts +11 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/serve.ts +24 -0
- package/src/cli/effect/prompt.ts +37 -0
- package/src/cli/effect-cmd.ts +96 -0
- package/src/cli/error.ts +130 -0
- package/src/cli/heap.ts +45 -0
- package/src/cli/logo.ts +2 -0
- package/src/cli/network.ts +80 -0
- package/src/cli/ui.ts +132 -0
- package/src/command/index.ts +177 -0
- package/src/command/template/initialize.txt +66 -0
- package/src/command/template/review.txt +101 -0
- package/src/config/agent.ts +59 -0
- package/src/config/command.ts +39 -0
- package/src/config/config.ts +707 -0
- package/src/config/entry-name.ts +19 -0
- package/src/config/managed.ts +69 -0
- package/src/config/markdown.ts +36 -0
- package/src/config/parse.ts +61 -0
- package/src/config/paths.ts +45 -0
- package/src/config/plugin.ts +79 -0
- package/src/config/v2-compat.ts +449 -0
- package/src/config/variable.ts +91 -0
- package/src/control-plane/adapters/index.ts +41 -0
- package/src/control-plane/adapters/worktree.ts +96 -0
- package/src/control-plane/dev/README.md +19 -0
- package/src/control-plane/dev/debug-workspace-plugin.ts +73 -0
- package/src/control-plane/types.ts +59 -0
- package/src/control-plane/util.ts +39 -0
- package/src/control-plane/workspace-adapter-runtime.ts +51 -0
- package/src/control-plane/workspace-context.ts +26 -0
- package/src/control-plane/workspace.ts +966 -0
- package/src/effect/app-node-builder-v1.ts +12 -0
- package/src/effect/app-runtime.ts +135 -0
- package/src/effect/bootstrap-runtime.ts +19 -0
- package/src/effect/bridge.ts +84 -0
- package/src/effect/config-service.ts +67 -0
- package/src/effect/instance-ref.ts +11 -0
- package/src/effect/instance-registry.ts +12 -0
- package/src/effect/instance-state.ts +69 -0
- package/src/effect/promise.ts +17 -0
- package/src/effect/run-service.ts +47 -0
- package/src/effect/runner.ts +217 -0
- package/src/effect/runtime-flags.ts +78 -0
- package/src/env/index.ts +41 -0
- package/src/event-manifest.ts +3 -0
- package/src/event-v2-bridge.ts +71 -0
- package/src/format/formatter.ts +404 -0
- package/src/format/index.ts +203 -0
- package/src/git/index.ts +348 -0
- package/src/id/id.ts +80 -0
- package/src/ide/index.ts +54 -0
- package/src/image/image.ts +172 -0
- package/src/index-headless.ts +89 -0
- package/src/index.ts +93 -0
- package/src/installation/index.ts +336 -0
- package/src/lsp/client.ts +650 -0
- package/src/lsp/diagnostic.ts +29 -0
- package/src/lsp/language.ts +121 -0
- package/src/lsp/launch.ts +21 -0
- package/src/lsp/lsp.ts +507 -0
- package/src/lsp/server.ts +1983 -0
- package/src/markdown.d.ts +4 -0
- package/src/mcp/auth.ts +163 -0
- package/src/mcp/browser.ts +37 -0
- package/src/mcp/catalog.ts +170 -0
- package/src/mcp/index.ts +1004 -0
- package/src/mcp/oauth-callback.ts +194 -0
- package/src/mcp/oauth-provider.ts +259 -0
- package/src/node.ts +4 -0
- package/src/patch/index.ts +686 -0
- package/src/permission/arity.ts +163 -0
- package/src/permission/evaluate.ts +1 -0
- package/src/permission/index.ts +223 -0
- package/src/plugin/github-copilot/copilot.ts +415 -0
- package/src/plugin/github-copilot/models.ts +261 -0
- package/src/plugin/index.ts +298 -0
- package/src/plugin/install.ts +439 -0
- package/src/plugin/loader.ts +237 -0
- package/src/plugin/meta.ts +188 -0
- package/src/plugin/openai/README.md +31 -0
- package/src/plugin/openai/codex.ts +575 -0
- package/src/plugin/openai/ws-pool.ts +271 -0
- package/src/plugin/openai/ws.ts +383 -0
- package/src/plugin/pty-environment.ts +24 -0
- package/src/plugin/shared.ts +323 -0
- package/src/project/bootstrap-service.ts +9 -0
- package/src/project/bootstrap.ts +58 -0
- package/src/project/instance-context.ts +24 -0
- package/src/project/instance-runtime.ts +16 -0
- package/src/project/instance-store.ts +213 -0
- package/src/project/project.ts +483 -0
- package/src/project/vcs.ts +423 -0
- package/src/provider/auth.ts +229 -0
- package/src/provider/error.ts +195 -0
- package/src/provider/model-status.ts +8 -0
- package/src/provider/provider.ts +1955 -0
- package/src/provider/transform.ts +1923 -0
- package/src/question/index.ts +161 -0
- package/src/question/schema.ts +4 -0
- package/src/server/auth.ts +48 -0
- package/src/server/event.ts +10 -0
- package/src/server/global-lifecycle.ts +28 -0
- package/src/server/mdns.ts +47 -0
- package/src/server/proxy-util.ts +48 -0
- package/src/server/routes/instance/httpapi/AGENTS.md +39 -0
- package/src/server/routes/instance/httpapi/api.ts +97 -0
- package/src/server/routes/instance/httpapi/errors.ts +193 -0
- package/src/server/routes/instance/httpapi/groups/config.ts +65 -0
- package/src/server/routes/instance/httpapi/groups/control-plane.ts +35 -0
- package/src/server/routes/instance/httpapi/groups/control.ts +76 -0
- package/src/server/routes/instance/httpapi/groups/event.ts +29 -0
- package/src/server/routes/instance/httpapi/groups/experimental.ts +275 -0
- package/src/server/routes/instance/httpapi/groups/file.ts +185 -0
- package/src/server/routes/instance/httpapi/groups/global.ts +139 -0
- package/src/server/routes/instance/httpapi/groups/instance.ts +206 -0
- package/src/server/routes/instance/httpapi/groups/mcp.ts +156 -0
- package/src/server/routes/instance/httpapi/groups/metadata.ts +18 -0
- package/src/server/routes/instance/httpapi/groups/permission.ts +61 -0
- package/src/server/routes/instance/httpapi/groups/project-copy.ts +32 -0
- package/src/server/routes/instance/httpapi/groups/project.ts +93 -0
- package/src/server/routes/instance/httpapi/groups/provider.ts +101 -0
- package/src/server/routes/instance/httpapi/groups/pty.ts +172 -0
- package/src/server/routes/instance/httpapi/groups/query.ts +12 -0
- package/src/server/routes/instance/httpapi/groups/question.ts +74 -0
- package/src/server/routes/instance/httpapi/groups/session.ts +462 -0
- package/src/server/routes/instance/httpapi/groups/sync.ts +113 -0
- package/src/server/routes/instance/httpapi/groups/tui.ts +208 -0
- package/src/server/routes/instance/httpapi/groups/workspace.ts +141 -0
- package/src/server/routes/instance/httpapi/handlers/config.ts +34 -0
- package/src/server/routes/instance/httpapi/handlers/control-plane.ts +37 -0
- package/src/server/routes/instance/httpapi/handlers/control.ts +43 -0
- package/src/server/routes/instance/httpapi/handlers/event.ts +99 -0
- package/src/server/routes/instance/httpapi/handlers/experimental.ts +193 -0
- package/src/server/routes/instance/httpapi/handlers/file.ts +139 -0
- package/src/server/routes/instance/httpapi/handlers/global.ts +126 -0
- package/src/server/routes/instance/httpapi/handlers/instance.ts +110 -0
- package/src/server/routes/instance/httpapi/handlers/mcp.ts +111 -0
- package/src/server/routes/instance/httpapi/handlers/permission.ts +41 -0
- package/src/server/routes/instance/httpapi/handlers/project-copy.ts +83 -0
- package/src/server/routes/instance/httpapi/handlers/project.ts +63 -0
- package/src/server/routes/instance/httpapi/handlers/provider.ts +116 -0
- package/src/server/routes/instance/httpapi/handlers/pty.ts +273 -0
- package/src/server/routes/instance/httpapi/handlers/question.ts +54 -0
- package/src/server/routes/instance/httpapi/handlers/session-errors.ts +21 -0
- package/src/server/routes/instance/httpapi/handlers/session.ts +442 -0
- package/src/server/routes/instance/httpapi/handlers/sync.ts +89 -0
- package/src/server/routes/instance/httpapi/handlers/tui.ts +131 -0
- package/src/server/routes/instance/httpapi/handlers/workspace.ts +102 -0
- package/src/server/routes/instance/httpapi/lifecycle.ts +54 -0
- package/src/server/routes/instance/httpapi/middleware/authorization.ts +150 -0
- package/src/server/routes/instance/httpapi/middleware/compression.ts +64 -0
- package/src/server/routes/instance/httpapi/middleware/cors-vary.ts +29 -0
- package/src/server/routes/instance/httpapi/middleware/error.ts +44 -0
- package/src/server/routes/instance/httpapi/middleware/fence.ts +25 -0
- package/src/server/routes/instance/httpapi/middleware/instance-context.ts +43 -0
- package/src/server/routes/instance/httpapi/middleware/proxy.ts +131 -0
- package/src/server/routes/instance/httpapi/middleware/schema-error.ts +41 -0
- package/src/server/routes/instance/httpapi/middleware/workspace-routing.ts +250 -0
- package/src/server/routes/instance/httpapi/public.ts +537 -0
- package/src/server/routes/instance/httpapi/server.ts +325 -0
- package/src/server/routes/instance/httpapi/websocket-tracker.ts +60 -0
- package/src/server/server.ts +224 -0
- package/src/server/shared/fence.ts +60 -0
- package/src/server/shared/pty-ticket.ts +15 -0
- package/src/server/shared/public-ui.ts +12 -0
- package/src/server/shared/tui-control.ts +28 -0
- package/src/server/shared/ui.ts +108 -0
- package/src/server/shared/workspace-routing.ts +45 -0
- package/src/server/tui-event.ts +1 -0
- package/src/session/compaction.ts +608 -0
- package/src/session/instruction.ts +237 -0
- package/src/session/llm/AGENTS.md +90 -0
- package/src/session/llm/ai-sdk.ts +291 -0
- package/src/session/llm/native-request.ts +196 -0
- package/src/session/llm/native-runtime.ts +195 -0
- package/src/session/llm/request.ts +226 -0
- package/src/session/llm.ts +390 -0
- package/src/session/message-error.ts +14 -0
- package/src/session/message-v2.ts +742 -0
- package/src/session/message.ts +148 -0
- package/src/session/overflow.ts +34 -0
- package/src/session/processor.ts +732 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/default.txt +35 -0
- package/src/session/prompt/plan-mode.txt +70 -0
- package/src/session/prompt/plan.txt +26 -0
- package/src/session/prompt.ts +1631 -0
- package/src/session/reminders.ts +92 -0
- package/src/session/retry.ts +209 -0
- package/src/session/revert.ts +136 -0
- package/src/session/run-state.ts +151 -0
- package/src/session/schema.ts +26 -0
- package/src/session/session.ts +1016 -0
- package/src/session/status.ts +56 -0
- package/src/session/summary.ts +160 -0
- package/src/session/system.ts +123 -0
- package/src/session/todo.ts +74 -0
- package/src/session/tools.ts +590 -0
- package/src/share/session.ts +58 -0
- package/src/share/share-next.ts +371 -0
- package/src/skill/discovery.ts +140 -0
- package/src/skill/index.ts +354 -0
- package/src/snapshot/index.ts +807 -0
- package/src/sql.d.ts +4 -0
- package/src/storage/schema.ts +5 -0
- package/src/storage/storage.ts +327 -0
- package/src/sync/README.md +179 -0
- package/src/sync/schema.ts +11 -0
- package/src/temporary.ts +31 -0
- package/src/tool/apply_patch.ts +313 -0
- package/src/tool/apply_patch.txt +14 -0
- package/src/tool/code-mode.ts +310 -0
- package/src/tool/edit.ts +737 -0
- package/src/tool/edit.txt +13 -0
- package/src/tool/external-directory.ts +49 -0
- package/src/tool/glob.ts +76 -0
- package/src/tool/glob.txt +7 -0
- package/src/tool/grep.ts +115 -0
- package/src/tool/grep.txt +7 -0
- package/src/tool/invalid.ts +21 -0
- package/src/tool/json-schema.ts +164 -0
- package/src/tool/lsp.ts +113 -0
- package/src/tool/lsp.txt +1 -0
- package/src/tool/mcp-websearch.ts +96 -0
- package/src/tool/plan-enter.txt +1 -0
- package/src/tool/plan-exit.txt +1 -0
- package/src/tool/plan.ts +79 -0
- package/src/tool/question.ts +44 -0
- package/src/tool/question.txt +1 -0
- package/src/tool/read.ts +386 -0
- package/src/tool/read.txt +13 -0
- package/src/tool/registry.ts +455 -0
- package/src/tool/schema.ts +14 -0
- package/src/tool/shell/id.ts +19 -0
- package/src/tool/shell/prompt.ts +293 -0
- package/src/tool/shell/shell.txt +21 -0
- package/src/tool/shell.ts +645 -0
- package/src/tool/skill.ts +70 -0
- package/src/tool/skill.txt +1 -0
- package/src/tool/task.ts +371 -0
- package/src/tool/task.txt +11 -0
- package/src/tool/todo.ts +46 -0
- package/src/tool/todowrite.txt +11 -0
- package/src/tool/tool.ts +183 -0
- package/src/tool/truncate.ts +156 -0
- package/src/tool/truncation-dir.ts +4 -0
- package/src/tool/webfetch.ts +192 -0
- package/src/tool/webfetch.txt +9 -0
- package/src/tool/websearch.ts +143 -0
- package/src/tool/websearch.txt +9 -0
- package/src/tool/write.ts +104 -0
- package/src/tool/write.txt +11 -0
- package/src/util/archive.ts +17 -0
- package/src/util/bom.ts +27 -0
- package/src/util/data-url.ts +9 -0
- package/src/util/defer.ts +10 -0
- package/src/util/effect-http-client.ts +11 -0
- package/src/util/error.ts +16 -0
- package/src/util/filesystem.ts +251 -0
- package/src/util/html.ts +8 -0
- package/src/util/iife.ts +3 -0
- package/src/util/lazy.ts +20 -0
- package/src/util/local-context.ts +25 -0
- package/src/util/locale.ts +4 -0
- package/src/util/media.ts +26 -0
- package/src/util/process.ts +177 -0
- package/src/util/proxy-env.ts +72 -0
- package/src/util/queue.ts +32 -0
- package/src/util/record.ts +3 -0
- package/src/util/repository.ts +232 -0
- package/src/util/rpc.ts +66 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +13 -0
- package/src/util/token.ts +1 -0
- package/src/util/wildcard.ts +59 -0
- package/src/worktree/index.ts +623 -0
- package/sst-env.d.ts +10 -0
- package/tsconfig.headless.json +13 -0
- package/tsconfig.json +16 -0
package/src/acp/event.ts
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import type { AgentSideConnection } from "@agentclientprotocol/sdk"
|
|
2
|
+
import type {
|
|
3
|
+
Event,
|
|
4
|
+
EventMessagePartDelta,
|
|
5
|
+
EventMessagePartUpdated,
|
|
6
|
+
OpencodeClient,
|
|
7
|
+
Part,
|
|
8
|
+
SessionMessageResponse,
|
|
9
|
+
ToolPart,
|
|
10
|
+
} from "@opencode-ai/sdk/v2"
|
|
11
|
+
import { Effect } from "effect"
|
|
12
|
+
import { ACPSession } from "./session"
|
|
13
|
+
import { ACPPermission } from "./permission"
|
|
14
|
+
import { partsToContentChunks, type ReplayPart } from "./content"
|
|
15
|
+
import {
|
|
16
|
+
duplicateRunningToolUpdate,
|
|
17
|
+
errorToolUpdate,
|
|
18
|
+
pendingToolCall,
|
|
19
|
+
runningToolUpdate,
|
|
20
|
+
shellOutputSnapshot,
|
|
21
|
+
completedToolUpdate,
|
|
22
|
+
} from "./tool"
|
|
23
|
+
|
|
24
|
+
type Connection = Pick<AgentSideConnection, "sessionUpdate"> &
|
|
25
|
+
Partial<Pick<AgentSideConnection, "requestPermission" | "writeTextFile">>
|
|
26
|
+
type GlobalEventEnvelope = {
|
|
27
|
+
payload?: Event
|
|
28
|
+
}
|
|
29
|
+
type GlobalEventStream = {
|
|
30
|
+
stream: AsyncIterable<GlobalEventEnvelope>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function start(input: { sdk: OpencodeClient; connection: Connection; session: ACPSession.Interface }) {
|
|
34
|
+
const subscription = new Subscription(input)
|
|
35
|
+
subscription.start()
|
|
36
|
+
return subscription
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class Subscription {
|
|
40
|
+
private readonly abort = new AbortController()
|
|
41
|
+
private readonly shellSnapshots = new Map<string, string>()
|
|
42
|
+
private readonly toolStarts = new Set<string>()
|
|
43
|
+
private readonly connectionWaiters = new Set<() => void>()
|
|
44
|
+
private readonly idleWaiters = new Map<string, Set<ReturnType<typeof signal>>>()
|
|
45
|
+
private readonly permission: ACPPermission.Handler
|
|
46
|
+
private connected = false
|
|
47
|
+
private started = false
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
private readonly input: {
|
|
51
|
+
sdk: OpencodeClient
|
|
52
|
+
connection: Connection
|
|
53
|
+
session: ACPSession.Interface
|
|
54
|
+
},
|
|
55
|
+
) {
|
|
56
|
+
this.permission = new ACPPermission.Handler(input)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
start() {
|
|
60
|
+
if (this.started) return
|
|
61
|
+
this.started = true
|
|
62
|
+
this.run().catch(() => {
|
|
63
|
+
if (this.abort.signal.aborted) return
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
stop() {
|
|
68
|
+
this.abort.abort()
|
|
69
|
+
this.disconnected()
|
|
70
|
+
for (const resolve of this.connectionWaiters) resolve()
|
|
71
|
+
this.connectionWaiters.clear()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async runUntilIdle<A>(sessionId: string, request: () => Promise<A>) {
|
|
75
|
+
await this.waitUntilConnected()
|
|
76
|
+
const waiter = signal()
|
|
77
|
+
const waiters = this.idleWaiters.get(sessionId) ?? new Set()
|
|
78
|
+
waiters.add(waiter)
|
|
79
|
+
this.idleWaiters.set(sessionId, waiters)
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Idle is queued after the turn's events, and this subscription awaits each update in order.
|
|
83
|
+
void waiter.promise.catch(() => {})
|
|
84
|
+
const response = await request()
|
|
85
|
+
await waiter.promise
|
|
86
|
+
return response
|
|
87
|
+
} finally {
|
|
88
|
+
waiters.delete(waiter)
|
|
89
|
+
if (waiters.size === 0) this.idleWaiters.delete(sessionId)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async handle(event: Event) {
|
|
94
|
+
switch (event.type) {
|
|
95
|
+
case "session.status":
|
|
96
|
+
if (event.properties.status.type === "idle") this.idle(event.properties.sessionID)
|
|
97
|
+
return
|
|
98
|
+
case "permission.asked":
|
|
99
|
+
this.permission.handle(event)
|
|
100
|
+
return
|
|
101
|
+
case "message.part.updated":
|
|
102
|
+
return this.handlePartUpdated(event)
|
|
103
|
+
case "message.part.delta":
|
|
104
|
+
return this.handlePartDelta(event)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async replayMessage(message: SessionMessageResponse) {
|
|
109
|
+
if (message.info.role !== "assistant" && message.info.role !== "user") return
|
|
110
|
+
|
|
111
|
+
const cwd = message.info.role === "assistant" ? message.info.path?.cwd : undefined
|
|
112
|
+
for (const part of message.parts) {
|
|
113
|
+
await this.recordFetchedPart(message.info.sessionID, message, part)
|
|
114
|
+
if (part.type === "tool") {
|
|
115
|
+
await this.handleToolPart(message.info.sessionID, part, cwd ?? process.cwd())
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
await this.replayContentPart(message, part)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async replayContentPart(message: SessionMessageResponse, part: Part) {
|
|
123
|
+
if (part.type !== "text" && part.type !== "file" && part.type !== "reasoning") return
|
|
124
|
+
|
|
125
|
+
const sessionUpdate =
|
|
126
|
+
part.type === "reasoning"
|
|
127
|
+
? "agent_thought_chunk"
|
|
128
|
+
: message.info.role === "user"
|
|
129
|
+
? "user_message_chunk"
|
|
130
|
+
: "agent_message_chunk"
|
|
131
|
+
|
|
132
|
+
for (const chunk of partsToContentChunks([part as ReplayPart])) {
|
|
133
|
+
await this.input.connection.sessionUpdate({
|
|
134
|
+
sessionId: message.info.sessionID,
|
|
135
|
+
update: {
|
|
136
|
+
sessionUpdate,
|
|
137
|
+
messageId: part.type === "reasoning" ? part.id : message.info.id,
|
|
138
|
+
...chunk,
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async run() {
|
|
145
|
+
while (!this.abort.signal.aborted) {
|
|
146
|
+
await this.consume().catch(() => {})
|
|
147
|
+
this.disconnected()
|
|
148
|
+
if (!this.abort.signal.aborted) await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async consume() {
|
|
153
|
+
const events = (await this.input.sdk.global.event({
|
|
154
|
+
signal: this.abort.signal,
|
|
155
|
+
})) as GlobalEventStream
|
|
156
|
+
this.connected = true
|
|
157
|
+
for (const resolve of this.connectionWaiters) resolve()
|
|
158
|
+
this.connectionWaiters.clear()
|
|
159
|
+
|
|
160
|
+
for await (const event of events.stream) {
|
|
161
|
+
if (this.abort.signal.aborted) return
|
|
162
|
+
if (!event.payload) continue
|
|
163
|
+
await this.handle(event.payload).catch(() => {})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async waitUntilConnected() {
|
|
168
|
+
while (!this.connected) {
|
|
169
|
+
if (this.abort.signal.aborted) throw new Error("ACP event subscription stopped")
|
|
170
|
+
await new Promise<void>((resolve) => this.connectionWaiters.add(resolve))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private disconnected() {
|
|
175
|
+
if (!this.connected) return
|
|
176
|
+
this.connected = false
|
|
177
|
+
const error = new Error("ACP event stream disconnected")
|
|
178
|
+
for (const waiters of this.idleWaiters.values()) {
|
|
179
|
+
for (const waiter of waiters) waiter.reject(error)
|
|
180
|
+
}
|
|
181
|
+
this.idleWaiters.clear()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private idle(sessionId: string) {
|
|
185
|
+
const waiters = this.idleWaiters.get(sessionId)
|
|
186
|
+
if (!waiters) return
|
|
187
|
+
this.idleWaiters.delete(sessionId)
|
|
188
|
+
for (const waiter of waiters) waiter.resolve()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async handlePartUpdated(event: EventMessagePartUpdated) {
|
|
192
|
+
const part = event.properties.part
|
|
193
|
+
const sessionId = part.sessionID || event.properties.sessionID
|
|
194
|
+
const session = await Effect.runPromise(this.input.session.tryGet(sessionId))
|
|
195
|
+
if (!session) return
|
|
196
|
+
|
|
197
|
+
await Effect.runPromise(
|
|
198
|
+
this.input.session.recordPartMetadata({
|
|
199
|
+
sessionId: session.id,
|
|
200
|
+
messageId: part.messageID,
|
|
201
|
+
partId: part.id,
|
|
202
|
+
partType: part.type,
|
|
203
|
+
role: part.type === "reasoning" ? "assistant" : undefined,
|
|
204
|
+
ignored: part.type === "text" ? part.ignored : undefined,
|
|
205
|
+
toolCallId: part.type === "tool" ? part.callID : undefined,
|
|
206
|
+
metadata: "metadata" in part ? part.metadata : undefined,
|
|
207
|
+
}),
|
|
208
|
+
)
|
|
209
|
+
if (part.type === "tool") {
|
|
210
|
+
await this.handleToolPart(session.id, part, session.cwd)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async handlePartDelta(event: EventMessagePartDelta) {
|
|
215
|
+
const props = event.properties
|
|
216
|
+
const session = await Effect.runPromise(this.input.session.tryGet(props.sessionID))
|
|
217
|
+
if (!session) return
|
|
218
|
+
|
|
219
|
+
const known = await Effect.runPromise(
|
|
220
|
+
this.input.session.tryGetPartMetadata({
|
|
221
|
+
sessionId: session.id,
|
|
222
|
+
messageId: props.messageID,
|
|
223
|
+
partId: props.partID,
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
226
|
+
const metadata =
|
|
227
|
+
known?.role && known.partType
|
|
228
|
+
? known
|
|
229
|
+
: await this.fetchPartMetadata(session.id, session.cwd, props.messageID, props.partID)
|
|
230
|
+
if (metadata?.role !== "assistant") return
|
|
231
|
+
if (metadata.partType === "text" && props.field === "text" && metadata.ignored !== true) {
|
|
232
|
+
await this.input.connection.sessionUpdate({
|
|
233
|
+
sessionId: session.id,
|
|
234
|
+
update: {
|
|
235
|
+
sessionUpdate: "agent_message_chunk",
|
|
236
|
+
messageId: props.messageID,
|
|
237
|
+
content: {
|
|
238
|
+
type: "text",
|
|
239
|
+
text: props.delta,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (metadata.partType === "reasoning" && props.field === "text") {
|
|
247
|
+
await this.input.connection.sessionUpdate({
|
|
248
|
+
sessionId: session.id,
|
|
249
|
+
update: {
|
|
250
|
+
sessionUpdate: "agent_thought_chunk",
|
|
251
|
+
messageId: props.partID,
|
|
252
|
+
content: {
|
|
253
|
+
type: "text",
|
|
254
|
+
text: props.delta,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async fetchPartMetadata(sessionId: string, cwd: string, messageId: string, partId: string) {
|
|
262
|
+
const message = await this.input.sdk.session
|
|
263
|
+
.message(
|
|
264
|
+
{
|
|
265
|
+
sessionID: sessionId,
|
|
266
|
+
messageID: messageId,
|
|
267
|
+
directory: cwd,
|
|
268
|
+
},
|
|
269
|
+
{ throwOnError: true },
|
|
270
|
+
)
|
|
271
|
+
.then((response) => response.data)
|
|
272
|
+
.catch(() => undefined)
|
|
273
|
+
if (!message) return
|
|
274
|
+
|
|
275
|
+
const part = message.parts.find((item) => item.id === partId)
|
|
276
|
+
if (!part) return
|
|
277
|
+
return await this.recordFetchedPart(sessionId, message, part)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private async recordFetchedPart(sessionId: string, message: SessionMessageResponse, part: Part) {
|
|
281
|
+
return await Effect.runPromise(
|
|
282
|
+
this.input.session.recordPartMetadata({
|
|
283
|
+
sessionId,
|
|
284
|
+
messageId: part.messageID,
|
|
285
|
+
partId: part.id,
|
|
286
|
+
partType: part.type,
|
|
287
|
+
role: message.info.role,
|
|
288
|
+
ignored: part.type === "text" ? part.ignored : undefined,
|
|
289
|
+
toolCallId: part.type === "tool" ? part.callID : undefined,
|
|
290
|
+
metadata: "metadata" in part ? part.metadata : undefined,
|
|
291
|
+
}),
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async handleToolPart(sessionId: string, part: ToolPart, cwd: string) {
|
|
296
|
+
await this.toolStart(sessionId, part, cwd)
|
|
297
|
+
|
|
298
|
+
switch (part.state.status) {
|
|
299
|
+
case "pending":
|
|
300
|
+
this.shellSnapshots.delete(part.callID)
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
case "running":
|
|
304
|
+
await this.runningTool(sessionId, part, cwd)
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
case "completed":
|
|
308
|
+
this.clearTool(part.callID)
|
|
309
|
+
await this.input.connection.sessionUpdate({
|
|
310
|
+
sessionId,
|
|
311
|
+
update: {
|
|
312
|
+
sessionUpdate: "tool_call_update",
|
|
313
|
+
...completedToolUpdate({
|
|
314
|
+
toolCallId: part.callID,
|
|
315
|
+
toolName: part.tool,
|
|
316
|
+
state: part.state,
|
|
317
|
+
cwd,
|
|
318
|
+
}),
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
case "error":
|
|
324
|
+
this.clearTool(part.callID)
|
|
325
|
+
await this.input.connection.sessionUpdate({
|
|
326
|
+
sessionId,
|
|
327
|
+
update: {
|
|
328
|
+
sessionUpdate: "tool_call_update",
|
|
329
|
+
...errorToolUpdate({
|
|
330
|
+
toolCallId: part.callID,
|
|
331
|
+
toolName: part.tool,
|
|
332
|
+
state: part.state,
|
|
333
|
+
cwd,
|
|
334
|
+
}),
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private async runningTool(sessionId: string, part: ToolPart, cwd: string) {
|
|
342
|
+
if (part.state.status !== "running") return
|
|
343
|
+
|
|
344
|
+
const output = part.tool === "bash" ? shellOutputSnapshot(part.state) : undefined
|
|
345
|
+
if (output !== undefined) {
|
|
346
|
+
if (this.shellSnapshots.get(part.callID) === output) {
|
|
347
|
+
await this.input.connection.sessionUpdate({
|
|
348
|
+
sessionId,
|
|
349
|
+
update: {
|
|
350
|
+
sessionUpdate: "tool_call_update",
|
|
351
|
+
...duplicateRunningToolUpdate({
|
|
352
|
+
toolCallId: part.callID,
|
|
353
|
+
toolName: part.tool,
|
|
354
|
+
state: part.state,
|
|
355
|
+
cwd,
|
|
356
|
+
}),
|
|
357
|
+
},
|
|
358
|
+
})
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
this.shellSnapshots.set(part.callID, output)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
await this.input.connection.sessionUpdate({
|
|
365
|
+
sessionId,
|
|
366
|
+
update: {
|
|
367
|
+
sessionUpdate: "tool_call_update",
|
|
368
|
+
...runningToolUpdate({
|
|
369
|
+
toolCallId: part.callID,
|
|
370
|
+
toolName: part.tool,
|
|
371
|
+
state: part.state,
|
|
372
|
+
output,
|
|
373
|
+
cwd,
|
|
374
|
+
}),
|
|
375
|
+
},
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private async toolStart(sessionId: string, part: ToolPart, cwd: string) {
|
|
380
|
+
if (this.toolStarts.has(part.callID)) return
|
|
381
|
+
this.toolStarts.add(part.callID)
|
|
382
|
+
await this.input.connection.sessionUpdate({
|
|
383
|
+
sessionId,
|
|
384
|
+
update: {
|
|
385
|
+
sessionUpdate: "tool_call",
|
|
386
|
+
...pendingToolCall({
|
|
387
|
+
toolCallId: part.callID,
|
|
388
|
+
toolName: part.tool,
|
|
389
|
+
state: part.state,
|
|
390
|
+
cwd,
|
|
391
|
+
}),
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private clearTool(toolCallId: string) {
|
|
397
|
+
this.toolStarts.delete(toolCallId)
|
|
398
|
+
this.shellSnapshots.delete(toolCallId)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function signal() {
|
|
403
|
+
const state: {
|
|
404
|
+
resolve: () => void
|
|
405
|
+
reject: (reason?: unknown) => void
|
|
406
|
+
} = {
|
|
407
|
+
resolve: () => {},
|
|
408
|
+
reject: () => {},
|
|
409
|
+
}
|
|
410
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
411
|
+
state.resolve = resolve
|
|
412
|
+
state.reject = reject
|
|
413
|
+
})
|
|
414
|
+
return {
|
|
415
|
+
promise,
|
|
416
|
+
resolve: () => state.resolve(),
|
|
417
|
+
reject: (reason?: unknown) => state.reject(reason),
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export * as ACPEvent from "./event"
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentSideConnection,
|
|
3
|
+
PermissionOption,
|
|
4
|
+
RequestPermissionResponse,
|
|
5
|
+
ToolCallContent,
|
|
6
|
+
ToolCallLocation,
|
|
7
|
+
ToolCallUpdate,
|
|
8
|
+
} from "@agentclientprotocol/sdk"
|
|
9
|
+
import type { Event, OpencodeClient } from "@opencode-ai/sdk/v2"
|
|
10
|
+
import { applyPatch } from "diff"
|
|
11
|
+
import { exists, readText } from "@/util/filesystem"
|
|
12
|
+
import type { ACPSession } from "./session"
|
|
13
|
+
import { pendingToolCall, toLocations, type ToolInput } from "./tool"
|
|
14
|
+
import { Effect } from "effect"
|
|
15
|
+
|
|
16
|
+
type PermissionEvent = Extract<Event, { type: "permission.asked" }>
|
|
17
|
+
type Reply = "once" | "always" | "reject"
|
|
18
|
+
type Connection = Partial<Pick<AgentSideConnection, "requestPermission" | "writeTextFile">>
|
|
19
|
+
|
|
20
|
+
const permissionOptions: PermissionOption[] = [
|
|
21
|
+
{ optionId: "once", kind: "allow_once", name: "Allow once" },
|
|
22
|
+
{ optionId: "always", kind: "allow_always", name: "Always allow" },
|
|
23
|
+
{ optionId: "reject", kind: "reject_once", name: "Reject" },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export class Handler {
|
|
27
|
+
private readonly queues = new Map<string, Promise<void>>()
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly input: {
|
|
31
|
+
sdk: OpencodeClient
|
|
32
|
+
connection: Connection
|
|
33
|
+
session: ACPSession.Interface
|
|
34
|
+
},
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
handle(event: PermissionEvent) {
|
|
38
|
+
const permission = event.properties
|
|
39
|
+
const previous = this.queues.get(permission.sessionID) ?? Promise.resolve()
|
|
40
|
+
const next = previous
|
|
41
|
+
.then(() => this.process(event))
|
|
42
|
+
.catch(() => {})
|
|
43
|
+
.finally(() => {
|
|
44
|
+
if (this.queues.get(permission.sessionID) === next) {
|
|
45
|
+
this.queues.delete(permission.sessionID)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
this.queues.set(permission.sessionID, next)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async process(event: PermissionEvent) {
|
|
52
|
+
const permission = event.properties
|
|
53
|
+
const session = await Effect.runPromise(this.input.session.tryGet(permission.sessionID))
|
|
54
|
+
if (!session) return
|
|
55
|
+
|
|
56
|
+
if (!this.input.connection.requestPermission) {
|
|
57
|
+
await this.reply(permission.id, "reject", session.cwd)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = await this.input.connection
|
|
62
|
+
.requestPermission({
|
|
63
|
+
sessionId: permission.sessionID,
|
|
64
|
+
toolCall: await permissionToolCall({
|
|
65
|
+
toolCallId: permission.tool?.callID ?? permission.id,
|
|
66
|
+
toolName: permission.permission,
|
|
67
|
+
input: permission.metadata,
|
|
68
|
+
}),
|
|
69
|
+
options: permissionOptions,
|
|
70
|
+
})
|
|
71
|
+
.catch(async () => {
|
|
72
|
+
await this.reply(permission.id, "reject", session.cwd)
|
|
73
|
+
return undefined
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (!result) return
|
|
77
|
+
|
|
78
|
+
const reply = selectedReply(result)
|
|
79
|
+
if (reply !== "once" && reply !== "always") {
|
|
80
|
+
await this.reply(permission.id, "reject", session.cwd)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (permission.permission === "edit") {
|
|
85
|
+
await this.writeProposedEdit(session.id, permission.metadata).catch(() => {})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await this.reply(permission.id, reply, session.cwd)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async reply(requestID: string, reply: Reply, directory: string) {
|
|
92
|
+
await this.input.sdk.permission.reply({
|
|
93
|
+
requestID,
|
|
94
|
+
reply,
|
|
95
|
+
directory,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async writeProposedEdit(sessionId: string, metadata: ToolInput) {
|
|
100
|
+
const filepath = stringValue(metadata.filepath)
|
|
101
|
+
const diff = stringValue(metadata.diff)
|
|
102
|
+
if (!filepath || !diff || !this.input.connection.writeTextFile) return
|
|
103
|
+
|
|
104
|
+
const content = (await exists(filepath)) ? await readText(filepath) : ""
|
|
105
|
+
const next = applyPatch(content, diff)
|
|
106
|
+
if (next === false) {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
void this.input.connection.writeTextFile({
|
|
111
|
+
sessionId,
|
|
112
|
+
path: filepath,
|
|
113
|
+
content: next,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function permissionToolCall(input: {
|
|
119
|
+
readonly toolCallId: string
|
|
120
|
+
readonly toolName: string
|
|
121
|
+
readonly input: ToolInput
|
|
122
|
+
}): Promise<ToolCallUpdate> {
|
|
123
|
+
const toolCall = pendingToolCall({
|
|
124
|
+
toolCallId: input.toolCallId,
|
|
125
|
+
toolName: input.toolName,
|
|
126
|
+
state: {
|
|
127
|
+
input: input.input,
|
|
128
|
+
title: permissionTitle(input.toolName, input.input),
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
const content = await permissionContent(input.toolName, input.input)
|
|
132
|
+
return {
|
|
133
|
+
...toolCall,
|
|
134
|
+
locations: permissionLocations(input.toolName, input.input),
|
|
135
|
+
...(content.length ? { content } : {}),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function permissionTitle(toolName: string, input: ToolInput) {
|
|
140
|
+
const tool = toolName.toLocaleLowerCase()
|
|
141
|
+
switch (tool) {
|
|
142
|
+
case "external_directory":
|
|
143
|
+
return stringValue(input.description) ?? stringValue(input.command) ?? stringValue(input.parentDir)
|
|
144
|
+
|
|
145
|
+
case "webfetch":
|
|
146
|
+
return stringValue(input.url)
|
|
147
|
+
|
|
148
|
+
case "websearch":
|
|
149
|
+
return stringValue(input.query)
|
|
150
|
+
|
|
151
|
+
case "grep":
|
|
152
|
+
case "glob":
|
|
153
|
+
return stringValue(input.pattern)
|
|
154
|
+
|
|
155
|
+
case "read":
|
|
156
|
+
case "edit":
|
|
157
|
+
case "write":
|
|
158
|
+
return editTitle(input)
|
|
159
|
+
|
|
160
|
+
default:
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function editTitle(input: ToolInput) {
|
|
166
|
+
const files = fileMetadata(input)
|
|
167
|
+
if (files.length === 1) return files[0]?.relativePath ?? files[0]?.filePath
|
|
168
|
+
if (files.length > 1) return `${files.length} files`
|
|
169
|
+
return stringValue(input.filePath) ?? stringValue(input.filepath) ?? stringValue(input.path)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function permissionLocations(toolName: string, input: ToolInput): ToolCallLocation[] {
|
|
173
|
+
const files = fileMetadata(input)
|
|
174
|
+
if (files.length) {
|
|
175
|
+
return Array.from(
|
|
176
|
+
new Set(files.flatMap((file) => [file.filePath, file.movePath].filter((path): path is string => !!path))),
|
|
177
|
+
(path) => ({ path }),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
return toLocations(toolName, input)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function permissionContent(toolName: string, input: ToolInput): Promise<ToolCallContent[]> {
|
|
184
|
+
if (toolName.toLocaleLowerCase() !== "edit") return []
|
|
185
|
+
|
|
186
|
+
const files = fileMetadata(input)
|
|
187
|
+
if (files.length) return diffContentForFiles(files)
|
|
188
|
+
|
|
189
|
+
const filepath = stringValue(input.filepath) ?? stringValue(input.filePath)
|
|
190
|
+
const diff = stringValue(input.diff)
|
|
191
|
+
if (!filepath || !diff) return []
|
|
192
|
+
const content = await diffContentForPatch(filepath, diff)
|
|
193
|
+
return content ? [content] : []
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function diffContentForFiles(files: PermissionFileMetadata[]) {
|
|
197
|
+
const content = await Promise.all(
|
|
198
|
+
files.map(async (file) => {
|
|
199
|
+
if (!file.patch) return []
|
|
200
|
+
const content = await diffContentForPatch(file.filePath, file.patch, file.movePath)
|
|
201
|
+
return content ? [content] : []
|
|
202
|
+
}),
|
|
203
|
+
)
|
|
204
|
+
return content.flat()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function diffContentForPatch(filepath: string, diff: string, displayPath = filepath) {
|
|
208
|
+
const content = (await exists(filepath)) ? await readText(filepath) : ""
|
|
209
|
+
const next = applyPatch(content, diff)
|
|
210
|
+
if (next === false) return undefined
|
|
211
|
+
return {
|
|
212
|
+
type: "diff" as const,
|
|
213
|
+
path: displayPath,
|
|
214
|
+
oldText: content,
|
|
215
|
+
newText: next,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function selectedReply(result: RequestPermissionResponse): Reply {
|
|
220
|
+
if (result.outcome.outcome !== "selected") return "reject"
|
|
221
|
+
if (result.outcome.optionId === "once" || result.outcome.optionId === "always") return result.outcome.optionId
|
|
222
|
+
return "reject"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function stringValue(value: unknown) {
|
|
226
|
+
return typeof value === "string" ? value : undefined
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
type PermissionFileMetadata = {
|
|
230
|
+
readonly filePath: string
|
|
231
|
+
readonly relativePath?: string
|
|
232
|
+
readonly movePath?: string
|
|
233
|
+
readonly patch?: string
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function fileMetadata(input: ToolInput): PermissionFileMetadata[] {
|
|
237
|
+
if (!Array.isArray(input.files)) return []
|
|
238
|
+
return input.files.flatMap((file): PermissionFileMetadata[] => {
|
|
239
|
+
if (!file || typeof file !== "object") return []
|
|
240
|
+
const info = file as Record<string, unknown>
|
|
241
|
+
const filePath = stringValue(info.filePath)
|
|
242
|
+
if (!filePath) return []
|
|
243
|
+
return [
|
|
244
|
+
{
|
|
245
|
+
filePath,
|
|
246
|
+
relativePath: stringValue(info.relativePath),
|
|
247
|
+
movePath: stringValue(info.movePath),
|
|
248
|
+
patch: stringValue(info.patch),
|
|
249
|
+
},
|
|
250
|
+
]
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export * as ACPPermission from "./permission"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const enabled = process.env.OPENCODE_ACP_PROFILE === "1"
|
|
2
|
+
const started = performance.now()
|
|
3
|
+
|
|
4
|
+
export function mark(name: string, fields?: Record<string, string | number | boolean | undefined>) {
|
|
5
|
+
if (!enabled) return
|
|
6
|
+
write(`${name}.mark`, performance.now() - started, fields)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function duration(
|
|
10
|
+
name: string,
|
|
11
|
+
startedAt: number,
|
|
12
|
+
fields?: Record<string, string | number | boolean | undefined>,
|
|
13
|
+
) {
|
|
14
|
+
if (!enabled) return
|
|
15
|
+
write(name, performance.now() - startedAt, fields)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function measure<T>(
|
|
19
|
+
name: string,
|
|
20
|
+
fn: () => Promise<T>,
|
|
21
|
+
fields?: Record<string, string | number | boolean | undefined>,
|
|
22
|
+
) {
|
|
23
|
+
if (!enabled) return fn()
|
|
24
|
+
const start = performance.now()
|
|
25
|
+
try {
|
|
26
|
+
return await fn()
|
|
27
|
+
} finally {
|
|
28
|
+
write(name, performance.now() - start, fields)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function write(name: string, durationMs: number, fields?: Record<string, string | number | boolean | undefined>) {
|
|
33
|
+
const extra = fields
|
|
34
|
+
? Object.entries(fields)
|
|
35
|
+
.filter((entry): entry is [string, string | number | boolean] => entry[1] !== undefined)
|
|
36
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
37
|
+
.join(" ")
|
|
38
|
+
: ""
|
|
39
|
+
console.error(`[acp-profile] ${name} ${Math.round(durationMs)}ms${extra ? ` ${extra}` : ""}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export * as ACPProfile from "./profile"
|