mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Rename selection rewrite state
This commit is contained in:
parent
c1af92c6e9
commit
4abd131a02
6 changed files with 56 additions and 54 deletions
|
|
@ -19,7 +19,7 @@ The source tree is organized by responsibility rather than by the original singl
|
|||
- `src/app-server/` owns app-server transport, connection lifecycle, compatibility helpers, and RPC facades.
|
||||
- `src/features/chat/` owns the main Codex chat surface: Obsidian `ItemView` classes, panel orchestration, request/app-server controllers, composer behavior, display models, chat-only UI renderers, approvals, user input, thread actions, and panel-specific state.
|
||||
- `src/features/threads-view/` owns the dedicated Obsidian thread list view, including app-server thread list rendering and live open-panel snapshot aggregation.
|
||||
- `src/features/selection-rewrite/` owns the Markdown editor selection rewrite command, popover, prompt/output handling, and short-lived rewrite session runner.
|
||||
- `src/features/selection-rewrite/` owns the Markdown editor selection rewrite command, popover, prompt/output handling, and ephemeral rewrite thread runner.
|
||||
- `src/runtime/` owns Codex runtime configuration projection, model metadata, collaboration mode, and compact labels used by views.
|
||||
- `src/shared/` contains feature-neutral helpers, including reusable DOM pieces and unified diff display helpers shared by chat and selection rewrite.
|
||||
- `src/settings/` contains Obsidian settings models, settings-tab rendering, and app-server-backed dynamic settings data.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { MarkdownView, Notice, type Editor, type Plugin } from "obsidian";
|
||||
|
||||
import type { SelectionRewriteRuntimeSettings, SelectionRewriteSession } from "./model";
|
||||
import type { SelectionRewriteRuntimeSettings, SelectionRewriteState } from "./model";
|
||||
import { SelectionRewritePopover } from "./popover";
|
||||
import type { SendShortcut } from "../../shared/ui/keyboard";
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ export function registerSelectionRewriteCommand(plugin: SelectionRewriteCommandH
|
|||
return;
|
||||
}
|
||||
|
||||
const session: SelectionRewriteSession = {
|
||||
const rewriteState: SelectionRewriteState = {
|
||||
filePath: view.file.path,
|
||||
targetRange: {
|
||||
from: clonePosition(editor.getCursor("from")),
|
||||
|
|
@ -57,7 +57,7 @@ export function registerSelectionRewriteCommand(plugin: SelectionRewriteCommandH
|
|||
onClose: () => activePopovers.delete(popover),
|
||||
runtimeSettings: plugin.settings,
|
||||
sendShortcut: plugin.settings.sendShortcut,
|
||||
session,
|
||||
state: rewriteState,
|
||||
});
|
||||
popover.open();
|
||||
activePopovers.add(popover);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { ReasoningEffort } from "../../generated/app-server/ReasoningEffort
|
|||
|
||||
export type SelectionRewriteStatus = "editing-prompt" | "generating" | "preview" | "applied" | "cancelled" | "failed";
|
||||
|
||||
export interface SelectionRewriteSession {
|
||||
export interface SelectionRewriteState {
|
||||
filePath: string;
|
||||
targetRange: {
|
||||
from: EditorPosition;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ 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 SelectionRewriteSession } from "./model";
|
||||
import { canApplySelectionRewrite, type SelectionRewriteRuntimeSettings, type SelectionRewriteState } from "./model";
|
||||
import { SelectionRewriteOutputError } from "./output";
|
||||
import { positionSelectionRewritePopover } from "./position";
|
||||
import { buildSelectionRewritePrompt } from "./prompt";
|
||||
|
|
@ -24,7 +24,7 @@ export interface SelectionRewritePopoverOptions {
|
|||
onClose?: () => void;
|
||||
runtimeSettings: SelectionRewriteRuntimeSettings;
|
||||
sendShortcut: SendShortcut;
|
||||
session: SelectionRewriteSession;
|
||||
state: SelectionRewriteState;
|
||||
}
|
||||
|
||||
type Cleanup = () => void;
|
||||
|
|
@ -44,7 +44,7 @@ export class SelectionRewritePopover {
|
|||
private statusActive = false;
|
||||
|
||||
constructor(private readonly options: SelectionRewritePopoverOptions) {
|
||||
this.instructionDraft = options.session.instruction;
|
||||
this.instructionDraft = options.state.instruction;
|
||||
}
|
||||
|
||||
open(): void {
|
||||
|
|
@ -80,8 +80,8 @@ export class SelectionRewritePopover {
|
|||
|
||||
close(): void {
|
||||
const hadElements = this.elements !== null;
|
||||
if (!hadElements && this.options.session.status !== "generating") return;
|
||||
if (this.options.session.status === "generating") {
|
||||
if (!hadElements && this.options.state.status !== "generating") return;
|
||||
if (this.options.state.status === "generating") {
|
||||
this.abortController?.abort();
|
||||
}
|
||||
for (const cleanup of this.cleanups.splice(0)) cleanup();
|
||||
|
|
@ -94,7 +94,7 @@ export class SelectionRewritePopover {
|
|||
}
|
||||
|
||||
private async generate(): Promise<void> {
|
||||
if (this.options.session.status === "generating") return;
|
||||
if (this.options.state.status === "generating") return;
|
||||
const instruction = this.instructionDraft.trim();
|
||||
if (!instruction) {
|
||||
new Notice("Enter a rewrite instruction first.");
|
||||
|
|
@ -112,7 +112,7 @@ export class SelectionRewritePopover {
|
|||
const output = await runSelectionRewrite({
|
||||
codexPath: this.options.codexPath,
|
||||
cwd: this.options.cwd,
|
||||
prompt: buildSelectionRewritePrompt(this.options.session),
|
||||
prompt: buildSelectionRewritePrompt(this.options.state),
|
||||
runtimeSettings: this.options.runtimeSettings,
|
||||
onActivity: (activity) => {
|
||||
this.updateActivity(activity);
|
||||
|
|
@ -126,26 +126,26 @@ export class SelectionRewritePopover {
|
|||
this.showSelectionRewritePreview(output.replacementText);
|
||||
} catch (error) {
|
||||
if (abortController.signal.aborted) {
|
||||
this.options.session.status = "cancelled";
|
||||
this.options.state.status = "cancelled";
|
||||
return;
|
||||
}
|
||||
this.showGenerationFailure(error);
|
||||
} finally {
|
||||
if (this.abortController === abortController) this.abortController = null;
|
||||
this.syncControls();
|
||||
if (this.options.session.status === "preview") this.focusApplyButton();
|
||||
if (this.options.state.status === "preview") this.focusApplyButton();
|
||||
this.position();
|
||||
}
|
||||
}
|
||||
|
||||
private cancel(): void {
|
||||
this.options.session.status = "cancelled";
|
||||
this.options.state.status = "cancelled";
|
||||
this.abortController?.abort();
|
||||
this.close();
|
||||
}
|
||||
|
||||
private updatePreview(text: string): void {
|
||||
this.options.session.streamText = text;
|
||||
this.options.state.streamText = text;
|
||||
this.setStatus("Writing replacement", { active: true });
|
||||
this.position();
|
||||
}
|
||||
|
|
@ -164,42 +164,42 @@ export class SelectionRewritePopover {
|
|||
}
|
||||
|
||||
private startGeneration(instruction: string): void {
|
||||
this.options.session.instruction = instruction;
|
||||
this.options.session.status = "generating";
|
||||
this.options.session.streamText = "";
|
||||
this.options.session.replacementText = null;
|
||||
this.options.session.debugText = null;
|
||||
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.renderView();
|
||||
}
|
||||
|
||||
private showSelectionRewritePreview(replacementText: string): void {
|
||||
this.options.session.replacementText = replacementText;
|
||||
this.options.session.status = "preview";
|
||||
this.options.session.streamText = "";
|
||||
this.options.state.replacementText = replacementText;
|
||||
this.options.state.status = "preview";
|
||||
this.options.state.streamText = "";
|
||||
this.setStatus("");
|
||||
}
|
||||
|
||||
private showGenerationFailure(error: unknown): void {
|
||||
this.options.session.status = "failed";
|
||||
this.options.session.debugText = error instanceof SelectionRewriteOutputError ? error.rawText : null;
|
||||
this.options.session.streamText = "";
|
||||
this.options.state.status = "failed";
|
||||
this.options.state.debugText = error instanceof SelectionRewriteOutputError ? error.rawText : null;
|
||||
this.options.state.streamText = "";
|
||||
this.setStatus(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
private apply(): void {
|
||||
const replacement = this.options.session.replacementText;
|
||||
const replacement = this.options.state.replacementText;
|
||||
if (replacement === null) return;
|
||||
|
||||
const { editor, session } = this.options;
|
||||
const currentText = editor.getRange(session.targetRange.from, session.targetRange.to);
|
||||
if (!canApplySelectionRewrite(currentText, session.originalText)) {
|
||||
const { editor, state } = this.options;
|
||||
const currentText = editor.getRange(state.targetRange.from, state.targetRange.to);
|
||||
if (!canApplySelectionRewrite(currentText, state.originalText)) {
|
||||
new Notice("Selection changed. Generate the rewrite again before applying.");
|
||||
this.setStatus("Selection changed. Generate the rewrite again before applying.");
|
||||
return;
|
||||
}
|
||||
|
||||
editor.replaceRange(replacement, session.targetRange.from, session.targetRange.to, "codex-panel-rewrite");
|
||||
session.status = "applied";
|
||||
editor.replaceRange(replacement, state.targetRange.from, state.targetRange.to, "codex-panel-rewrite");
|
||||
state.status = "applied";
|
||||
this.close();
|
||||
}
|
||||
|
||||
|
|
@ -233,17 +233,17 @@ export class SelectionRewritePopover {
|
|||
|
||||
private renderView(elements: SelectionRewriteElements | null = this.elements): void {
|
||||
if (!elements) return;
|
||||
const session = this.options.session;
|
||||
const replacement = session.replacementText;
|
||||
const state = this.options.state;
|
||||
const replacement = state.replacementText;
|
||||
renderReactRoot(
|
||||
elements.root,
|
||||
<SelectionRewritePopoverView
|
||||
applyButtonRef={(element) => {
|
||||
elements.applyButton = element;
|
||||
}}
|
||||
debugText={session.debugText}
|
||||
diff={replacement === null ? null : buildSelectionUnifiedDiff(session.filePath, session.originalText, replacement)}
|
||||
generating={session.status === "generating"}
|
||||
debugText={state.debugText}
|
||||
diff={replacement === null ? null : buildSelectionUnifiedDiff(state.filePath, state.originalText, replacement)}
|
||||
generating={state.status === "generating"}
|
||||
hasInstruction={Boolean(this.instructionDraft.trim())}
|
||||
hasReplacement={replacement !== null}
|
||||
instruction={this.instructionDraft}
|
||||
|
|
@ -264,7 +264,7 @@ export class SelectionRewritePopover {
|
|||
this.position();
|
||||
}}
|
||||
onInstructionKeyDown={(event) => {
|
||||
const hasReplacement = this.options.session.replacementText !== null;
|
||||
const hasReplacement = this.options.state.replacementText !== null;
|
||||
if (
|
||||
!(hasReplacement
|
||||
? isSelectionRewriteActionKey(event.nativeEvent)
|
||||
|
|
@ -283,7 +283,7 @@ export class SelectionRewritePopover {
|
|||
}}
|
||||
statusActive={this.statusActive}
|
||||
statusText={this.statusText}
|
||||
streamPreview={session.streamText.trim()}
|
||||
streamPreview={state.streamText.trim()}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { SelectionRewriteSession } from "./model";
|
||||
import type { SelectionRewriteState } from "./model";
|
||||
|
||||
const MAX_NOTE_CONTEXT_CHARS = 20_000;
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ export const SELECTION_REWRITE_DEVELOPER_INSTRUCTIONS = [
|
|||
"Preserve Obsidian-specific syntax such as wikilinks, block ids, callouts, frontmatter-like text, and Dataview blocks unless the user's instruction explicitly asks to change them.",
|
||||
].join("\n");
|
||||
|
||||
export function buildSelectionRewritePrompt(session: SelectionRewriteSession): string {
|
||||
export function buildSelectionRewritePrompt(state: SelectionRewriteState): string {
|
||||
return [
|
||||
"Rewrite the selected text according to the user's instruction.",
|
||||
"",
|
||||
|
|
@ -23,18 +23,18 @@ export function buildSelectionRewritePrompt(session: SelectionRewriteSession): s
|
|||
"- Preserve the language and style of the surrounding note unless instructed otherwise.",
|
||||
"",
|
||||
"Target:",
|
||||
`- File: ${session.filePath}`,
|
||||
`- Selection: ${positionLabel(session.targetRange.from)} to ${positionLabel(session.targetRange.to)}`,
|
||||
`- File: ${state.filePath}`,
|
||||
`- Selection: ${positionLabel(state.targetRange.from)} to ${positionLabel(state.targetRange.to)}`,
|
||||
"- Context mode: Selection + note context",
|
||||
"",
|
||||
"User instruction:",
|
||||
session.instruction,
|
||||
state.instruction,
|
||||
"",
|
||||
"Selected text:",
|
||||
fenced(session.originalText),
|
||||
fenced(state.originalText),
|
||||
"",
|
||||
"Current note context:",
|
||||
fenced(truncateNoteContext(session.noteText)),
|
||||
fenced(truncateNoteContext(state.noteText)),
|
||||
"",
|
||||
"Reminder: use the note context only to make the selected-text replacement coherent.",
|
||||
].join("\n");
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
isSelectionRewriteGenerateKey,
|
||||
type SelectionRewriteGenerateKeyEvent,
|
||||
} from "../../../src/features/selection-rewrite/keys";
|
||||
import { canApplySelectionRewrite, type SelectionRewriteSession } from "../../../src/features/selection-rewrite/model";
|
||||
import { canApplySelectionRewrite, type SelectionRewriteState } from "../../../src/features/selection-rewrite/model";
|
||||
import {
|
||||
parseSelectionRewriteOutput,
|
||||
selectionRewriteOutputFromTurn,
|
||||
|
|
@ -68,7 +68,7 @@ describe("selection selection rewrite output", () => {
|
|||
|
||||
describe("selection rewrite prompt", () => {
|
||||
it("always includes note context with the selected text", () => {
|
||||
const prompt = buildSelectionRewritePrompt(session());
|
||||
const prompt = buildSelectionRewritePrompt(rewriteState());
|
||||
|
||||
expect(prompt).toContain("Context mode: Selection + note context");
|
||||
expect(prompt).toContain("Current note context:");
|
||||
|
|
@ -77,7 +77,7 @@ describe("selection rewrite prompt", () => {
|
|||
|
||||
it("uses fences that cannot be closed by note code blocks", () => {
|
||||
const prompt = buildSelectionRewritePrompt(
|
||||
session({
|
||||
rewriteState({
|
||||
originalText: "```ts\nconst value = 1;\n```",
|
||||
noteText: "````markdown\n```ts\nconst value = 1;\n```\n````",
|
||||
}),
|
||||
|
|
@ -137,7 +137,7 @@ describe("selection rewrite apply guard", () => {
|
|||
|
||||
describe("selection rewrite popover", () => {
|
||||
it("enables Generate only after the instruction has content", () => {
|
||||
const popover = new SelectionRewritePopover(popoverOptions({ session: session({ instruction: "" }) }));
|
||||
const popover = new SelectionRewritePopover(popoverOptions({ state: rewriteState({ instruction: "" }) }));
|
||||
|
||||
popover.open();
|
||||
|
||||
|
|
@ -162,7 +162,9 @@ describe("selection rewrite popover", () => {
|
|||
options.onPreview?.("Rewritten sentence.");
|
||||
return { replacementText: "Rewritten sentence." };
|
||||
});
|
||||
const popover = new SelectionRewritePopover(popoverOptions({ editor: editor.editor, onClose, session: session({ instruction: "" }) }));
|
||||
const popover = new SelectionRewritePopover(
|
||||
popoverOptions({ editor: editor.editor, onClose, state: rewriteState({ instruction: "" }) }),
|
||||
);
|
||||
|
||||
popover.open();
|
||||
const instruction = expectPresent(document.querySelector<HTMLTextAreaElement>(".codex-panel-selection-rewrite__instruction"));
|
||||
|
|
@ -257,7 +259,7 @@ describe("selection rewrite keys", () => {
|
|||
});
|
||||
});
|
||||
|
||||
function session(overrides: Partial<SelectionRewriteSession> = {}): SelectionRewriteSession {
|
||||
function rewriteState(overrides: Partial<SelectionRewriteState> = {}): SelectionRewriteState {
|
||||
return {
|
||||
filePath: "Note.md",
|
||||
targetRange: {
|
||||
|
|
@ -284,7 +286,7 @@ function popoverOptions(
|
|||
editor: editorFixture().editor,
|
||||
runtimeSettings: { rewriteSelectionModel: null, rewriteSelectionEffort: null },
|
||||
sendShortcut: "enter",
|
||||
session: session(),
|
||||
state: rewriteState(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue