Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Pre-release packages are published on matching npm dist-tags instead of `latest`
2. Use the picker buttons, or pass a filter like `/codex_resume release-fix` or `/codex_resume --projects`.
3. Send normal chat messages once the thread is bound.
4. Use control commands such as `/codex_status`, `/codex_plan`, `/codex_review`, `/codex_model`, and `/codex_stop` as needed.
5. If you leave plan mode through the normal `Implement this plan` button, you do not need `/codex_plan off`; use `/codex_plan off` only when you want to exit planning manually instead.

## Command Reference

Expand All @@ -65,7 +66,7 @@ Pre-release packages are published on matching npm dist-tags instead of `latest`
| `/codex_stop` | Interrupt the active Codex run. | Only applies when a turn is currently in progress. |
| `/codex_steer <message>` | Send follow-up steer text to an active run. | Example: `/codex_steer focus on the failing tests first` |
| `/codex_plan <goal>` | Ask Codex to plan instead of execute. | The plugin relays plan questions and the final plan back into chat. |
| `/codex_plan off` | Exit plan mode for this conversation. | Interrupts a lingering plan run so future turns go back to default coding mode. |
| `/codex_plan off` | Exit plan mode for this conversation. | Use this when you want to leave planning manually instead of through the normal `Implement this plan` button. |
| `/codex_review` | Review the current uncommitted changes in the bound workspace. | Requires an existing binding. |
| `/codex_review <focus>` | Review with custom instructions. | Example: `/codex_review focus on thread selection regressions` |
| `/codex_compact` | Compact the bound Codex thread. | The plugin posts progress and final context usage. |
Expand Down
43 changes: 43 additions & 0 deletions src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,49 @@ describe("buildTurnStartPayloads", () => {
},
]);
});

it("uses the fallback model to send an explicit default collaboration mode", () => {
expect(
__testing.buildTurnStartPayloads({
threadId: "thread-123",
prompt: "run it",
collaborationMode: {
mode: "default",
settings: {
developerInstructions: null,
},
},
collaborationFallbackModel: "gpt-5.4",
}),
).toEqual([
{
threadId: "thread-123",
input: [{ type: "text", text: "run it" }],
collaborationMode: {
mode: "default",
settings: {
model: "gpt-5.4",
developerInstructions: null,
},
},
},
{
threadId: "thread-123",
input: [{ type: "text", text: "run it" }],
collaboration_mode: {
mode: "default",
settings: {
model: "gpt-5.4",
developer_instructions: null,
},
},
},
{
threadId: "thread-123",
input: [{ type: "text", text: "run it" }],
},
]);
});
});

