fix(ui): offer only what helps in the token limit note

Every model stops at 512 output tokens, so at the maximum the note no
longer offers larger models for a longer answer. It says they have the
same limit and suggests asking about one part or asking for the rest.
Below the maximum the list stays, without promising longer answers and
without the awkward memory sentence. Under an automatic explanation, which
writes at most 128 tokens, the raise hint and its button appear only when
the reader's limit is below 128, and that limit is called theirs instead
of a choice to keep explanations short.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
Bernhard Jackiewicz committed 2026-10-04 14:40:26 +02:00
1 parent 94ac1219dc
commit 68dcdef529
4 files changed
+88 -11

No files matched your search

@@ -1102,7 +1102,7 @@ describe('graph answers and answer limits', () => {
expect(document.activeElement?.id).toBe('cbm-chat-output-tokens');
});
it('does not offer to change an output limit that is already at its maximum, and still lists the larger models (C8)', async () => {
it('does not offer to change an output limit that is already at its maximum, nor larger models with the same limit (C8, W6)', async () => {
const { props, runtime } = fixture();
runtime.chat.mockImplementationOnce(async (_messages, _onToken, options) => {
options?.onComplete?.({ stopReason: 'length' }); return 'A long answer that';
@@ -1110,9 +1110,11 @@ describe('graph answers and answer limits', () => {
await render({ ...props, attachment: selection }); await click('Download & load');
await type('Explain this part'); await click('Send ↑');
const note = container.querySelector<HTMLDetailsElement>('.cbm-chat-turn details.cbm-chat-limit-note');
expect(note?.textContent).toContain('The output limit is at the maximum of 512 tokens for this model.');
expect(note?.textContent).toContain('The output limit is at its maximum of 512 tokens.');
expect([...note?.querySelectorAll('button') ?? []].map(item => item.textContent)).not.toContain('Change the output limit');
expect(note?.textContent).toContain('Qwen3 0.6B · 579 MB download');
// Every larger model stops at 512 output tokens as well, so none is offered for a longer answer.
expect(note?.textContent).toContain('Larger models have the same limit.');
expect(note?.textContent).not.toContain('Qwen3 0.6B · 579 MB download');
});
it('says why an automatic explanation stops early and that a question may answer longer (K1)', async () => {
+11 -5
View File
@@ -98,17 +98,23 @@ function SourceDisclosure({ children, title }: { children?: ReactNode; title?: s
}
/** Where an answer stopped and what can be changed: the limits it ran into, the way to
* the output limit and the larger models with their download. */
* the output limit and the larger models with their download. Only what helps is offered:
* an automatic explanation writes at most AUTO_OUTPUT_TOKENS whatever the limit, and at the
* maximum a larger model stops at the same limit (W6). */
interface LimitNote { limit: TokenLimits; automatic?: boolean; chat: TokenLimits; model: BrowserModel; onChangeOutput: () => void }
function TokenLimitNote({ limit, automatic, chat, model, onChangeOutput }: LimitNote): JSX.Element {
const larger = BROWSER_MODELS.filter(candidate => candidate.availability === 'available' && candidate.bytes > model.bytes);
const sameLimit = larger.every(candidate => candidate.maxOutputTokens <= model.maxOutputTokens);
const atMaximum = chat.outputTokens >= model.maxOutputTokens;
const raise = automatic ? chat.outputTokens < AUTO_OUTPUT_TOKENS : !atMaximum;
return <details className="cbm-chat-limit-note">
<summary>{browserChatText.shortened}</summary>
<p>{automatic ? browserChatText.limitAutomatic(limit.inputTokens, limit.outputTokens, chat.inputTokens, chat.outputTokens) : browserChatText.limitReached(limit.inputTokens, limit.outputTokens)}</p>
<p>{browserChatText.outputRoom(chat.outputTokens, model.maxOutputTokens)}</p>
<p>{automatic ? browserChatText.limitAutomatic(limit.inputTokens, limit.outputTokens, chat.inputTokens, chat.outputTokens, AUTO_OUTPUT_TOKENS) : browserChatText.limitReached(limit.inputTokens, limit.outputTokens)}</p>
{automatic ? raise && <p>{browserChatText.automaticRoom(AUTO_OUTPUT_TOKENS)}</p> : <p>{browserChatText.outputRoom(chat.outputTokens, model.maxOutputTokens)}</p>}
{!automatic && atMaximum && sameLimit && <p>{browserChatText.narrower(larger.length > 0)}</p>}
{/* At its maximum the limit cannot be raised, so there is nothing to change (C8). */}
{chat.outputTokens < model.maxOutputTokens && <button type="button" onClick={onChangeOutput}>{browserChatText.changeOutputLimit}</button>}
{larger.length > 0 && <><p>{browserChatText.largerModels}</p>
{raise && <button type="button" onClick={onChangeOutput}>{browserChatText.changeOutputLimit}</button>}
{!automatic && larger.length > 0 && (!atMaximum || !sameLimit) && <><p>{browserChatText.largerModels(sameLimit)}</p>
<ul>{larger.map(candidate => <li key={candidate.id}>{browserChatText.modelDownload(candidate.displayName, sizeLabel(candidate.bytes))}</li>)}</ul></>}
</details>;
}
@@ -6,6 +6,8 @@ import BrowserChatDock, { type BrowserChatDockProps, type BrowserChatReaderConte
import type { BrowserAiProgress, BrowserChatMessage } from './browser-ai-runtime';
import type { BrowserChatOptions } from './browser-ai-controller';
import { jsonbAggEvidence } from './galaxy-evidence.fixture';
import { AGENT_PREFERENCES_KEY } from './agent-preferences';
import { BROWSER_MODELS } from './model-policy';
/* The chat dock shows the wording of the third review (W5 to W10) where it builds it. */
@@ -88,3 +90,62 @@ describe('the note under a left out model sentence (W5)', () => {
expect(card()).toContain('Read from the file. The model\'s text named something the file does not show (here: flake8) and was left out.');
});
});
describe('the token limit note (W6)', () => {
const outputLimit = (outputTokens: number) => window.localStorage.setItem(AGENT_PREFERENCES_KEY, JSON.stringify({ version: 1,
preferences: { modelId: BROWSER_MODELS[0].id, automatic: true, limits: { [BROWSER_MODELS[0].id]: { inputTokens: 2048, outputTokens } } } }));
const cut = (runtime: ReturnType<typeof fixture>['runtime'], text = 'A long answer that') => runtime.chat.mockImplementationOnce(async (_messages, _onToken, options) => {
options?.onComplete?.({ stopReason: 'length' }); return text;
});
const note = (scope: string) => container.querySelector<HTMLDetailsElement>(`${scope} details.cbm-chat-limit-note`);
const noteButtons = (scope: string) => [...note(scope)?.querySelectorAll('button') ?? []].map(item => item.textContent);
it('at the maximum says that larger models have the same limit and how to get the rest, without offering them', async () => {
const { props, runtime } = fixture(); cut(runtime);
await render({ ...props, proactiveSelection: jsonbAggEvidence() }); await load();
await ask('Explain this class in detail, line by line.');
const text = note('.cbm-chat-turn')?.textContent ?? '';
expect(text).toContain('The output limit is at its maximum of 512 tokens.');
expect(text).toContain('Larger models have the same limit. Ask about one part of the code, or ask for the rest of the answer.');
expect(text).not.toMatch(/A larger model|download/);
expect(note('.cbm-chat-turn')?.querySelectorAll('li')).toHaveLength(0);
expect(noteButtons('.cbm-chat-turn')).toEqual([]);
});
it('below the maximum offers the higher limit and lists larger models without promising a longer answer', async () => {
outputLimit(256);
const { props, runtime } = fixture(); cut(runtime);
await render({ ...props, proactiveSelection: jsonbAggEvidence() }); await load();
await ask('Explain this class in detail, line by line.');
const text = note('.cbm-chat-turn')?.textContent ?? '';
expect(text).toContain('You can raise the output limit up to 512 tokens in the agent configuration.');
expect(text).toContain('A larger model may stay closer to the question, but its output limit is the same. Each is a one-time download and needs more memory than its download size:');
expect(text).not.toContain('memory use is higher than the download');
expect(text).toContain('Qwen3 0.6B · 579 MB download');
expect(noteButtons('.cbm-chat-turn')).toEqual(['Change the output limit']);
});
it('under an automatic explanation offers a higher limit only below the 128 tokens it can use', async () => {
vi.useFakeTimers();
outputLimit(256);
const { props, runtime } = fixture(); cut(runtime, 'JSONBAgg sets the function to JSONB_AGG and');
await render({ ...props, proactive: true, proactiveSelection: jsonbAggEvidence() }); await load(); await settle();
const text = note('.cbm-chat-explanation')?.textContent ?? '';
expect(text).toContain('Automatic explanations stop after 128 output tokens so they stay short');
expect(text).toContain('Ask in the chat for a longer answer.');
expect(text).not.toMatch(/raise the output limit|A larger model/i);
expect(noteButtons('.cbm-chat-explanation')).toEqual([]);
});
it('does not call the reader\'s own lower limit a design choice', async () => {
vi.useFakeTimers();
outputLimit(32);
const { props, runtime } = fixture(); cut(runtime, 'JSONBAgg sets the function to JSONB_AGG and');
await render({ ...props, proactive: true, proactiveSelection: jsonbAggEvidence() }); await load(); await settle();
const text = note('.cbm-chat-explanation')?.textContent ?? '';
expect(text).toContain('Automatic explanations stop after 32 output tokens, your output limit, and read at most 1,536 input tokens of source and facts.');
expect(text).not.toContain('so they stay short');
expect(text).toContain('Raise the output limit in the agent configuration and automatic explanations can use up to 128 output tokens.');
expect(noteButtons('.cbm-chat-explanation')).toEqual(['Change the output limit']);
});
});
+11 -3
View File
@@ -190,11 +190,19 @@ export const browserChatText = {
shortened: 'Token limit reached: the answer was cut short',
/** What the expanded token-limit note says, with the limits the answer ran into. */
limitReached: (input: number, output: number) => `This answer used all ${tokens(output)} output tokens it was allowed. The input limit is ${tokens(input)} tokens for the question, its source and earlier messages.`,
limitAutomatic: (automaticInput: number, automatic: number, input: number, output: number) => `Automatic explanations stop after ${tokens(automatic)} output tokens so they stay short, and read at most ${tokens(automaticInput)} input tokens of source and facts. A question in the chat may read up to ${tokens(input)} input tokens and answer with up to ${tokens(output)} output tokens.`,
/** `cap` is what automatic explanations write at most; below it they stop at the reader's own limit,
* which is no design choice to keep them short (W6). */
limitAutomatic: (automaticInput: number, automatic: number, input: number, output: number, cap: number) => `Automatic explanations stop after ${tokens(automatic)} output tokens`
+ `${automatic < cap ? ', your output limit,' : ' so they stay short,'} and read at most ${tokens(automaticInput)} input tokens of source and facts. `
+ `A question in the chat may read up to ${tokens(input)} input tokens and answer with up to ${tokens(output)} output tokens.${output > automatic ? ' Ask in the chat for a longer answer.' : ''}`,
automaticRoom: (cap: number) => `Raise the output limit in the agent configuration and automatic explanations can use up to ${tokens(cap)} output tokens.`,
outputRoom: (output: number, max: number) => output < max ? `You can raise the output limit up to ${tokens(max)} tokens in the agent configuration.`
: `The output limit is at the maximum of ${tokens(max)} tokens for this model.`,
: `The output limit is at its maximum of ${tokens(max)} tokens.`,
/** At the maximum a larger model writes no longer answer: every model stops at the same limit (W6). */
narrower: (sameForLarger: boolean) => `${sameForLarger ? 'Larger models have the same limit. ' : ''}Ask about one part of the code, or ask for the rest of the answer.`,
changeOutputLimit: 'Change the output limit',
largerModels: 'A larger model may stay closer to the question. Each needs a one-time download, and its memory use is higher than the download:',
largerModels: (sameLimit: boolean) => `A larger model may stay closer to the question${sameLimit ? ', but its output limit is the same' : ''}. `
+ 'Each is a one-time download and needs more memory than its download size:',
modelDownload: (name: string, size: string) => `${name} · ${size} download`,
newConversation: 'New conversation',
/** The model's state in the agent configuration (K10). */