Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 10 additions & 7 deletions gui/src/styles-models-workspace.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
/*
The catalog wants a wider column than the 980px default.

Scoped to a VISIBLE catalog panel, not merely a present one: panels mount lazily and
then stay mounted so drafts survive a tab hop, so a bare `:has(.models-workspace-shell)`
keeps matching after the catalog has been opened once. Routing would then render at
980px on a direct visit and 1200px afterwards — a width that depends on browsing
history. No surface renders the shell outside a tabpanel any more, so the old
direct-child arm is gone with the standalone pages it served.
Scoped to a VISIBLE catalog or routing panel, not merely a present one: panels mount
lazily and then stay mounted so drafts survive a tab hop, so a bare
`:has(.models-workspace-shell)` keeps matching after the catalog has been opened once.
Routing must share the catalog's 1200px page width or switching between those tabs
shifts the content horizontally. No surface renders the shell outside a tabpanel any
more, so the old direct-child arm is gone with the standalone pages it served.
*/
.main-inner:has(#models-panel-catalog:not([hidden]) .models-workspace-shell) {
.main-inner:has(
#models-panel-catalog:not([hidden]),
#models-panel-routing:not([hidden])
) {
max-width: 1200px;
}

Expand Down
11 changes: 11 additions & 0 deletions gui/tests/models-provider-head.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ test("Models workspace stacks via content-width container query before mobile dr
expect(css).toContain("@media (max-width: 768px)");
});

test("Models catalog and routing tabs keep the same wide page width", async () => {
const css = await Bun.file(new URL("../src/styles-models-workspace.css", import.meta.url)).text();

// Both panels stay mounted after first visit, so the width rule must follow the
// visible panel rather than the catalog shell's historical presence in the DOM.
expect(css).toMatch(
/\.main-inner:has\(\s*#models-panel-catalog:not\(\[hidden\]\),\s*#models-panel-routing:not\(\[hidden\]\)\s*\)\s*\{\s*max-width:\s*1200px;/s,
);
expect(css).not.toContain("#models-panel-catalog:not([hidden]) .models-workspace-shell");
});

test("Models exposes provider and per-model context-window controls (#1073)", async () => {
const page = await Bun.file(new URL("../src/pages/Models.tsx", import.meta.url)).text();
const groups = await Bun.file(new URL("../src/models-groups.ts", import.meta.url)).text();
Expand Down
92 changes: 92 additions & 0 deletions gui/tests/models-workspace-panels.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,101 @@ async function mountModels(): Promise<{ container: HTMLElement; root: Root }> {
return { container, root };
}

/**
* App owns `.main-inner` and the page-width rules that key off visible tab panels.
* happy-dom does not parse the multiline `:has()` selector in the shipped stylesheet,
* so the test injects a single-line form that matches the production rule.
*/
function injectModelsPageWidthCss(doc: Document): void {
const style = doc.createElement("style");
style.textContent = `
.main-inner { max-width: 980px; margin: 0 auto; }
.main-inner:has(#models-panel-catalog:not([hidden]), #models-panel-routing:not([hidden])) { max-width: 1200px; }
`;
doc.head.appendChild(style);
}

async function mountModelsInMainInner(hash = "http://localhost/#models"): Promise<{
container: HTMLElement;
mainInner: HTMLElement;
root: Root;
}> {
testWindow.location.href = hash;
injectModelsPageWidthCss(document);
const { createRoot } = await import("react-dom/client");
const outer = document.createElement("div");
const mainInner = document.createElement("div");
mainInner.className = "main-inner";
outer.append(mainInner);
document.body.append(outer);
let root!: Root;
await act(async () => {
root = createRoot(mainInner);
root.render(
<LanguageProvider>
<Models apiBase={API_BASE} />
</LanguageProvider>,
);
});
await act(async () => { await Promise.resolve(); });
return { container: mainInner, mainInner, root };
}

function mainInnerMaxWidth(mainInner: HTMLElement): string {
return testWindow.getComputedStyle(mainInner).maxWidth;
}

async function clickTab(container: HTMLElement, id: string): Promise<void> {
await act(async () => {
(container.querySelector(`#models-tab-${id}`) as HTMLButtonElement).click();
});
await act(async () => { await Promise.resolve(); });
}

const tabs = (container: HTMLElement) => [...container.querySelectorAll('[role="tab"]')] as HTMLButtonElement[];
const panel = (container: HTMLElement, id: string) => container.querySelector(`#models-panel-${id}`);

/*
* The layout drift bug: routing rendered at 980px while catalog used 1200px, and a
* mounted-but-hidden catalog could keep the wide rule latched via the old shell selector.
* Source-string CSS tests cannot see computed width; this one drives real tab switches.
*/
test("catalog and routing keep the same main-inner width across tab switches", async () => {
installFetch();
const { container, mainInner, root } = await mountModelsInMainInner();
try {
await act(async () => { await Promise.resolve(); });
expect(mainInnerMaxWidth(mainInner)).toBe("1200px");

await clickTab(container, "routing");
expect(mainInnerMaxWidth(mainInner)).toBe("1200px");
expect(panel(container, "catalog")?.hasAttribute("hidden")).toBe(true);

await clickTab(container, "catalog");
expect(mainInnerMaxWidth(mainInner)).toBe("1200px");
expect(panel(container, "routing")?.hasAttribute("hidden")).toBe(true);
} finally {
await act(async () => root.unmount());
}
});

test("routing keeps the wide layout on a direct visit while catalog stays mounted hidden", async () => {
installFetch();
const { container, mainInner, root } = await mountModelsInMainInner("http://localhost/#models/routing");
try {
await act(async () => { await Promise.resolve(); });
expect(panel(container, "catalog")).toBeTruthy();
expect(panel(container, "catalog")?.hasAttribute("hidden")).toBe(true);
expect(mainInnerMaxWidth(mainInner)).toBe("1200px");

await clickTab(container, "catalog");
await clickTab(container, "routing");
expect(mainInnerMaxWidth(mainInner)).toBe("1200px");
} finally {
await act(async () => root.unmount());
}
});

test("the strip renders all four tabs with the catalog selected on the bare hash", async () => {
installFetch();
const { container, root } = await mountModels();
Expand Down
Loading