From 2c2730926a90e24aae68c55b855d73e084139d2e Mon Sep 17 00:00:00 2001 From: murashit Date: Mon, 18 May 2026 11:57:43 +0900 Subject: [PATCH] Support rewrite runtime overrides and debug output --- src/editor-rewrite/command.ts | 6 ++- src/editor-rewrite/modal.ts | 7 ++- src/editor-rewrite/model.ts | 7 +++ src/editor-rewrite/output.ts | 22 ++++++++- src/editor-rewrite/runner.ts | 45 +++++++++++++++-- tests/editor-rewrite/editor-rewrite.test.ts | 54 ++++++++++++++++++++- 6 files changed, 132 insertions(+), 9 deletions(-) diff --git a/src/editor-rewrite/command.ts b/src/editor-rewrite/command.ts index ec53e752..82932f02 100644 --- a/src/editor-rewrite/command.ts +++ b/src/editor-rewrite/command.ts @@ -1,12 +1,12 @@ import { MarkdownView, Notice, type Editor, type Plugin } from "obsidian"; import { RewriteSelectionModal } from "./modal"; -import type { RewriteSession } from "./model"; +import type { RewriteRuntimeSettings, RewriteSession } from "./model"; export interface RewriteSelectionCommandHost extends Plugin { settings: { codexPath: string; - }; + } & RewriteRuntimeSettings; vaultPath: string; } @@ -38,12 +38,14 @@ export function registerRewriteSelectionCommand(plugin: RewriteSelectionCommandH status: "editing-prompt", streamText: "", replacementText: null, + debugText: null, }; new RewriteSelectionModal(plugin.app, { codexPath: plugin.settings.codexPath, cwd: plugin.vaultPath, editor, + runtimeSettings: plugin.settings, session, }).open(); }, diff --git a/src/editor-rewrite/modal.ts b/src/editor-rewrite/modal.ts index 022a5917..6cd56c60 100644 --- a/src/editor-rewrite/modal.ts +++ b/src/editor-rewrite/modal.ts @@ -2,7 +2,8 @@ import { Modal, Notice, type App, type Editor } from "obsidian"; import { renderUnifiedDiff } from "../ui/turn-diff"; import { buildSelectionUnifiedDiff } from "./diff"; -import { canApplyRewrite, type RewriteSession } from "./model"; +import { canApplyRewrite, type RewriteRuntimeSettings, type RewriteSession } from "./model"; +import { RewriteOutputError } from "./output"; import { buildRewritePrompt } from "./prompt"; import { runRewriteSelection } from "./runner"; @@ -10,6 +11,7 @@ export interface RewriteSelectionModalOptions { codexPath: string; cwd: string; editor: Editor; + runtimeSettings: RewriteRuntimeSettings; session: RewriteSession; } @@ -84,6 +86,7 @@ export class RewriteSelectionModal extends Modal { this.options.session.status = "generating"; this.options.session.streamText = ""; this.options.session.replacementText = null; + this.options.session.debugText = null; this.previewEl?.setText(""); this.diffEl?.empty(); this.setStatus("Generating patch..."); @@ -94,6 +97,7 @@ export class RewriteSelectionModal extends Modal { codexPath: this.options.codexPath, cwd: this.options.cwd, prompt: buildRewritePrompt(this.options.session), + runtimeSettings: this.options.runtimeSettings, onPreview: (text) => this.updatePreview(text), }); this.options.session.replacementText = output.replacementText; @@ -102,6 +106,7 @@ export class RewriteSelectionModal extends Modal { this.setStatus("Review the patch before applying it."); } catch (error) { this.options.session.status = "failed"; + this.options.session.debugText = error instanceof RewriteOutputError ? error.rawText : null; this.setStatus(error instanceof Error ? error.message : String(error)); } finally { this.syncControls(); diff --git a/src/editor-rewrite/model.ts b/src/editor-rewrite/model.ts index 525d5c86..a5d7c55f 100644 --- a/src/editor-rewrite/model.ts +++ b/src/editor-rewrite/model.ts @@ -1,4 +1,5 @@ import type { EditorPosition } from "obsidian"; +import type { ReasoningEffort } from "../generated/app-server/ReasoningEffort"; export type RewriteStatus = "editing-prompt" | "generating" | "preview" | "applied" | "cancelled" | "failed"; @@ -14,6 +15,12 @@ export interface RewriteSession { status: RewriteStatus; streamText: string; replacementText: string | null; + debugText: string | null; +} + +export interface RewriteRuntimeSettings { + rewriteSelectionModel: string | null; + rewriteSelectionEffort: ReasoningEffort | null; } export function canApplyRewrite(currentText: string, originalText: string): boolean { diff --git a/src/editor-rewrite/output.ts b/src/editor-rewrite/output.ts index 8d8d1eee..77457307 100644 --- a/src/editor-rewrite/output.ts +++ b/src/editor-rewrite/output.ts @@ -4,6 +4,21 @@ export interface RewriteOutput { replacementText: string; } +export interface RewriteOutputParseResult { + output: RewriteOutput | null; + rawText: string | null; +} + +export class RewriteOutputError extends Error { + constructor( + message: string, + readonly rawText: string | null, + ) { + super(message); + this.name = "RewriteOutputError"; + } +} + export function parseRewriteOutput(text: string): RewriteOutput | null { try { const parsed = JSON.parse(text.trim()) as unknown; @@ -17,8 +32,13 @@ export function parseRewriteOutput(text: string): RewriteOutput | null { } export function rewriteOutputFromTurn(turn: Turn): RewriteOutput | null { + return rewriteOutputParseResultFromTurn(turn).output; +} + +export function rewriteOutputParseResultFromTurn(turn: Turn): RewriteOutputParseResult { const text = lastAgentMessageText(turn); - return text ? parseRewriteOutput(text) : null; + if (!text) return { output: null, rawText: null }; + return { output: parseRewriteOutput(text), rawText: text }; } function lastAgentMessageText(turn: Turn): string | null { diff --git a/src/editor-rewrite/runner.ts b/src/editor-rewrite/runner.ts index 0ff3d852..bf0e052f 100644 --- a/src/editor-rewrite/runner.ts +++ b/src/editor-rewrite/runner.ts @@ -1,10 +1,14 @@ import { AppServerClient } from "../app-server/client"; import type { ServerNotification } from "../generated/app-server/ServerNotification"; import type { JsonValue } from "../generated/app-server/serde_json/JsonValue"; +import type { ReasoningEffort } from "../generated/app-server/ReasoningEffort"; +import type { Model } from "../generated/app-server/v2/Model"; import type { ThreadItem } from "../generated/app-server/v2/ThreadItem"; import type { Turn } from "../generated/app-server/v2/Turn"; +import { runtimeOverride, validatedRuntimeOverride } from "../runtime/model"; +import type { RewriteRuntimeSettings } from "./model"; import { REWRITE_DEVELOPER_INSTRUCTIONS, REWRITE_SERVICE_NAME } from "./prompt"; -import { rewriteOutputFromTurn, type RewriteOutput } from "./output"; +import { RewriteOutputError, rewriteOutputParseResultFromTurn, type RewriteOutput } from "./output"; const REWRITE_TIMEOUT_MS = 120_000; @@ -23,6 +27,7 @@ export interface RunRewriteSelectionOptions { codexPath: string; cwd: string; prompt: string; + runtimeSettings?: RewriteRuntimeSettings; onPreview?: (text: string) => void; } @@ -82,13 +87,21 @@ export async function runRewriteSelection(options: RunRewriteSelectionOptions): try { await client.connect(); + const runtime = options.runtimeSettings ? await rewriteRuntimeForClient(client, options.runtimeSettings) : {}; const threadResponse = await client.startEphemeralThread(options.cwd, REWRITE_SERVICE_NAME, REWRITE_DEVELOPER_INSTRUCTIONS); threadId = threadResponse.thread.id; - const turnResponse = await client.startStructuredTurn(threadId, options.cwd, options.prompt, REWRITE_OUTPUT_SCHEMA); + const turnResponse = await client.startStructuredTurn( + threadId, + options.cwd, + options.prompt, + REWRITE_OUTPUT_SCHEMA, + runtime.model, + runtime.effort, + ); expectedTurnId = turnResponse.turn.id; const turn = turnResponse.turn.status === "completed" ? turnWithCollectedItems(turnResponse.turn, completedItems) : await completedTurn; - const output = rewriteOutputFromTurn(turn); - if (!output) throw new Error("Codex did not return a valid rewrite patch."); + const { output, rawText } = rewriteOutputParseResultFromTurn(turn); + if (!output) throw new RewriteOutputError("Codex did not return a valid rewrite patch.", rawText); return output; } finally { completed = true; @@ -101,3 +114,27 @@ function turnWithCollectedItems(turn: Turn, completedItems: ThreadItem[]): Turn if (turn.items.length > 0 || completedItems.length === 0) return turn; return { ...turn, items: completedItems }; } + +export interface RewriteRuntime { + model?: string; + effort?: ReasoningEffort; +} + +export function rewriteRuntime(settings: RewriteRuntimeSettings): RewriteRuntime { + return runtimeOverride({ model: settings.rewriteSelectionModel, effort: settings.rewriteSelectionEffort }); +} + +export function validatedRewriteRuntime(settings: RewriteRuntimeSettings, models: Model[]): RewriteRuntime { + return validatedRuntimeOverride({ model: settings.rewriteSelectionModel, effort: settings.rewriteSelectionEffort }, models); +} + +async function rewriteRuntimeForClient(client: AppServerClient, settings: RewriteRuntimeSettings): Promise { + const runtime = rewriteRuntime(settings); + if (!runtime.model || !runtime.effort) return runtime; + try { + const response = await client.listModels(false); + return validatedRewriteRuntime(settings, response.data); + } catch { + return runtime; + } +} diff --git a/tests/editor-rewrite/editor-rewrite.test.ts b/tests/editor-rewrite/editor-rewrite.test.ts index 8fedcb63..97bd6ed1 100644 --- a/tests/editor-rewrite/editor-rewrite.test.ts +++ b/tests/editor-rewrite/editor-rewrite.test.ts @@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest"; import { buildSelectionUnifiedDiff } from "../../src/editor-rewrite/diff"; import { canApplyRewrite, type RewriteSession } from "../../src/editor-rewrite/model"; -import { parseRewriteOutput, rewriteOutputFromTurn } from "../../src/editor-rewrite/output"; +import { parseRewriteOutput, rewriteOutputFromTurn, rewriteOutputParseResultFromTurn } from "../../src/editor-rewrite/output"; import { buildRewritePrompt } from "../../src/editor-rewrite/prompt"; +import { rewriteRuntime, validatedRewriteRuntime } from "../../src/editor-rewrite/runner"; +import type { Model } from "../../src/generated/app-server/v2/Model"; import type { Turn } from "../../src/generated/app-server/v2/Turn"; describe("editor rewrite output", () => { @@ -27,6 +29,14 @@ describe("editor rewrite output", () => { ), ).toEqual({ replacementText: "final" }); }); + + it("keeps raw model text when rewrite output parsing fails", () => { + expect( + rewriteOutputParseResultFromTurn( + turn([{ type: "agentMessage", id: "a1", text: "replacementText: final", phase: "final_answer", memoryCitation: null }]), + ), + ).toEqual({ output: null, rawText: "replacementText: final" }); + }); }); describe("editor rewrite prompt", () => { @@ -67,6 +77,27 @@ describe("editor rewrite apply guard", () => { }); }); +describe("editor rewrite runtime", () => { + it("uses explicit rewrite runtime settings", () => { + expect(rewriteRuntime({ rewriteSelectionModel: "gpt-5.4-mini", rewriteSelectionEffort: "minimal" })).toEqual({ + model: "gpt-5.4-mini", + effort: "minimal", + }); + }); + + it("omits rewrite runtime overrides that are set to Codex default", () => { + expect(rewriteRuntime({ rewriteSelectionModel: null, rewriteSelectionEffort: null })).toEqual({}); + }); + + it("omits an explicit rewrite effort when the selected model does not support it", () => { + expect( + validatedRewriteRuntime({ rewriteSelectionModel: "gpt-5.4-mini", rewriteSelectionEffort: "minimal" }, [ + model("gpt-5.4-mini", ["low", "medium", "high", "xhigh"]), + ]), + ).toEqual({ model: "gpt-5.4-mini" }); + }); +}); + function session(overrides: Partial = {}): RewriteSession { return { filePath: "Note.md", @@ -80,6 +111,7 @@ function session(overrides: Partial = {}): RewriteSession { status: "editing-prompt", streamText: "", replacementText: null, + debugText: null, ...overrides, }; } @@ -97,3 +129,23 @@ function turn(items: Turn["items"], overrides: Partial = {}): Turn { ...overrides, }; } + +function model(name: string, efforts: Model["supportedReasoningEfforts"][number]["reasoningEffort"][]): Model { + return { + id: name, + model: name, + upgrade: null, + upgradeInfo: null, + availabilityNux: null, + displayName: name, + description: "", + hidden: false, + supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort, description: "" })), + defaultReasoningEffort: efforts[0] ?? "low", + inputModalities: ["text"], + supportsPersonality: false, + additionalSpeedTiers: [], + serviceTiers: [], + isDefault: false, + }; +}