fix: align console metadata and details controls
This commit is contained in:
@@ -64,6 +64,62 @@ Deno.test("Console pending scopes are independent and match Tasks row typography
|
|||||||
`${url}/w/console-history-review/workers/W-900-console-fixture/console?pending=${mode}`,
|
`${url}/w/console-history-review/workers/W-900-console-fixture/console?pending=${mode}`,
|
||||||
);
|
);
|
||||||
await page.locator(".task-mini-row").waitFor();
|
await page.locator(".task-mini-row").waitFor();
|
||||||
|
const metadata = page.getByLabel("Worker model and context", { exact: true });
|
||||||
|
const statusGeometry = await metadata.evaluate((status) => {
|
||||||
|
const task = document.querySelector(".task-mini-row")!;
|
||||||
|
const composer = document.querySelector(".console-composer")!;
|
||||||
|
const metrics = (node: Element) => {
|
||||||
|
const style = getComputedStyle(node);
|
||||||
|
return [style.fontFamily, style.fontSize, style.lineHeight];
|
||||||
|
};
|
||||||
|
const rect = status.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
typography: metrics(status),
|
||||||
|
taskTypography: metrics(task),
|
||||||
|
height: rect.height,
|
||||||
|
belowComposer: rect.top >= composer.getBoundingClientRect().bottom,
|
||||||
|
alignedWithTasks: rect.left + parseFloat(getComputedStyle(status).paddingLeft) === task.getBoundingClientRect().left,
|
||||||
|
visible: rect.bottom <= innerHeight,
|
||||||
|
wrap: getComputedStyle(status).flexWrap,
|
||||||
|
pageOverflow: document.documentElement.scrollWidth > innerWidth,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assertEquals(statusGeometry.typography, statusGeometry.taskTypography);
|
||||||
|
assertEquals(statusGeometry.height, 16);
|
||||||
|
assertEquals(statusGeometry.belowComposer, true);
|
||||||
|
assertEquals(statusGeometry.alignedWithTasks, true);
|
||||||
|
assertEquals(statusGeometry.visible, true);
|
||||||
|
assertEquals(statusGeometry.wrap, "nowrap");
|
||||||
|
assertEquals(statusGeometry.pageOverflow, false);
|
||||||
|
const details = metadata.getByRole("button", { name: "Details", exact: true });
|
||||||
|
assertEquals(await page.locator(".console-header").getByRole("button", { name: "Details" }).count(), 0);
|
||||||
|
const detailsGeometry = await details.evaluate((button) => {
|
||||||
|
const row = button.closest(".worker-context-status")!;
|
||||||
|
const rect = button.getBoundingClientRect();
|
||||||
|
const rowRect = row.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
rightAligned: Math.abs(rect.right - (rowRect.right - parseFloat(getComputedStyle(row).paddingRight))) < 1,
|
||||||
|
centered: Math.abs(rect.top + rect.height / 2 - (rowRect.top + rowRect.height / 2)) < 1,
|
||||||
|
width: rect.width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assertEquals(detailsGeometry.rightAligned, true);
|
||||||
|
assertEquals(detailsGeometry.centered, true);
|
||||||
|
assert(detailsGeometry.width > 0);
|
||||||
|
await details.focus();
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
assertEquals(await details.getAttribute("aria-expanded"), "true");
|
||||||
|
await page.getByRole("button", { name: "Close", exact: true }).waitFor();
|
||||||
|
await page.keyboard.press("Space");
|
||||||
|
assertEquals(await details.getAttribute("aria-expanded"), "false");
|
||||||
|
await details.click();
|
||||||
|
assertEquals(await details.getAttribute("aria-expanded"), "true");
|
||||||
|
await page.getByRole("button", { name: "Close", exact: true }).click();
|
||||||
|
assertEquals(await details.getAttribute("aria-expanded"), "false");
|
||||||
|
if (mode === "both") {
|
||||||
|
await Deno.mkdir(join(root, "target/web-ux/composer-metadata"), { recursive: true });
|
||||||
|
await page.screenshot({ path: join(root, `target/web-ux/composer-metadata/${colorScheme}-${width}.png`) });
|
||||||
|
}
|
||||||
const queue = mode === "both" || mode === "queue";
|
const queue = mode === "both" || mode === "queue";
|
||||||
const notifications = ["both", "notifications", "legacy"].includes(mode);
|
const notifications = ["both", "notifications", "legacy"].includes(mode);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
|||||||
@@ -6,6 +6,23 @@ import WorkerContextStatus from "./WorkerContextStatus.svelte";
|
|||||||
|
|
||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
test("the three-dot Details control invokes the existing toggle and reflects its state", async () => {
|
||||||
|
let toggles = 0;
|
||||||
|
const view = render(WorkerContextStatus, {
|
||||||
|
metadata: null,
|
||||||
|
detailsOpen: false,
|
||||||
|
onToggleDetails: () => { toggles++; },
|
||||||
|
});
|
||||||
|
const button = view.getByRole("button", { name: "Details" });
|
||||||
|
expect(button.querySelectorAll("circle")).toHaveLength(3);
|
||||||
|
expect(button.textContent?.trim()).toBe("");
|
||||||
|
expect(button.getAttribute("aria-expanded")).toBe("false");
|
||||||
|
button.click();
|
||||||
|
expect(toggles).toBe(1);
|
||||||
|
await view.rerender({ detailsOpen: true });
|
||||||
|
expect(button.getAttribute("aria-expanded")).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
test("renders resolved model, reasoning, and measured context accessibly", () => {
|
test("renders resolved model, reasoning, and measured context accessibly", () => {
|
||||||
const view = render(WorkerContextStatus, {
|
const view = render(WorkerContextStatus, {
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -19,13 +36,13 @@ test("renders resolved model, reasoning, and measured context accessibly", () =>
|
|||||||
|
|
||||||
const status = view.getByLabelText("Worker model and context");
|
const status = view.getByLabelText("Worker model and context");
|
||||||
expect(status.textContent).toContain("gpt-6-astra · high");
|
expect(status.textContent).toContain("gpt-6-astra · high");
|
||||||
expect(status.textContent).toContain("Context 142.0k / 272.0k (52%)");
|
expect(status.textContent).toContain("142.0k / 272.0k (52%)");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders honest unavailable state instead of a fabricated zero percent", async () => {
|
test("renders honest unavailable state instead of a fabricated zero percent", async () => {
|
||||||
const view = render(WorkerContextStatus, { metadata: null });
|
const view = render(WorkerContextStatus, { metadata: null });
|
||||||
expect(view.getByText("Model unavailable")).not.toBeNull();
|
expect(view.getByText("Model unavailable")).not.toBeNull();
|
||||||
expect(view.getByText("Context unavailable")).not.toBeNull();
|
expect(view.getByText("unavailable")).not.toBeNull();
|
||||||
expect(view.container.textContent).not.toContain("0%");
|
expect(view.container.textContent).not.toContain("0%");
|
||||||
|
|
||||||
await view.rerender({
|
await view.rerender({
|
||||||
@@ -38,7 +55,7 @@ test("renders honest unavailable state instead of a fabricated zero percent", as
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(view.container.textContent).toContain("claude · 8.2k token budget");
|
expect(view.container.textContent).toContain("claude · 8.2k token budget");
|
||||||
expect(view.container.textContent).toContain("Context ~12.0k / 64.0k (19%)");
|
expect(view.container.textContent).toContain("~12.0k / 64.0k (19%)");
|
||||||
|
|
||||||
await view.rerender({
|
await view.rerender({
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
metadata: ConsoleWorkerMetadata | null;
|
metadata: ConsoleWorkerMetadata | null;
|
||||||
|
detailsOpen?: boolean;
|
||||||
|
onToggleDetails?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { metadata }: Props = $props();
|
let { metadata, detailsOpen = false, onToggleDetails }: Props = $props();
|
||||||
const modelSummary = $derived(formatModelSummary(metadata));
|
const modelSummary = $derived(formatModelSummary(metadata));
|
||||||
const contextSummary = $derived(formatContextSummary(metadata));
|
const contextSummary = $derived(formatContextSummary(metadata));
|
||||||
</script>
|
</script>
|
||||||
@@ -18,12 +20,32 @@
|
|||||||
<span class="worker-model" title={modelSummary}>{modelSummary}</span>
|
<span class="worker-model" title={modelSummary}>{modelSummary}</span>
|
||||||
<span class="separator" aria-hidden="true">|</span>
|
<span class="separator" aria-hidden="true">|</span>
|
||||||
<span class="worker-context" title={contextSummary}>{contextSummary}</span>
|
<span class="worker-context" title={contextSummary}>{contextSummary}</span>
|
||||||
|
{#if onToggleDetails}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="details-button"
|
||||||
|
aria-label="Details"
|
||||||
|
aria-expanded={detailsOpen}
|
||||||
|
onclick={onToggleDetails}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<circle cx="5" cy="12" r="2" />
|
||||||
|
<circle cx="12" cy="12" r="2" />
|
||||||
|
<circle cx="19" cy="12" r="2" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.worker-context-status {
|
.worker-context-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
flex-wrap: nowrap;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-inline: var(--space-3);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -50,20 +72,35 @@
|
|||||||
color: var(--line-strong);
|
color: var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
.details-button {
|
||||||
.worker-context-status {
|
display: inline-flex;
|
||||||
width: 100%;
|
flex: 0 0 auto;
|
||||||
flex-wrap: wrap;
|
align-items: center;
|
||||||
row-gap: 0;
|
justify-content: center;
|
||||||
}
|
width: 20px;
|
||||||
|
height: var(--line-height-compact);
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.separator {
|
.details-button:hover,
|
||||||
display: none;
|
.details-button[aria-expanded="true"],
|
||||||
}
|
.details-button:focus-visible {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.worker-model,
|
.details-button:focus-visible {
|
||||||
.worker-context {
|
outline: 1px solid currentColor;
|
||||||
flex: 1 1 100%;
|
outline-offset: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details-button svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely"
|
|||||||
contextTokens: 142_000,
|
contextTokens: 142_000,
|
||||||
contextSource: "estimated",
|
contextSource: "estimated",
|
||||||
}),
|
}),
|
||||||
"Context ~142.0k / 272.0k (52%)",
|
"~142.0k / 272.0k (52%)",
|
||||||
);
|
);
|
||||||
assertEquals(formatModelSummary(null), "Model unavailable");
|
assertEquals(formatModelSummary(null), "Model unavailable");
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@@ -64,7 +64,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely"
|
|||||||
formatReasoning({ kind: "budget_tokens", budget_tokens: -2 }),
|
formatReasoning({ kind: "budget_tokens", budget_tokens: -2 }),
|
||||||
"reasoning unavailable",
|
"reasoning unavailable",
|
||||||
);
|
);
|
||||||
assertEquals(formatContextSummary(null), "Context unavailable");
|
assertEquals(formatContextSummary(null), "unavailable");
|
||||||
assertEquals(
|
assertEquals(
|
||||||
formatContextSummary({
|
formatContextSummary({
|
||||||
model: null,
|
model: null,
|
||||||
@@ -73,7 +73,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely"
|
|||||||
contextTokens: 0,
|
contextTokens: 0,
|
||||||
contextSource: null,
|
contextSource: null,
|
||||||
}),
|
}),
|
||||||
"Context unavailable",
|
"unavailable",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -738,6 +738,26 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps"
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("Model information stays below Composer in one compact monospace row", async () => {
|
||||||
|
const page = await Deno.readTextFile(new URL(
|
||||||
|
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||||
|
import.meta.url,
|
||||||
|
));
|
||||||
|
const composer = page.indexOf('<form class="console-composer"');
|
||||||
|
const composerEnd = page.indexOf("</form>", composer);
|
||||||
|
const status = page.indexOf("<WorkerContextStatus");
|
||||||
|
assert(composer >= 0 && composerEnd > composer && status > composerEnd, "Model information must follow Composer");
|
||||||
|
assert(page.includes("metadata={selectedConsoleProjection.workerMetadata}"), "Keep selected Worker metadata");
|
||||||
|
assert(page.indexOf("<WorkerContextStatus", status + 1) === -1, "Render model information only once");
|
||||||
|
const component = await Deno.readTextFile(new URL("./WorkerContextStatus.svelte", import.meta.url));
|
||||||
|
for (const rule of [
|
||||||
|
"flex-wrap: nowrap", "white-space: nowrap", "text-overflow: ellipsis",
|
||||||
|
"font-family: var(--font-mono)", "font-size: var(--font-size-compact)",
|
||||||
|
"line-height: var(--line-height-compact)", "padding-inline: var(--space-3)",
|
||||||
|
]) assert(component.includes(rule), rule);
|
||||||
|
assert(!component.includes("@media"), "Narrow layouts must retain the same single row");
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("Console spacing and text metrics use existing design tokens", async () => {
|
Deno.test("Console spacing and text metrics use existing design tokens", async () => {
|
||||||
const files = [
|
const files = [
|
||||||
"ComposerInput.svelte",
|
"ComposerInput.svelte",
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ export function formatContextSummary(
|
|||||||
used === null || used === undefined || limit === null ||
|
used === null || used === undefined || limit === null ||
|
||||||
limit === undefined || !Number.isSafeInteger(used) ||
|
limit === undefined || !Number.isSafeInteger(used) ||
|
||||||
!Number.isSafeInteger(limit) || used < 0 || limit <= 0
|
!Number.isSafeInteger(limit) || used < 0 || limit <= 0
|
||||||
) return "Context unavailable";
|
) return "unavailable";
|
||||||
const percent = Math.round((used / limit) * 100);
|
const percent = Math.round((used / limit) * 100);
|
||||||
if (!Number.isFinite(percent)) return "Context unavailable";
|
if (!Number.isFinite(percent)) return "unavailable";
|
||||||
const estimate = metadata?.contextSource === "estimated" ? "~" : "";
|
const estimate = metadata?.contextSource === "estimated" ? "~" : "";
|
||||||
return `Context ${estimate}${formatRunTokens(used)} / ${
|
return `${estimate}${formatRunTokens(used)} / ${
|
||||||
formatRunTokens(limit)
|
formatRunTokens(limit)
|
||||||
} (${percent}%)`;
|
} (${percent}%)`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1652,7 +1652,6 @@
|
|||||||
|
|
||||||
<div class="console-shell worker-console-shell">
|
<div class="console-shell worker-console-shell">
|
||||||
<section class="console-header card" aria-label="Worker controls">
|
<section class="console-header card" aria-label="Worker controls">
|
||||||
<WorkerContextStatus metadata={selectedConsoleProjection.workerMetadata} />
|
|
||||||
<div class="console-header-actions">
|
<div class="console-header-actions">
|
||||||
<div
|
<div
|
||||||
class="console-view-modes"
|
class="console-view-modes"
|
||||||
@@ -1676,17 +1675,6 @@
|
|||||||
Normal
|
Normal
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="secondary-button"
|
|
||||||
aria-expanded={workerDetailsOpen}
|
|
||||||
onclick={() => {
|
|
||||||
workerDetailsOpen = !workerDetailsOpen;
|
|
||||||
if (workerDetailsOpen) taskPaneOpen = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Details
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -2094,6 +2082,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<WorkerContextStatus
|
||||||
|
metadata={selectedConsoleProjection.workerMetadata}
|
||||||
|
detailsOpen={workerDetailsOpen}
|
||||||
|
onToggleDetails={() => {
|
||||||
|
workerDetailsOpen = !workerDetailsOpen;
|
||||||
|
if (workerDetailsOpen) taskPaneOpen = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -2123,7 +2119,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user