Skip to content
Merged
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
8 changes: 5 additions & 3 deletions apps/desktop/src/features/chat/composer/ComposerModelList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { providerDisplayName } from "../../../lib/provider-display";
import { formatTokenCount, type ModelInfo, type ProviderPublic } from "@pi-desktop/shared";
import type { TFunction } from "i18next";
import type { RefObject } from "react";
Expand All @@ -9,6 +8,7 @@ import {
composerModelDisplayName,
sameComposerModelId,
} from "../../../lib/composer-models";
import { providerDisplayName } from "../../../lib/provider-display";

export type ComposerModelGroup = {
provider: ProviderPublic;
Expand Down Expand Up @@ -73,7 +73,9 @@ export function ComposerModelList({
>
<span className="composer-model-option-main">
<span className="composer-model-label">{composerModelDisplayName(provider, model.modelId)}</span>
<span className="composer-model-option-meta">{providerDisplayName(provider)}</span>
<span className="composer-model-option-meta" title={providerDisplayName(provider)}>
{providerDisplayName(provider)}
</span>
</span>
{active ? <IconCheck size={14} aria-hidden="true" /> : null}
</button>
Expand Down Expand Up @@ -137,7 +139,7 @@ export function ComposerModelList({
>
{alias || optionDisplayName}
</span>
<span className="composer-model-option-meta">
<span className="composer-model-option-meta" title={optionTitle}>
{composerModelBadges(model, group.provider).map((badge) => {
const badgeLabel = t(
badge === "reasoning"
Expand Down
34 changes: 23 additions & 11 deletions apps/desktop/src/styles/composer-menus.css
Original file line number Diff line number Diff line change
Expand Up @@ -533,18 +533,18 @@
}

/*
The full wire id is the identity, and it stays whole: a long route name wraps
instead of hiding the part that distinguishes two configured models. The id,
its friendly name and the badges share one line — id first, name next, badges
on the trailing edge — because the stacked column that preceded it pushed
every row's content into the left half of the menu and left the rest empty.
One row per model, and it stays one row: the friendly name takes the space it
needs up to the row's trailing markers (provider, capability icons, context
window), and anything longer than that is truncated with an ellipsis instead
of wrapping the row onto a second line. The full name and the wire id remain
reachable as the button's title, so identity is still one hover away.
*/
.composer-model-option-main {
display: flex;
min-width: 0;
flex: 1;
flex-direction: row;
flex-wrap: wrap;
flex-wrap: nowrap;
align-items: baseline;
gap: 2px 8px;
}
Expand All @@ -561,8 +561,9 @@
flex: 0 1 auto;
max-width: 100%;
color: var(--ds-text-primary);
overflow-wrap: anywhere;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* A name the user set is a label of its own, and reads as one: the catalog name
Expand All @@ -577,14 +578,25 @@

.composer-model-option-meta {
display: flex;
flex: 0 0 auto;
/* Pinned to the row's trailing edge: the capability icons and the window are
what a row shows on the right. */
flex: 0 1 auto;
min-width: 0;
/* Pinned to the row's trailing edge: the provider, the capability icons and
the window are what a row shows on the right. The provider name gives way
first, so a long model name keeps its own space. */
margin-left: auto;
align-items: center;
gap: 4px;
}

/* The provider trailing a row is supporting text on a shared line: it yields
width before the model name and never wraps the row. */
.composer-model-option-meta > span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* Capability markers are icons, not words: a row carries up to two of them, and
the spelled-out labels were the widest thing in it. The accessible name stays
the translated label, so nothing is lost to a screen reader. */
Expand Down
24 changes: 15 additions & 9 deletions apps/desktop/test/composer-model-thinking-menu.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -157,29 +157,35 @@ test("reasoning projection uses the selected exact catalog row and binding", asy
assert.match(source, /sameComposerModelId\(candidate\.id, modelId\)/);
});

test("a model row spends the panel's width instead of stacking at its left edge", () => {
// The label and the capability icons share one line, with the icons pinned to
// the trailing edge: stacking them in a column left the right half of the
// menu empty next to every row.
test("a model row stays on one line and truncates a name that outgrows it", () => {
// The label and the row's trailing markers (provider, capability icons,
// context window) share one line, with the markers pinned to the trailing
// edge. The row never wraps: a name longer than the space left for it is
// truncated with an ellipsis, and its wire id stays the row's title.
assert.match(
stylesSource,
/\.composer-model-option-main\s*\{[^}]*flex-direction:\s*row;/,
);
assert.match(
stylesSource,
/\.composer-model-option-main\s*\{[^}]*flex-wrap:\s*wrap;/,
/\.composer-model-option-main\s*\{[^}]*flex-wrap:\s*nowrap;/,
);
assert.match(
stylesSource,
/\.composer-model-option-meta\s*\{[^}]*margin-left:\s*auto;/,
);
// One label per row, and a long name wraps in full rather than truncating.
assert.doesNotMatch(stylesSource, /\.composer-model-full-id/);
assert.doesNotMatch(stylesSource, /\.composer-model-display-name/);
assert.match(
stylesSource,
/\.composer-model-label\s*\{[^}]*overflow-wrap:\s*anywhere;[^}]*white-space:\s*normal;/,
/\.composer-model-label\s*\{[^}]*overflow:\s*hidden;[^}]*text-overflow:\s*ellipsis;[^}]*white-space:\s*nowrap;/,
);
// The provider trailing a row yields width before the model name, so a long
// name keeps its own space on the shared line.
assert.match(
stylesSource,
/\.composer-model-option-meta\s*\{[^}]*flex:\s*0 1 auto;/,
);
assert.doesNotMatch(stylesSource, /\.composer-model-full-id/);
assert.doesNotMatch(stylesSource, /\.composer-model-display-name/);
});

test("an alias is told apart from the catalog name, and capabilities are icons", () => {
Expand Down
Loading