Skip to content

Commit 0ebc36b

Browse files
authored
Merge branch 'anomalyco:dev' into dev
2 parents 268f835 + 79254c1 commit 0ebc36b

5 files changed

Lines changed: 344 additions & 92 deletions

File tree

packages/opencode/src/acp/agent.ts

Lines changed: 88 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { Config } from "@/config/config"
4141
import { Todo } from "@/session/todo"
4242
import { z } from "zod"
4343
import { LoadAPIKeyError } from "ai"
44-
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
44+
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
4545
import { applyPatch } from "diff"
4646

4747
type ModeOption = { id: string; name: string; description?: string }
@@ -135,6 +135,8 @@ export namespace ACP {
135135
private sessionManager: ACPSessionManager
136136
private eventAbort = new AbortController()
137137
private eventStarted = false
138+
private bashSnapshots = new Map<string, string>()
139+
private toolStarts = new Set<string>()
138140
private permissionQueues = new Map<string, Promise<void>>()
139141
private permissionOptions: PermissionOption[] = [
140142
{ optionId: "once", kind: "allow_once", name: "Allow once" },
@@ -266,47 +268,68 @@ export namespace ACP {
266268
const session = this.sessionManager.tryGet(part.sessionID)
267269
if (!session) return
268270
const sessionId = session.id
269-
const directory = session.cwd
270-
271-
const message = await this.sdk.session
272-
.message(
273-
{
274-
sessionID: part.sessionID,
275-
messageID: part.messageID,
276-
directory,
277-
},
278-
{ throwOnError: true },
279-
)
280-
.then((x) => x.data)
281-
.catch((error) => {
282-
log.error("unexpected error when fetching message", { error })
283-
return undefined
284-
})
285-
286-
if (!message || message.info.role !== "assistant") return
287271

288272
if (part.type === "tool") {
273+
if (!this.toolStarts.has(part.callID)) {
274+
this.toolStarts.add(part.callID)
275+
await this.connection
276+
.sessionUpdate({
277+
sessionId,
278+
update: {
279+
sessionUpdate: "tool_call",
280+
toolCallId: part.callID,
281+
title: part.tool,
282+
kind: toToolKind(part.tool),
283+
status: "pending",
284+
locations: [],
285+
rawInput: {},
286+
},
287+
})
288+
.catch((error) => {
289+
log.error("failed to send tool pending to ACP", { error })
290+
})
291+
}
292+
289293
switch (part.state.status) {
290294
case "pending":
291-
await this.connection
292-
.sessionUpdate({
293-
sessionId,
294-
update: {
295-
sessionUpdate: "tool_call",
296-
toolCallId: part.callID,
297-
title: part.tool,
298-
kind: toToolKind(part.tool),
299-
status: "pending",
300-
locations: [],
301-
rawInput: {},
302-
},
303-
})
304-
.catch((error) => {
305-
log.error("failed to send tool pending to ACP", { error })
306-
})
295+
this.bashSnapshots.delete(part.callID)
307296
return
308297

309298
case "running":
299+
const output = this.bashOutput(part)
300+
const content: ToolCallContent[] = []
301+
if (output) {
302+
const hash = String(Bun.hash(output))
303+
if (part.tool === "bash") {
304+
if (this.bashSnapshots.get(part.callID) === hash) {
305+
await this.connection
306+
.sessionUpdate({
307+
sessionId,
308+
update: {
309+
sessionUpdate: "tool_call_update",
310+
toolCallId: part.callID,
311+
status: "in_progress",
312+
kind: toToolKind(part.tool),
313+
title: part.tool,
314+
locations: toLocations(part.tool, part.state.input),
315+
rawInput: part.state.input,
316+
},
317+
})
318+
.catch((error) => {
319+
log.error("failed to send tool in_progress to ACP", { error })
320+
})
321+
return
322+
}
323+
this.bashSnapshots.set(part.callID, hash)
324+
}
325+
content.push({
326+
type: "content",
327+
content: {
328+
type: "text",
329+
text: output,
330+
},
331+
})
332+
}
310333
await this.connection
311334
.sessionUpdate({
312335
sessionId,
@@ -318,6 +341,7 @@ export namespace ACP {
318341
title: part.tool,
319342
locations: toLocations(part.tool, part.state.input),
320343
rawInput: part.state.input,
344+
...(content.length > 0 && { content }),
321345
},
322346
})
323347
.catch((error) => {
@@ -326,6 +350,8 @@ export namespace ACP {
326350
return
327351

328352
case "completed": {
353+
this.toolStarts.delete(part.callID)
354+
this.bashSnapshots.delete(part.callID)
329355
const kind = toToolKind(part.tool)
330356
const content: ToolCallContent[] = [
331357
{
@@ -405,6 +431,8 @@ export namespace ACP {
405431
return
406432
}
407433
case "error":
434+
this.toolStarts.delete(part.callID)
435+
this.bashSnapshots.delete(part.callID)
408436
await this.connection
409437
.sessionUpdate({
410438
sessionId,
@@ -426,6 +454,7 @@ export namespace ACP {
426454
],
427455
rawOutput: {
428456
error: part.state.error,
457+
metadata: part.state.metadata,
429458
},
430459
},
431460
})
@@ -802,6 +831,7 @@ export namespace ACP {
802831
if (part.type === "tool") {
803832
switch (part.state.status) {
804833
case "pending":
834+
this.bashSnapshots.delete(part.callID)
805835
await this.connection
806836
.sessionUpdate({
807837
sessionId,
@@ -820,6 +850,17 @@ export namespace ACP {
820850
})
821851
break
822852
case "running":
853+
const output = this.bashOutput(part)
854+
const runningContent: ToolCallContent[] = []
855+
if (output) {
856+
runningContent.push({
857+
type: "content",
858+
content: {
859+
type: "text",
860+
text: output,
861+
},
862+
})
863+
}
823864
await this.connection
824865
.sessionUpdate({
825866
sessionId,
@@ -831,13 +872,15 @@ export namespace ACP {
831872
title: part.tool,
832873
locations: toLocations(part.tool, part.state.input),
833874
rawInput: part.state.input,
875+
...(runningContent.length > 0 && { content: runningContent }),
834876
},
835877
})
836878
.catch((err) => {
837879
log.error("failed to send tool in_progress to ACP", { error: err })
838880
})
839881
break
840882
case "completed":
883+
this.bashSnapshots.delete(part.callID)
841884
const kind = toToolKind(part.tool)
842885
const content: ToolCallContent[] = [
843886
{
@@ -916,6 +959,7 @@ export namespace ACP {
916959
})
917960
break
918961
case "error":
962+
this.bashSnapshots.delete(part.callID)
919963
await this.connection
920964
.sessionUpdate({
921965
sessionId,
@@ -937,6 +981,7 @@ export namespace ACP {
937981
],
938982
rawOutput: {
939983
error: part.state.error,
984+
metadata: part.state.metadata,
940985
},
941986
},
942987
})
@@ -1063,6 +1108,14 @@ export namespace ACP {
10631108
}
10641109
}
10651110

1111+
private bashOutput(part: ToolPart) {
1112+
if (part.tool !== "bash") return
1113+
if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
1114+
const output = part.state.metadata["output"]
1115+
if (typeof output !== "string") return
1116+
return output
1117+
}
1118+
10661119
private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
10671120
const agents = await this.config.sdk.app
10681121
.agents(

packages/opencode/src/snapshot/index.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export namespace Snapshot {
6464
.nothrow()
6565
// Configure git to not convert line endings on Windows
6666
await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow()
67+
await $`git --git-dir ${git} config core.longpaths true`.quiet().nothrow()
68+
await $`git --git-dir ${git} config core.symlinks true`.quiet().nothrow()
69+
await $`git --git-dir ${git} config core.fsmonitor false`.quiet().nothrow()
6770
log.info("initialized")
6871
}
6972
await add(git)
@@ -86,7 +89,7 @@ export namespace Snapshot {
8689
const git = gitdir()
8790
await add(git)
8891
const result =
89-
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .`
92+
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .`
9093
.quiet()
9194
.cwd(Instance.directory)
9295
.nothrow()
@@ -113,7 +116,7 @@ export namespace Snapshot {
113116
log.info("restore", { commit: snapshot })
114117
const git = gitdir()
115118
const result =
116-
await $`git --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f`
119+
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f`
117120
.quiet()
118121
.cwd(Instance.worktree)
119122
.nothrow()
@@ -135,14 +138,15 @@ export namespace Snapshot {
135138
for (const file of item.files) {
136139
if (files.has(file)) continue
137140
log.info("reverting", { file, hash: item.hash })
138-
const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
139-
.quiet()
140-
.cwd(Instance.worktree)
141-
.nothrow()
141+
const result =
142+
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
143+
.quiet()
144+
.cwd(Instance.worktree)
145+
.nothrow()
142146
if (result.exitCode !== 0) {
143147
const relativePath = path.relative(Instance.worktree, file)
144148
const checkTree =
145-
await $`git --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}`
149+
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}`
146150
.quiet()
147151
.cwd(Instance.worktree)
148152
.nothrow()
@@ -164,7 +168,7 @@ export namespace Snapshot {
164168
const git = gitdir()
165169
await add(git)
166170
const result =
167-
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
171+
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
168172
.quiet()
169173
.cwd(Instance.worktree)
170174
.nothrow()
@@ -201,7 +205,7 @@ export namespace Snapshot {
201205
const status = new Map<string, "added" | "deleted" | "modified">()
202206

203207
const statuses =
204-
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-status --no-renames ${from} ${to} -- .`
208+
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-status --no-renames ${from} ${to} -- .`
205209
.quiet()
206210
.cwd(Instance.directory)
207211
.nothrow()
@@ -215,7 +219,7 @@ export namespace Snapshot {
215219
status.set(file, kind)
216220
}
217221

218-
for await (const line of $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
222+
for await (const line of $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
219223
.quiet()
220224
.cwd(Instance.directory)
221225
.nothrow()
@@ -225,13 +229,13 @@ export namespace Snapshot {
225229
const isBinaryFile = additions === "-" && deletions === "-"
226230
const before = isBinaryFile
227231
? ""
228-
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
232+
: await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
229233
.quiet()
230234
.nothrow()
231235
.text()
232236
const after = isBinaryFile
233237
? ""
234-
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
238+
: await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
235239
.quiet()
236240
.nothrow()
237241
.text()
@@ -256,7 +260,10 @@ export namespace Snapshot {
256260

257261
async function add(git: string) {
258262
await syncExclude(git)
259-
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
263+
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} add .`
264+
.quiet()
265+
.cwd(Instance.directory)
266+
.nothrow()
260267
}
261268

262269
async function syncExclude(git: string) {

0 commit comments

Comments
 (0)