feat: polish assistant conversations

This commit is contained in:
ajnart committed 2026-08-14 11:32:10 +02:00
1 parent 1ad1439828
commit f991dc6334
14 files changed
+509 -120

No files matched your search

@@ -2,7 +2,19 @@
import { useEffect, useRef, useState } from "react";
import type { ToolCallMessagePartProps, ToolCallMessagePartStatus } from "@assistant-ui/react";
import { Alert, Badge, Box, Button, Group, Skeleton, Stack, Text, TextInput, ThemeIcon } from "@mantine/core";
import {
Alert,
Badge,
Box,
Button,
Group,
Skeleton,
Stack,
Text,
TextInput,
ThemeIcon,
UnstyledButton,
} from "@mantine/core";
import {
IconAlertTriangle,
IconArrowRight,
@@ -170,36 +182,45 @@ export const AssistantAskUserTool = ({
const color = kind === "affirmative" ? "green" : kind === "negative" ? "red" : "gray";
return (
<Button
<UnstyledButton
key={`${option.id}:${index}`}
className={classes.humanToolOption}
variant={kind === "alternative" ? "default" : "light"}
color={color}
size="md"
fullWidth
justify="space-between"
leftSection={icon}
rightSection={
<Badge variant="light" color={color} size="xs">
{t(`optionKind.${kind}`)}
</Badge>
}
data-kind={kind}
disabled={submitting}
onClick={() =>
submitResult({ answer: option.label, optionId: option.id, optionKind: kind, source: "option" })
}
>
<Box ta="start">
<Text component="span" size="sm" fw={650}>
<ThemeIcon className={classes.humanToolOptionIcon} variant="light" color={color} size="sm">
{icon}
</ThemeIcon>
<Box className={classes.humanToolOptionText} ta="start">
<Text component="span" size="sm" fw={650} className={classes.humanToolOptionLabel}>
{option.label}
</Text>
{option.description && (
<Text component="span" display="block" size="xs" c="dimmed" fw={400}>
<Text
component="span"
display="block"
size="xs"
c="dimmed"
fw={400}
className={classes.humanToolOptionDescription}
>
{option.description}
</Text>
)}
</Box>
</Button>
<Badge
className={classes.humanToolOptionBadge}
classNames={{ label: classes.humanToolOptionBadgeLabel }}
variant="light"
color={color}
size="xs"
>
{t(`optionKind.${kind}`)}
</Badge>
</UnstyledButton>
);
})
)}
@@ -0,0 +1,43 @@
import { describe, expect, test } from "vitest";
import { resolveAssistantDirectiveEntity, transformAssistantMarkdownDirectives } from "./assistant-markdown-directives";
describe("assistant markdown directives", () => {
test("turns assistant item citations into typed inline elements", () => {
const directive: {
type: string;
name: string;
attributes: { name: string };
children: { type: string; value: string }[];
data?: Record<string, unknown>;
} = {
type: "textDirective",
name: "app",
attributes: { name: "app-1" },
children: [{ type: "text", value: "Discord" }],
};
transformAssistantMarkdownDirectives({ type: "root", children: [directive] });
expect(directive.data).toEqual({
hName: "span",
hProperties: {
"data-assistant-directive": "true",
"data-directive-id": "app-1",
"data-directive-label": "Discord",
"data-directive-type": "app",
},
});
});
test("resolves shorthand citations only when their label is unambiguous", () => {
const entities = [
{ id: "app-1", type: "app" as const, label: "Discord", description: "Chat", iconUrl: "/discord.svg" },
{ id: "board-1", type: "board" as const, label: "Home", description: "Home board" },
];
expect(resolveAssistantDirectiveEntity(entities, { id: "Discord", label: "Discord", type: "app" })?.id).toBe(
"app-1",
);
expect(resolveAssistantDirectiveEntity(entities, { id: "Home", label: "Home", type: "board" })?.id).toBe("board-1");
});
});
@@ -0,0 +1,63 @@
export type AssistantDirectiveType = "app" | "integration" | "board" | "widget";
export type AssistantDirectiveEntity = {
id: string;
type: AssistantDirectiveType;
label: string;
description: string;
iconUrl?: string;
};
type MarkdownNode = {
type?: string;
name?: string;
value?: string;
attributes?: Record<string, string | null | undefined> | null;
children?: MarkdownNode[];
data?: Record<string, unknown>;
};
const directiveTypes = new Set<AssistantDirectiveType>(["app", "integration", "board", "widget"]);
const getNodeText = (node: MarkdownNode): string =>
typeof node.value === "string" ? node.value : (node.children ?? []).map(getNodeText).join("");
export const transformAssistantMarkdownDirectives = (tree: MarkdownNode) => {
const visit = (node: MarkdownNode) => {
if (node.type === "textDirective" && node.name && directiveTypes.has(node.name as AssistantDirectiveType)) {
const label = getNodeText(node).trim();
const directiveId = node.attributes?.name?.trim() || label;
if (label && directiveId) {
node.data = {
...node.data,
hName: "span",
hProperties: {
"data-assistant-directive": "true",
"data-directive-id": directiveId,
"data-directive-label": label,
"data-directive-type": node.name,
},
};
}
}
for (const child of node.children ?? []) visit(child);
};
visit(tree);
};
export const remarkAssistantDirectives = () => transformAssistantMarkdownDirectives;
export const resolveAssistantDirectiveEntity = (
entities: AssistantDirectiveEntity[],
directive: { id: string; label: string; type: string },
) => {
if (!directiveTypes.has(directive.type as AssistantDirectiveType)) return undefined;
const matchingType = entities.filter((entity) => entity.type === directive.type);
const exactId = matchingType.find((entity) => entity.id === directive.id);
if (exactId) return exactId;
const normalizedLabel = directive.label.trim().toLocaleLowerCase();
const labelMatches = matchingType.filter((entity) => entity.label.trim().toLocaleLowerCase() === normalizedLabel);
return labelMatches.length === 1 ? labelMatches[0] : undefined;
};
@@ -1,6 +1,10 @@
import { describe, expect, test } from "vitest";
import { getAssistantTelemetry, getAssistantUsage } from "./assistant-message-metadata";
import {
getAssistantTelemetry,
getAssistantUsage,
resolveAssistantContextWindowTelemetry,
} from "./assistant-message-metadata";
describe("getAssistantTelemetry", () => {
test("extracts persisted request telemetry", () => {
@@ -117,3 +121,53 @@ describe("getAssistantTelemetry", () => {
).toEqual({ inputTokens: 120, cachedInputTokens: 24 });
});
});
describe("resolveAssistantContextWindowTelemetry", () => {
const current = {
requestId: "current",
provider: "openrouter",
modelId: "example/model",
startedAt: "2026-08-04T10:00:00.000Z",
contextLength: 128_000,
steps: [],
};
const previousMetadata = {
custom: {
telemetry: {
...current,
requestId: "previous",
completedAt: "2026-08-04T09:59:00.000Z",
contextUsed: 12_500,
},
},
};
test("keeps the previous completed reading while a matching request streams", () => {
expect(resolveAssistantContextWindowTelemetry(current, [previousMetadata])).toMatchObject({
source: "previous",
telemetry: { contextUsed: 12_500 },
});
});
test("prefers a current completed reading", () => {
expect(
resolveAssistantContextWindowTelemetry({ ...current, contextUsed: 13_000 }, [previousMetadata]),
).toMatchObject({ source: "current", telemetry: { contextUsed: 13_000 } });
});
test("does not mix readings from a model with another context capacity", () => {
expect(
resolveAssistantContextWindowTelemetry(current, [
{
custom: {
telemetry: {
...previousMetadata.custom.telemetry,
modelId: "another/model",
contextLength: 1_000_000,
},
},
},
]),
).toBeNull();
});
});
@@ -87,6 +87,11 @@ export type AssistantMessageMetadata = {
export type AssistantUIMessage = UIMessage<AssistantMessageMetadata>;
export type AssistantContextWindowTelemetry = {
telemetry: AssistantRequestTelemetry;
source: "current" | "previous";
};
const getFiniteNonNegativeNumber = (value: unknown) =>
typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
@@ -300,3 +305,26 @@ export const getAssistantTelemetry = (metadata: unknown): AssistantRequestTeleme
...(typeof value.finishReason === "string" ? { finishReason: value.finishReason } : {}),
};
};
export const resolveAssistantContextWindowTelemetry = (
current: AssistantRequestTelemetry,
previousMetadata: unknown[],
): AssistantContextWindowTelemetry | null => {
if (current.contextLength !== undefined && current.contextUsed !== undefined) {
return { telemetry: current, source: "current" };
}
for (const metadata of previousMetadata.toReversed()) {
const previous = getAssistantTelemetry(metadata);
if (
previous?.completedAt &&
previous.requestId !== current.requestId &&
previous.modelId === current.modelId &&
previous.contextLength === current.contextLength &&
previous.contextUsed !== undefined
) {
return { telemetry: previous, source: "previous" };
}
}
return null;
};
@@ -48,6 +48,7 @@
border-radius: var(--mantine-radius-sm);
color: var(--mantine-color-yellow-light-color);
background: var(--mantine-color-yellow-light);
white-space: nowrap;
}
.activityDismiss {
@@ -213,8 +214,8 @@
}
.composerInput {
min-height: 2.625rem;
padding: 0.6rem 0.5rem;
min-height: 2.25rem;
padding: 0.35rem 0.45rem;
}
.runtimeSelectorTrigger {
@@ -390,6 +391,13 @@
text-overflow: ellipsis;
}
.directiveChipImage {
width: 0.9rem;
height: 0.9rem;
flex: 0 0 auto;
object-fit: contain;
}
.messageMarkdown {
line-height: 1.65;
overflow-wrap: anywhere;
@@ -670,16 +678,65 @@
}
.humanToolOption {
display: grid;
width: 100%;
height: auto;
min-height: 3.25rem;
padding-block: 0.55rem;
padding-inline: 0.8rem;
white-space: normal;
grid-template-columns: max-content minmax(0, 1fr) max-content;
align-items: center;
gap: 0.65rem;
padding: 0.55rem 0.7rem;
border: 1px solid var(--mantine-color-default-border);
border-radius: var(--mantine-radius-md);
background: var(--mantine-color-body);
text-align: start;
}
.humanToolOption :global(.mantine-Button-inner),
.humanToolOption :global(.mantine-Button-label) {
width: 100%;
.humanToolOption:hover:not(:disabled) {
border-color: var(--mantine-color-default-border);
background: var(--mantine-color-default-hover);
}
.humanToolOption:focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: 2px;
}
.humanToolOption:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.humanToolOption[data-kind="affirmative"] {
border-color: color-mix(in srgb, var(--mantine-color-green-filled) 35%, var(--mantine-color-default-border));
}
.humanToolOption[data-kind="negative"] {
border-color: color-mix(in srgb, var(--mantine-color-red-filled) 30%, var(--mantine-color-default-border));
}
.humanToolOptionIcon,
.humanToolOptionBadge {
width: max-content;
flex: 0 0 auto;
overflow: visible;
white-space: nowrap;
}
.humanToolOptionText {
min-width: 0;
}
.humanToolOptionBadgeLabel {
width: max-content;
overflow: visible;
text-overflow: clip;
}
.humanToolOptionLabel,
.humanToolOptionDescription {
overflow-wrap: anywhere;
white-space: normal;
}
.humanToolOtherInput {
@@ -797,20 +854,25 @@
background: var(--mantine-color-default);
}
.reasoning summary {
.reasoningToggle {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
border-radius: var(--mantine-radius-sm);
cursor: pointer;
list-style: none;
}
.reasoning summary::-webkit-details-marker {
display: none;
.reasoningToggle:focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: 2px;
}
.reasoningChevron {
transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
.reasoning[open] .reasoningChevron {
.reasoning[data-opened] .reasoningChevron {
transform: rotate(180deg);
}
@@ -1091,27 +1153,35 @@
.composerInput {
position: relative;
min-height: 2.5rem;
min-height: 2.25rem;
max-height: 9rem;
flex: 1;
padding: 0.55rem 0.65rem;
box-sizing: border-box;
padding: 0.35rem 0.45rem;
color: var(--mantine-color-text);
background: transparent;
font: inherit;
line-height: 1.5;
cursor: text;
}
.composerInput :global(.aui-lexical-input) {
min-height: 1.5rem;
line-height: 1.5;
outline: 0;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.composerInput :global(.aui-lexical-input > p) {
margin: 0;
}
.composerInput :global(.aui-lexical-placeholder) {
position: absolute;
inset: 0.55rem 0.65rem auto;
inset: 0.35rem 0.45rem auto;
color: var(--mantine-color-dimmed);
line-height: 1.5;
pointer-events: none;
}
@@ -1128,7 +1198,7 @@
width: fit-content;
min-width: 0;
max-width: min(24rem, 100%);
padding: 0.3rem 0.45rem;
padding: 0.22rem 0.4rem;
border: 1px solid var(--mantine-color-default-border);
border-radius: var(--mantine-radius-sm);
color: var(--mantine-color-text);
@@ -1149,10 +1219,6 @@
cursor: not-allowed;
}
.runtimeSelectorIcon {
flex: 0 0 auto;
}
.runtimeSelectorName {
min-width: 0;
flex: 1;
@@ -1165,7 +1231,6 @@
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 0.2rem;
text-transform: none;
}
@@ -1180,7 +1245,16 @@
}
.modelOption {
padding-block: 0.45rem;
margin-inline: 0.25rem;
padding: 0.32rem 0.45rem;
border-radius: var(--mantine-radius-sm);
}
.modelOption[data-combobox-selected],
.modelOption[data-combobox-active] {
color: var(--mantine-color-text);
background: var(--mantine-color-default-hover);
box-shadow: inset 2px 0 0 var(--mantine-primary-color-filled);
}
.modelOptionText {
@@ -1193,13 +1267,17 @@
color: var(--mantine-color-dimmed);
}
.modelOptionDescription {
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-3));
}
.runtimeOptionCheck {
flex: 0 0 auto;
color: var(--mantine-primary-color-filled);
}
.reasoningSelector {
padding: 0.55rem;
padding: 0.4rem 0.45rem 0.45rem;
}
.reasoningSegmentedControl :global(.mantine-SegmentedControl-label) {
@@ -1342,8 +1420,8 @@
}
.composerInput {
min-height: 2.625rem;
padding: 0.6rem 0.55rem;
min-height: 2.25rem;
padding: 0.35rem 0.45rem;
}
.composerFooter {
@@ -1369,10 +1447,6 @@
}
@media (max-width: 30em) {
.activityReviewAction > span {
display: none;
}
.historyButton {
width: 2.625rem;
max-width: 2.625rem;
@@ -67,7 +67,6 @@ import {
IconArrowUp,
IconArrowsMaximize,
IconAt,
IconBrain,
IconCheck,
IconChevronDown,
IconChevronLeft,
@@ -92,7 +91,6 @@ import {
IconRobot,
IconSearch,
IconShieldCheck,
IconSparkles,
IconThumbDown,
IconThumbUp,
IconTool,
@@ -100,6 +98,7 @@ import {
IconX,
} from "@tabler/icons-react";
import remarkBreaks from "remark-breaks";
import remarkDirective from "remark-directive";
import remarkGfm from "remark-gfm";
import { clientApi } from "@homarr/api/client";
@@ -110,9 +109,15 @@ import { useScopedI18n } from "@homarr/translation/client";
import classes from "./assistant-panel.module.css";
import { useAssistantAutoApproval, useAssistantAutomaticAction } from "./assistant-auto-approval";
import { getAssistantDirectiveTranslationKey, parseAssistantDirectives } from "./assistant-directives";
import { remarkAssistantDirectives, resolveAssistantDirectiveEntity } from "./assistant-markdown-directives";
import type { AssistantDirectiveEntity } from "./assistant-markdown-directives";
import { normalizeAssistantMarkdown } from "./assistant-markdown";
import { getSafeAssistantMarkdownImageSource } from "./assistant-markdown-image";
import { getAssistantTelemetry, getAssistantUsage } from "./assistant-message-metadata";
import {
getAssistantTelemetry,
getAssistantUsage,
resolveAssistantContextWindowTelemetry,
} from "./assistant-message-metadata";
import type { AssistantPendingAction } from "./assistant-pending-action";
import { AssistantQuestionPortalProvider } from "./assistant-question-portal";
import type { AssistantReasoningMode, AssistantRuntimeModelOption } from "./assistant-preferences";
@@ -144,7 +149,7 @@ interface AssistantPanelProps extends AssistantConversationControls {
pendingAction: AssistantPendingAction | undefined;
}
const markdownRemarkPlugins = [remarkGfm, remarkBreaks];
const markdownRemarkPlugins = [remarkDirective, remarkAssistantDirectives, remarkGfm, remarkBreaks];
const contextIcons = {
app: IconApps,
@@ -155,9 +160,15 @@ const contextIcons = {
tools: IconTool,
};
const ContextDirectiveChip = ({ directiveId, directiveType, label }: DirectiveChipProps) => {
const ContextDirectiveChip = ({
directiveId,
directiveType,
label,
iconUrl,
}: DirectiveChipProps & { iconUrl?: string }) => {
const t = useScopedI18n("common.assistant");
const Icon = contextIcons[directiveType as keyof typeof contextIcons] ?? IconAt;
const safeIconUrl = getSafeAssistantMarkdownImageSource(iconUrl);
const translationKey = getAssistantDirectiveTranslationKey(directiveType);
const typeLabel = t(`mentions.${translationKey}`);
@@ -168,7 +179,11 @@ const ContextDirectiveChip = ({ directiveId, directiveType, label }: DirectiveCh
data-directive-id={directiveId}
title={`${typeLabel}: ${label}`}
>
<Icon size={12} aria-hidden />
{safeIconUrl ? (
<img className={classes.directiveChipImage} src={safeIconUrl} alt="" aria-hidden referrerPolicy="no-referrer" />
) : (
<Icon size={12} aria-hidden />
)}
<span className={classes.directiveChipLabel}>{label}</span>
</span>
);
@@ -273,34 +288,78 @@ const MermaidDiagram = ({ code, components }: SyntaxHighlighterProps) => {
);
};
const AssistantTextPart = () => (
<MarkdownTextPrimitive
className={classes.messageMarkdown}
preprocess={normalizeAssistantMarkdown}
remarkPlugins={markdownRemarkPlugins}
components={{ a: MarkdownLink, img: MarkdownImage, table: MarkdownTable }}
componentsByLanguage={{ mermaid: { SyntaxHighlighter: MermaidDiagram } }}
defer
/>
);
type MarkdownSpanProps = ComponentPropsWithoutRef<"span"> & {
"data-assistant-directive"?: string;
"data-directive-id"?: string;
"data-directive-label"?: string;
"data-directive-type"?: string;
};
const AssistantDirectiveEntitiesContext = createContext<AssistantDirectiveEntity[]>([]);
const AssistantMarkdownSpan = ({
children,
"data-assistant-directive": assistantDirective,
"data-directive-id": directiveId,
"data-directive-label": directiveLabel,
"data-directive-type": directiveType,
...props
}: MarkdownSpanProps) => {
const entities = useContext(AssistantDirectiveEntitiesContext);
if (assistantDirective === "true" && directiveId && directiveLabel && directiveType) {
const entity = resolveAssistantDirectiveEntity(entities, {
id: directiveId,
label: directiveLabel,
type: directiveType,
});
return (
<ContextDirectiveChip
directiveId={entity?.id ?? directiveId}
directiveType={directiveType}
label={entity?.label ?? directiveLabel}
iconUrl={entity?.iconUrl}
/>
);
}
return <span {...props}>{children}</span>;
};
const AssistantTextPart = () => {
const { data: entities = [] } = clientApi.assistant.getContextEntities.useQuery(undefined, { staleTime: 60_000 });
return (
<AssistantDirectiveEntitiesContext.Provider value={entities}>
<MarkdownTextPrimitive
className={classes.messageMarkdown}
preprocess={normalizeAssistantMarkdown}
remarkPlugins={markdownRemarkPlugins}
components={{ a: MarkdownLink, img: MarkdownImage, span: AssistantMarkdownSpan, table: MarkdownTable }}
componentsByLanguage={{ mermaid: { SyntaxHighlighter: MermaidDiagram } }}
defer
/>
</AssistantDirectiveEntitiesContext.Provider>
);
};
const UserTextPart = () => {
const { text } = useMessagePartText();
const { data: entities = [] } = clientApi.assistant.getContextEntities.useQuery(undefined, { staleTime: 60_000 });
const segments = parseAssistantDirectives(text);
return (
<span className={classes.messageText}>
{segments.map((segment, index) =>
segment.kind === "text" ? (
<span key={`${index}:${segment.text}`}>{segment.text}</span>
) : (
{segments.map((segment, index) => {
if (segment.kind === "text") return <span key={`${index}:${segment.text}`}>{segment.text}</span>;
const entity = resolveAssistantDirectiveEntity(entities, segment);
return (
<ContextDirectiveChip
key={`${index}:${segment.type}:${segment.id}`}
directiveId={segment.id}
directiveId={entity?.id ?? segment.id}
directiveType={segment.type}
label={segment.label}
label={entity?.label ?? segment.label}
iconUrl={entity?.iconUrl}
/>
),
)}
);
})}
</span>
);
};
@@ -333,23 +392,40 @@ const ReasoningPart = ({ text }: ReasoningMessagePartProps) => (
const ReasoningGroup = ({ children, running }: { children: ReactNode; running: boolean }) => {
const t = useScopedI18n("common.assistant");
const contentId = useId();
const [opened, setOpened] = useState(running);
const [manuallyToggled, setManuallyToggled] = useState(false);
useEffect(() => {
if (running && !manuallyToggled) setOpened(true);
}, [manuallyToggled, running]);
return (
<details className={classes.reasoning} open={running}>
<summary>
<Group gap="xs" wrap="nowrap" justify="space-between">
<Group gap="xs" wrap="nowrap">
{running ? <Loader type="bars" size="xs" color="gray" /> : <IconBrain size={14} />}
<Text size="xs" fw={600} c="dimmed">
{t("reasoning")}
</Text>
</Group>
<IconChevronDown size={14} className={classes.reasoningChevron} />
<Box className={classes.reasoning} data-opened={opened || undefined}>
<UnstyledButton
className={classes.reasoningToggle}
type="button"
aria-expanded={opened}
aria-controls={contentId}
onClick={() => {
setManuallyToggled(true);
setOpened((value) => !value);
}}
>
<Group gap="xs" wrap="nowrap">
{running && <Loader type="bars" size="xs" color="gray" />}
<Text size="xs" fw={650} c="dimmed">
{t("reasoning")}
</Text>
</Group>
</summary>
<Box className={classes.reasoningContent} aria-busy={running}>
{children}
</Box>
</details>
<IconChevronDown size={14} className={classes.reasoningChevron} />
</UnstyledButton>
<Collapse expanded={opened}>
<Box id={contentId} className={classes.reasoningContent} aria-busy={running}>
{children}
</Box>
</Collapse>
</Box>
);
};
@@ -847,6 +923,7 @@ const RequestTelemetry = () => {
const metadata = useAuiState((state) => state.message.metadata);
const messageId = useAuiState((state) => state.message.id);
const threadId = useAuiState((state) => state.threadListItem.remoteId);
const threadMessageMetadata = useAuiState((state) => state.thread.messages.map((message) => message.metadata));
const persistedTelemetry = getAssistantTelemetry(metadata);
const persistedUsage = getAssistantUsage(metadata);
const generationQuery = clientApi.assistant.getGenerationTelemetry.useQuery(
@@ -941,13 +1018,18 @@ const RequestTelemetry = () => {
: {}),
};
const contextLength = telemetry.contextLength ?? 0;
const contextUsed = telemetry.contextUsed ?? 0;
const hasContextWindow = telemetry.contextLength !== undefined && telemetry.contextUsed !== undefined;
const contextWindow = resolveAssistantContextWindowTelemetry(telemetry, threadMessageMetadata);
const contextTelemetry = contextWindow?.telemetry;
const contextLength = contextTelemetry?.contextLength ?? 0;
const contextUsed = contextTelemetry?.contextUsed ?? 0;
const hasContextWindow = contextTelemetry?.contextLength !== undefined && contextTelemetry.contextUsed !== undefined;
const contextPercentage = hasContextWindow
? Math.min(
100,
Math.max(0, Math.round((telemetry.contextUtilization ?? contextUsed / Math.max(contextLength, 1)) * 100)),
Math.max(
0,
Math.round((contextTelemetry.contextUtilization ?? contextUsed / Math.max(contextLength, 1)) * 100),
),
)
: 0;
const contextRemaining = hasContextWindow ? Math.max(contextLength - contextUsed, 0) : undefined;
@@ -1054,7 +1136,7 @@ const RequestTelemetry = () => {
{t("usage.used")}
</Text>
<Text size="sm" fw={600}>
{telemetry.contextUsed?.toLocaleString() ?? t("usage.notReported")}
{contextTelemetry?.contextUsed?.toLocaleString() ?? t("usage.notReported")}
</Text>
</div>
<div>
@@ -1070,7 +1152,7 @@ const RequestTelemetry = () => {
{t("usage.capacity")}
</Text>
<Text size="sm" fw={600}>
{telemetry.contextLength?.toLocaleString() ?? t("usage.notReported")}
{contextTelemetry?.contextLength?.toLocaleString() ?? t("usage.notReported")}
</Text>
</div>
</Box>
@@ -1929,7 +2011,11 @@ type ComposerProps = AssistantConversationControls & { pendingAction: AssistantP
const getModelProviderLabel = (modelId: string, fallback: string) => {
const separator = modelId.indexOf("/");
if (separator <= 0) return fallback;
const provider = modelId.slice(0, separator).replaceAll(/[-_]/gu, " ");
const provider = modelId
.slice(0, separator)
.replace(/^[^\p{L}\p{N}]+/u, "")
.replaceAll(/[-_]/gu, " ");
if (!provider) return fallback;
return `${provider.charAt(0).toLocaleUpperCase()}${provider.slice(1)}`;
};
@@ -1981,7 +2067,7 @@ const RuntimeControls = ({
};
return (
<Combobox store={modelCombobox} onOptionSubmit={selectModel} withinPortal position="top-start" width={360}>
<Combobox store={modelCombobox} onOptionSubmit={selectModel} withinPortal position="top-start" width={340}>
<Combobox.Target>
<UnstyledButton
className={classes.runtimeSelectorTrigger}
@@ -1992,14 +2078,11 @@ const RuntimeControls = ({
aria-expanded={modelCombobox.dropdownOpened}
>
<Group gap="xs" wrap="nowrap">
<ThemeIcon className={classes.runtimeSelectorIcon} size="sm" variant="light" color="gray">
{modelOptionsLoading ? <Loader size={12} /> : <IconSparkles size={13} />}
</ThemeIcon>
{modelOptionsLoading && <Loader size={12} />}
<Text className={classes.runtimeSelectorName} size="xs" fw={650} lineClamp={1}>
{selectedModel?.name ?? t("runtime.model")}
</Text>
<Badge className={classes.runtimeSelectorEffort} size="xs" variant="light" color="gray">
<IconBrain size={11} />
{t(`runtime.reasoning.${reasoning}`)}
</Badge>
<Combobox.Chevron size="xs" />
@@ -2015,6 +2098,7 @@ const RuntimeControls = ({
}}
placeholder={t("runtime.searchModels")}
aria-label={t("runtime.searchModels")}
size="xs"
/>
<Combobox.Options className={classes.modelOptions}>
{[...groupedModels.entries()].map(([provider, providerModels]) => (
@@ -2027,14 +2111,11 @@ const RuntimeControls = ({
active={model.id === modelId}
>
<Group gap="xs" wrap="nowrap">
<ThemeIcon size="md" radius="md" variant="light" color="gray">
<IconSparkles size={14} />
</ThemeIcon>
<Stack gap={1} className={classes.modelOptionText}>
<Text size="sm" fw={model.id === modelId ? 650 : 500} lineClamp={1}>
{model.name}
</Text>
<Text size="xs" c="dimmed" lineClamp={1}>
<Text size="xs" className={classes.modelOptionDescription} lineClamp={1}>
{model.description?.trim() || model.id}
</Text>
</Stack>
@@ -2060,12 +2141,9 @@ const RuntimeControls = ({
</Combobox.Options>
<Divider />
<Box className={classes.reasoningSelector}>
<Group gap="xs" mb={6} wrap="nowrap">
<IconBrain size={14} />
<Text size="xs" fw={650}>
{t("runtime.thinking")}
</Text>
</Group>
<Text size="xs" fw={650} mb={5}>
{t("runtime.thinking")}
</Text>
<SegmentedControl
className={classes.reasoningSegmentedControl}
value={reasoning}