Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0f6804e
[FEATURE] Mistral Vibe support
Jul 28, 2026
002c5e3
[FIX] Mistral on the bottom in provider dropdown
Jul 28, 2026
bc4107c
[[FIX] Model registry for local
Jul 28, 2026
7095ce7
Merge branch 'pingdotgg:main' into main
GijsRommers Jul 28, 2026
da755c1
[FIX] Stale session, inallid auth state and losing turn
Jul 28, 2026
e51d2e3
Merge branch 'pingdotgg:main' into main
GijsRommers Jul 28, 2026
4770cae
[FIX] stopcontext, drainevent now makes it no op when stopped
Jul 28, 2026
e45435a
[FIX] Serialize Vibe session startup per thread
Jul 28, 2026
111a9e2
[FIX] Always settle Vibe turns after ACP drain failures
Jul 28, 2026
ba392f0
Merge branch 'pingdotgg:main' into main
GijsRommers Jul 28, 2026
9c298bf
[FIX] Harden Vibe text generation and turn cancellation
Jul 28, 2026
5b9f40e
[FIX] Reserve the Vibe turn slot under the per-thread lock
Jul 28, 2026
cb81a26
Merge branch 'main' into main
GijsRommers Jul 28, 2026
5363235
fix(vibe): harden lifecycle cancellation and trust handling
Jul 28, 2026
363dc08
Merge branch 'main' into main
GijsRommers Jul 28, 2026
5717f95
[FIX] Cancel holds thread lock - release lock before ACP cancel
Jul 28, 2026
374d7a0
[FIX] Wrong-session cancel race - capture acp reference under lock
Jul 28, 2026
68c43f1
[FIX] Rapid follow-up messages fail due to double lock acquisition
Jul 28, 2026
0921d86
Fix Vibe turn steering for rapid follow-up sendTurn calls
Jul 28, 2026
04e8bf8
fix(vibe) follow up handling
Jul 28, 2026
b15b270
fix(vibe): harden turn cancellation and native event logging
Jul 28, 2026
c260753
fix(vibe): avoid terminal events for pre-start cancellation
Jul 28, 2026
4214b3e
fix(vibe): preserve prompt reservations during interruption
Jul 28, 2026
637b4fc
fix(vibe): handle missing workspace trust decisions
Jul 28, 2026
2332eaf
fix(vibe): prefer workspace-scoped trust grants
Jul 28, 2026
0cd5ae4
Merge branch 'pingdotgg:main' into main
GijsRommers Jul 29, 2026
5179dce
Merge branch 'pingdotgg:main' into main
GijsRommers Jul 29, 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
163 changes: 139 additions & 24 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const emitAskQuestion = process.env.T3_ACP_EMIT_ASK_QUESTION === "1";
const emitXAiAskUserQuestion = process.env.T3_ACP_EMIT_XAI_ASK_USER_QUESTION === "1";
const emitXAiPromptCompleteThenHang = process.env.T3_ACP_EMIT_XAI_PROMPT_COMPLETE_THEN_HANG === "1";
const emitForeignSessionUpdates = process.env.T3_ACP_EMIT_FOREIGN_SESSION_UPDATES === "1";
const useVibeConfig = process.env.T3_ACP_VIBE_CONFIG === "1";
const vibeTrustStatus = process.env.T3_ACP_VIBE_TRUST_STATUS ?? "trusted";
// Decisions Vibe currently has pending. Empty means no trust prompt is
// outstanding, and `_trust/decision` is rejected the way the real agent does.
const vibeTrustDecisions = (process.env.T3_ACP_VIBE_TRUST_DECISIONS ?? "")
.split(",")
.map((decision) => decision.trim())
.filter((decision) => decision.length > 0);
const hangPromptForever = process.env.T3_ACP_HANG_PROMPT_FOREVER === "1";
const hangFirstPromptForever = process.env.T3_ACP_HANG_FIRST_PROMPT_FOREVER === "1";
const emitLateUpdateAfterCancel = process.env.T3_ACP_EMIT_LATE_UPDATE_AFTER_CANCEL === "1";
Expand All @@ -40,22 +48,39 @@ const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1";
const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1";
const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT;
const promptDelayMs = Number(process.env.T3_ACP_PROMPT_DELAY_MS ?? "0");
const setConfigOptionDelayMs = Number(process.env.T3_ACP_SET_CONFIG_OPTION_DELAY_MS ?? "0");
const permissionOptionIds = {
allowOnce: process.env.T3_ACP_ALLOW_ONCE_OPTION_ID ?? "allow-once",
allowAlways: process.env.T3_ACP_ALLOW_ALWAYS_OPTION_ID ?? "allow-always",
rejectOnce: process.env.T3_ACP_REJECT_ONCE_OPTION_ID ?? "reject-once",
};
const sessionId = "mock-session-1";

let currentModeId = "ask";
let currentModelId = "default";
let currentModeId = useVibeConfig ? "default" : "ask";
let currentModelId = useVibeConfig ? "mistral-medium-3.5" : "default";
let parameterizedModelPicker = false;
let currentReasoning = "medium";
let currentContext = "272k";
let currentFast = false;
let promptCount = 0;
let overlappingFirstPromptId: string | undefined;
const cancelledSessions = new Set<string>();
const pendingCancellationResolvers = new Map<string, () => void>();

function awaitCancellation(cancelledSessionId: string): Effect.Effect<void> {
return Effect.callback<void>((resume) => {
const resolve = () => resume(Effect.void);
pendingCancellationResolvers.set(cancelledSessionId, resolve);
if (cancelledSessions.has(cancelledSessionId)) {
resolve();
}
return Effect.sync(() => {
if (pendingCancellationResolvers.get(cancelledSessionId) === resolve) {
pendingCancellationResolvers.delete(cancelledSessionId);
}
});
});
}

function promptIdFromRequestMeta(
request: Pick<AcpSchema.PromptRequest, "_meta">,
Expand Down Expand Up @@ -94,6 +119,47 @@ process.once("exit", (code) => {
});

function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> {
if (useVibeConfig) {
return [
{
id: "mode",
name: "Mode",
category: "mode",
type: "select",
currentValue: currentModeId,
options: availableModes.map((mode) => ({
value: mode.id,
name: mode.name,
...(mode.description ? { description: mode.description } : {}),
})),
},
{
id: "model",
name: "Model",
category: "model",
type: "select",
currentValue: currentModelId,
options: [
{ value: "mistral-medium-3.5", name: "Mistral Medium 3.5" },
{ value: "devstral-small", name: "Devstral Small" },
{ value: "local", name: "Local" },
],
},
{
id: "thinking",
name: "Thinking",
category: "model_config",
type: "select",
currentValue: "high",
options: [
{ value: "off", name: "Off" },
{ value: "low", name: "Low" },
{ value: "high", name: "High" },
],
},
];
}

if (parameterizedModelPicker) {
const baseOptions: Array<AcpSchema.SessionConfigOption> = [
{
Expand Down Expand Up @@ -253,23 +319,30 @@ function availableModels(): ReadonlyArray<{
}));
}

const availableModes: ReadonlyArray<AcpSchema.SessionMode> = [
{
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 availableModes: ReadonlyArray<AcpSchema.SessionMode> = useVibeConfig
? [
{ id: "default", name: "Default" },
{ id: "accept-edits", name: "Accept edits" },
{ id: "auto-approve", name: "Auto approve" },
{ id: "plan", name: "Plan" },
]
: [
{
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",
},
];

function modeState(): AcpSchema.SessionModeState {
return {
Expand All @@ -282,14 +355,20 @@ const grokAcpModels: ReadonlyArray<AcpSchema.ModelInfo> = [
{ modelId: "grok-build", name: "Grok Build" },
{ modelId: "grok-mock-alt", name: "Grok Mock Alt" },
];
const vibeAcpModels: ReadonlyArray<AcpSchema.ModelInfo> = [
{ modelId: "mistral-medium-3.5", name: "Mistral Medium 3.5" },
{ modelId: "devstral-small", name: "Devstral Small" },
{ modelId: "local", name: "Local" },
];

function modelState(): AcpSchema.SessionModelState {
const modelId = grokAcpModels.some((model) => model.modelId === currentModelId)
const availableModels = useVibeConfig ? vibeAcpModels : grokAcpModels;
const modelId = availableModels.some((model) => model.modelId === currentModelId)
? currentModelId
: "grok-build";
: availableModels[0]!.modelId;
return {
currentModelId: modelId,
availableModels: grokAcpModels,
availableModels,
};
}

Expand Down Expand Up @@ -382,7 +461,8 @@ const program = Effect.gen(function* () {

yield* agent.handleSetSessionModel((request) =>
Effect.gen(function* () {
if (!grokAcpModels.some((model) => model.modelId === request.modelId)) {
const availableModels = useVibeConfig ? vibeAcpModels : grokAcpModels;
if (!availableModels.some((model) => model.modelId === request.modelId)) {
return yield* AcpError.AcpRequestError.invalidParams(
`Unknown mock model id: ${request.modelId}`,
{
Expand All @@ -403,6 +483,9 @@ const program = Effect.gen(function* () {
process.exit(7);
});
}
if (Number.isFinite(setConfigOptionDelayMs) && setConfigOptionDelayMs > 0) {
yield* Effect.sleep(`${setConfigOptionDelayMs} millis`);
}
if (failSetConfigOption) {
return yield* AcpError.AcpRequestError.invalidParams(
"Mock invalid params for session/set_config_option",
Expand Down Expand Up @@ -449,6 +532,7 @@ const program = Effect.gen(function* () {
});
});
}
pendingCancellationResolvers.get(cancelledSessionId)?.();
}),
);

Expand Down Expand Up @@ -519,7 +603,9 @@ const program = Effect.gen(function* () {
}

if (hangPromptForever || (hangFirstPromptForever && promptCount === 1)) {
return yield* Effect.never;
yield* awaitCancellation(requestedSessionId);
cancelledSessions.delete(requestedSessionId);
return { stopReason: "cancelled" };
}

if (emitXAiPromptCompleteThenHang) {
Expand Down Expand Up @@ -878,6 +964,35 @@ const program = Effect.gen(function* () {
);

yield* agent.handleUnknownExtRequest((method, params) => {
if (method === "_trust/status") {
return Effect.succeed({
trust_status: vibeTrustStatus,
details:
vibeTrustDecisions.length > 0
? {
cwd: process.cwd(),
repoRoot: null,
ignoredFiles: [],
availableDecisions: vibeTrustDecisions,
}
: null,
});
}
if (method === "_trust/decision") {
const requested =
typeof params === "object" && params !== null && "decision" in params
? params.decision
: undefined;
// Matches the real agent: a decision can only be answered while one is
// pending, otherwise `-32602 No workspace trust decision is available`.
if (typeof requested !== "string" || !vibeTrustDecisions.includes(requested)) {
return Effect.fail(
AcpError.AcpRequestError.invalidParams("No workspace trust decision is available."),
);
}
return Effect.succeed({ trust_status: "trusted", details: null });
}

if (method === "cursor/list_available_models") {
return Effect.succeed({
models: availableModels(),
Expand Down
Loading
Loading