mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Support keyboard apply and regenerate in rewrite popover
This commit is contained in:
parent
60181f3239
commit
a1f550a5d7
5 changed files with 83 additions and 6 deletions
|
|
@ -13,6 +13,13 @@ export interface RewriteSelectionCommandHost extends Plugin {
|
|||
}
|
||||
|
||||
export function registerRewriteSelectionCommand(plugin: RewriteSelectionCommandHost): void {
|
||||
const activePopovers = new Set<RewriteSelectionPopover>();
|
||||
|
||||
plugin.register(() => {
|
||||
for (const popover of activePopovers) popover.close();
|
||||
activePopovers.clear();
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
id: "rewrite-selection",
|
||||
name: "Rewrite selection",
|
||||
|
|
@ -43,14 +50,17 @@ export function registerRewriteSelectionCommand(plugin: RewriteSelectionCommandH
|
|||
debugText: null,
|
||||
};
|
||||
|
||||
new RewriteSelectionPopover({
|
||||
const popover = new RewriteSelectionPopover({
|
||||
codexPath: plugin.settings.codexPath,
|
||||
cwd: plugin.vaultPath,
|
||||
editor,
|
||||
onClose: () => activePopovers.delete(popover),
|
||||
runtimeSettings: plugin.settings,
|
||||
sendShortcut: plugin.settings.sendShortcut,
|
||||
session,
|
||||
}).open();
|
||||
});
|
||||
popover.open();
|
||||
activePopovers.add(popover);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,3 +6,8 @@ export type RewriteGenerateKeyEvent = ComposerSendKeyEvent;
|
|||
export function isRewriteGenerateKey(event: RewriteGenerateKeyEvent, shortcut: SendShortcut): boolean {
|
||||
return isComposerSendKey(event, shortcut);
|
||||
}
|
||||
|
||||
export function isRewriteActionKey(event: RewriteGenerateKeyEvent): boolean {
|
||||
if (event.isComposing || event.key !== "Enter" || event.shiftKey || event.altKey) return false;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { createIconButton } from "../ui/components";
|
|||
import { syncTextareaHeight } from "../ui/textarea-autogrow";
|
||||
import { diffLineClass, displayDiffLineText, displayDiffLines } from "../ui/turn-diff";
|
||||
import { buildSelectionUnifiedDiff } from "./diff";
|
||||
import { isRewriteGenerateKey } from "./keys";
|
||||
import { isRewriteActionKey, isRewriteGenerateKey } from "./keys";
|
||||
import { canApplyRewrite, type RewriteRuntimeSettings, type RewriteSession } from "./model";
|
||||
import { RewriteOutputError } from "./output";
|
||||
import { positionRewritePopover } from "./position";
|
||||
|
|
@ -18,6 +18,7 @@ export interface RewriteSelectionPopoverOptions {
|
|||
codexPath: string;
|
||||
cwd: string;
|
||||
editor: Editor;
|
||||
onClose?: () => void;
|
||||
runtimeSettings: RewriteRuntimeSettings;
|
||||
sendShortcut: SendShortcut;
|
||||
session: RewriteSession;
|
||||
|
|
@ -32,6 +33,7 @@ interface RewritePopoverElements {
|
|||
applyButton: HTMLButtonElement;
|
||||
resultRow: HTMLElement;
|
||||
status: HTMLElement;
|
||||
streamPreview: HTMLElement;
|
||||
diff: HTMLElement;
|
||||
debug: HTMLDetailsElement | null;
|
||||
}
|
||||
|
|
@ -66,12 +68,15 @@ export class RewriteSelectionPopover {
|
|||
}
|
||||
|
||||
close(): void {
|
||||
const hadElements = this.elements !== null;
|
||||
if (!hadElements && this.options.session.status !== "generating") return;
|
||||
if (this.options.session.status === "generating") {
|
||||
this.abortController?.abort();
|
||||
}
|
||||
for (const cleanup of this.cleanups.splice(0)) cleanup();
|
||||
this.elements?.root.remove();
|
||||
this.elements = null;
|
||||
this.options.onClose?.();
|
||||
}
|
||||
|
||||
private async generate(): Promise<void> {
|
||||
|
|
@ -122,6 +127,7 @@ export class RewriteSelectionPopover {
|
|||
|
||||
private updatePreview(text: string): void {
|
||||
this.options.session.streamText = text;
|
||||
this.renderStreamPreview();
|
||||
this.setStatus("Writing replacement", { active: true });
|
||||
this.position();
|
||||
}
|
||||
|
|
@ -134,6 +140,7 @@ export class RewriteSelectionPopover {
|
|||
const root = activeDocument.body.createDiv({ cls: "codex-panel-rewrite-popover" });
|
||||
root.setAttr("role", "dialog");
|
||||
root.setAttr("aria-label", "Rewrite selection");
|
||||
root.onkeydown = (event) => this.handlePopoverKeydown(event);
|
||||
|
||||
const instruction = root.createEl("textarea", {
|
||||
cls: "codex-panel-rewrite-popover__instruction",
|
||||
|
|
@ -146,8 +153,10 @@ export class RewriteSelectionPopover {
|
|||
this.position();
|
||||
};
|
||||
instruction.onkeydown = (event) => {
|
||||
if (!isRewriteGenerateKey(event, this.options.sendShortcut)) return;
|
||||
const hasReplacement = this.options.session.replacementText !== null;
|
||||
if (!(hasReplacement ? isRewriteActionKey(event) : isRewriteGenerateKey(event, this.options.sendShortcut))) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void this.generate();
|
||||
};
|
||||
|
||||
|
|
@ -160,12 +169,13 @@ export class RewriteSelectionPopover {
|
|||
cancelButton.onclick = () => this.cancel();
|
||||
|
||||
const status = root.createDiv({ cls: "codex-panel-rewrite-popover__status" });
|
||||
const streamPreview = root.createEl("pre", { cls: "codex-panel-rewrite-popover__stream-preview is-hidden" });
|
||||
const resultRow = root.createDiv({ cls: "codex-panel-rewrite-popover__result-row" });
|
||||
const diff = resultRow.createDiv({ cls: "codex-panel-rewrite-popover__diff" });
|
||||
const applyButton = createIconButton(resultRow, "check", "Apply rewrite", "codex-panel-rewrite-popover__icon-button mod-cta");
|
||||
applyButton.onclick = () => this.apply();
|
||||
|
||||
return { root, instruction, generateButton, applyButton, resultRow, status, diff, debug: null };
|
||||
return { root, instruction, generateButton, applyButton, resultRow, status, streamPreview, diff, debug: null };
|
||||
}
|
||||
|
||||
private startGeneration(instruction: string): void {
|
||||
|
|
@ -175,12 +185,15 @@ export class RewriteSelectionPopover {
|
|||
this.options.session.replacementText = null;
|
||||
this.options.session.debugText = null;
|
||||
this.elements?.diff.empty();
|
||||
this.renderStreamPreview();
|
||||
this.renderDebug(null);
|
||||
}
|
||||
|
||||
private showRewritePreview(replacementText: string): void {
|
||||
this.options.session.replacementText = replacementText;
|
||||
this.options.session.status = "preview";
|
||||
this.options.session.streamText = "";
|
||||
this.renderStreamPreview();
|
||||
this.renderDiff();
|
||||
this.setStatus("");
|
||||
}
|
||||
|
|
@ -188,10 +201,20 @@ export class RewriteSelectionPopover {
|
|||
private showGenerationFailure(error: unknown): void {
|
||||
this.options.session.status = "failed";
|
||||
this.options.session.debugText = error instanceof RewriteOutputError ? error.rawText : null;
|
||||
this.options.session.streamText = "";
|
||||
this.renderStreamPreview();
|
||||
this.renderDebug(this.options.session.debugText);
|
||||
this.setStatus(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
private renderStreamPreview(): void {
|
||||
if (!this.elements) return;
|
||||
const preview = this.options.session.streamText.trim();
|
||||
this.elements.streamPreview.empty();
|
||||
this.elements.streamPreview.classList.toggle("is-hidden", !preview);
|
||||
if (preview) this.elements.streamPreview.createSpan({ text: preview });
|
||||
}
|
||||
|
||||
private renderDiff(): void {
|
||||
const replacement = this.options.session.replacementText;
|
||||
if (replacement === null || !this.elements) return;
|
||||
|
|
@ -231,6 +254,15 @@ export class RewriteSelectionPopover {
|
|||
this.close();
|
||||
}
|
||||
|
||||
private handlePopoverKeydown(event: KeyboardEvent): void {
|
||||
if (!this.elements || event.target === this.elements.instruction) return;
|
||||
if (isInteractiveEventTarget(event.target)) return;
|
||||
if (this.options.session.replacementText === null || this.options.session.status === "generating") return;
|
||||
if (!isRewriteActionKey(event)) return;
|
||||
event.preventDefault();
|
||||
this.apply();
|
||||
}
|
||||
|
||||
private setStatus(text: string, options: { active?: boolean } = {}): void {
|
||||
if (!this.elements) return;
|
||||
const { status } = this.elements;
|
||||
|
|
@ -304,3 +336,8 @@ function renderRewriteDiff(parent: HTMLElement, diff: string): void {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isInteractiveEventTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
return Boolean(target.closest("button, input, textarea, select, a, [role='button']"));
|
||||
}
|
||||
|
|
|
|||
17
styles.css
17
styles.css
|
|
@ -1509,6 +1509,23 @@
|
|||
animation-delay: 0.36s;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__stream-preview {
|
||||
box-sizing: border-box;
|
||||
max-height: min(180px, 28vh);
|
||||
margin: 0;
|
||||
padding: var(--size-2-3, 6px) var(--size-4-2, 8px);
|
||||
overflow: auto;
|
||||
border-left: 2px solid var(--background-modifier-border);
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-monospace);
|
||||
font-size: var(--font-smaller);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__stream-preview.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codex-panel-rewrite-popover__diff-body {
|
||||
box-sizing: border-box;
|
||||
max-height: min(300px, 38vh);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildSelectionUnifiedDiff } from "../../src/editor-rewrite/diff";
|
||||
import { isRewriteGenerateKey, type RewriteGenerateKeyEvent } from "../../src/editor-rewrite/keys";
|
||||
import { isRewriteActionKey, isRewriteGenerateKey, type RewriteGenerateKeyEvent } from "../../src/editor-rewrite/keys";
|
||||
import { canApplyRewrite, type RewriteSession } from "../../src/editor-rewrite/model";
|
||||
import { parseRewriteOutput, rewriteOutputFromTurn, rewriteOutputParseResultFromTurn } from "../../src/editor-rewrite/output";
|
||||
import { buildRewritePrompt } from "../../src/editor-rewrite/prompt";
|
||||
|
|
@ -132,6 +132,14 @@ describe("editor rewrite keys", () => {
|
|||
expect(isRewriteGenerateKey({ ...baseEvent, ctrlKey: true }, "mod-enter")).toBe(true);
|
||||
expect(isRewriteGenerateKey(baseEvent, "mod-enter")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats plain Enter and Cmd/Ctrl+Enter as preview action keys", () => {
|
||||
expect(isRewriteActionKey(baseEvent)).toBe(true);
|
||||
expect(isRewriteActionKey({ ...baseEvent, metaKey: true })).toBe(true);
|
||||
expect(isRewriteActionKey({ ...baseEvent, ctrlKey: true })).toBe(true);
|
||||
expect(isRewriteActionKey({ ...baseEvent, shiftKey: true })).toBe(false);
|
||||
expect(isRewriteActionKey({ ...baseEvent, isComposing: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function session(overrides: Partial<RewriteSession> = {}): RewriteSession {
|
||||
|
|
|
|||
Loading…
Reference in a new issue