diff --git a/README.md b/README.md index 2e66018..3e28d09 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 ` | Send follow-up steer text to an active run. | Example: `/codex_steer focus on the failing tests first` | | `/codex_plan ` | 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 ` | 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. | diff --git a/src/client.test.ts b/src/client.test.ts index 2151348..185a2cc 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -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", () => { diff --git a/src/client.ts b/src/client.ts index eedd2b6..c191a52 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1072,6 +1072,7 @@ function buildTurnStartPayloads(params: { prompt: string; model?: string; collaborationMode?: CollaborationMode; + collaborationFallbackModel?: string; }): unknown[] { const base: Record = { threadId: params.threadId, @@ -1085,7 +1086,7 @@ function buildTurnStartPayloads(params: { } const collaborationPayloads = buildCollaborationModePayloads( params.collaborationMode, - params.model, + params.collaborationFallbackModel ?? params.model, ).flatMap((variant) => [ { ...base, @@ -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; @@ -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 = ""; @@ -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 || ""} reasoningEffort=${threadReasoningEffort || ""}`, + ); } 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 || ""} reasoningEffort=${threadReasoningEffort || ""}`, + ); + } + 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() || ""} threadModel=${threadModel || ""} 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() || ""} threadModel=${threadModel || ""}`, + ); } 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); diff --git a/src/controller.ts b/src/controller.ts index 295c160..1b02b79 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -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 ?? ""}`, + ); if (active?.mode === "plan") { this.activeRuns.delete(key); await active.handle.interrupt().catch(() => undefined); @@ -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 ?? ""} threadModel=${threadState?.model?.trim() || ""} threadCwd=${threadState?.cwd?.trim() || ""}`, + ); return formatCodexStatusText({ threadState,