mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Add composer status interactions
This commit is contained in:
parent
d39403c8f5
commit
75ee797ed9
12 changed files with 571 additions and 40 deletions
|
|
@ -29,6 +29,9 @@ export interface ChatComposerControllerOptions {
|
|||
composerPlaceholder: () => string;
|
||||
composerMeta: () => ComposerMetaViewModel;
|
||||
currentModelForSuggestions: () => string | null;
|
||||
togglePlan: () => void;
|
||||
toggleAutoReview: () => void;
|
||||
toggleFast: () => void;
|
||||
renderIfDetached: () => void;
|
||||
onDraftChange: () => void;
|
||||
onComposerResize: () => void;
|
||||
|
|
@ -108,6 +111,15 @@ export class ChatComposerController {
|
|||
onSendOrInterrupt: () => {
|
||||
this.options.onSubmit();
|
||||
},
|
||||
onTogglePlan: () => {
|
||||
this.options.togglePlan();
|
||||
},
|
||||
onToggleAutoReview: () => {
|
||||
this.options.toggleAutoReview();
|
||||
},
|
||||
onToggleFast: () => {
|
||||
this.options.toggleFast();
|
||||
},
|
||||
onSuggestionHover: (index) => {
|
||||
this.selectSuggestion(index);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -253,6 +253,9 @@ export function createChatViewControllerAssembly(host: ChatViewControllerAssembl
|
|||
composerPlaceholder: host.composerPlaceholder,
|
||||
composerMeta: host.composerMetaViewModel,
|
||||
currentModelForSuggestions: () => currentModel(host.runtimeSnapshot()),
|
||||
togglePlan: () => void runtimeSettings.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => void runtimeSettings.toggleAutoReview(),
|
||||
toggleFast: () => void runtimeSettings.toggleFastMode(),
|
||||
renderIfDetached: host.effects.render.now,
|
||||
onDraftChange: host.effects.liveState.refresh,
|
||||
onComposerResize: () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { setIcon } from "obsidian";
|
||||
import type { ButtonHTMLAttributes, ComponentChild as UiNode, Ref } from "preact";
|
||||
import { useLayoutEffect, useRef } from "preact/hooks";
|
||||
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import type { ComposerSuggestion } from "../composer/suggestions";
|
||||
import type { ComposerMetaViewModel } from "../view-model";
|
||||
import { IconButton, ObsidianIcon } from "../../../shared/ui/components";
|
||||
import type { ToolbarChoice } from "../toolbar-model";
|
||||
import { IconButton } from "../../../shared/ui/components";
|
||||
import { renderUiRoot } from "../../../shared/ui/ui-root";
|
||||
import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow";
|
||||
|
||||
|
|
@ -17,6 +19,9 @@ export interface ComposerCallbacks {
|
|||
onUpdateSuggestions: () => void;
|
||||
onKeydown: (event: KeyboardEvent) => void;
|
||||
onSendOrInterrupt: () => void;
|
||||
onTogglePlan?: () => void;
|
||||
onToggleAutoReview?: () => void;
|
||||
onToggleFast?: () => void;
|
||||
onSuggestionHover: (index: number) => void;
|
||||
onSuggestionInsert: (suggestion: ComposerSuggestion) => void;
|
||||
}
|
||||
|
|
@ -36,11 +41,14 @@ const DEFAULT_COMPOSER_META: ComposerMetaViewModel = {
|
|||
],
|
||||
percent: "--%",
|
||||
},
|
||||
statusSummary: "Context unavailable, plan off, auto-review off, fast off, model default, reasoning effort default",
|
||||
model: "default",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
autoReviewActive: false,
|
||||
fastActive: false,
|
||||
modelChoices: [],
|
||||
effortChoices: [],
|
||||
};
|
||||
|
||||
export function renderComposerShell(
|
||||
|
|
@ -143,7 +151,7 @@ function ComposerShell({
|
|||
callbacks.onKeydown(event);
|
||||
}}
|
||||
/>
|
||||
<ComposerMeta meta={meta} sendMode={sendMode} onSendOrInterrupt={callbacks.onSendOrInterrupt} />
|
||||
<ComposerMeta meta={meta} sendMode={sendMode} callbacks={callbacks} />
|
||||
<ComposerSuggestions
|
||||
containerRef={suggestionsRef}
|
||||
selectedRef={selectedSuggestionRef}
|
||||
|
|
@ -159,13 +167,17 @@ function ComposerShell({
|
|||
function ComposerMeta({
|
||||
meta,
|
||||
sendMode,
|
||||
onSendOrInterrupt,
|
||||
callbacks,
|
||||
}: {
|
||||
meta: ComposerMetaViewModel;
|
||||
sendMode: ComposerSendMode;
|
||||
onSendOrInterrupt: () => void;
|
||||
callbacks: ComposerCallbacks;
|
||||
}): UiNode {
|
||||
const metaRef = useRef<HTMLDivElement | null>(null);
|
||||
const statusRef = useRef<HTMLSpanElement | null>(null);
|
||||
const modelTriggerRef = useRef<HTMLSpanElement | null>(null);
|
||||
const effortTriggerRef = useRef<HTMLSpanElement | null>(null);
|
||||
const [picker, setPicker] = useState<ComposerMetaPickerState | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const status = statusRef.current;
|
||||
if (!status) return;
|
||||
|
|
@ -190,40 +202,134 @@ function ComposerMeta({
|
|||
win.removeEventListener("resize", scheduleUpdate);
|
||||
};
|
||||
}, [meta]);
|
||||
useLayoutEffect(() => {
|
||||
if (!picker) return;
|
||||
const metaRoot = metaRef.current;
|
||||
const doc = metaRoot?.ownerDocument;
|
||||
if (!metaRoot || !doc) return;
|
||||
const closeOnOutsideMouse = (event: MouseEvent) => {
|
||||
if (event.target && metaRoot.contains(event.target as Node)) return;
|
||||
setPicker(null);
|
||||
};
|
||||
const closeOnEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") setPicker(null);
|
||||
};
|
||||
doc.addEventListener("mousedown", closeOnOutsideMouse);
|
||||
doc.addEventListener("keydown", closeOnEscape);
|
||||
return () => {
|
||||
doc.removeEventListener("mousedown", closeOnOutsideMouse);
|
||||
doc.removeEventListener("keydown", closeOnEscape);
|
||||
};
|
||||
}, [picker]);
|
||||
const openPicker = (kind: ComposerMetaPickerKind) => {
|
||||
const nextPicker = composerMetaPickerState(
|
||||
kind,
|
||||
kind === "model" ? modelTriggerRef.current : effortTriggerRef.current,
|
||||
metaRef.current,
|
||||
);
|
||||
setPicker((current) => (current?.kind === kind ? null : nextPicker));
|
||||
};
|
||||
const closePicker = () => {
|
||||
setPicker(null);
|
||||
};
|
||||
if (meta.fatal) {
|
||||
return (
|
||||
<div className="codex-panel__composer-meta codex-panel__composer-meta--fatal">
|
||||
<span className="codex-panel__composer-meta-fatal">{meta.fatal}</span>
|
||||
<ComposerSendButton sendMode={sendMode} onSendOrInterrupt={onSendOrInterrupt} />
|
||||
<ComposerSendButton sendMode={sendMode} onSendOrInterrupt={callbacks.onSendOrInterrupt} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="codex-panel__composer-meta">
|
||||
<span ref={statusRef} className="codex-panel__composer-meta-status" aria-hidden="true">
|
||||
<span className="codex-panel__composer-meta-modes">
|
||||
<ComposerMetaIcon icon="list-todo" active={meta.planActive} />
|
||||
<ComposerMetaIcon icon="shield" active={meta.autoReviewActive} />
|
||||
<ComposerMetaIcon icon="zap" active={meta.fastActive} />
|
||||
</span>
|
||||
<span className="codex-panel__composer-meta-separator">|</span>
|
||||
<ComposerContextMeter context={meta.context} />
|
||||
<span className="codex-panel__composer-meta-field codex-panel__composer-meta-field--model">
|
||||
<span className="codex-panel__composer-meta-separator">|</span>
|
||||
<span className="codex-panel__composer-meta-model">{meta.model}</span>
|
||||
</span>
|
||||
{meta.effort ? (
|
||||
<span className="codex-panel__composer-meta-field codex-panel__composer-meta-field--effort">
|
||||
<span className="codex-panel__composer-meta-separator">|</span>
|
||||
<span className="codex-panel__composer-meta-effort">{meta.effort}</span>
|
||||
<div ref={metaRef} className="codex-panel__composer-meta">
|
||||
<span ref={statusRef} className="codex-panel__composer-meta-status">
|
||||
<span className="codex-panel__composer-meta-summary">{meta.statusSummary}</span>
|
||||
<span className="codex-panel__composer-meta-status-visual" aria-hidden="true">
|
||||
<span className="codex-panel__composer-meta-modes">
|
||||
<ComposerMetaModeButton
|
||||
icon="list-todo"
|
||||
active={meta.planActive}
|
||||
onMouseDown={() => {
|
||||
callbacks.onTogglePlan?.();
|
||||
}}
|
||||
/>
|
||||
<ComposerMetaModeButton
|
||||
icon="shield"
|
||||
active={meta.autoReviewActive}
|
||||
onMouseDown={() => {
|
||||
callbacks.onToggleAutoReview?.();
|
||||
}}
|
||||
/>
|
||||
<ComposerMetaModeButton
|
||||
icon="zap"
|
||||
active={meta.fastActive}
|
||||
onMouseDown={() => {
|
||||
callbacks.onToggleFast?.();
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
<span className="codex-panel__composer-meta-separator">|</span>
|
||||
<ComposerContextMeter context={meta.context} />
|
||||
<span className="codex-panel__composer-meta-field codex-panel__composer-meta-field--model">
|
||||
<span className="codex-panel__composer-meta-separator">|</span>
|
||||
<ComposerMetaPickerButton
|
||||
triggerRef={modelTriggerRef}
|
||||
kind="model"
|
||||
value={meta.model}
|
||||
onMouseDown={() => {
|
||||
openPicker("model");
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{meta.effort ? (
|
||||
<span className="codex-panel__composer-meta-field codex-panel__composer-meta-field--effort">
|
||||
<span className="codex-panel__composer-meta-separator">|</span>
|
||||
<ComposerMetaPickerButton
|
||||
triggerRef={effortTriggerRef}
|
||||
kind="effort"
|
||||
value={meta.effort}
|
||||
onMouseDown={() => {
|
||||
openPicker("effort");
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</span>
|
||||
<ComposerSendButton sendMode={sendMode} onSendOrInterrupt={onSendOrInterrupt} />
|
||||
{picker ? (
|
||||
<ComposerMetaChoicePopover
|
||||
kind={picker.kind}
|
||||
choices={picker.kind === "model" ? (meta.modelChoices ?? []) : (meta.effortChoices ?? [])}
|
||||
left={picker.left}
|
||||
onClose={closePicker}
|
||||
/>
|
||||
) : null}
|
||||
<ComposerSendButton sendMode={sendMode} onSendOrInterrupt={callbacks.onSendOrInterrupt} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ComposerMetaPickerKind = "model" | "effort";
|
||||
|
||||
interface ComposerMetaPickerState {
|
||||
kind: ComposerMetaPickerKind;
|
||||
left: number;
|
||||
}
|
||||
|
||||
function composerMetaPickerState(
|
||||
kind: ComposerMetaPickerKind,
|
||||
trigger: HTMLElement | null,
|
||||
metaRoot: HTMLElement | null,
|
||||
): ComposerMetaPickerState {
|
||||
if (!trigger || !metaRoot) return { kind, left: 0 };
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const metaRect = metaRoot.getBoundingClientRect();
|
||||
return {
|
||||
kind,
|
||||
left: Math.max(0, triggerRect.left - metaRect.left),
|
||||
};
|
||||
}
|
||||
|
||||
const COMPOSER_META_EFFORT_HIDDEN_CLASS = "is-effort-hidden";
|
||||
const COMPOSER_META_MODEL_HIDDEN_CLASS = "is-model-hidden";
|
||||
|
||||
|
|
@ -261,8 +367,93 @@ function ComposerContextMeter({ context }: { context: ComposerMetaViewModel["con
|
|||
);
|
||||
}
|
||||
|
||||
function ComposerMetaIcon({ icon, active }: { icon: string; active: boolean }): UiNode {
|
||||
return <ObsidianIcon icon={icon} className={["codex-panel__composer-meta-icon", active ? "is-active" : ""].filter(Boolean).join(" ")} />;
|
||||
function ComposerMetaModeButton({ icon, active, onMouseDown }: { icon: string; active: boolean; onMouseDown: () => void }): UiNode {
|
||||
const iconRef = useRef<HTMLSpanElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const element = iconRef.current;
|
||||
if (!element) return;
|
||||
element.replaceChildren();
|
||||
setIcon(element, icon);
|
||||
}, [icon]);
|
||||
return (
|
||||
<span
|
||||
ref={iconRef}
|
||||
className={["codex-panel__composer-meta-trigger", "codex-panel__composer-meta-icon", active ? "is-active" : ""]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
onMouseDown();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ComposerMetaPickerButton({
|
||||
triggerRef,
|
||||
kind,
|
||||
value,
|
||||
onMouseDown,
|
||||
}: {
|
||||
triggerRef: Ref<HTMLSpanElement>;
|
||||
kind: ComposerMetaPickerKind;
|
||||
value: string;
|
||||
onMouseDown: () => void;
|
||||
}): UiNode {
|
||||
return (
|
||||
<span
|
||||
ref={triggerRef}
|
||||
className={`codex-panel__composer-meta-trigger codex-panel__composer-meta-value codex-panel__composer-meta-${kind}`}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
onMouseDown();
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ComposerMetaChoicePopover({
|
||||
kind,
|
||||
choices,
|
||||
left,
|
||||
onClose,
|
||||
}: {
|
||||
kind: ComposerMetaPickerKind;
|
||||
choices: ToolbarChoice[];
|
||||
left: number;
|
||||
onClose: () => void;
|
||||
}): UiNode {
|
||||
const style = {
|
||||
"--codex-panel-composer-meta-popover-left": `${String(Math.round(left))}px`,
|
||||
};
|
||||
return (
|
||||
<div className={`codex-panel__composer-meta-popover codex-panel__composer-meta-popover--${kind}`} style={style}>
|
||||
{choices.map((choice) => (
|
||||
<ComposerMetaChoice key={choice.label} choice={choice} onClose={onClose} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ComposerMetaChoice({ choice, onClose }: { choice: ToolbarChoice; onClose: () => void }): UiNode {
|
||||
const onSelect = () => {
|
||||
if (choice.disabled) return;
|
||||
choice.onClick();
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={["codex-panel__composer-meta-option", choice.disabled ? "is-disabled" : ""].filter(Boolean).join(" ")}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
onSelect();
|
||||
}}
|
||||
>
|
||||
{choice.label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ComposerSendMode {
|
||||
|
|
|
|||
|
|
@ -27,11 +27,14 @@ export interface RuntimeSnapshotInput {
|
|||
export interface ComposerMetaViewModel {
|
||||
fatal: string | null;
|
||||
context: ComposerContextMeterViewModel;
|
||||
statusSummary: string;
|
||||
model: string;
|
||||
effort: string | null;
|
||||
planActive: boolean;
|
||||
autoReviewActive: boolean;
|
||||
fastActive: boolean;
|
||||
modelChoices?: ToolbarChoice[];
|
||||
effortChoices?: ToolbarChoice[];
|
||||
}
|
||||
|
||||
export interface ComposerContextMeterCellViewModel {
|
||||
|
|
@ -160,6 +163,7 @@ export function composerMetaViewModel(state: ChatState, snapshot: RuntimeSnapsho
|
|||
return {
|
||||
fatal: "Codex app-server disconnected",
|
||||
context: contextComposerMeter(null),
|
||||
statusSummary: "Codex app-server disconnected",
|
||||
model: "",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
|
|
@ -172,17 +176,53 @@ export function composerMetaViewModel(state: ChatState, snapshot: RuntimeSnapsho
|
|||
const context = contextSummary(snapshot);
|
||||
const model = currentModel(snapshot, config);
|
||||
const effort = currentReasoningEffort(snapshot, config);
|
||||
const composerContext = contextComposerMeter(context?.percent ?? null);
|
||||
const compactEffort = effort ? compactReasoningEffortLabel(effort) : null;
|
||||
const planActive = state.selectedCollaborationMode === "plan";
|
||||
const reviewActive = autoReviewActive(snapshot, config);
|
||||
const fastActive = fastModeActive(snapshot, config);
|
||||
return {
|
||||
fatal: null,
|
||||
context: contextComposerMeter(context?.percent ?? null),
|
||||
context: composerContext,
|
||||
statusSummary: composerStatusSummary({
|
||||
context: composerContext,
|
||||
model: model ?? "default",
|
||||
effort: compactEffort,
|
||||
planActive,
|
||||
autoReviewActive: reviewActive,
|
||||
fastActive,
|
||||
}),
|
||||
model: model ?? "default",
|
||||
effort: effort ? compactReasoningEffortLabel(effort) : null,
|
||||
planActive: state.selectedCollaborationMode === "plan",
|
||||
autoReviewActive: autoReviewActive(snapshot, config),
|
||||
fastActive: fastModeActive(snapshot, config),
|
||||
effort: compactEffort,
|
||||
planActive,
|
||||
autoReviewActive: reviewActive,
|
||||
fastActive,
|
||||
};
|
||||
}
|
||||
|
||||
function composerStatusSummary(input: {
|
||||
context: ComposerContextMeterViewModel;
|
||||
model: string;
|
||||
effort: string | null;
|
||||
planActive: boolean;
|
||||
autoReviewActive: boolean;
|
||||
fastActive: boolean;
|
||||
}): string {
|
||||
const context = input.context.percent === "--%" ? "Context unavailable" : `Context ${input.context.percent.trim()}`;
|
||||
return [
|
||||
context,
|
||||
`plan ${onOffLabel(input.planActive)}`,
|
||||
`auto-review ${onOffLabel(input.autoReviewActive)}`,
|
||||
`fast ${onOffLabel(input.fastActive)}`,
|
||||
`model ${input.model}`,
|
||||
`reasoning effort ${input.effort ?? "default"}`,
|
||||
].join(", ");
|
||||
}
|
||||
|
||||
function onOffLabel(active: boolean): string {
|
||||
return active ? "on" : "off";
|
||||
}
|
||||
|
||||
export function toolbarViewModel(input: ToolbarViewModelInput): ToolbarViewModel {
|
||||
const { state, snapshot } = input;
|
||||
const config = readRuntimeConfig(state.effectiveConfig);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,12 @@ export function composerSlotSnapshot(state: ChatState, activeComposerThreadName:
|
|||
activeTurnId(state),
|
||||
state.activeModel,
|
||||
state.activeReasoningEffort,
|
||||
state.activeCollaborationMode,
|
||||
state.activeServiceTier,
|
||||
state.activeApprovalsReviewer,
|
||||
state.selectedCollaborationMode,
|
||||
state.requestedServiceTier,
|
||||
state.requestedApprovalsReviewer,
|
||||
state.requestedModel,
|
||||
state.requestedReasoningEffort,
|
||||
state.tokenUsage,
|
||||
|
|
|
|||
|
|
@ -556,7 +556,15 @@ export class CodexChatView extends ItemView {
|
|||
}
|
||||
|
||||
private composerMetaViewModel() {
|
||||
return buildComposerMetaViewModel(this.state, this.runtimeSnapshot());
|
||||
return {
|
||||
...buildComposerMetaViewModel(this.state, this.runtimeSnapshot()),
|
||||
...runtimeToolbarChoices({
|
||||
state: this.state,
|
||||
snapshot: this.runtimeSnapshot(),
|
||||
setRequestedModel: (model) => void this.setRequestedModelFromUi(model),
|
||||
setRequestedReasoningEffort: (effort) => void this.setRequestedReasoningEffortFromUi(effort),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private activeComposerThreadName(): string | null {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
}
|
||||
|
||||
.codex-panel__composer-meta {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
min-width: 0;
|
||||
|
|
@ -37,6 +38,7 @@
|
|||
}
|
||||
|
||||
.codex-panel__composer-meta-status {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
|
|
@ -47,6 +49,24 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-summary {
|
||||
position: absolute;
|
||||
width: var(--codex-panel-rail-width);
|
||||
height: var(--codex-panel-rail-width);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-status-visual {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
gap: var(--size-4-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-context {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
|
|
@ -117,11 +137,54 @@
|
|||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-model,
|
||||
.codex-panel__composer-meta-effort {
|
||||
.codex-panel__composer-meta-value.codex-panel__composer-meta-value {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-popover {
|
||||
position: absolute;
|
||||
bottom: calc(100% + var(--codex-panel-panel-gap));
|
||||
left: var(--codex-panel-composer-meta-popover-left);
|
||||
z-index: var(--layer-popover, 30);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: max-content;
|
||||
max-width: calc(100% - var(--codex-panel-composer-meta-popover-left));
|
||||
max-height: min(var(--codex-panel-size-toolbar-panel-max-height), 36vh);
|
||||
overflow-y: auto;
|
||||
border: var(--codex-panel-border);
|
||||
border-radius: var(--codex-panel-card-radius);
|
||||
background: var(--codex-panel-surface);
|
||||
box-shadow: var(--shadow-s);
|
||||
padding: var(--codex-panel-panel-gap);
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-option {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
padding: var(--size-4-1) var(--size-4-2);
|
||||
color: var(--text-muted);
|
||||
font: inherit;
|
||||
line-height: var(--line-height-tight);
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-option:where(:hover, :focus, :focus-visible, :active):not(.is-disabled) {
|
||||
color: var(--nav-item-color-hover, var(--text-normal));
|
||||
background: var(--background-modifier-hover);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-option.is-disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta--fatal {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ describe("ChatComposerController", () => {
|
|||
],
|
||||
percent: "--%",
|
||||
},
|
||||
statusSummary: "Context unavailable, plan off, auto-review off, fast off, model default, reasoning effort default",
|
||||
model: "default",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
|
|
@ -42,6 +43,9 @@ describe("ChatComposerController", () => {
|
|||
fastActive: false,
|
||||
}),
|
||||
currentModelForSuggestions: () => null,
|
||||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
renderIfDetached: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
onComposerResize: vi.fn(),
|
||||
|
|
@ -64,6 +68,56 @@ describe("ChatComposerController", () => {
|
|||
expect(composer(parent).getAttribute("aria-expanded")).toBe("false");
|
||||
expect(composer(parent).hasAttribute("aria-activedescendant")).toBe(false);
|
||||
});
|
||||
|
||||
it("delegates composer runtime toggles without forcing a local rerender", () => {
|
||||
const stateStore = createChatStateStore();
|
||||
const parent = document.createElement("div");
|
||||
const togglePlan = vi.fn();
|
||||
const controller = new ChatComposerController({
|
||||
app: app(),
|
||||
stateStore,
|
||||
viewId: "view",
|
||||
sendShortcut: () => "enter",
|
||||
scrollThreadFromComposerEdges: () => false,
|
||||
canInterrupt: () => false,
|
||||
composerPlaceholder: () => "Ask Codex to work on this task...",
|
||||
composerMeta: () => ({
|
||||
fatal: null,
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: "--%",
|
||||
},
|
||||
statusSummary: "Context unavailable, plan off, auto-review off, fast off, model default, reasoning effort default",
|
||||
model: "default",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
autoReviewActive: false,
|
||||
fastActive: false,
|
||||
}),
|
||||
currentModelForSuggestions: () => null,
|
||||
togglePlan,
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
renderIfDetached: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
onComposerResize: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onThreadScrollFromComposer: vi.fn(),
|
||||
});
|
||||
|
||||
controller.render(parent);
|
||||
expect(parent.querySelector<HTMLElement>(".codex-panel__composer-meta-icon")?.classList.contains("is-active")).toBe(false);
|
||||
|
||||
parent.querySelector<HTMLElement>(".codex-panel__composer-meta-icon")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
|
||||
expect(togglePlan).toHaveBeenCalledOnce();
|
||||
expect(parent.querySelector<HTMLElement>(".codex-panel__composer-meta-icon")?.classList.contains("is-active")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function app(): App {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ function composerCallbacks() {
|
|||
onUpdateSuggestions: vi.fn(),
|
||||
onKeydown: vi.fn(),
|
||||
onSendOrInterrupt: vi.fn(),
|
||||
onTogglePlan: vi.fn(),
|
||||
onToggleAutoReview: vi.fn(),
|
||||
onToggleFast: vi.fn(),
|
||||
onSuggestionHover: vi.fn(),
|
||||
onSuggestionInsert: vi.fn(),
|
||||
};
|
||||
|
|
@ -43,7 +46,7 @@ describe("composer renderer decisions", () => {
|
|||
expect(composer.getAttribute("placeholder")).toBe("Ask Codex to work on “Renamed thread”...");
|
||||
});
|
||||
|
||||
it("renders composer meta as non-interactive context and runtime text", () => {
|
||||
it("renders composer meta as interactive context and runtime text without changing normal text", () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderComposerShell(parent, "view", "", false, false, "Ask Codex to work on this task...", [], 0, composerCallbacks(), {
|
||||
|
|
@ -57,20 +60,33 @@ describe("composer renderer decisions", () => {
|
|||
],
|
||||
percent: "42%",
|
||||
},
|
||||
statusSummary: "Context 42%, plan on, auto-review off, fast on, model gpt-5.5, reasoning effort high",
|
||||
model: "gpt-5.5",
|
||||
effort: "high",
|
||||
planActive: true,
|
||||
autoReviewActive: false,
|
||||
fastActive: true,
|
||||
modelChoices: [
|
||||
{ label: "gpt-5.5", selected: true, onClick: vi.fn() },
|
||||
{ label: "gpt-5.4", onClick: vi.fn() },
|
||||
],
|
||||
effortChoices: [
|
||||
{ label: "medium", onClick: vi.fn() },
|
||||
{ label: "high", selected: true, onClick: vi.fn() },
|
||||
],
|
||||
});
|
||||
|
||||
const meta = parent.querySelector<HTMLElement>(".codex-panel__composer-meta");
|
||||
const statusItems = Array.from(parent.querySelectorAll<HTMLElement>(".codex-panel__composer-meta-status > span"));
|
||||
const statusItems = Array.from(parent.querySelectorAll<HTMLElement>(".codex-panel__composer-meta-status-visual > span"));
|
||||
const fields = Array.from(parent.querySelectorAll<HTMLElement>(".codex-panel__composer-meta-field"));
|
||||
const contextDots = Array.from(parent.querySelectorAll<HTMLElement>(".codex-panel__composer-meta-context-dot"));
|
||||
const modeIcons = Array.from(parent.querySelectorAll<HTMLElement>(".codex-panel__composer-meta-icon"));
|
||||
const statusSummary = parent.querySelector<HTMLElement>(".codex-panel__composer-meta-summary");
|
||||
const statusVisual = parent.querySelector<HTMLElement>(".codex-panel__composer-meta-status-visual");
|
||||
expect(meta?.getAttribute("aria-hidden")).toBeNull();
|
||||
expect(meta?.textContent).toBe("|⣿⣶⣀⣀42%|gpt-5.5|high");
|
||||
expect(statusSummary?.textContent).toBe("Context 42%, plan on, auto-review off, fast on, model gpt-5.5, reasoning effort high");
|
||||
expect(statusVisual?.getAttribute("aria-hidden")).toBe("true");
|
||||
expect(statusVisual?.textContent).toBe("|⣿⣶⣀⣀42%|gpt-5.5|high");
|
||||
expect(statusItems.map((item) => item.className)).toEqual([
|
||||
"codex-panel__composer-meta-modes",
|
||||
"codex-panel__composer-meta-separator",
|
||||
|
|
@ -79,7 +95,7 @@ describe("composer renderer decisions", () => {
|
|||
"codex-panel__composer-meta-field codex-panel__composer-meta-field--effort",
|
||||
]);
|
||||
expect(fields.map((field) => field.textContent)).toEqual(["|gpt-5.5", "|high"]);
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-status")?.getAttribute("aria-hidden")).toBe("true");
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-status")?.getAttribute("aria-hidden")).toBeNull();
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-context")?.textContent).toBe("⣿⣶⣀⣀42%");
|
||||
expect(contextDots.map((dot) => dot.textContent)).toEqual(["⣿", "⣶", "⣀", "⣀"]);
|
||||
expect(contextDots.map((dot) => dot.classList.contains("is-placeholder"))).toEqual([false, false, true, true]);
|
||||
|
|
@ -87,10 +103,107 @@ describe("composer renderer decisions", () => {
|
|||
expect(parent.querySelector(".codex-panel__composer-meta-effort")?.textContent).toBe("high");
|
||||
expect(modeIcons.map((icon) => icon.dataset["icon"])).toEqual(["list-todo", "shield", "zap"]);
|
||||
expect(modeIcons.map((icon) => icon.classList.contains("is-active"))).toEqual([true, false, true]);
|
||||
expect(modeIcons.map((icon) => icon.tagName)).toEqual(["SPAN", "SPAN", "SPAN"]);
|
||||
expect(modeIcons.map((icon) => icon.getAttribute("role"))).toEqual([null, null, null]);
|
||||
expect(modeIcons.map((icon) => icon.getAttribute("tabindex"))).toEqual([null, null, null]);
|
||||
expect(modeIcons.map((icon) => icon.getAttribute("aria-label"))).toEqual([null, null, null]);
|
||||
expect(modeIcons.every((icon) => icon.classList.contains("codex-panel__composer-meta-trigger"))).toBe(true);
|
||||
expect(modeIcons.every((icon) => !icon.className.includes("clickable-icon"))).toBe(true);
|
||||
expect(parent.querySelector<HTMLElement>(".codex-panel__composer-meta-model")?.tagName).toBe("SPAN");
|
||||
expect(parent.querySelector<HTMLElement>(".codex-panel__composer-meta-effort")?.tagName).toBe("SPAN");
|
||||
expect(parent.querySelector<HTMLElement>(".codex-panel__composer-meta-model")?.getAttribute("role")).toBeNull();
|
||||
expect(parent.querySelector<HTMLElement>(".codex-panel__composer-meta-effort")?.getAttribute("tabindex")).toBeNull();
|
||||
expect(parent.querySelector(".codex-panel__composer-action.codex-panel__send")).not.toBeNull();
|
||||
expect(parent.querySelector(".codex-panel__new-chat")).toBeNull();
|
||||
});
|
||||
|
||||
it("toggles composer runtime controls and opens separate lightweight pickers", async () => {
|
||||
const parent = document.createElement("div");
|
||||
const callbacks = composerCallbacks();
|
||||
const selectModel = vi.fn();
|
||||
const selectEffort = vi.fn();
|
||||
|
||||
renderComposerShell(parent, "view", "", false, false, "Ask Codex to work on this task...", [], 0, callbacks, {
|
||||
fatal: null,
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣿", placeholder: false },
|
||||
{ text: "⣶", placeholder: false },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: "42%",
|
||||
},
|
||||
statusSummary: "Context 42%, plan on, auto-review off, fast on, model gpt-5.5, reasoning effort high",
|
||||
model: "gpt-5.5",
|
||||
effort: "high",
|
||||
planActive: true,
|
||||
autoReviewActive: false,
|
||||
fastActive: true,
|
||||
modelChoices: [
|
||||
{ label: "gpt-5.5", selected: true, onClick: vi.fn() },
|
||||
{ label: "gpt-5.4", onClick: selectModel },
|
||||
],
|
||||
effortChoices: [
|
||||
{ label: "medium", onClick: selectEffort },
|
||||
{ label: "high", selected: true, onClick: vi.fn() },
|
||||
],
|
||||
});
|
||||
|
||||
const [planButton, reviewButton, fastButton] = Array.from(parent.querySelectorAll<HTMLElement>(".codex-panel__composer-meta-icon"));
|
||||
planButton?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
reviewButton?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
fastButton?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
expect(callbacks.onTogglePlan).toHaveBeenCalledOnce();
|
||||
expect(callbacks.onToggleAutoReview).toHaveBeenCalledOnce();
|
||||
expect(callbacks.onToggleFast).toHaveBeenCalledOnce();
|
||||
|
||||
parent.querySelector<HTMLElement>(".codex-panel__composer-meta-model")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
await waitForAsyncWork(() => {
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-popover--model")?.textContent).toBe("gpt-5.5gpt-5.4");
|
||||
});
|
||||
const modelOptions = Array.from(parent.querySelectorAll<HTMLElement>(".codex-panel__composer-meta-option"));
|
||||
expect(modelOptions.map((option) => option.getAttribute("role"))).toEqual([null, null]);
|
||||
expect(modelOptions.map((option) => option.getAttribute("tabindex"))).toEqual([null, null]);
|
||||
expect(modelOptions.map((option) => option.getAttribute("aria-selected"))).toEqual([null, null]);
|
||||
expect(modelOptions.every((option) => !option.classList.contains("is-selected"))).toBe(true);
|
||||
expect(modelOptions.every((option) => !option.classList.contains("codex-panel-ui__nav-item"))).toBe(true);
|
||||
expect(modelOptions.every((option) => !option.classList.contains("codex-panel__toolbar-panel-item"))).toBe(true);
|
||||
|
||||
parent.querySelector<HTMLElement>(".codex-panel__composer-meta-effort")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
await waitForAsyncWork(() => {
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-popover--model")).toBeNull();
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-popover--effort")?.textContent).toBe("mediumhigh");
|
||||
});
|
||||
|
||||
parent.querySelector<HTMLElement>(".codex-panel__composer-meta-option")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
expect(selectEffort).toHaveBeenCalledOnce();
|
||||
await waitForAsyncWork(() => {
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-popover")).toBeNull();
|
||||
});
|
||||
|
||||
parent.querySelector<HTMLElement>(".codex-panel__composer-meta-model")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
await waitForAsyncWork(() => {
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-popover--model")).not.toBeNull();
|
||||
});
|
||||
parent
|
||||
.querySelectorAll<HTMLElement>(".codex-panel__composer-meta-option")[1]
|
||||
?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
expect(selectModel).toHaveBeenCalledOnce();
|
||||
await waitForAsyncWork(() => {
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-popover")).toBeNull();
|
||||
});
|
||||
|
||||
parent.querySelector<HTMLElement>(".codex-panel__composer-meta-model")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
await waitForAsyncWork(() => {
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-popover--model")).not.toBeNull();
|
||||
});
|
||||
document.dispatchEvent(new Event("mousedown", { bubbles: true }));
|
||||
await waitForAsyncWork(() => {
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-popover")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("hides composer meta fields only after measured overflow", async () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
|
|
@ -105,6 +218,7 @@ describe("composer renderer decisions", () => {
|
|||
],
|
||||
percent: "42%",
|
||||
},
|
||||
statusSummary: "Context 42%, plan on, auto-review off, fast on, model gpt-5.5, reasoning effort high",
|
||||
model: "gpt-5.5",
|
||||
effort: "high",
|
||||
planActive: true,
|
||||
|
|
@ -147,6 +261,7 @@ describe("composer renderer decisions", () => {
|
|||
],
|
||||
percent: "--%",
|
||||
},
|
||||
statusSummary: "Codex app-server disconnected",
|
||||
model: "",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
|
|
|
|||
|
|
@ -336,6 +336,9 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
|
||||
expect(composerSlotSnapshot({ ...state, effectiveConfig: effectiveConfig("gpt-updated") }, null)).not.toBe(base);
|
||||
expect(composerSlotSnapshot({ ...state, requestedModel: { kind: "set", value: "gpt-requested" } }, null)).not.toBe(base);
|
||||
expect(composerSlotSnapshot({ ...state, selectedCollaborationMode: "plan" }, null)).not.toBe(base);
|
||||
expect(composerSlotSnapshot({ ...state, requestedApprovalsReviewer: { kind: "set", value: "auto_review" } }, null)).not.toBe(base);
|
||||
expect(composerSlotSnapshot({ ...state, requestedServiceTier: { kind: "set", value: "fast" } }, null)).not.toBe(base);
|
||||
expect(composerSlotSnapshot({ ...state, availableSkills: [skillFixture("reader")] }, null)).not.toBe(base);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ describe("chat view model", () => {
|
|||
],
|
||||
percent: "42%",
|
||||
},
|
||||
statusSummary: "Context 42%, plan on, auto-review on, fast on, model gpt-5.5, reasoning effort high",
|
||||
model: "gpt-5.5",
|
||||
effort: "high",
|
||||
planActive: true,
|
||||
|
|
@ -139,6 +140,7 @@ describe("chat view model", () => {
|
|||
],
|
||||
percent: "--%",
|
||||
},
|
||||
statusSummary: "Context unavailable, plan off, auto-review off, fast off, model gpt-5.5, reasoning effort default",
|
||||
model: "gpt-5.5",
|
||||
effort: null,
|
||||
});
|
||||
|
|
@ -163,6 +165,7 @@ describe("chat view model", () => {
|
|||
],
|
||||
percent: " 0%",
|
||||
},
|
||||
statusSummary: "Context 0%, plan off, auto-review off, fast off, model default, reasoning effort default",
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -181,6 +184,7 @@ describe("chat view model", () => {
|
|||
],
|
||||
percent: "--%",
|
||||
},
|
||||
statusSummary: "Codex app-server disconnected",
|
||||
model: "",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
|
|
|
|||
|
|
@ -128,15 +128,30 @@ describe("chat toolbar CSS", () => {
|
|||
const composerMetaStatus = /\.codex-panel__composer-meta-status \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const composerMetaFatal = /\.codex-panel__composer-meta-fatal \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
|
||||
expect(composerMetaStatus).toContain("position: relative");
|
||||
expect(composerMetaStatus).toContain("padding-inline-start: calc(var(--size-4-1) / 2)");
|
||||
expect(composerMetaFatal).toContain("padding-inline-start: calc(var(--size-4-1) / 2)");
|
||||
});
|
||||
|
||||
it("keeps composer status accessible summary visually hidden", () => {
|
||||
const summary = /\.codex-panel__composer-meta-summary \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const visual = /\.codex-panel__composer-meta-status-visual \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
|
||||
expect(summary).toContain("position: absolute");
|
||||
expect(summary).toContain("width: var(--codex-panel-rail-width)");
|
||||
expect(summary).toContain("height: var(--codex-panel-rail-width)");
|
||||
expect(summary).toContain("clip-path: inset(50%)");
|
||||
expect(summary).not.toContain("clip:");
|
||||
expect(visual).toContain("display: flex");
|
||||
expect(visual).toContain("flex: 0 0 auto");
|
||||
expect(visual).toContain("gap: var(--size-4-2)");
|
||||
});
|
||||
|
||||
it("keeps composer runtime mode icons compact and state-colored", () => {
|
||||
const modes = /\.codex-panel__composer-meta-modes \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const field = /\.codex-panel__composer-meta-field \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const labels =
|
||||
/\.codex-panel__composer-meta-model,\n\.codex-panel__composer-meta-effort \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
/\.codex-panel__composer-meta-value\.codex-panel__composer-meta-value \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const icon = /\.codex-panel__composer-meta-icon \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const activeIcon = /\.codex-panel__composer-meta-icon\.is-active \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const iconSvg = /\.codex-panel__composer-meta-icon svg \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
|
|
@ -146,6 +161,7 @@ describe("chat toolbar CSS", () => {
|
|||
expect(field).toContain("flex: 0 0 auto");
|
||||
expect(field).toContain("gap: var(--size-4-2)");
|
||||
expect(labels).toContain("white-space: nowrap");
|
||||
expect(labels).toContain("display: inline-flex");
|
||||
expect(labels).not.toContain("text-overflow");
|
||||
expect(icon).toContain("width: var(--codex-panel-size-icon-xs)");
|
||||
expect(icon).toContain("height: var(--codex-panel-size-icon-xs)");
|
||||
|
|
@ -160,6 +176,22 @@ describe("chat toolbar CSS", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("keeps composer status triggers visually unstyled", () => {
|
||||
const popover = /\.codex-panel__composer-meta-popover \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const option = /\.codex-panel__composer-meta-option \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
|
||||
expect(styles).not.toContain(".codex-panel__composer-meta-trigger {");
|
||||
expect(styles).not.toContain(".codex-panel__composer-meta-trigger:hover");
|
||||
expect(styles).not.toContain("button.codex-panel__composer-meta-trigger");
|
||||
expect(popover).toContain("position: absolute");
|
||||
expect(popover).toContain("bottom: calc(100% + var(--codex-panel-panel-gap))");
|
||||
expect(popover).toContain("width: max-content");
|
||||
expect(popover).toContain("max-width: calc(100% - var(--codex-panel-composer-meta-popover-left))");
|
||||
expect(styles).not.toContain(".codex-panel__composer-meta-option.is-selected");
|
||||
expect(option).not.toContain("padding-left");
|
||||
expect(option).not.toContain("grid-template-columns");
|
||||
});
|
||||
|
||||
it("keeps nav inline input reset in the shared primitive", () => {
|
||||
const navInlineInput =
|
||||
/\.codex-panel-ui__nav-inline-input\.codex-panel-ui__nav-inline-input \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
|
|
|
|||
Loading…
Reference in a new issue