From 4abd131a02d3b6d111fa47359e217cfd3d52a701 Mon Sep 17 00:00:00 2001 From: murashit Date: Thu, 28 May 2026 17:42:20 +0900 Subject: [PATCH] Rename selection rewrite state --- docs/development.md | 2 +- src/features/selection-rewrite/command.ts | 6 +- src/features/selection-rewrite/model.ts | 2 +- src/features/selection-rewrite/popover.tsx | 70 +++++++++---------- src/features/selection-rewrite/prompt.ts | 14 ++-- .../selection-rewrite.test.ts | 16 +++-- 6 files changed, 56 insertions(+), 54 deletions(-) diff --git a/docs/development.md b/docs/development.md index f7d78ba2..56bb6ad9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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. diff --git a/src/features/selection-rewrite/command.ts b/src/features/selection-rewrite/command.ts index 0edd96ed..ca677e07 100644 --- a/src/features/selection-rewrite/command.ts +++ b/src/features/selection-rewrite/command.ts @@ -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); diff --git a/src/features/selection-rewrite/model.ts b/src/features/selection-rewrite/model.ts index 1ef1aad9..241e60c3 100644 --- a/src/features/selection-rewrite/model.ts +++ b/src/features/selection-rewrite/model.ts @@ -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; diff --git a/src/features/selection-rewrite/popover.tsx b/src/features/selection-rewrite/popover.tsx index 61a3eb88..b0a4633c 100644 --- a/src/features/selection-rewrite/popover.tsx +++ b/src/features/selection-rewrite/popover.tsx @@ -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 { - 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, { 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()} />, ); } diff --git a/src/features/selection-rewrite/prompt.ts b/src/features/selection-rewrite/prompt.ts index d8a10456..69869021 100644 --- a/src/features/selection-rewrite/prompt.ts +++ b/src/features/selection-rewrite/prompt.ts @@ -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"); diff --git a/tests/features/selection-rewrite/selection-rewrite.test.ts b/tests/features/selection-rewrite/selection-rewrite.test.ts index 76958b2b..30f1c55c 100644 --- a/tests/features/selection-rewrite/selection-rewrite.test.ts +++ b/tests/features/selection-rewrite/selection-rewrite.test.ts @@ -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(".codex-panel-selection-rewrite__instruction")); @@ -257,7 +259,7 @@ describe("selection rewrite keys", () => { }); }); -function session(overrides: Partial = {}): SelectionRewriteSession { +function rewriteState(overrides: Partial = {}): 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, }; }