Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7165d9f
Add Cursor provider session and model selection support
juliusmarminge Mar 24, 2026
10129ed
Use server-driven ModelCapabilities for Cursor traits
juliusmarminge Mar 26, 2026
d4561ad
Switch Cursor model selection to in-session via session/set_config_op…
juliusmarminge Mar 26, 2026
97d5288
Merge remote-tracking branch 'origin/main' into t3code/greeting
juliusmarminge Mar 26, 2026
d747322
Refactor Cursor model handling in ChatView and ProviderModelPicker
juliusmarminge Mar 26, 2026
55cef69
rm probes
juliusmarminge Mar 26, 2026
86e60de
rm probe
juliusmarminge Mar 26, 2026
e7c5ac3
Probe Cursor ACP session setup in one script
juliusmarminge Mar 26, 2026
c37322e
rm unused probe
juliusmarminge Mar 26, 2026
e1f8939
Normalize provider model IDs and Cursor options
juliusmarminge Mar 27, 2026
f5e64d6
fix picker
juliusmarminge Mar 27, 2026
4d4bee2
fix spawning
juliusmarminge Mar 27, 2026
3a21c19
Add Cursor text generation and model mapping
juliusmarminge Mar 27, 2026
88696b4
fix effect lsp issues
juliusmarminge Mar 27, 2026
ea47805
use constructors
juliusmarminge Mar 27, 2026
97ed04d
kewl
juliusmarminge Mar 27, 2026
0575c11
move claude model id lookup
juliusmarminge Mar 27, 2026
aa5b8d2
rm unused test
juliusmarminge Mar 27, 2026
26ef535
kewl
juliusmarminge Mar 27, 2026
c029bac
nit
juliusmarminge Mar 27, 2026
f7b2b07
kewl
juliusmarminge Mar 27, 2026
accc67d
effect-acp
juliusmarminge Mar 27, 2026
84f9533
improve sdk and use it
juliusmarminge Mar 27, 2026
22594d6
nit
juliusmarminge Mar 27, 2026
0c218d2
noExternal
juliusmarminge Mar 27, 2026
f76321f
Log ACP requests and preserve turn-start failures
juliusmarminge Mar 27, 2026
1787185
tidy up effect-acp
juliusmarminge Mar 27, 2026
ba6604b
error on fmt
juliusmarminge Mar 27, 2026
1f7a48a
small nit
juliusmarminge Mar 27, 2026
9476dd2
Handle cancelled Cursor ACP turns and simplify model ids
juliusmarminge Mar 27, 2026
058822b
Extract ACP runtime event helpers and preserve model slugs
juliusmarminge Mar 27, 2026
146e102
Constrain ACP runtime event source types
juliusmarminge Mar 27, 2026
e9c611f
Merge origin/main into t3code/greeting
juliusmarminge Mar 28, 2026
f2614d0
Settle pending user input on session stop
juliusmarminge Mar 28, 2026
fe988ec
Log outgoing ACP notifications before sending
juliusmarminge Mar 28, 2026
c4e9e40
Remove child-process export from effect-acp package
juliusmarminge Mar 28, 2026
65621de
Patch ACP protocol to use request envelopes
juliusmarminge Mar 28, 2026
cad3cbe
Propagate ACP child exits through protocol termination
juliusmarminge Mar 28, 2026
ff8577a
servicify
juliusmarminge Mar 28, 2026
7192f24
Merge origin/main into t3code/greeting
juliusmarminge Mar 28, 2026
81d1239
Align Effect catalog with merged main
juliusmarminge Mar 28, 2026
b994a72
fix ubild
juliusmarminge Mar 28, 2026
5cb6d05
agent/client separation
juliusmarminge Mar 28, 2026
e44eea6
fix
juliusmarminge Mar 28, 2026
399be25
kewl
juliusmarminge Mar 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 315 additions & 0 deletions apps/server/scripts/acp-mock-agent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
#!/usr/bin/env node
/**
* Minimal NDJSON JSON-RPC "agent" for ACP client tests.
* Reads stdin lines; writes responses/notifications to stdout.
*/
import * as readline from "node:readline";
import { appendFileSync } from "node:fs";

const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
const requestLogPath = process.env.T3_ACP_REQUEST_LOG_PATH;
const emitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS === "1";
const sessionId = "mock-session-1";
let currentModeId = "ask";
let currentModelId = "default";
let nextRequestId = 1;

function configOptions() {
return [
{
id: "model",
name: "Model",
category: "model",
type: "select",
currentValue: currentModelId,
options: [
{ value: "default", name: "Auto" },
{ value: "composer-2", name: "Composer 2" },
{ value: "composer-2[fast=true]", name: "Composer 2 Fast" },
{ value: "gpt-5.3-codex[reasoning=medium,fast=false]", name: "Codex 5.3" },
],
},
];
}

const availableModes = [
{
id: "ask",
name: "Ask",
description: "Request permission before making any changes",
},
{
id: "architect",
name: "Architect",
description: "Design and plan software systems without implementation",
},
{
id: "code",
name: "Code",
description: "Write and modify code with full tool access",
},
];
const pendingPermissionRequests = new Map();

function send(obj) {
process.stdout.write(`${JSON.stringify(obj)}\n`);
}

function modeState() {
return {
currentModeId,
availableModes,
};
}

function sendSessionUpdate(update, session = sessionId) {
send({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: session,
update,
},
});
}

rl.on("line", (line) => {
const trimmed = line.trim();
if (!trimmed) return;
let msg;
try {
msg = JSON.parse(trimmed);
} catch {
return;
}
if (!msg || typeof msg !== "object") return;
if (requestLogPath) {
appendFileSync(requestLogPath, `${JSON.stringify(msg)}\n`, "utf8");
}

const id = msg.id;
const method = msg.method;

if (method === undefined && id !== undefined && pendingPermissionRequests.has(id)) {
const pending = pendingPermissionRequests.get(id);
pendingPermissionRequests.delete(id);
sendSessionUpdate(
{
sessionUpdate: "tool_call_update",
toolCallId: pending.toolCallId,
title: "Terminal",
kind: "execute",
status: "completed",
rawOutput: {
exitCode: 0,
stdout: '{ "name": "t3" }',
stderr: "",
},
},
pending.sessionId,
);
sendSessionUpdate(
{
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "hello from mock" },
},
pending.sessionId,
);
send({
jsonrpc: "2.0",
id: pending.promptRequestId,
result: { stopReason: "end_turn" },
});
return;
}

if (method === "initialize" && id !== undefined) {
send({
jsonrpc: "2.0",
id,
result: {
protocolVersion: 1,
agentCapabilities: { loadSession: true },
},
});
return;
}

if (method === "authenticate" && id !== undefined) {
send({ jsonrpc: "2.0", id, result: { authenticated: true } });
return;
}

if (method === "session/new" && id !== undefined) {
send({
jsonrpc: "2.0",
id,
result: {
sessionId,
modes: modeState(),
configOptions: configOptions(),
},
});
return;
}

if (method === "session/load" && id !== undefined) {
const requestedSessionId = msg.params?.sessionId ?? sessionId;
sendSessionUpdate(
{
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "replay" },
},
requestedSessionId,
);
send({
jsonrpc: "2.0",
id,
result: {
modes: modeState(),
configOptions: configOptions(),
},
});
return;
}

if (method === "session/set_config_option" && id !== undefined) {
const configId = msg.params?.configId;
const value = msg.params?.value;
if (configId === "model" && typeof value === "string") {
currentModelId = value;
}
send({
jsonrpc: "2.0",
id,
result: { configOptions: configOptions() },
});
return;
}