describe("buildTurnSteerPayloads", () => {
Expand Down
80 changes: 70 additions & 10 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,7 @@ function buildTurnStartPayloads(params: {
prompt: string;
model?: string;
collaborationMode?: CollaborationMode;
collaborationFallbackModel?: string;
}): unknown[] {
const base: Record<string, unknown> = {
threadId: params.threadId,
Expand All @@ -1085,7 +1086,7 @@ function buildTurnStartPayloads(params: {
}
const collaborationPayloads = buildCollaborationModePayloads(
params.collaborationMode,
params.model,
params.collaborationFallbackModel ?? params.model,
).flatMap((variant) => [
{
...base,
Expand All @@ -1099,6 +1100,34 @@ function buildTurnStartPayloads(params: {
return [...collaborationPayloads, base];
}

function buildDefaultCollaborationMode(settings: {
model?: string;
reasoningEffort?: string;
}): CollaborationMode | undefined {
const model = settings.model?.trim();
if (!model) {
return undefined;
}
return {
mode: "default",
settings: {
model,
...(settings.reasoningEffort?.trim()
? { reasoningEffort: settings.reasoningEffort.trim() }
: {}),
developerInstructions: null,
},
};
}

function payloadHasCollaborationMode(payload: unknown): boolean {
const record = asRecord(payload);
return Boolean(
record &&
(Object.hasOwn(record, "collaborationMode") || Object.hasOwn(record, "collaboration_mode")),
);
}

function buildTurnSteerPayloads(params: {
threadId: string;
turnId: string;
Expand Down Expand Up @@ -3104,6 +3133,8 @@ export class CodexAppServerClient {
}): ActiveCodexRun {
let threadId = params.existingThreadId?.trim() || "";
let turnId = "";
let threadModel = "";
let threadReasoningEffort = "";
let assistantText = "";
let sawAssistantOutput = false;
let assistantItemId = "";
Expand Down Expand Up @@ -3352,29 +3383,58 @@ export class CodexAppServerClient {
],
timeoutMs: this.settings.requestTimeoutMs,
});
const createdState = extractThreadState(created);
threadId = extractIds(created).threadId ?? "";
threadModel = createdState.model?.trim() || threadModel;
threadReasoningEffort = createdState.reasoningEffort?.trim() || threadReasoningEffort;
if (!threadId) {
throw new Error("Codex App Server did not return a thread id.");
}
this.logger.debug(`codex turn thread created run=${params.runId} thread=${threadId}`);
this.logger.debug(
`codex turn thread created run=${params.runId} thread=${threadId} model=${threadModel || "<none>"} reasoningEffort=${threadReasoningEffort || "<none>"}`,
);
} else {
await requestWithFallbacks({
const resumed = await requestWithFallbacks({
client,
methods: ["thread/resume"],
payloads: [{ threadId }],
timeoutMs: this.settings.requestTimeoutMs,
}).catch(() => undefined);
this.logger.debug(`codex turn thread resumed run=${params.runId} thread=${threadId}`);
const resumedState = resumed ? extractThreadState(resumed) : undefined;
threadModel = resumedState?.model?.trim() || threadModel;
threadReasoningEffort =
resumedState?.reasoningEffort?.trim() || threadReasoningEffort;
this.logger.debug(
`codex turn thread resumed run=${params.runId} thread=${threadId} model=${threadModel || "<none>"} reasoningEffort=${threadReasoningEffort || "<none>"}`,
);
}
const synthesizedDefaultMode = buildDefaultCollaborationMode({
model: params.model?.trim() || threadModel,
reasoningEffort: threadReasoningEffort,
});
const collaborationMode = params.collaborationMode ?? synthesizedDefaultMode;
const turnStartPayloads = buildTurnStartPayloads({
threadId,
prompt: params.prompt,
model: params.model,
collaborationMode,
collaborationFallbackModel: params.model?.trim() || threadModel,
});
const collaborationPayload = turnStartPayloads.some((payload) =>
payloadHasCollaborationMode(payload),
);
this.logger.debug(
`codex turn start payload run=${params.runId} thread=${threadId} requestedMode=${params.collaborationMode?.mode ?? "default"} modeSource=${params.collaborationMode ? "explicit" : "synthesized"} requestedModel=${params.model?.trim() || "<none>"} threadModel=${threadModel || "<none>"} collaborationPayload=${collaborationPayload ? "yes" : "no"}`,
);
if (collaborationMode && !collaborationPayload) {
this.logger.warn(
`codex turn start omitted collaboration mode payload run=${params.runId} thread=${threadId} requestedMode=${collaborationMode.mode} requestedModel=${params.model?.trim() || "<none>"} threadModel=${threadModel || "<none>"}`,
);
}
const started = await requestWithFallbacks({
client,
methods: ["turn/start"],
payloads: buildTurnStartPayloads({
threadId,
prompt: params.prompt,
model: params.model,
collaborationMode: params.collaborationMode,
}),
payloads: turnStartPayloads,
timeoutMs: this.settings.requestTimeoutMs,
});
const startedIds = extractIds(started);
Expand Down
6 changes: 6 additions & 0 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,9 @@ export class CodexPluginController {
if (parsed.mode === "off") {
const key = buildConversationKey(conversation);
const active = this.activeRuns.get(key);
this.api.logger.debug?.(
`codex plan off requested ${this.formatConversationForLog(conversation)} active=${active?.mode ?? "none"} boundThread=${binding?.threadId ?? "<none>"}`,
);
if (active?.mode === "plan") {
this.activeRuns.delete(key);
await active.handle.interrupt().catch(() => undefined);
Expand Down Expand Up @@ -3239,6 +3242,9 @@ export class CodexPluginController {
}).catch(() => []),
this.resolveProjectFolder(binding?.workspaceDir || workspaceDir),
]);
this.api.logger.debug?.(
`codex status snapshot bindingActive=${bindingActive ? "yes" : "no"} activeRun=${activeRun?.mode ?? "none"} boundThread=${binding?.threadId ?? "<none>"} threadModel=${threadState?.model?.trim() || "<none>"} threadCwd=${threadState?.cwd?.trim() || "<none>"}`,
);

return formatCodexStatusText({
threadState,
Expand Down