Skip to content

Commit 63c69a7

Browse files
committed
refactor: simplify command parsing and formatting in ACP agent
1 parent 47608a1 commit 63c69a7

3 files changed

Lines changed: 24 additions & 178 deletions

File tree

Lines changed: 7 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,21 @@
11
import type { ToolKind } from "@agentclientprotocol/sdk"
22

33
export namespace ParseCommand {
4-
export type Parsed =
5-
| { type: "read"; cmd: string; name: string; path: string }
6-
| { type: "list"; cmd: string; path?: string }
7-
| { type: "search"; cmd: string; query?: string; path?: string }
8-
| { type: "unknown"; cmd: string }
9-
104
export interface Result {
115
kind: ToolKind
126
title: string
137
locations: { path: string }[]
148
terminalOutput: boolean
159
}
1610

17-
const LIST_COMMANDS = new Set(["ls", "dir", "exa", "eza", "tree", "lsd"])
18-
const READ_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "bat", "view"])
19-
const SEARCH_COMMANDS = new Set(["grep", "rg", "ag", "ack", "find", "fd", "fzf", "locate"])
20-
21-
export function parse(command: string): Parsed {
22-
const trimmed = command.trim()
23-
const parts = trimmed.split(/\s+/).filter(Boolean)
24-
const cmd = parts[0] || ""
25-
const args = parts.slice(1).filter((p) => !p.startsWith("-"))
26-
27-
if (LIST_COMMANDS.has(cmd)) {
28-
return { type: "list", cmd, path: args[0] }
29-
}
30-
31-
if (READ_COMMANDS.has(cmd)) {
32-
if (args[0]) {
33-
const name = args[0].split("/").pop() || args[0]
34-
return { type: "read", cmd, name, path: args[0] }
35-
}
36-
return { type: "unknown", cmd: trimmed }
37-
}
11+
export function format(command: string, description: string, cwd: string): Result {
12+
const title = description || command || "Terminal"
3813

39-
if (SEARCH_COMMANDS.has(cmd)) {
40-
return { type: "search", cmd, query: args[0], path: args[1] }
14+
return {
15+
kind: "other",
16+
title,
17+
locations: cwd ? [{ path: cwd }] : [],
18+
terminalOutput: true,
4119
}
42-
43-
return { type: "unknown", cmd: trimmed }
44-
}
45-
46-
export function format(parsed: Parsed, cwd: string): Result {
47-
switch (parsed.type) {
48-
case "read":
49-
return {
50-
kind: "read",
51-
title: `Read ${parsed.name}`,
52-
locations: [{ path: parsed.path }],
53-
terminalOutput: false,
54-
}
55-
56-
case "list": {
57-
const dir = parsed.path ? (parsed.path.startsWith("/") ? parsed.path : `${cwd}/${parsed.path}`) : cwd || "."
58-
return {
59-
kind: "search",
60-
title: `List ${dir}`,
61-
locations: [{ path: dir }],
62-
terminalOutput: false,
63-
}
64-
}
65-
66-
case "search": {
67-
const title =
68-
parsed.query && parsed.path
69-
? `Search ${parsed.query} in ${parsed.path}`
70-
: parsed.query
71-
? `Search ${parsed.query}`
72-
: `Search ${parsed.cmd}`
73-
return {
74-
kind: "search",
75-
title: truncate(title, 50),
76-
locations: parsed.path ? [{ path: parsed.path }] : [],
77-
terminalOutput: false,
78-
}
79-
}
80-
81-
case "unknown":
82-
return {
83-
kind: "execute",
84-
title: `Run ${truncate(parsed.cmd, 40)}`,
85-
locations: [],
86-
terminalOutput: true,
87-
}
88-
}
89-
}
90-
91-
function truncate(str: string, max: number): string {
92-
return str.length > max ? str.substring(0, max - 3) + "..." : str
9320
}
9421
}

packages/opencode/src/acp/tool-format.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ export function toolCallFromPart(tool: string, input: Record<string, unknown>):
9999
case "shell":
100100
case "terminal": {
101101
const command = getCommand(input)
102+
const description = getDescription(input)
102103
const cwd = str(input.cwd ?? input.workdir ?? input.workingDir ?? input.directory)
103-
const parsed = ParseCommand.parse(command)
104-
const result = ParseCommand.format(parsed, cwd)
104+
const result = ParseCommand.format(command, description, cwd)
105105
return {
106106
title: result.title,
107107
kind: result.kind,

packages/opencode/test/acp/parse-command.test.ts

Lines changed: 15 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -2,111 +2,30 @@ import { describe, expect, it } from "bun:test"
22
import { ParseCommand } from "../../src/acp/parse-command"
33

44
describe("ParseCommand", () => {
5-
describe("parse", () => {
6-
it("parses ls as list command", () => {
7-
const result = ParseCommand.parse("ls")
8-
expect(result).toEqual({ type: "list", cmd: "ls", path: undefined })
9-
})
10-
11-
it("parses ls with path as list command", () => {
12-
const result = ParseCommand.parse("ls /some/path")
13-
expect(result).toEqual({ type: "list", cmd: "ls", path: "/some/path" })
14-
})
15-
16-
it("parses ls with flags correctly", () => {
17-
const result = ParseCommand.parse("ls -la /some/path")
18-
expect(result).toEqual({ type: "list", cmd: "ls", path: "/some/path" })
19-
})
20-
21-
it("parses cat as read command", () => {
22-
const result = ParseCommand.parse("cat file.txt")
23-
expect(result).toEqual({ type: "read", cmd: "cat", name: "file.txt", path: "file.txt" })
24-
})
25-
26-
it("parses cat with path as read command", () => {
27-
const result = ParseCommand.parse("cat /some/path/file.txt")
28-
expect(result).toEqual({ type: "read", cmd: "cat", name: "file.txt", path: "/some/path/file.txt" })
29-
})
30-
31-
it("parses grep as search command", () => {
32-
const result = ParseCommand.parse("grep pattern")
33-
expect(result).toEqual({ type: "search", cmd: "grep", query: "pattern", path: undefined })
34-
})
35-
36-
it("parses grep with path as search command", () => {
37-
const result = ParseCommand.parse("grep pattern /some/path")
38-
expect(result).toEqual({ type: "search", cmd: "grep", query: "pattern", path: "/some/path" })
39-
})
40-
41-
it("parses rg as search command", () => {
42-
const result = ParseCommand.parse("rg pattern")
43-
expect(result).toEqual({ type: "search", cmd: "rg", query: "pattern", path: undefined })
44-
})
45-
46-
it("parses unknown command", () => {
47-
const result = ParseCommand.parse("npm install")
48-
expect(result).toEqual({ type: "unknown", cmd: "npm install" })
49-
})
50-
51-
it("parses cat without args as unknown", () => {
52-
const result = ParseCommand.parse("cat")
53-
expect(result).toEqual({ type: "unknown", cmd: "cat" })
54-
})
55-
})
56-
575
describe("format", () => {
58-
it("formats list command with cwd", () => {
59-
const parsed = ParseCommand.parse("ls")
60-
const result = ParseCommand.format(parsed, "/home/user")
61-
expect(result.kind).toBe("search")
62-
expect(result.title).toBe("List /home/user")
63-
expect(result.locations).toEqual([{ path: "/home/user" }])
64-
})
65-
66-
it("formats list command with explicit path", () => {
67-
const parsed = ParseCommand.parse("ls /some/path")
68-
const result = ParseCommand.format(parsed, "/home/user")
69-
expect(result.kind).toBe("search")
70-
expect(result.title).toBe("List /some/path")
71-
expect(result.locations).toEqual([{ path: "/some/path" }])
6+
it("uses description as title when provided", () => {
7+
const result = ParseCommand.format("ls", "List files in current directory", "/home/user")
8+
expect(result.title).toBe("List files in current directory")
9+
expect(result.kind).toBe("other")
7210
})
7311

74-
it("formats list command with relative path", () => {
75-
const parsed = ParseCommand.parse("ls subdir")
76-
const result = ParseCommand.format(parsed, "/home/user")
77-
expect(result.kind).toBe("search")
78-
expect(result.title).toBe("List /home/user/subdir")
79-
expect(result.locations).toEqual([{ path: "/home/user/subdir" }])
12+
it("falls back to command when no description", () => {
13+
const result = ParseCommand.format("ls -la", "", "/home/user")
14+
expect(result.title).toBe("ls -la")
8015
})
8116

82-
it("formats read command", () => {
83-
const parsed = ParseCommand.parse("cat /some/file.txt")
84-
const result = ParseCommand.format(parsed, "/home/user")
85-
expect(result.kind).toBe("read")
86-
expect(result.title).toBe("Read file.txt")
87-
expect(result.locations).toEqual([{ path: "/some/file.txt" }])
88-
})
89-
90-
it("formats search command with query", () => {
91-
const parsed = ParseCommand.parse("grep pattern")
92-
const result = ParseCommand.format(parsed, "/home/user")
93-
expect(result.kind).toBe("search")
94-
expect(result.title).toBe("Search pattern")
17+
it("includes cwd in locations", () => {
18+
const result = ParseCommand.format("ls", "List files", "/home/user")
19+
expect(result.locations).toEqual([{ path: "/home/user" }])
9520
})
9621

97-
it("formats search command with query and path", () => {
98-
const parsed = ParseCommand.parse("grep pattern /some/path")
99-
const result = ParseCommand.format(parsed, "/home/user")
100-
expect(result.kind).toBe("search")
101-
expect(result.title).toBe("Search pattern in /some/path")
102-
expect(result.locations).toEqual([{ path: "/some/path" }])
22+
it("handles empty cwd", () => {
23+
const result = ParseCommand.format("ls", "List files", "")
24+
expect(result.locations).toEqual([])
10325
})
10426

105-
it("formats unknown command as execute", () => {
106-
const parsed = ParseCommand.parse("npm install")
107-
const result = ParseCommand.format(parsed, "/home/user")
108-
expect(result.kind).toBe("execute")
109-
expect(result.title).toBe("Run npm install")
27+
it("sets terminalOutput to true", () => {
28+
const result = ParseCommand.format("npm install", "Install dependencies", "/home/user")
11029
expect(result.terminalOutput).toBe(true)
11130
})
11231
})

0 commit comments

Comments
 (0)