fix: align console metadata and details controls

This commit is contained in:
2026-10-04 21:03:16 +09:00
parent 1bbc48dad0
commit b0d4973717
7 changed files with 162 additions and 36 deletions
@@ -6,6 +6,23 @@ import WorkerContextStatus from "./WorkerContextStatus.svelte";
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", () => {
const view = render(WorkerContextStatus, {
metadata: {
@@ -19,13 +36,13 @@ test("renders resolved model, reasoning, and measured context accessibly", () =>
const status = view.getByLabelText("Worker model and context");
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 () => {
const view = render(WorkerContextStatus, { metadata: null });
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%");
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("Context ~12.0k / 64.0k (19%)");
expect(view.container.textContent).toContain("~12.0k / 64.0k (19%)");
await view.rerender({
metadata: {
@@ -7,9 +7,11 @@
type Props = {
metadata: ConsoleWorkerMetadata | null;
detailsOpen?: boolean;
onToggleDetails?: () => void;
};
let { metadata }: Props = $props();
let { metadata, detailsOpen = false, onToggleDetails }: Props = $props();
const modelSummary = $derived(formatModelSummary(metadata));
const contextSummary = $derived(formatContextSummary(metadata));
</script>
@@ -18,12 +20,32 @@
<span class="worker-model" title={modelSummary}>{modelSummary}</span>
<span class="separator" aria-hidden="true">|</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>
<style>
.worker-context-status {
display: flex;
flex: 0 0 auto;
flex-wrap: nowrap;
min-width: 0;
overflow: hidden;
white-space: nowrap;
padding-inline: var(--space-3);
align-items: center;
gap: var(--space-2);
color: var(--text-muted);
@@ -50,20 +72,35 @@
color: var(--line-strong);
}
@media (max-width: 640px) {
.worker-context-status {
width: 100%;
flex-wrap: wrap;
row-gap: 0;
}
.details-button {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
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 {
display: none;
}
.details-button:hover,
.details-button[aria-expanded="true"],
.details-button:focus-visible {
color: var(--text);
}
.worker-model,
.worker-context {
flex: 1 1 100%;
}
.details-button:focus-visible {
outline: 1px solid currentColor;
outline-offset: -1px;
}
.details-button svg {
width: 16px;
height: 16px;
fill: currentColor;
}
</style>
@@ -45,7 +45,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely"
contextTokens: 142_000,
contextSource: "estimated",
}),
"Context ~142.0k / 272.0k (52%)",
"~142.0k / 272.0k (52%)",
);
assertEquals(formatModelSummary(null), "Model unavailable");
assertEquals(
@@ -64,7 +64,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely"
formatReasoning({ kind: "budget_tokens", budget_tokens: -2 }),
"reasoning unavailable",
);
assertEquals(formatContextSummary(null), "Context unavailable");
assertEquals(formatContextSummary(null), "unavailable");
assertEquals(
formatContextSummary({
model: null,
@@ -73,7 +73,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely"
contextTokens: 0,
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 () => {
const files = [
"ComposerInput.svelte",
@@ -36,11 +36,11 @@ export function formatContextSummary(
used === null || used === undefined || limit === null ||
limit === undefined || !Number.isSafeInteger(used) ||
!Number.isSafeInteger(limit) || used < 0 || limit <= 0
) return "Context unavailable";
) return "unavailable";
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" ? "~" : "";
return `Context ${estimate}${formatRunTokens(used)} / ${
return `${estimate}${formatRunTokens(used)} / ${
formatRunTokens(limit)
} (${percent}%)`;
}
@@ -1652,7 +1652,6 @@
<div class="console-shell worker-console-shell">
<section class="console-header card" aria-label="Worker controls">
<WorkerContextStatus metadata={selectedConsoleProjection.workerMetadata} />
<div class="console-header-actions">
<div
class="console-view-modes"
@@ -1676,17 +1675,6 @@
Normal
</button>
</div>
<button
type="button"
class="secondary-button"
aria-expanded={workerDetailsOpen}
onclick={() => {
workerDetailsOpen = !workerDetailsOpen;
if (workerDetailsOpen) taskPaneOpen = false;
}}
>
Details
</button>
</div>
</section>
@@ -2094,6 +2082,14 @@
</div>
</div>
</form>
<WorkerContextStatus
metadata={selectedConsoleProjection.workerMetadata}
detailsOpen={workerDetailsOpen}
onToggleDetails={() => {
workerDetailsOpen = !workerDetailsOpen;
if (workerDetailsOpen) taskPaneOpen = false;
}}
/>
</div>
<style>
@@ -2123,7 +2119,7 @@
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
justify-content: flex-end;
gap: var(--space-4);
}