if (method === "session/prompt" && id !== undefined) {
const requestedSessionId = msg.params?.sessionId ?? sessionId;
if (emitToolCalls) {
const toolCallId = "tool-call-1";
const permissionRequestId = nextRequestId++;
sendSessionUpdate(
{
sessionUpdate: "tool_call",
toolCallId,
title: "Terminal",
kind: "execute",
status: "pending",
rawInput: {
command: ["cat", "server/package.json"],
},
},
requestedSessionId,
);
sendSessionUpdate(
{
sessionUpdate: "tool_call_update",
toolCallId,
status: "in_progress",
},
requestedSessionId,
);
pendingPermissionRequests.set(permissionRequestId, {
promptRequestId: id,
sessionId: requestedSessionId,
toolCallId,
});
send({
jsonrpc: "2.0",
id: permissionRequestId,
method: "session/request_permission",
params: {
sessionId: requestedSessionId,
toolCall: {
toolCallId,
title: "`cat server/package.json`",
kind: "execute",
status: "pending",
content: [
{
type: "content",
content: {
type: "text",
text: "Not in allowlist: cat server/package.json",
},
},
],
},
options: [
{ optionId: "allow-once", name: "Allow once", kind: "allow_once" },
{ optionId: "allow-always", name: "Allow always", kind: "allow_always" },
{ optionId: "reject-once", name: "Reject", kind: "reject_once" },
],
},
});
return;
}
sendSessionUpdate(
{
sessionUpdate: "plan",
explanation: `Mock plan while in ${currentModeId}`,
entries: [
{
content: "Inspect mock ACP state",
priority: "high",
status: "completed",
},
{
content: "Implement the requested change",
priority: "high",
status: "in_progress",
},
],
},
requestedSessionId,
);
sendSessionUpdate(
{
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "hello from mock" },
},
requestedSessionId,
);
send({
jsonrpc: "2.0",
id,
result: { stopReason: "end_turn" },
});
return;
}

if ((method === "session/set_mode" || method === "session/mode/set") && id !== undefined) {
const nextModeId =
typeof msg.params?.modeId === "string"
? msg.params.modeId
: typeof msg.params?.mode === "string"
? msg.params.mode
: undefined;
if (typeof nextModeId === "string" && nextModeId.trim()) {
currentModeId = nextModeId.trim();
sendSessionUpdate({
sessionUpdate: "current_mode_update",
currentModeId,
});
}
send({ jsonrpc: "2.0", id, result: null });
return;
}

if (method === "session/cancel" && id !== undefined) {
send({ jsonrpc: "2.0", id, result: null });
return;
}

if (id !== undefined) {
send({
jsonrpc: "2.0",
id,
error: { code: -32601, message: `Unhandled method: ${String(method)}` },
});
}
});
4 changes: 2 additions & 2 deletions apps/server/src/git/Layers/ClaudeTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { Effect, Layer, Option, Schema, Stream } from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { ClaudeModelSelection } from "@t3tools/contracts";
import { resolveApiModelId } from "@t3tools/shared/model";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";

import { TextGenerationError } from "../Errors.ts";
Expand All @@ -27,6 +26,7 @@ import {
sanitizePrTitle,
toJsonSchemaObject,
} from "../Utils.ts";
import { resolveClaudeApiModelId } from "../../provider/Layers/ClaudeModelId.ts";
import { normalizeClaudeModelOptions } from "../../provider/Layers/ClaudeProvider.ts";
import { ServerSettingsService } from "../../serverSettings.ts";

Expand Down Expand Up @@ -104,7 +104,7 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
"--json-schema",
jsonSchemaStr,
"--model",
resolveApiModelId(modelSelection),
resolveClaudeApiModelId(modelSelection),
...(normalizedOptions?.effort ? ["--effort", normalizedOptions.effort] : []),
...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []),
"--dangerously-skip-permissions",
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/git/Services/TextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { ChatAttachment, ModelSelection } from "@t3tools/contracts";
import type { TextGenerationError } from "../Errors.ts";

/** Providers that support git text generation (commit messages, PR content, branch names). */
export type TextGenerationProvider = "codex" | "claudeAgent";
export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor";

export interface CommitMessageGenerationInput {
cwd: string;
Expand Down
Loading
Loading