mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Show runtime mode icons in status bar
This commit is contained in:
parent
85d8f8c84d
commit
d39403c8f5
7 changed files with 380 additions and 33 deletions
|
|
@ -3,7 +3,7 @@ import { useLayoutEffect, useRef } from "preact/hooks";
|
|||
|
||||
import type { ComposerSuggestion } from "../composer/suggestions";
|
||||
import type { ComposerMetaViewModel } from "../view-model";
|
||||
import { IconButton } from "../../../shared/ui/components";
|
||||
import { IconButton, ObsidianIcon } from "../../../shared/ui/components";
|
||||
import { renderUiRoot } from "../../../shared/ui/ui-root";
|
||||
import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow";
|
||||
|
||||
|
|
@ -27,9 +27,20 @@ type ButtonProps = ButtonHTMLAttributes & {
|
|||
|
||||
const DEFAULT_COMPOSER_META: ComposerMetaViewModel = {
|
||||
fatal: null,
|
||||
context: "ctx --%",
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: "--%",
|
||||
},
|
||||
model: "default",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
autoReviewActive: false,
|
||||
fastActive: false,
|
||||
};
|
||||
|
||||
export function renderComposerShell(
|
||||
|
|
@ -154,6 +165,31 @@ function ComposerMeta({
|
|||
sendMode: ComposerSendMode;
|
||||
onSendOrInterrupt: () => void;
|
||||
}): UiNode {
|
||||
const statusRef = useRef<HTMLSpanElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const status = statusRef.current;
|
||||
if (!status) return;
|
||||
const win = status.win;
|
||||
let frame = 0;
|
||||
const update = () => {
|
||||
frame = 0;
|
||||
updateComposerMetaStatusOverflow(status);
|
||||
};
|
||||
const scheduleUpdate = () => {
|
||||
if (frame) win.cancelAnimationFrame(frame);
|
||||
frame = win.requestAnimationFrame(update);
|
||||
};
|
||||
update();
|
||||
const ResizeObserverCtor = (win as Window & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver;
|
||||
const observer = ResizeObserverCtor ? new ResizeObserverCtor(scheduleUpdate) : null;
|
||||
observer?.observe(status);
|
||||
win.addEventListener("resize", scheduleUpdate);
|
||||
return () => {
|
||||
if (frame) win.cancelAnimationFrame(frame);
|
||||
observer?.disconnect();
|
||||
win.removeEventListener("resize", scheduleUpdate);
|
||||
};
|
||||
}, [meta]);
|
||||
if (meta.fatal) {
|
||||
return (
|
||||
<div className="codex-panel__composer-meta codex-panel__composer-meta--fatal">
|
||||
|
|
@ -164,15 +200,23 @@ function ComposerMeta({
|
|||
}
|
||||
return (
|
||||
<div className="codex-panel__composer-meta">
|
||||
<span className="codex-panel__composer-meta-status" aria-hidden="true">
|
||||
<span className="codex-panel__composer-meta-context">{meta.context}</span>
|
||||
<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>
|
||||
<span className="codex-panel__composer-meta-model">{meta.model}</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>
|
||||
</>
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<ComposerSendButton sendMode={sendMode} onSendOrInterrupt={onSendOrInterrupt} />
|
||||
|
|
@ -180,6 +224,47 @@ function ComposerMeta({
|
|||
);
|
||||
}
|
||||
|
||||
const COMPOSER_META_EFFORT_HIDDEN_CLASS = "is-effort-hidden";
|
||||
const COMPOSER_META_MODEL_HIDDEN_CLASS = "is-model-hidden";
|
||||
|
||||
function updateComposerMetaStatusOverflow(status: HTMLElement): void {
|
||||
status.classList.remove(COMPOSER_META_EFFORT_HIDDEN_CLASS, COMPOSER_META_MODEL_HIDDEN_CLASS);
|
||||
if (!composerMetaStatusOverflowing(status)) return;
|
||||
if (status.querySelector(".codex-panel__composer-meta-field--effort")) {
|
||||
status.classList.add(COMPOSER_META_EFFORT_HIDDEN_CLASS);
|
||||
}
|
||||
if (!composerMetaStatusOverflowing(status)) return;
|
||||
if (status.querySelector(".codex-panel__composer-meta-field--model")) {
|
||||
status.classList.add(COMPOSER_META_MODEL_HIDDEN_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
function composerMetaStatusOverflowing(status: HTMLElement): boolean {
|
||||
return status.scrollWidth > status.clientWidth;
|
||||
}
|
||||
|
||||
function ComposerContextMeter({ context }: { context: ComposerMetaViewModel["context"] }): UiNode {
|
||||
return (
|
||||
<span className="codex-panel__composer-meta-context">
|
||||
<span className="codex-panel__composer-meta-context-dots">
|
||||
{context.cells.map((cell, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={["codex-panel__composer-meta-context-dot", cell.placeholder ? "is-placeholder" : ""].filter(Boolean).join(" ")}
|
||||
>
|
||||
{cell.text}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<span className="codex-panel__composer-meta-context-percent">{context.percent}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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(" ")} />;
|
||||
}
|
||||
|
||||
interface ComposerSendMode {
|
||||
icon: string;
|
||||
label: string;
|
||||
|
|
|
|||
|
|
@ -26,9 +26,22 @@ export interface RuntimeSnapshotInput {
|
|||
|
||||
export interface ComposerMetaViewModel {
|
||||
fatal: string | null;
|
||||
context: string;
|
||||
context: ComposerContextMeterViewModel;
|
||||
model: string;
|
||||
effort: string | null;
|
||||
planActive: boolean;
|
||||
autoReviewActive: boolean;
|
||||
fastActive: boolean;
|
||||
}
|
||||
|
||||
export interface ComposerContextMeterCellViewModel {
|
||||
text: string;
|
||||
placeholder: boolean;
|
||||
}
|
||||
|
||||
export interface ComposerContextMeterViewModel {
|
||||
cells: ComposerContextMeterCellViewModel[];
|
||||
percent: string;
|
||||
}
|
||||
|
||||
export interface ToolbarViewModelInput {
|
||||
|
|
@ -146,9 +159,12 @@ export function composerMetaViewModel(state: ChatState, snapshot: RuntimeSnapsho
|
|||
if (state.status === "Connection failed.") {
|
||||
return {
|
||||
fatal: "Codex app-server disconnected",
|
||||
context: "",
|
||||
context: contextComposerMeter(null),
|
||||
model: "",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
autoReviewActive: false,
|
||||
fastActive: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -158,9 +174,12 @@ export function composerMetaViewModel(state: ChatState, snapshot: RuntimeSnapsho
|
|||
const effort = currentReasoningEffort(snapshot, config);
|
||||
return {
|
||||
fatal: null,
|
||||
context: contextComposerLabel(context?.percent ?? null),
|
||||
context: contextComposerMeter(context?.percent ?? null),
|
||||
model: model ?? "default",
|
||||
effort: effort ? compactReasoningEffortLabel(effort) : null,
|
||||
planActive: state.selectedCollaborationMode === "plan",
|
||||
autoReviewActive: autoReviewActive(snapshot, config),
|
||||
fastActive: fastModeActive(snapshot, config),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -205,27 +224,30 @@ const CONTEXT_DOT_WIDTH = 4;
|
|||
const CONTEXT_CELL_PERCENT = 100 / CONTEXT_DOT_WIDTH;
|
||||
const CONTEXT_PARTIAL_DOTS = ["", "⣀", "⣤", "⣶", "⣿"] as const;
|
||||
const CONTEXT_FULL_DOT = "⣿";
|
||||
const CONTEXT_EMPTY_DOT = "⣀";
|
||||
|
||||
function contextComposerLabel(percent: number | null): string {
|
||||
const dots = contextBrailleDots(percent);
|
||||
function contextComposerMeter(percent: number | null): ComposerContextMeterViewModel {
|
||||
const percentLabel = percent === null ? "--%" : `${String(Math.round(Math.max(0, Math.min(100, percent)))).padStart(2, " ")}%`;
|
||||
return `ctx ${dots} ${percentLabel}`;
|
||||
return {
|
||||
cells: contextBrailleCells(percent),
|
||||
percent: percentLabel,
|
||||
};
|
||||
}
|
||||
|
||||
function contextBrailleDots(percent: number | null): string {
|
||||
if (percent === null) return " ".repeat(CONTEXT_DOT_WIDTH);
|
||||
function contextBrailleCells(percent: number | null): ComposerContextMeterCellViewModel[] {
|
||||
if (percent === null) return Array.from({ length: CONTEXT_DOT_WIDTH }, () => ({ text: CONTEXT_EMPTY_DOT, placeholder: true }));
|
||||
const clamped = Math.max(0, Math.min(100, percent));
|
||||
const chars: string[] = [];
|
||||
const cells: ComposerContextMeterCellViewModel[] = [];
|
||||
for (let index = 0; index < CONTEXT_DOT_WIDTH; index += 1) {
|
||||
const remaining = clamped - index * CONTEXT_CELL_PERCENT;
|
||||
if (remaining <= 0) {
|
||||
chars.push(index === 0 ? CONTEXT_PARTIAL_DOTS[1] : " ");
|
||||
cells.push({ text: CONTEXT_EMPTY_DOT, placeholder: true });
|
||||
continue;
|
||||
}
|
||||
const partialIndex = Math.min(CONTEXT_PARTIAL_DOTS.length - 1, Math.ceil((remaining / CONTEXT_CELL_PERCENT) * 4));
|
||||
chars.push(CONTEXT_PARTIAL_DOTS[partialIndex] ?? CONTEXT_FULL_DOT);
|
||||
cells.push({ text: CONTEXT_PARTIAL_DOTS[partialIndex] ?? CONTEXT_FULL_DOT, placeholder: false });
|
||||
}
|
||||
return chars.join("");
|
||||
return cells;
|
||||
}
|
||||
|
||||
export function connectionDiagnosticsModel(input: ConnectionDiagnosticsModelInput): ReturnType<typeof connectionDiagnosticSections> {
|
||||
|
|
|
|||
|
|
@ -48,8 +48,29 @@
|
|||
}
|
||||
|
||||
.codex-panel__composer-meta-context {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
gap: var(--codex-panel-control-gap);
|
||||
align-items: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-context-dots {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-context-dot {
|
||||
color: currentcolor;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-context-dot.is-placeholder {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-context-percent {
|
||||
flex: 0 0 auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
|
|
@ -58,11 +79,46 @@
|
|||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-field {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
gap: var(--size-4-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-field--model {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-modes {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
gap: var(--codex-panel-control-gap);
|
||||
align-items: center;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-icon {
|
||||
display: inline-flex;
|
||||
width: var(--codex-panel-size-icon-xs);
|
||||
height: var(--codex-panel-size-icon-xs);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-icon.is-active {
|
||||
color: var(--icon-color-active);
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-icon svg {
|
||||
width: calc(var(--codex-panel-size-icon-xs) - var(--codex-panel-rail-width) / 2);
|
||||
height: calc(var(--codex-panel-size-icon-xs) - var(--codex-panel-rail-width) / 2);
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-model,
|
||||
.codex-panel__composer-meta-effort {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
@ -78,6 +134,14 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-status.is-effort-hidden .codex-panel__composer-meta-field--effort {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codex-panel__composer-meta-status.is-model-hidden .codex-panel__composer-meta-field--model {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codex-panel__composer-suggestions {
|
||||
position: absolute;
|
||||
right: var(--codex-panel-edge-padding-x);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,23 @@ describe("ChatComposerController", () => {
|
|||
scrollThreadFromComposerEdges: () => false,
|
||||
canInterrupt: () => false,
|
||||
composerPlaceholder: () => "Ask Codex to work on this task...",
|
||||
composerMeta: () => ({ fatal: null, context: "ctx --%", model: "default", effort: null }),
|
||||
composerMeta: () => ({
|
||||
fatal: null,
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: "--%",
|
||||
},
|
||||
model: "default",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
autoReviewActive: false,
|
||||
fastActive: false,
|
||||
}),
|
||||
currentModelForSuggestions: () => null,
|
||||
renderIfDetached: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderComposerShell, scrollComposerSuggestionIntoView, syncComposerHeight } from "../../../../../src/features/chat/ui/composer";
|
||||
import { waitForAsyncWork } from "../../../../support/async";
|
||||
import { changeInputValue, composerSuggestionScrollFixture, installObsidianDomShims } from "../../../../support/dom";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
|
@ -47,30 +48,110 @@ describe("composer renderer decisions", () => {
|
|||
|
||||
renderComposerShell(parent, "view", "", false, false, "Ask Codex to work on this task...", [], 0, composerCallbacks(), {
|
||||
fatal: null,
|
||||
context: "ctx ⣿⣶ 42%",
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣿", placeholder: false },
|
||||
{ text: "⣶", placeholder: false },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: "42%",
|
||||
},
|
||||
model: "gpt-5.5",
|
||||
effort: "high",
|
||||
planActive: true,
|
||||
autoReviewActive: false,
|
||||
fastActive: true,
|
||||
});
|
||||
|
||||
const meta = parent.querySelector<HTMLElement>(".codex-panel__composer-meta");
|
||||
const statusItems = Array.from(parent.querySelectorAll<HTMLElement>(".codex-panel__composer-meta-status > 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"));
|
||||
expect(meta?.getAttribute("aria-hidden")).toBeNull();
|
||||
expect(meta?.textContent).toBe("ctx ⣿⣶ 42%|gpt-5.5|high");
|
||||
expect(meta?.textContent).toBe("|⣿⣶⣀⣀42%|gpt-5.5|high");
|
||||
expect(statusItems.map((item) => item.className)).toEqual([
|
||||
"codex-panel__composer-meta-modes",
|
||||
"codex-panel__composer-meta-separator",
|
||||
"codex-panel__composer-meta-context",
|
||||
"codex-panel__composer-meta-field codex-panel__composer-meta-field--model",
|
||||
"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-context")?.textContent).toBe("ctx ⣿⣶ 42%");
|
||||
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]);
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-model")?.textContent).toBe("gpt-5.5");
|
||||
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(parent.querySelector(".codex-panel__composer-action.codex-panel__send")).not.toBeNull();
|
||||
expect(parent.querySelector(".codex-panel__new-chat")).toBeNull();
|
||||
});
|
||||
|
||||
it("hides composer meta fields only after measured overflow", async () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderComposerShell(parent, "view", "", false, false, "Ask Codex to work on this task...", [], 0, composerCallbacks(), {
|
||||
fatal: null,
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣿", placeholder: false },
|
||||
{ text: "⣶", placeholder: false },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: "42%",
|
||||
},
|
||||
model: "gpt-5.5",
|
||||
effort: "high",
|
||||
planActive: true,
|
||||
autoReviewActive: false,
|
||||
fastActive: true,
|
||||
});
|
||||
|
||||
const status = parent.querySelector<HTMLElement>(".codex-panel__composer-meta-status");
|
||||
if (!status) throw new Error("Expected composer meta status to render.");
|
||||
Object.defineProperty(status, "clientWidth", { configurable: true, value: 100 });
|
||||
Object.defineProperty(status, "scrollWidth", {
|
||||
configurable: true,
|
||||
get: () => (status.classList.contains("is-model-hidden") ? 80 : status.classList.contains("is-effort-hidden") ? 120 : 140),
|
||||
});
|
||||
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await new Promise<void>((resolve) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await waitForAsyncWork(() => {
|
||||
expect(status.classList.contains("is-effort-hidden")).toBe(true);
|
||||
expect(status.classList.contains("is-model-hidden")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("replaces composer meta with fatal status text", () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderComposerShell(parent, "view", "", false, false, "Ask Codex to work on this task...", [], 0, composerCallbacks(), {
|
||||
fatal: "Codex app-server disconnected",
|
||||
context: "",
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: "--%",
|
||||
},
|
||||
model: "",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
autoReviewActive: false,
|
||||
fastActive: false,
|
||||
});
|
||||
|
||||
expect(parent.querySelector(".codex-panel__composer-meta-fatal")?.textContent).toBe("Codex app-server disconnected");
|
||||
|
|
|
|||
|
|
@ -80,7 +80,13 @@ describe("chat view model", () => {
|
|||
it("builds composer meta from context and runtime state", () => {
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-1";
|
||||
state.effectiveConfig = effectiveConfigFixture({ model: "gpt-5.5", model_reasoning_effort: "high" });
|
||||
state.selectedCollaborationMode = "plan";
|
||||
state.effectiveConfig = effectiveConfigFixture({
|
||||
model: "gpt-5.5",
|
||||
model_reasoning_effort: "high",
|
||||
approvals_reviewer: "auto_review",
|
||||
service_tier: "fast",
|
||||
});
|
||||
state.tokenUsage = {
|
||||
last: { inputTokens: 42, cachedInputTokens: 0, outputTokens: 2, reasoningOutputTokens: 0, totalTokens: 44 },
|
||||
total: { inputTokens: 40, cachedInputTokens: 0, outputTokens: 2, reasoningOutputTokens: 0, totalTokens: 42 },
|
||||
|
|
@ -89,9 +95,20 @@ describe("chat view model", () => {
|
|||
|
||||
expect(composerMetaViewModel(state, runtimeSnapshotForChatState({ state }))).toEqual({
|
||||
fatal: null,
|
||||
context: "ctx ⣿⣶ 42%",
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣿", placeholder: false },
|
||||
{ text: "⣶", placeholder: false },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: "42%",
|
||||
},
|
||||
model: "gpt-5.5",
|
||||
effort: "high",
|
||||
planActive: true,
|
||||
autoReviewActive: true,
|
||||
fastActive: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -113,7 +130,15 @@ describe("chat view model", () => {
|
|||
|
||||
expect(composerMetaViewModel(state, runtimeSnapshotForChatState({ state }))).toMatchObject({
|
||||
fatal: null,
|
||||
context: "ctx --%",
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: "--%",
|
||||
},
|
||||
model: "gpt-5.5",
|
||||
effort: null,
|
||||
});
|
||||
|
|
@ -129,7 +154,15 @@ describe("chat view model", () => {
|
|||
};
|
||||
|
||||
expect(composerMetaViewModel(state, runtimeSnapshotForChatState({ state }))).toMatchObject({
|
||||
context: "ctx ⣀ 0%",
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: " 0%",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -139,9 +172,20 @@ describe("chat view model", () => {
|
|||
|
||||
expect(composerMetaViewModel(state, runtimeSnapshotForChatState({ state }))).toEqual({
|
||||
fatal: "Codex app-server disconnected",
|
||||
context: "",
|
||||
context: {
|
||||
cells: [
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
{ text: "⣀", placeholder: true },
|
||||
],
|
||||
percent: "--%",
|
||||
},
|
||||
model: "",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
autoReviewActive: false,
|
||||
fastActive: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -110,9 +110,16 @@ describe("chat toolbar CSS", () => {
|
|||
|
||||
it("keeps the composer context status text fixed-width", () => {
|
||||
const contextMeter = /\.codex-panel__composer-meta-context \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const contextDots = /\.codex-panel__composer-meta-context-dots \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const placeholderDot =
|
||||
/\.codex-panel__composer-meta-context-dot\.is-placeholder \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const contextPercent = /\.codex-panel__composer-meta-context-percent \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
|
||||
expect(contextMeter).toContain("font-variant-numeric: tabular-nums");
|
||||
expect(contextMeter).toContain("white-space: pre");
|
||||
expect(contextMeter).toContain("white-space: nowrap");
|
||||
expect(contextDots).toContain("display: inline-flex");
|
||||
expect(placeholderDot).toContain("color: var(--text-faint)");
|
||||
expect(contextPercent).toContain("white-space: pre");
|
||||
expect(styles).not.toContain(".codex-panel__composer-meta-context::before");
|
||||
expect(styles).not.toContain("conic-gradient");
|
||||
});
|
||||
|
|
@ -125,6 +132,34 @@ describe("chat toolbar CSS", () => {
|
|||
expect(composerMetaFatal).toContain("padding-inline-start: calc(var(--size-4-1) / 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"] ?? "";
|
||||
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"] ?? "";
|
||||
|
||||
expect(modes).toContain("gap: var(--codex-panel-control-gap)");
|
||||
expect(field).toContain("display: inline-flex");
|
||||
expect(field).toContain("flex: 0 0 auto");
|
||||
expect(field).toContain("gap: var(--size-4-2)");
|
||||
expect(labels).toContain("white-space: nowrap");
|
||||
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)");
|
||||
expect(activeIcon).toContain("color: var(--icon-color-active)");
|
||||
expect(iconSvg).toContain("width: calc(var(--codex-panel-size-icon-xs) - var(--codex-panel-rail-width) / 2)");
|
||||
expect(iconSvg).toContain("height: calc(var(--codex-panel-size-icon-xs) - var(--codex-panel-rail-width) / 2)");
|
||||
expect(styles).toContain(
|
||||
".codex-panel__composer-meta-status.is-effort-hidden .codex-panel__composer-meta-field--effort {\n display: none;",
|
||||
);
|
||||
expect(styles).toContain(
|
||||
".codex-panel__composer-meta-status.is-model-hidden .codex-panel__composer-meta-field--model {\n display: none;",
|
||||
);
|
||||
});
|
||||
|
||||
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