mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Support rewrite runtime overrides and debug output
This commit is contained in:
parent
2dd6043340
commit
2c2730926a
6 changed files with 132 additions and 9 deletions
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<RewriteRuntime> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> = {}): RewriteSession {
|
||||
return {
|
||||
filePath: "Note.md",
|
||||
|
|
@ -80,6 +111,7 @@ function session(overrides: Partial<RewriteSession> = {}): RewriteSession {
|
|||
status: "editing-prompt",
|
||||
streamText: "",
|
||||
replacementText: null,
|
||||
debugText: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
@ -97,3 +129,23 @@ function turn(items: Turn["items"], overrides: Partial<Turn> = {}): 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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue