Skip to content

Commit 7447a4e

Browse files
committed
fix: accept acp slash commands
1 parent 81f2b40 commit 7447a4e

5 files changed

Lines changed: 63 additions & 3 deletions

File tree

src/acp/__tests__/prompt-lines.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ test('strips an optional leading agent-device token', () => {
2525
assert.equal((line as RunnableLine).command, 'devices');
2626
});
2727

28+
test('accepts ACP slash commands for advertised commands', () => {
29+
const lines = parsePromptCommandLines('/devices\n/snapshot -i', { cwd: CWD });
30+
assert.deepEqual(
31+
lines.map((line) => {
32+
assert.equal(line.kind, 'command');
33+
return (line as RunnableLine).command;
34+
}),
35+
['devices', 'snapshot'],
36+
);
37+
assert.equal((lines[1] as RunnableLine).flags.snapshotInteractiveOnly, true);
38+
});
39+
2840
test('tokenizes quoted arguments with spaces like replay scripts', () => {
2941
const line = parseOne('fill "id:search" "hello world" --platform ios');
3042
assert.equal(line.kind, 'command');

src/acp/__tests__/router.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,31 @@ test('unknown methods, unknown sessions, and bad params return JSON-RPC errors',
200200
assert.equal(unknownSession.error.code, -32602);
201201
});
202202

203+
test('session/new requires protocol-shaped session params', async () => {
204+
const harness = makeRouter();
205+
const cases: Array<{ name: string; params: unknown; message: RegExp }> = [
206+
{ name: 'relative cwd', params: { cwd: 'relative', mcpServers: [] }, message: /absolute/ },
207+
{ name: 'missing mcpServers', params: { cwd: '/tmp' }, message: /mcpServers/ },
208+
{ name: 'non-array mcpServers', params: { cwd: '/tmp', mcpServers: {} }, message: /array/ },
209+
{
210+
name: 'non-object mcpServers entry',
211+
params: { cwd: '/tmp', mcpServers: [42] },
212+
message: /mcpServers\[0\]/,
213+
},
214+
];
215+
216+
for (const [index, entry] of cases.entries()) {
217+
const response = (await harness.router.handleAcpPayload({
218+
jsonrpc: '2.0',
219+
id: index + 1,
220+
method: 'session/new',
221+
params: entry.params,
222+
})) as { error: { code: number; message: string } };
223+
assert.equal(response.error.code, -32602, entry.name);
224+
assert.match(response.error.message, entry.message, entry.name);
225+
}
226+
});
227+
203228
test('notifications get no response and cancel notifications mark the session', async () => {
204229
let observedCancelled = false;
205230
const harness = makeRouter(

src/acp/prompt-lines.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function parsePromptCommandLine(line: string, options: { cwd: string }): PromptL
6060
return { kind: 'error', line, error: error instanceof Error ? error.message : String(error) };
6161
}
6262
if (tokens[0] === 'agent-device') tokens = tokens.slice(1);
63+
if (tokens[0]?.startsWith('/')) tokens = [tokens[0].slice(1), ...tokens.slice(1)];
6364
if (tokens.length === 0) {
6465
return { kind: 'error', line, error: 'Empty command line.' };
6566
}

src/acp/router.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isAbsolute } from 'node:path';
12
import { listMcpCommandMetadata } from '../commands/command-metadata.ts';
23
import { jsonRpcRequestSchema, type JsonRpcId } from '../kernel/contracts.ts';
34
import { errorResponse, successResponse, type JsonRpcResponse } from '../mcp/router.ts';
@@ -50,7 +51,7 @@ export function createAcpRouter(deps: {
5051
};
5152

5253
const newSession = (params: unknown): { sessionId: string } => {
53-
const cwd = stringField(asRecord(params), 'cwd');
54+
const { cwd } = readSessionNewParams(params);
5455
const session = sessions.create(cwd);
5556
// The session/new response must reach the client before the first
5657
// notification for that session; the response is written when this handler
@@ -121,6 +122,27 @@ export function createAcpRouter(deps: {
121122
};
122123
}
123124

125+
function readSessionNewParams(params: unknown): { cwd: string } {
126+
const record = asRecord(params);
127+
const cwd = stringField(record, 'cwd');
128+
if (!isAbsolute(cwd)) {
129+
throw new AcpInvalidParamsError('Expected cwd to be an absolute path.');
130+
}
131+
validateMcpServers(record.mcpServers);
132+
return { cwd };
133+
}
134+
135+
function validateMcpServers(value: unknown): void {
136+
if (!Array.isArray(value)) {
137+
throw new AcpInvalidParamsError('Expected mcpServers to be an array.');
138+
}
139+
for (const [index, entry] of value.entries()) {
140+
if (!asOptionalRecord(entry)) {
141+
throw new AcpInvalidParamsError(`Expected mcpServers[${index}] to be an object.`);
142+
}
143+
}
144+
}
145+
124146
function markCancelled(
125147
getSession: (id: string) => AcpSessionState | undefined,
126148
params: unknown,

website/docs/docs/agent-setup.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Registry metadata uses MCP name `io.github.callstackincubator/agent-device`, npm
9292

9393
## ACP agent (Zed and other ACP clients)
9494

95-
`agent-device acp` starts a stdio [ACP](https://agentclientprotocol.com/) (Agent Client Protocol) agent, so ACP clients such as Zed can drive devices from the agent panel. It is a deterministic agent, not an LLM: each line of a prompt is one agent-device command in CLI syntax (an optional leading `agent-device` is accepted), executed through the same command contracts and `AgentDeviceClient` path as MCP tools. Command executions stream back as ACP tool calls with daemon progress updates, screenshots attach as inline images, and available commands are advertised per session.
95+
`agent-device acp` starts a stdio [ACP](https://agentclientprotocol.com/) (Agent Client Protocol) agent, so ACP clients such as Zed can drive devices from the agent panel. It is a deterministic agent, not an LLM: each line of a prompt is one agent-device command in CLI syntax (ACP slash commands such as `/devices` and an optional leading `agent-device` are accepted), executed through the same command contracts and `AgentDeviceClient` path as MCP tools. Command executions stream back as ACP tool calls with daemon progress updates, screenshots attach as inline images, and available commands are advertised per session.
9696

9797
Target selection sticks within a session: after `open com.example.app --platform ios`, later lines such as `snapshot -i` reuse the same `--platform`, `--device`, `--udid`, `--session`, and `--state-dir` values until a line overrides them. Project config resolution uses the working directory the ACP client passes on `session/new`.
9898

@@ -114,7 +114,7 @@ Prompts are literal command lines, one per line:
114114

115115
```
116116
open com.example.app --platform ios
117-
snapshot -i
117+
/snapshot -i
118118
press @e2
119119
screenshot
120120
close

0 commit comments

Comments
 (0)