feat(modules/claude-code): make the module ready for Coder Tasks (#160)

Related to https://github.com/coder/internal/issues/700

This PR:

- makes AgentAPI a required dependency of the module. It's now used:
- to improve task reporting (by exporting `CODER_MCP_AI_AGENTAPI_URL`
before running `coder exp mcp configure claude-code`)
- to add a web chat interface to Claude (using the `Claude Code Web`
workspace app)
- removes support for tmux and screen since we don't need them if we
have AgentAPI
- makes the Claude Code CLI workspace app optional and disabled by
default - a new `experiment_cli_app` module variable controls its
presence
- makes the module spawn the `coder_ai_task` resource, which makes the
module compatible with the new Coder Tasks feature
- makes Claude Code remember the conversation between workspace restarts
using the `--continue` flag. Previously the module's implementation was
a bit bugged

Note: the filebrowser tests stopped passing because of an upstream
update in the filebrowser project around required password length. I
confirmed they are not related to this PR's changes.

---------

Co-authored-by: Ben Potter <me@bpmct.net>
This commit is contained in:
Hugo Dutka
2025-07-01 19:02:13 +02:00
committed by GitHub
parent 225aff06a7
commit 58faf32b81
10 changed files with 679 additions and 219 deletions
+44 -6
View File
@@ -30,6 +30,21 @@ export const runContainer = async (
return containerID.trim();
};
export const removeContainer = async (id: string) => {
const proc = spawn(["docker", "rm", "-f", id], {
stderr: "pipe",
stdout: "pipe",
});
const exitCode = await proc.exited;
const [stderr, stdout] = await Promise.all([
readableStreamToText(proc.stderr ?? new ReadableStream()),
readableStreamToText(proc.stdout ?? new ReadableStream()),
]);
if (exitCode !== 0) {
throw new Error(`${stderr}\n${stdout}`);
}
};
export interface scriptOutput {
exitCode: number;
stdout: string[];
@@ -279,10 +294,33 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => {
};
export const writeCoder = async (id: string, script: string) => {
const exec = await execContainer(id, [
"sh",
"-c",
`echo '${script}' > /usr/bin/coder && chmod +x /usr/bin/coder`,
]);
expect(exec.exitCode).toBe(0);
await writeFileContainer(id, "/usr/bin/coder", script, {
user: "root",
});
const execResult = await execContainer(
id,
["chmod", "755", "/usr/bin/coder"],
["--user", "root"],
);
expect(execResult.exitCode).toBe(0);
};
export const writeFileContainer = async (
id: string,
path: string,
content: string,
options?: {
user?: string;
},
) => {
const contentBase64 = Buffer.from(content).toString("base64");
const proc = await execContainer(
id,
["sh", "-c", `echo '${contentBase64}' | base64 -d > '${path}'`],
options?.user ? ["--user", options.user] : undefined,
);
if (proc.exitCode !== 0) {
throw new Error(`Failed to write file: ${proc.stderr}`);
}
expect(proc.exitCode).toBe(0);
};