Refine selection rewrite frames

This commit is contained in:
murashit 2026-06-05 14:15:39 +09:00
parent f7c91d880a
commit 9071fb238c
4 changed files with 134 additions and 42 deletions

View file

@ -91,6 +91,9 @@ export class SelectionRewritePopover {
this.cancel();
}
});
this.addDomListener(activeDocument, "pointerdown", (event) => {
if (!this.elements?.root.contains(event.target as Node | null)) this.cancel();
});
this.setStatus("");
this.syncInstructionHeight();
@ -252,7 +255,7 @@ export class SelectionRewritePopover {
private syncInstructionHeight(): void {
const instruction = this.elements?.instruction ?? null;
syncTextareaHeight(instruction, {
minHeightFallback: 56,
minHeightFallback: 26,
maxHeightFallback: instruction ? Math.min(180, instruction.win.innerHeight * 0.3) : 180,
});
}
@ -284,9 +287,6 @@ export class SelectionRewritePopover {
onApply={() => {
this.apply();
}}
onCancel={() => {
this.cancel();
}}
onGenerate={() => void this.generate()}
onInstructionInput={(value) => {
this.instructionDraft = value;
@ -455,7 +455,6 @@ interface SelectionRewritePopoverViewProps {
instructionRef: (element: HTMLTextAreaElement | null) => void;
onApply: () => void;
onApplyKeyDown: (event: TargetedKeyboardEvent<HTMLButtonElement>) => void;
onCancel: () => void;
onGenerate: () => void;
onInstructionInput: (value: string) => void;
onInstructionKeyDown: (event: TargetedKeyboardEvent<HTMLTextAreaElement>) => void;
@ -474,7 +473,6 @@ function SelectionRewritePopoverView({
instructionRef,
onApply,
onApplyKeyDown,
onCancel,
onGenerate,
onInstructionInput,
onInstructionKeyDown,
@ -484,18 +482,18 @@ function SelectionRewritePopoverView({
return (
<>
<div className="codex-panel-selection-rewrite__prompt-row">
<textarea
ref={instructionRef}
className="codex-panel-ui__text-input codex-panel-selection-rewrite__instruction"
disabled={generating}
onInput={(event) => {
onInstructionInput(event.currentTarget.value);
}}
onKeyDown={onInstructionKeyDown}
placeholder="How should Codex rewrite this selection?"
value={instruction}
/>
<div className="codex-panel-ui__action-stack codex-panel-selection-rewrite__controls">
<div className="codex-panel-selection-rewrite__composer-frame">
<textarea
ref={instructionRef}
className="codex-panel-ui__text-input codex-panel-selection-rewrite__instruction"
disabled={generating}
onInput={(event) => {
onInstructionInput(event.currentTarget.value);
}}
onKeyDown={onInstructionKeyDown}
placeholder="How should Codex rewrite this selection?"
value={instruction}
/>
<IconButton
icon="sparkles"
label={hasReplacement ? "Regenerate" : "Generate"}
@ -503,29 +501,25 @@ function SelectionRewritePopoverView({
disabled={generating || !hasInstruction}
onClick={onGenerate}
/>
<IconButton
icon="x"
label="Cancel"
className="clickable-icon codex-panel-ui__icon-button codex-panel-selection-rewrite__icon-button"
onClick={onCancel}
/>
</div>
</div>
<SelectionRewriteStatus status={status} />
<pre className={`codex-panel-selection-rewrite__stream-preview${streamPreview ? "" : " is-hidden"}`}>{streamPreview}</pre>
<div className={`codex-panel-selection-rewrite__result-row${hasReplacement ? "" : " is-hidden"}`}>
<div className={`codex-panel-selection-rewrite__result${hasReplacement ? "" : " is-hidden"}`}>
<div className="codex-panel-selection-rewrite__diff">{diff ? <SelectionRewriteDiff diff={diff} /> : null}</div>
<IconButton
buttonRef={applyButtonRef}
icon="check"
label="Apply"
className={`clickable-icon codex-panel-ui__icon-button codex-panel-selection-rewrite__icon-button${
hasReplacement ? "" : " is-hidden"
}`}
disabled={generating || !hasReplacement}
onClick={onApply}
onKeyDown={onApplyKeyDown}
/>
<div className="codex-panel-selection-rewrite__result-actions">
<IconButton
buttonRef={applyButtonRef}
icon="check"
label="Apply"
className={`clickable-icon codex-panel-ui__icon-button codex-panel-selection-rewrite__icon-button${
hasReplacement ? "" : " is-hidden"
}`}
disabled={generating || !hasReplacement}
onClick={onApply}
onKeyDown={onApplyKeyDown}
/>
</div>
</div>
{debugText ? (
<details className="codex-panel-selection-rewrite__debug">

View file

@ -20,33 +20,76 @@
}
.codex-panel-selection-rewrite__prompt-row {
min-width: 0;
}
.codex-panel-selection-rewrite__composer-frame {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--codex-panel-section-gap);
align-items: stretch;
gap: var(--codex-panel-control-gap);
min-width: 0;
align-items: end;
border: var(--input-border-width, var(--border-width, 1px)) solid var(--background-modifier-border);
border-radius: var(--input-radius, var(--radius-s));
background: var(--background-modifier-form-field);
}
.codex-panel-selection-rewrite__composer-frame:focus-within {
border-color: var(--background-modifier-border-focus);
box-shadow: 0 0 0 var(--input-border-width, var(--border-width, 1px)) var(--background-modifier-border-focus);
}
.codex-panel-selection-rewrite__instruction {
min-height: var(--codex-panel-composer-min-height);
min-height: var(--codex-panel-size-user-input-text-min-height);
max-height: min(var(--codex-panel-size-selection-rewrite-preview-max-height), 30vh);
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.codex-panel-selection-rewrite__result-row {
.codex-panel-selection-rewrite__instruction.codex-panel-selection-rewrite__instruction:hover,
.codex-panel-selection-rewrite__instruction.codex-panel-selection-rewrite__instruction:focus,
.codex-panel-selection-rewrite__instruction.codex-panel-selection-rewrite__instruction:focus-visible,
.codex-panel-selection-rewrite__instruction.codex-panel-selection-rewrite__instruction:active {
border: 0;
background: transparent;
box-shadow: none;
outline: none;
}
.codex-panel-selection-rewrite__result {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--codex-panel-section-gap);
gap: var(--codex-panel-control-gap);
min-width: 0;
align-items: start;
border: var(--codex-panel-border);
border-radius: var(--codex-panel-card-radius);
background: var(--background-modifier-form-field);
}
.codex-panel-selection-rewrite__result-row.is-hidden,
.codex-panel-selection-rewrite__result.is-hidden,
.codex-panel-selection-rewrite__diff:empty {
display: none;
}
.codex-panel-selection-rewrite__result-actions {
display: flex;
padding: 0;
justify-content: flex-end;
}
.codex-panel-selection-rewrite__icon-button.is-hidden {
display: none;
}
.codex-panel-selection-rewrite__icon-button {
min-width: calc(var(--codex-panel-control-icon-size) + var(--codex-panel-panel-gap) * 2);
margin: var(--codex-panel-panel-gap);
padding: var(--codex-panel-panel-gap);
}
.codex-panel-selection-rewrite__status {
padding-inline-start: calc(var(--size-4-1) / 2);
color: var(--codex-panel-text-muted);
@ -101,6 +144,8 @@
margin: 0;
padding: var(--codex-panel-item-gap) var(--codex-panel-section-gap);
overflow: auto;
border-radius: 0;
background: transparent;
white-space: pre-wrap;
}

View file

@ -298,6 +298,7 @@ describe("selection rewrite popover", () => {
openPopover(popover);
expect(document.querySelector(".codex-panel-selection-rewrite__instruction")).not.toBeNull();
expect(document.querySelector<HTMLButtonElement>('button[aria-label="Cancel"]')).toBeNull();
closePopover(popover);
@ -305,6 +306,27 @@ describe("selection rewrite popover", () => {
expect(onClose).toHaveBeenCalledOnce();
});
it("closes from outside pointerdown without closing internal controls", () => {
const onClose = vi.fn();
const popover = new SelectionRewritePopover(popoverOptions({ onClose }));
openPopover(popover);
const generate = expectPresent(document.querySelector<HTMLButtonElement>('button[aria-label="Generate"]'));
void act(() => {
generate.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
});
expect(document.querySelector(".codex-panel-selection-rewrite")).not.toBeNull();
expect(onClose).not.toHaveBeenCalled();
void act(() => {
document.body.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
});
expect(document.querySelector(".codex-panel-selection-rewrite")).toBeNull();
expect(onClose).toHaveBeenCalledOnce();
});
it("aborts the active generation when closed and ignores its late result", async () => {
const rewrite = deferred<{ replacementText: string }>();
let signal: AbortSignal | undefined;

View file

@ -256,6 +256,37 @@ describe("chat message CSS", () => {
});
describe("selection rewrite CSS", () => {
it("uses a composer-style instruction frame without the old action stack", () => {
const frame = /\.codex-panel-selection-rewrite__composer-frame \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const instruction = /\.codex-panel-selection-rewrite__instruction \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const iconButton = /\.codex-panel-selection-rewrite__icon-button \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
expect(frame).toContain("grid-template-columns: minmax(0, 1fr) auto");
expect(frame).toContain("background: var(--background-modifier-form-field)");
expect(frame).toContain("border: var(--input-border-width, var(--border-width, 1px)) solid var(--background-modifier-border)");
expect(instruction).toContain("min-height: var(--codex-panel-size-user-input-text-min-height)");
expect(instruction).toContain("background: transparent");
expect(instruction).toContain("border: 0");
expect(iconButton).toContain("min-width: calc(var(--codex-panel-control-icon-size) + var(--codex-panel-panel-gap) * 2)");
expect(iconButton).toContain("margin: var(--codex-panel-panel-gap)");
expect(iconButton).toContain("padding: var(--codex-panel-panel-gap)");
expect(styles).not.toContain(".codex-panel-selection-rewrite__controls");
});
it("keeps the apply action inside the selection rewrite result frame", () => {
const result = /\.codex-panel-selection-rewrite__result \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const actions = /\.codex-panel-selection-rewrite__result-actions \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const diffBody = /\.codex-panel-selection-rewrite__diff-body \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
expect(result).toContain("grid-template-columns: minmax(0, 1fr) auto");
expect(result).toContain("border: var(--codex-panel-border)");
expect(result).toContain("background: var(--background-modifier-form-field)");
expect(actions).toContain("padding: 0");
expect(diffBody).toContain("margin: 0");
expect(diffBody).toContain("border-radius: 0");
expect(diffBody).toContain("background: transparent");
});
it("aligns generation status text with the instruction input text inset", () => {
const status = /\.codex-panel-selection-rewrite__status \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";