mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Model selection rewrite lifecycle transitions
This commit is contained in:
parent
1d88833f19
commit
4e30caa2e3
3 changed files with 120 additions and 17 deletions
|
|
@ -23,6 +23,60 @@ export interface SelectionRewriteRuntimeSettings {
|
|||
rewriteSelectionEffort: ReasoningEffort | null;
|
||||
}
|
||||
|
||||
export type SelectionRewriteLifecycleEvent =
|
||||
| { type: "generation-started"; instruction: string }
|
||||
| { type: "preview-updated"; text: string }
|
||||
| { type: "generation-succeeded"; replacementText: string }
|
||||
| { type: "generation-failed"; debugText: string | null }
|
||||
| { type: "cancelled" }
|
||||
| { type: "applied" };
|
||||
|
||||
export function canApplySelectionRewrite(currentText: string, originalText: string): boolean {
|
||||
return currentText === originalText;
|
||||
}
|
||||
|
||||
export function transitionSelectionRewriteState(
|
||||
state: SelectionRewriteState,
|
||||
event: SelectionRewriteLifecycleEvent,
|
||||
): SelectionRewriteState {
|
||||
switch (event.type) {
|
||||
case "generation-started":
|
||||
return {
|
||||
...state,
|
||||
instruction: event.instruction,
|
||||
status: "generating",
|
||||
streamText: "",
|
||||
replacementText: null,
|
||||
debugText: null,
|
||||
};
|
||||
case "preview-updated":
|
||||
if (state.status !== "generating") return state;
|
||||
return { ...state, streamText: event.text };
|
||||
case "generation-succeeded":
|
||||
if (state.status !== "generating") return state;
|
||||
return {
|
||||
...state,
|
||||
status: "preview",
|
||||
streamText: "",
|
||||
replacementText: event.replacementText,
|
||||
};
|
||||
case "generation-failed":
|
||||
if (state.status !== "generating") return state;
|
||||
return {
|
||||
...state,
|
||||
status: "failed",
|
||||
streamText: "",
|
||||
debugText: event.debugText,
|
||||
};
|
||||
case "cancelled":
|
||||
return {
|
||||
...state,
|
||||
status: "cancelled",
|
||||
};
|
||||
case "applied":
|
||||
return {
|
||||
...state,
|
||||
status: "applied",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ import { renderReactRoot, unmountReactRoot } from "../../shared/ui/react-root";
|
|||
import { syncTextareaHeight } from "../../shared/ui/textarea-autogrow";
|
||||
import { buildSelectionUnifiedDiff } from "./diff";
|
||||
import { isSelectionRewriteActionKey, isSelectionRewriteGenerateKey } from "./keys";
|
||||
import { canApplySelectionRewrite, type SelectionRewriteRuntimeSettings, type SelectionRewriteState } from "./model";
|
||||
import {
|
||||
canApplySelectionRewrite,
|
||||
transitionSelectionRewriteState,
|
||||
type SelectionRewriteLifecycleEvent,
|
||||
type SelectionRewriteRuntimeSettings,
|
||||
type SelectionRewriteState,
|
||||
} from "./model";
|
||||
import { SelectionRewriteOutputError } from "./output";
|
||||
import { positionSelectionRewritePopover } from "./position";
|
||||
import { buildSelectionRewritePrompt } from "./prompt";
|
||||
|
|
@ -126,7 +132,7 @@ export class SelectionRewritePopover {
|
|||
this.showSelectionRewritePreview(output.replacementText);
|
||||
} catch (error) {
|
||||
if (abortController.signal.aborted) {
|
||||
this.options.state.status = "cancelled";
|
||||
this.transitionState({ type: "cancelled" });
|
||||
return;
|
||||
}
|
||||
this.showGenerationFailure(error);
|
||||
|
|
@ -139,13 +145,13 @@ export class SelectionRewritePopover {
|
|||
}
|
||||
|
||||
private cancel(): void {
|
||||
this.options.state.status = "cancelled";
|
||||
this.transitionState({ type: "cancelled" });
|
||||
this.abortController?.abort();
|
||||
this.close();
|
||||
}
|
||||
|
||||
private updatePreview(text: string): void {
|
||||
this.options.state.streamText = text;
|
||||
this.transitionState({ type: "preview-updated", text });
|
||||
this.setStatus("Writing replacement", { active: true });
|
||||
this.position();
|
||||
}
|
||||
|
|
@ -164,25 +170,20 @@ export class SelectionRewritePopover {
|
|||
}
|
||||
|
||||
private startGeneration(instruction: string): void {
|
||||
this.options.state.instruction = instruction;
|
||||
this.options.state.status = "generating";
|
||||
this.options.state.streamText = "";
|
||||
this.options.state.replacementText = null;
|
||||
this.options.state.debugText = null;
|
||||
this.transitionState({ type: "generation-started", instruction });
|
||||
this.renderView();
|
||||
}
|
||||
|
||||
private showSelectionRewritePreview(replacementText: string): void {
|
||||
this.options.state.replacementText = replacementText;
|
||||
this.options.state.status = "preview";
|
||||
this.options.state.streamText = "";
|
||||
this.transitionState({ type: "generation-succeeded", replacementText });
|
||||
this.setStatus("");
|
||||
}
|
||||
|
||||
private showGenerationFailure(error: unknown): void {
|
||||
this.options.state.status = "failed";
|
||||
this.options.state.debugText = error instanceof SelectionRewriteOutputError ? error.rawText : null;
|
||||
this.options.state.streamText = "";
|
||||
this.transitionState({
|
||||
type: "generation-failed",
|
||||
debugText: error instanceof SelectionRewriteOutputError ? error.rawText : null,
|
||||
});
|
||||
this.setStatus(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
|
|
@ -199,10 +200,14 @@ export class SelectionRewritePopover {
|
|||
}
|
||||
|
||||
editor.replaceRange(replacement, state.targetRange.from, state.targetRange.to, "codex-panel-rewrite");
|
||||
state.status = "applied";
|
||||
this.transitionState({ type: "applied" });
|
||||
this.close();
|
||||
}
|
||||
|
||||
private transitionState(event: SelectionRewriteLifecycleEvent): void {
|
||||
this.options.state = transitionSelectionRewriteState(this.options.state, event);
|
||||
}
|
||||
|
||||
private focusApplyButton(): void {
|
||||
if (!this.elements?.applyButton || this.elements.applyButton.disabled) return;
|
||||
this.elements.applyButton.focus({ preventScroll: true });
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ import {
|
|||
isSelectionRewriteGenerateKey,
|
||||
type SelectionRewriteGenerateKeyEvent,
|
||||
} from "../../../src/features/selection-rewrite/keys";
|
||||
import { canApplySelectionRewrite, type SelectionRewriteState } from "../../../src/features/selection-rewrite/model";
|
||||
import {
|
||||
canApplySelectionRewrite,
|
||||
transitionSelectionRewriteState,
|
||||
type SelectionRewriteState,
|
||||
} from "../../../src/features/selection-rewrite/model";
|
||||
import {
|
||||
parseSelectionRewriteOutput,
|
||||
selectionRewriteOutputFromTurn,
|
||||
|
|
@ -135,6 +139,46 @@ describe("selection rewrite apply guard", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("selection rewrite lifecycle", () => {
|
||||
it("transitions through generation and preview states", () => {
|
||||
const generating = transitionSelectionRewriteState(rewriteState({ replacementText: "old", debugText: "old debug" }), {
|
||||
type: "generation-started",
|
||||
instruction: "Shorten it.",
|
||||
});
|
||||
|
||||
expect(generating).toMatchObject({
|
||||
instruction: "Shorten it.",
|
||||
status: "generating",
|
||||
streamText: "",
|
||||
replacementText: null,
|
||||
debugText: null,
|
||||
});
|
||||
|
||||
const streaming = transitionSelectionRewriteState(generating, { type: "preview-updated", text: "New" });
|
||||
expect(streaming.streamText).toBe("New");
|
||||
|
||||
const preview = transitionSelectionRewriteState(streaming, { type: "generation-succeeded", replacementText: "New text." });
|
||||
expect(preview).toMatchObject({
|
||||
status: "preview",
|
||||
streamText: "",
|
||||
replacementText: "New text.",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores stale generation callbacks after generation is no longer active", () => {
|
||||
const state = rewriteState({ status: "cancelled", streamText: "" });
|
||||
|
||||
expect(transitionSelectionRewriteState(state, { type: "preview-updated", text: "late" })).toBe(state);
|
||||
expect(transitionSelectionRewriteState(state, { type: "generation-succeeded", replacementText: "late" })).toBe(state);
|
||||
expect(transitionSelectionRewriteState(state, { type: "generation-failed", debugText: "late" })).toBe(state);
|
||||
});
|
||||
|
||||
it("marks terminal user actions explicitly", () => {
|
||||
expect(transitionSelectionRewriteState(rewriteState(), { type: "cancelled" }).status).toBe("cancelled");
|
||||
expect(transitionSelectionRewriteState(rewriteState({ replacementText: "New text." }), { type: "applied" }).status).toBe("applied");
|
||||
});
|
||||
});
|
||||
|
||||
describe("selection rewrite popover", () => {
|
||||
it("enables Generate only after the instruction has content", () => {
|
||||
const popover = new SelectionRewritePopover(popoverOptions({ state: rewriteState({ instruction: "" }) }));
|
||||
|
|
|
|||
Loading…
Reference in a new issue